diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..c7553e0 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..04683a1 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..5a8ad0f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..5f1b1e8 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,55 @@ +# Generated by Django 5.2.7 on 2026-02-07 03:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Dosen', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nidn', models.CharField(max_length=20, unique=True)), + ('nama', models.CharField(max_length=100)), + ], + options={ + 'verbose_name_plural': 'Dosen', + }, + ), + migrations.CreateModel( + name='Hari', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama', models.CharField(max_length=20)), + ], + options={ + 'verbose_name_plural': 'Hari', + }, + ), + migrations.CreateModel( + name='ProgramStudi', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama', models.CharField(max_length=100)), + ], + options={ + 'verbose_name_plural': 'Program Studi', + }, + ), + migrations.CreateModel( + name='TahunAkademik', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama', models.CharField(max_length=20)), + ], + options={ + 'verbose_name_plural': 'Tahun Akademik', + }, + ), + ] diff --git a/core/migrations/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.py b/core/migrations/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.py new file mode 100644 index 0000000..fd80e81 --- /dev/null +++ b/core/migrations/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.py @@ -0,0 +1,90 @@ +# Generated by Django 5.2.7 on 2026-02-07 03:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Jam', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('range_waktu', models.CharField(max_length=50)), + ], + options={ + 'verbose_name_plural': 'Jam', + }, + ), + migrations.CreateModel( + name='Kelas', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama', models.CharField(max_length=20)), + ], + options={ + 'verbose_name_plural': 'Kelas', + }, + ), + migrations.CreateModel( + name='Ruangan', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nama', models.CharField(max_length=50)), + ('kapasitas', models.IntegerField()), + ], + options={ + 'verbose_name_plural': 'Ruangan', + }, + ), + migrations.AddField( + model_name='tahunakademik', + name='is_active', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='MataKuliah', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('kode', models.CharField(max_length=20, unique=True)), + ('nama', models.CharField(max_length=100)), + ('sks', models.IntegerField()), + ('semester', models.IntegerField()), + ('prodi', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.programstudi')), + ], + options={ + 'verbose_name_plural': 'Mata Kuliah', + }, + ), + migrations.CreateModel( + name='DosenPengampu', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dosen', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.dosen')), + ('tahun_akademik', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.tahunakademik')), + ('matkul', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.matakuliah')), + ], + options={ + 'verbose_name_plural': 'Dosen Pengampu', + }, + ), + migrations.CreateModel( + name='Jadwal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('dosen_pengampu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.dosenpengampu')), + ('hari', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hari')), + ('jam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.jam')), + ('kelas', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.kelas')), + ('ruangan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.ruangan')), + ], + options={ + 'verbose_name_plural': 'Jadwal', + }, + ), + ] diff --git a/core/migrations/0003_kelas_prodi_ruangan_prodi.py b/core/migrations/0003_kelas_prodi_ruangan_prodi.py new file mode 100644 index 0000000..987237a --- /dev/null +++ b/core/migrations/0003_kelas_prodi_ruangan_prodi.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-02-07 03:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='kelas', + name='prodi', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.programstudi'), + ), + migrations.AddField( + model_name='ruangan', + name='prodi', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.programstudi'), + ), + ] diff --git a/core/migrations/0004_daftarhadir.py b/core/migrations/0004_daftarhadir.py new file mode 100644 index 0000000..db7552c --- /dev/null +++ b/core/migrations/0004_daftarhadir.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.7 on 2026-02-07 04:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_kelas_prodi_ruangan_prodi'), + ] + + operations = [ + migrations.CreateModel( + name='DaftarHadir', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tanggal', models.DateField()), + ('pertemuan_ke', models.IntegerField(default=1)), + ('keterangan', models.TextField(blank=True, null=True)), + ('jadwal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.jadwal')), + ], + options={ + 'verbose_name_plural': 'Daftar Hadir', + }, + ), + ] diff --git a/core/migrations/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.py b/core/migrations/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.py new file mode 100644 index 0000000..58284c9 --- /dev/null +++ b/core/migrations/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-07 04:45 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_daftarhadir'), + ] + + operations = [ + migrations.AddField( + model_name='dosenpengampu', + name='kelas', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.kelas'), + ), + migrations.AddField( + model_name='dosenpengampu', + name='prodi', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.programstudi'), + ), + migrations.AlterField( + model_name='matakuliah', + name='kode', + field=models.CharField(max_length=20), + ), + migrations.AlterUniqueTogether( + name='matakuliah', + unique_together={('kode', 'prodi'), ('nama', 'prodi')}, + ), + ] diff --git a/core/migrations/0006_jadwal_is_manual.py b/core/migrations/0006_jadwal_is_manual.py new file mode 100644 index 0000000..3a85d93 --- /dev/null +++ b/core/migrations/0006_jadwal_is_manual.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-07 05:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='jadwal', + name='is_manual', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..5ec1451 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.cpython-311.pyc new file mode 100644 index 0000000..10a785b Binary files /dev/null and b/core/migrations/__pycache__/0002_jam_kelas_ruangan_tahunakademik_is_active_matakuliah_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_kelas_prodi_ruangan_prodi.cpython-311.pyc b/core/migrations/__pycache__/0003_kelas_prodi_ruangan_prodi.cpython-311.pyc new file mode 100644 index 0000000..965c5fc Binary files /dev/null and b/core/migrations/__pycache__/0003_kelas_prodi_ruangan_prodi.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_daftarhadir.cpython-311.pyc b/core/migrations/__pycache__/0004_daftarhadir.cpython-311.pyc new file mode 100644 index 0000000..64f2242 Binary files /dev/null and b/core/migrations/__pycache__/0004_daftarhadir.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.cpython-311.pyc b/core/migrations/__pycache__/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.cpython-311.pyc new file mode 100644 index 0000000..6bbc985 Binary files /dev/null and b/core/migrations/__pycache__/0005_dosenpengampu_kelas_dosenpengampu_prodi_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_jadwal_is_manual.cpython-311.pyc b/core/migrations/__pycache__/0006_jadwal_is_manual.cpython-311.pyc new file mode 100644 index 0000000..53a3769 Binary files /dev/null and b/core/migrations/__pycache__/0006_jadwal_is_manual.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..d0b818f 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,122 @@ from django.db import models -# Create your models here. +class ProgramStudi(models.Model): + nama = models.CharField(max_length=100) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Program Studi" + +class Dosen(models.Model): + nidn = models.CharField(max_length=20, unique=True) + nama = models.CharField(max_length=100) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Dosen" + +class TahunAkademik(models.Model): + nama = models.CharField(max_length=20) # e.g., 2025/2026 + is_active = models.BooleanField(default=False) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Tahun Akademik" + +class Hari(models.Model): + nama = models.CharField(max_length=20) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Hari" + +class MataKuliah(models.Model): + kode = models.CharField(max_length=20) + nama = models.CharField(max_length=100) + sks = models.IntegerField() + semester = models.IntegerField() + prodi = models.ForeignKey(ProgramStudi, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.kode} - {self.nama}" + + class Meta: + verbose_name_plural = "Mata Kuliah" + unique_together = [['kode', 'prodi'], ['nama', 'prodi']] + +class Ruangan(models.Model): + nama = models.CharField(max_length=50) + kapasitas = models.IntegerField() + prodi = models.ForeignKey(ProgramStudi, on_delete=models.CASCADE, null=True, blank=True) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Ruangan" + +class Kelas(models.Model): + nama = models.CharField(max_length=20) # e.g., A, B, C or 1A, 1B + prodi = models.ForeignKey(ProgramStudi, on_delete=models.CASCADE, null=True, blank=True) + + def __str__(self): + return self.nama + + class Meta: + verbose_name_plural = "Kelas" + +class Jam(models.Model): + range_waktu = models.CharField(max_length=50) # e.g., 07:00 - 08:40 + + def __str__(self): + return self.range_waktu + + class Meta: + verbose_name_plural = "Jam" + +class DosenPengampu(models.Model): + dosen = models.ForeignKey(Dosen, on_delete=models.CASCADE) + matkul = models.ForeignKey(MataKuliah, on_delete=models.CASCADE) + tahun_akademik = models.ForeignKey(TahunAkademik, on_delete=models.CASCADE) + prodi = models.ForeignKey(ProgramStudi, on_delete=models.CASCADE, null=True, blank=True) + kelas = models.ForeignKey(Kelas, on_delete=models.CASCADE, null=True, blank=True) + + def __str__(self): + return f"{self.dosen.nama} - {self.matkul.nama} ({self.kelas.nama if self.kelas else '-'})" + + class Meta: + verbose_name_plural = "Dosen Pengampu" + +class Jadwal(models.Model): + dosen_pengampu = models.ForeignKey(DosenPengampu, on_delete=models.CASCADE) + ruangan = models.ForeignKey(Ruangan, on_delete=models.CASCADE) + hari = models.ForeignKey(Hari, on_delete=models.CASCADE) + jam = models.ForeignKey(Jam, on_delete=models.CASCADE) + kelas = models.ForeignKey(Kelas, on_delete=models.CASCADE) + is_manual = models.BooleanField(default=False) + + def __str__(self): + return f"{self.dosen_pengampu.matkul.nama} - {self.hari.nama} {self.jam.range_waktu}" + + class Meta: + verbose_name_plural = "Jadwal" + +class DaftarHadir(models.Model): + jadwal = models.ForeignKey(Jadwal, on_delete=models.CASCADE) + tanggal = models.DateField() + pertemuan_ke = models.IntegerField(default=1) + keterangan = models.TextField(null=True, blank=True) + + def __str__(self): + return f"Hadir: {self.jadwal} - Tgl: {self.tanggal}" + + class Meta: + verbose_name_plural = "Daftar Hadir" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..384ac75 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,172 @@ - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Sistem Jadwal Perkuliahan{% endblock %} + + + + + + + + + + {% load static %} + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - - + + + + +
+
+ +
+
+

Administrator

+ Super Admin +
+
+ +
+
+
+ +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + + {% block content %}{% endblock %} +
+
+ + + + + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/buat_jadwal.html b/core/templates/core/buat_jadwal.html new file mode 100644 index 0000000..247bb0b --- /dev/null +++ b/core/templates/core/buat_jadwal.html @@ -0,0 +1,31 @@ +{% extends 'base.html' %} + +{% block title %}Buat Jadwal - SJP Admin{% endblock %} +{% block page_title %}Generate Jadwal Otomatis{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+
+

Auto-Generate Jadwal

+

Sistem akan menyusun jadwal perkuliahan secara otomatis berdasarkan data master (Dosen Pengampu, Ruangan, Hari, Jam, dan Kelas) yang telah diinputkan. Sistem akan meminimalkan bentrok jadwal.

+ +
+ Pastikan semua data master telah terisi lengkap sebelum memulai proses generate. +
+ +
+ {% csrf_token %} + +
+
+
+
+{% endblock %} diff --git a/core/templates/core/daftar_hadir_list.html b/core/templates/core/daftar_hadir_list.html new file mode 100644 index 0000000..7db4305 --- /dev/null +++ b/core/templates/core/daftar_hadir_list.html @@ -0,0 +1,175 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Hadir Perkuliahan{% endblock %} +{% block page_title %}Daftar Hadir Perkuliahan{% endblock %} + +{% block content %} +
+
+ +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + + + + {% for hadir in hadir_all %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
TanggalPertemuanJadwal / MatkulDosenKeteranganAksi
+ + {{ hadir.tanggal|date:"d/m/Y" }}Ke-{{ hadir.pertemuan_ke }} +
{{ hadir.jadwal.dosen_pengampu.matkul.nama }}
+ {{ hadir.jadwal.hari.nama }}, {{ hadir.jadwal.jam.range_waktu }} +
{{ hadir.jadwal.dosen_pengampu.dosen.nama }}{{ hadir.keterangan|default:"-" }} + + + + +
Belum ada data kehadiran.
+
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..aba2818 --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,145 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard - SJP Admin{% endblock %} +{% block page_title %}Dashboard Overview{% endblock %} + +{% block content %} +
+ +
+
+
+
+
+ +
+
+

{{ count_prodi }}

+

Program Studi

+
+
+
+
+
+
+
+
+ +
+
+

{{ count_dosen }}

+

Total Dosen

+
+
+
+
+
+
+
+
+ +
+
+

{{ count_matkul }}

+

Mata Kuliah

+
+
+
+
+
+
+
+
+ +
+
+

{{ count_daftar_hadir }}

+

Log Kehadiran

+
+
+
+
+ +
+
+ +
+
+
+
+
Status Sistem
+
+
+
+
+
+
Jadwal Terdaftar
+

{{ count_jadwal }} Entry

+
+
+
+
+
+
Kelas Aktif
+

{{ count_kelas }} Kelas

+
+
+
+
+
+
Ruangan Terdaftar
+

{{ count_ruangan }} Ruangan

+
+
+
+
+

Versi Aplikasi 1.1.0

+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/dosen_list.html b/core/templates/core/dosen_list.html new file mode 100644 index 0000000..9a09628 --- /dev/null +++ b/core/templates/core/dosen_list.html @@ -0,0 +1,162 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Dosen{% endblock %} +{% block page_title %}Daftar Dosen{% endblock %} + +{% block content %} +
+
+ + + + Template + +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + {% for dosen in dosen_all %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NIDNNama DosenAksi
+ + {{ dosen.nidn }}{{ dosen.nama }} + + + + +
Belum ada data dosen.
+
+
+
+ + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/hari_list.html b/core/templates/core/hari_list.html new file mode 100644 index 0000000..484fcdd --- /dev/null +++ b/core/templates/core/hari_list.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}Daftar Hari{% endblock %} +{% block page_title %}Daftar Hari{% endblock %} +{% block content %} +
+ +
{% csrf_token %}
+
+
+{% for h in hari_all %} + + + + + +{% endfor %} +
Nama HariAksi
{{ h.nama }} + + +
+ + +{% endblock %} +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/jadwal_list.html b/core/templates/core/jadwal_list.html new file mode 100644 index 0000000..f0afe3b --- /dev/null +++ b/core/templates/core/jadwal_list.html @@ -0,0 +1,257 @@ +{% extends 'base.html' %} + +{% block title %}Hasil Jadwal Perkuliahan{% endblock %} +{% block page_title %}Hasil Jadwal Perkuliahan{% endblock %} + +{% block content %} +
+
+ + Generate Ulang + + + + Export Excel (.xlsx) + +
+
+ +
+
+
+ {% csrf_token %} + +
+ + + + + + + + + + + + + + + + + {% for jadwal in jadwal_all %} + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Hari & JamKode MKMata KuliahDosenProdiSem.KelasRuanganAksi
+
{{ jadwal.hari.nama }}
+ {{ jadwal.jam.range_waktu }} +
{{ jadwal.dosen_pengampu.matkul.kode }} +
{{ jadwal.dosen_pengampu.matkul.nama }}
+ {% if jadwal.is_manual %} + Manual + {% endif %} +
{{ jadwal.dosen_pengampu.dosen.nama }}{{ jadwal.dosen_pengampu.matkul.prodi.nama }}{{ jadwal.dosen_pengampu.matkul.semester }}{{ jadwal.kelas.nama }}{{ jadwal.ruangan.nama }} +
+ + + + +
+
Belum ada jadwal yang digenerate.
+
+ {% if jadwal_all %} +
+ +
+ {% endif %} +
+
+
+ + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/jam_list.html b/core/templates/core/jam_list.html new file mode 100644 index 0000000..c7e07bd --- /dev/null +++ b/core/templates/core/jam_list.html @@ -0,0 +1,168 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Waktu / Jam{% endblock %} +{% block page_title %}Daftar Waktu / Jam{% endblock %} + +{% block content %} +
+
+ + +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + {% for jam in jam_all %} + + + + + + {% empty %} + + + + {% endfor %} + +
Range WaktuAksi
+ + {{ jam.range_waktu }} + + + + +
Belum ada data jam.
+
+
+
+ + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/kelas_list.html b/core/templates/core/kelas_list.html new file mode 100644 index 0000000..ed273f3 --- /dev/null +++ b/core/templates/core/kelas_list.html @@ -0,0 +1,170 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Kelas{% endblock %} +{% block page_title %}Daftar Kelas{% endblock %} + +{% block content %} +
+
+ + + + Template + +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + {% for kelas in kelas_all %} + + + + + + + {% empty %} + + + + {% endfor %} + +
Nama KelasProgram StudiAksi
+ + {{ kelas.nama }}{{ kelas.prodi.nama|default:"-" }} + + + + +
Belum ada data kelas.
+
+
+
+ + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/matkul_list.html b/core/templates/core/matkul_list.html new file mode 100644 index 0000000..02e8260 --- /dev/null +++ b/core/templates/core/matkul_list.html @@ -0,0 +1,211 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Mata Kuliah{% endblock %} +{% block page_title %}Daftar Mata Kuliah{% endblock %} + +{% block content %} +
+
+ + + + Template + +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + + + + {% for matkul in matkul_all %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
KodeNama MatkulSKSSemesterProdiAksi
+ + {{ matkul.kode }}{{ matkul.nama }}{{ matkul.sks }}{{ matkul.semester }}{{ matkul.prodi.nama }} + + + + +
Belum ada data mata kuliah.
+
+
+
+ + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/pengampu_list.html b/core/templates/core/pengampu_list.html new file mode 100644 index 0000000..bd55388 --- /dev/null +++ b/core/templates/core/pengampu_list.html @@ -0,0 +1,267 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Dosen Pengampu{% endblock %} +{% block page_title %}Daftar Dosen Pengampu{% endblock %} + +{% block content %} +
+
+ +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + + + + {% for pengampu in pengampu_all %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
DosenProgram StudiMata KuliahKelasTahun AkademikAksi
+ + +
{{ pengampu.dosen.nama }}
+ NIDN: {{ pengampu.dosen.nidn }} +
{{ pengampu.prodi.nama|default:"-" }} +
{{ pengampu.matkul.nama }}
+ {{ pengampu.matkul.kode }} ({{ pengampu.matkul.sks }} SKS) +
{{ pengampu.kelas.nama|default:"-" }}{{ pengampu.tahun_akademik.nama }} + + + + +
Belum ada data dosen pengampu.
+
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/prodi_list.html b/core/templates/core/prodi_list.html new file mode 100644 index 0000000..58041c5 --- /dev/null +++ b/core/templates/core/prodi_list.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}Program Studi{% endblock %} +{% block page_title %}Program Studi{% endblock %} +{% block content %} +
+ +
{% csrf_token %}
+
+
+{% for p in prodi_all %} + + + + + +{% endfor %} +
Nama Program StudiAksi
{{ p.nama }} + + +
+ + +{% endblock %} +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/ruangan_list.html b/core/templates/core/ruangan_list.html new file mode 100644 index 0000000..1c4c27a --- /dev/null +++ b/core/templates/core/ruangan_list.html @@ -0,0 +1,182 @@ +{% extends 'base.html' %} + +{% block title %}Daftar Ruangan{% endblock %} +{% block page_title %}Daftar Ruangan{% endblock %} + +{% block content %} +
+
+ + + + Template + +
+
+ {% csrf_token %} + +
+
+ +
+
+
+ + + + + + + + + + + + {% for ruangan in ruangan_all %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Nama RuanganKapasitasProgram StudiAksi
+ + {{ ruangan.nama }}{{ ruangan.kapasitas }} Kursi{{ ruangan.prodi.nama|default:"-" }} + + + + +
Belum ada data ruangan.
+
+
+
+ + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/core/templates/core/tahun_list.html b/core/templates/core/tahun_list.html new file mode 100644 index 0000000..7cab57f --- /dev/null +++ b/core/templates/core/tahun_list.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% block title %}Tahun Akademik{% endblock %} +{% block page_title %}Tahun Akademik{% endblock %} +{% block content %} +
+ +
{% csrf_token %}
+
+
+{% for t in tahun_all %} + + + + + + +{% endfor %} +
Tahun AkademikStatusAksi
{{ t.nama }}{% if t.is_active %}Aktif{% else %}Non-Aktif{% endif %} + + +
+ + +{% endblock %} +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..9aec7cc 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,54 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.dashboard, name='dashboard'), + + # AJAX Helpers + path('ajax/get-kelas/', views.get_kelas_by_prodi, name='get_kelas_by_prodi'), + path('ajax/get-matkul/', views.get_matkul_by_prodi, name='get_matkul_by_prodi'), + + path('prodi/', views.prodi_list, name='prodi_list'), + path('prodi/delete//', views.prodi_delete, name='prodi_delete'), + + path('dosen/', views.dosen_list, name='dosen_list'), + path('dosen/delete//', views.dosen_delete, name='dosen_delete'), + path('dosen/template/', views.dosen_template, name='dosen_template'), + path('dosen/import/', views.dosen_import, name='dosen_import'), + + path('matkul/', views.matkul_list, name='matkul_list'), + path('matkul/delete//', views.matkul_delete, name='matkul_delete'), + path('matkul/template/', views.matkul_template, name='matkul_template'), + path('matkul/import/', views.matkul_import, name='matkul_import'), + + path('ruangan/', views.ruangan_list, name='ruangan_list'), + path('ruangan/delete//', views.ruangan_delete, name='ruangan_delete'), + path('ruangan/template/', views.ruangan_template, name='ruangan_template'), + path('ruangan/import/', views.ruangan_import, name='ruangan_import'), + + path('kelas/', views.kelas_list, name='kelas_list'), + path('kelas/delete//', views.kelas_delete, name='kelas_delete'), + path('kelas/template/', views.kelas_template, name='kelas_template'), + path('kelas/import/', views.kelas_import, name='kelas_import'), + + path('jam/', views.jam_list, name='jam_list'), + path('jam/delete//', views.jam_delete, name='jam_delete'), + path('jam/generate/', views.jam_generate, name='jam_generate'), + + path('tahun/', views.tahun_list, name='tahun_list'), + path('tahun/delete//', views.tahun_delete, name='tahun_delete'), + + path('hari/', views.hari_list, name='hari_list'), + path('hari/delete//', views.hari_delete, name='hari_delete'), + + path('pengampu/', views.pengampu_list, name='pengampu_list'), + path('pengampu/delete//', views.pengampu_delete, name='pengampu_delete'), + + path('daftar-hadir/', views.daftar_hadir_list, name='daftar_hadir_list'), + path('daftar-hadir/delete//', views.daftar_hadir_delete, name='daftar_hadir_delete'), + + path('jadwal/', views.jadwal_list, name='jadwal_list'), + path('jadwal/buat/', views.buat_jadwal, name='buat_jadwal'), + path('jadwal/delete//', views.jadwal_delete, name='jadwal_delete'), + path('jadwal/export/', views.jadwal_export_excel, name='jadwal_export_excel'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..9a3c43f 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,651 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - - -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() +import csv +import datetime +import random +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpResponse, JsonResponse +from django.contrib import messages +from django.db import IntegrityError +from openpyxl import Workbook +from .models import ( + ProgramStudi, Dosen, TahunAkademik, Hari, + MataKuliah, Ruangan, Kelas, Jam, DosenPengampu, Jadwal, DaftarHadir +) +def dashboard(request): context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "count_prodi": ProgramStudi.objects.count(), + "count_dosen": Dosen.objects.count(), + "count_matkul": MataKuliah.objects.count(), + "count_ruangan": Ruangan.objects.count(), + "count_kelas": Kelas.objects.count(), + "count_jadwal": Jadwal.objects.count(), + "count_daftar_hadir": DaftarHadir.objects.count(), } - return render(request, "core/index.html", context) + return render(request, "core/dashboard.html", context) + +# --- AJAX HELPERS --- +def get_kelas_by_prodi(request): + prodi_id = request.GET.get('prodi_id') + kelas = Kelas.objects.filter(prodi_id=prodi_id).values('id', 'nama') + return JsonResponse(list(kelas), safe=False) + +def get_matkul_by_prodi(request): + prodi_id = request.GET.get('prodi_id') + matkul = MataKuliah.objects.filter(prodi_id=prodi_id).values('id', 'kode', 'nama') + return JsonResponse(list(matkul), safe=False) + +# --- PRODI --- +def prodi_list(request): + prodi_all = ProgramStudi.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + ProgramStudi.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Program Studi.") + else: + pk = request.POST.get("pk") + nama = request.POST.get("nama") + if pk: + obj = get_object_or_404(ProgramStudi, pk=pk) + obj.nama = nama + obj.save() + messages.success(request, "Program Studi berhasil diupdate.") + else: + ProgramStudi.objects.create(nama=nama) + messages.success(request, "Program Studi berhasil ditambahkan.") + return redirect("prodi_list") + return render(request, "core/prodi_list.html", {"prodi_all": prodi_all}) + +def prodi_delete(request, pk): + get_object_or_404(ProgramStudi, pk=pk).delete() + messages.success(request, "Program Studi berhasil dihapus.") + return redirect("prodi_list") + +# --- DOSEN --- +def dosen_list(request): + dosen_all = Dosen.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Dosen.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Dosen.") + else: + pk = request.POST.get("pk") + nidn = request.POST.get("nidn") + nama = request.POST.get("nama") + if pk: + obj = get_object_or_404(Dosen, pk=pk) + obj.nidn = nidn + obj.nama = nama + obj.save() + messages.success(request, "Dosen berhasil diupdate.") + else: + Dosen.objects.create(nidn=nidn, nama=nama) + messages.success(request, "Dosen berhasil ditambahkan.") + return redirect("dosen_list") + return render(request, "core/dosen_list.html", {"dosen_all": dosen_all}) + +def dosen_delete(request, pk): + get_object_or_404(Dosen, pk=pk).delete() + messages.success(request, "Dosen berhasil dihapus.") + return redirect("dosen_list") + +def dosen_template(request): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="template_dosen.csv"' + writer = csv.writer(response) + writer.writerow(['nidn', 'nama']) + writer.writerow(['12345678', 'Dr. Nama Dosen, M.Kom']) + return response + +def dosen_import(request): + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + decoded_file = csv_file.read().decode('utf-8').splitlines() + reader = csv.DictReader(decoded_file) + count = 0 + for row in reader: + Dosen.objects.update_or_create( + nidn=row['nidn'], + defaults={'nama': row['nama']} + ) + count += 1 + messages.success(request, f"Berhasil mengimport {count} data Dosen.") + return redirect('dosen_list') + +# --- MATA KULIAH --- +def matkul_list(request): + matkul_all = MataKuliah.objects.all() + prodi_all = ProgramStudi.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + MataKuliah.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Mata Kuliah.") + else: + pk = request.POST.get("pk") + kode = request.POST.get("kode") + nama = request.POST.get("nama") + sks = request.POST.get("sks") + semester = request.POST.get("semester") + prodi_id = request.POST.get("prodi") + prodi = get_object_or_404(ProgramStudi, id=prodi_id) + try: + if pk: + obj = get_object_or_404(MataKuliah, pk=pk) + obj.kode = kode + obj.nama = nama + obj.sks = sks + obj.semester = semester + obj.prodi = prodi + obj.save() + messages.success(request, "Mata Kuliah berhasil diupdate.") + else: + MataKuliah.objects.create(kode=kode, nama=nama, sks=sks, semester=semester, prodi=prodi) + messages.success(request, "Mata Kuliah berhasil ditambahkan.") + except IntegrityError: + messages.error(request, "Gagal menyimpan. Kode atau Nama Mata Kuliah sudah ada di Program Studi ini.") + return redirect("matkul_list") + return render(request, "core/matkul_list.html", {"matkul_all": matkul_all, "prodi_all": prodi_all}) + +def matkul_delete(request, pk): + get_object_or_404(MataKuliah, pk=pk).delete() + messages.success(request, "Mata Kuliah berhasil dihapus.") + return redirect("matkul_list") + +def matkul_template(request): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="template_matkul.csv"' + writer = csv.writer(response) + writer.writerow(['kode', 'nama', 'sks', 'semester', 'program_studi']) + writer.writerow(['MK001', 'Algoritma Pemrograman', '3', '1', 'Teknik Informatika']) + return response + +def matkul_import(request): + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + decoded_file = csv_file.read().decode('utf-8').splitlines() + reader = csv.DictReader(decoded_file) + count = 0 + for row in reader: + prodi_nama = row.get('program_studi') + prodi, _ = ProgramStudi.objects.get_or_create(nama=prodi_nama) + MataKuliah.objects.update_or_create( + kode=row['kode'], + prodi=prodi, + defaults={ + 'nama': row['nama'], + 'sks': row['sks'], + 'semester': row['semester'] + } + ) + count += 1 + messages.success(request, f"Berhasil mengimport {count} data Mata Kuliah.") + return redirect('matkul_list') + +# --- RUANGAN --- +def ruangan_list(request): + ruangan_all = Ruangan.objects.all() + prodi_all = ProgramStudi.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Ruangan.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Ruangan.") + else: + pk = request.POST.get("pk") + nama = request.POST.get("nama") + kapasitas = request.POST.get("kapasitas") + prodi_id = request.POST.get("prodi") + prodi = get_object_or_404(ProgramStudi, id=prodi_id) if prodi_id else None + if pk: + obj = get_object_or_404(Ruangan, pk=pk) + obj.nama = nama + obj.kapasitas = kapasitas + obj.prodi = prodi + obj.save() + messages.success(request, "Ruangan berhasil diupdate.") + else: + Ruangan.objects.create(nama=nama, kapasitas=kapasitas, prodi=prodi) + messages.success(request, "Ruangan berhasil ditambahkan.") + return redirect("ruangan_list") + return render(request, "core/ruangan_list.html", {"ruangan_all": ruangan_all, "prodi_all": prodi_all}) + +def ruangan_delete(request, pk): + get_object_or_404(Ruangan, pk=pk).delete() + messages.success(request, "Ruangan berhasil dihapus.") + return redirect("ruangan_list") + +def ruangan_template(request): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="template_ruangan.csv"' + writer = csv.writer(response) + writer.writerow(['nama', 'kapasitas', 'program_studi']) + writer.writerow(['Ruang A1', '40', 'Teknik Informatika']) + return response + +def ruangan_import(request): + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + decoded_file = csv_file.read().decode('utf-8').splitlines() + reader = csv.DictReader(decoded_file) + count = 0 + for row in reader: + prodi_nama = row.get('program_studi') + prodi = None + if prodi_nama: + prodi, _ = ProgramStudi.objects.get_or_create(nama=prodi_nama) + Ruangan.objects.create(nama=row['nama'], kapasitas=row['kapasitas'], prodi=prodi) + count += 1 + messages.success(request, f"Berhasil mengimport {count} data Ruangan.") + return redirect('ruangan_list') + +# --- KELAS --- +def kelas_list(request): + kelas_all = Kelas.objects.all() + prodi_all = ProgramStudi.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Kelas.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Kelas.") + else: + pk = request.POST.get("pk") + nama = request.POST.get("nama") + prodi_id = request.POST.get("prodi") + prodi = get_object_or_404(ProgramStudi, id=prodi_id) if prodi_id else None + if pk: + obj = get_object_or_404(Kelas, pk=pk) + obj.nama = nama + obj.prodi = prodi + obj.save() + messages.success(request, "Kelas berhasil diupdate.") + else: + Kelas.objects.create(nama=nama, prodi=prodi) + messages.success(request, "Kelas berhasil ditambahkan.") + return redirect("kelas_list") + return render(request, "core/kelas_list.html", {"kelas_all": kelas_all, "prodi_all": prodi_all}) + +def kelas_delete(request, pk): + get_object_or_404(Kelas, pk=pk).delete() + messages.success(request, "Kelas berhasil dihapus.") + return redirect("kelas_list") + +def kelas_template(request): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="template_kelas.csv"' + writer = csv.writer(response) + writer.writerow(['nama', 'program_studi']) + writer.writerow(['Kelas A', 'Teknik Informatika']) + return response + +def kelas_import(request): + if request.method == 'POST' and request.FILES.get('csv_file'): + csv_file = request.FILES['csv_file'] + decoded_file = csv_file.read().decode('utf-8').splitlines() + reader = csv.DictReader(decoded_file) + count = 0 + for row in reader: + prodi_nama = row.get('program_studi') + prodi = None + if prodi_nama: + prodi, _ = ProgramStudi.objects.get_or_create(nama=prodi_nama) + Kelas.objects.create(nama=row['nama'], prodi=prodi) + count += 1 + messages.success(request, f"Berhasil mengimport {count} data Kelas.") + return redirect('kelas_list') + +# --- JAM --- +def jam_list(request): + jam_all = Jam.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Jam.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Jam.") + else: + pk = request.POST.get("pk") + range_waktu = request.POST.get("range_waktu") + if pk: + obj = get_object_or_404(Jam, pk=pk) + obj.range_waktu = range_waktu + obj.save() + messages.success(request, "Jam berhasil diupdate.") + else: + Jam.objects.create(range_waktu=range_waktu) + messages.success(request, "Jam berhasil ditambahkan.") + return redirect("jam_list") + return render(request, "core/jam_list.html", {"jam_all": jam_all}) + +def jam_delete(request, pk): + get_object_or_404(Jam, pk=pk).delete() + messages.success(request, "Jam berhasil dihapus.") + return redirect("jam_list") + +def jam_generate(request): + if request.method == "POST": + start_str = request.POST.get("jam_mulai") # e.g. "07:00" + end_str = request.POST.get("jam_selesai") # e.g. "17:00" + mins_per_sks = int(request.POST.get("durasi_per_sks", 50)) + sks_per_slot = int(request.POST.get("jumlah_sks", 2)) + jeda_mins = int(request.POST.get("jeda", 0)) + + start_time = datetime.datetime.strptime(start_str, "%H:%M") + end_time = datetime.datetime.strptime(end_str, "%H:%M") + + slot_duration = mins_per_sks * sks_per_slot + current_time = start_time + + count = 0 + while current_time + datetime.timedelta(minutes=slot_duration) <= end_time: + slot_end = current_time + datetime.timedelta(minutes=slot_duration) + range_waktu = f"{current_time.strftime('%H:%M')} - {slot_end.strftime('%H:%M')}" + Jam.objects.get_or_create(range_waktu=range_waktu) + current_time = slot_end + datetime.timedelta(minutes=jeda_mins) + count += 1 + + messages.success(request, f"Berhasil menggenerate {count} slot waktu.") + return redirect("jam_list") + +# --- TAHUN AKADEMIK --- +def tahun_list(request): + tahun_all = TahunAkademik.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + TahunAkademik.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Tahun Akademik.") + else: + pk = request.POST.get("pk") + nama = request.POST.get("nama") + is_active = request.POST.get("is_active") == "on" + if pk: + obj = get_object_or_404(TahunAkademik, pk=pk) + if is_active: + TahunAkademik.objects.update(is_active=False) + obj.nama = nama + obj.is_active = is_active + obj.save() + messages.success(request, "Tahun Akademik berhasil diupdate.") + else: + if is_active: + TahunAkademik.objects.update(is_active=False) + TahunAkademik.objects.create(nama=nama, is_active=is_active) + messages.success(request, "Tahun Akademik berhasil ditambahkan.") + return redirect("tahun_list") + return render(request, "core/tahun_list.html", {"tahun_all": tahun_all}) + +def tahun_delete(request, pk): + get_object_or_404(TahunAkademik, pk=pk).delete() + messages.success(request, "Tahun Akademik berhasil dihapus.") + return redirect("tahun_list") + +# --- HARI --- +def hari_list(request): + hari_all = Hari.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Hari.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Hari.") + else: + pk = request.POST.get("pk") + nama = request.POST.get("nama") + if pk: + obj = get_object_or_404(Hari, pk=pk) + obj.nama = nama + obj.save() + messages.success(request, "Hari berhasil diupdate.") + else: + Hari.objects.create(nama=nama) + messages.success(request, "Hari berhasil ditambahkan.") + return redirect("hari_list") + return render(request, "core/hari_list.html", {"hari_all": hari_all}) + +def hari_delete(request, pk): + get_object_or_404(Hari, pk=pk).delete() + messages.success(request, "Hari berhasil dihapus.") + return redirect("hari_list") + +# --- DOSEN PENGAMPU --- +def pengampu_list(request): + pengampu_all = DosenPengampu.objects.all() + dosen_all = Dosen.objects.all() + matkul_all = MataKuliah.objects.all() + tahun_all = TahunAkademik.objects.all() + prodi_all = ProgramStudi.objects.all() + kelas_all = Kelas.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + DosenPengampu.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Dosen Pengampu.") + else: + pk = request.POST.get("pk") + dosen_id = request.POST.get("dosen") + matkul_id = request.POST.get("matkul") + tahun_id = request.POST.get("tahun") + prodi_id = request.POST.get("prodi") + kelas_id = request.POST.get("kelas") + + dosen = get_object_or_404(Dosen, id=dosen_id) + matkul = get_object_or_404(MataKuliah, id=matkul_id) + tahun = get_object_or_404(TahunAkademik, id=tahun_id) + prodi = get_object_or_404(ProgramStudi, id=prodi_id) if prodi_id else None + kelas = get_object_or_404(Kelas, id=kelas_id) if kelas_id else None + + if pk: + obj = get_object_or_404(DosenPengampu, pk=pk) + obj.dosen = dosen + obj.matkul = matkul + obj.tahun_akademik = tahun + obj.prodi = prodi + obj.kelas = kelas + obj.save() + messages.success(request, "Dosen Pengampu berhasil diupdate.") + else: + DosenPengampu.objects.create( + dosen=dosen, matkul=matkul, tahun_akademik=tahun, + prodi=prodi, kelas=kelas + ) + messages.success(request, "Dosen Pengampu berhasil ditambahkan.") + return redirect("pengampu_list") + return render(request, "core/pengampu_list.html", { + "pengampu_all": pengampu_all, + "dosen_all": dosen_all, + "matkul_all": matkul_all, + "tahun_all": tahun_all, + "prodi_all": prodi_all, + "kelas_all": kelas_all, + }) + +def pengampu_delete(request, pk): + get_object_or_404(DosenPengampu, pk=pk).delete() + messages.success(request, "Dosen Pengampu berhasil dihapus.") + return redirect("pengampu_list") + +# --- DAFTAR HADIR --- +def daftar_hadir_list(request): + hadir_all = DaftarHadir.objects.all() + jadwal_all = Jadwal.objects.all() + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + DaftarHadir.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Daftar Hadir.") + else: + pk = request.POST.get("pk") + jadwal_id = request.POST.get("jadwal") + tanggal = request.POST.get("tanggal") + pertemuan = request.POST.get("pertemuan_ke") + ket = request.POST.get("keterangan") + jadwal = get_object_or_404(Jadwal, id=jadwal_id) + if pk: + obj = get_object_or_404(DaftarHadir, pk=pk) + obj.jadwal = jadwal + obj.tanggal = tanggal + obj.pertemuan_ke = pertemuan + obj.keterangan = ket + obj.save() + messages.success(request, "Daftar Hadir berhasil diupdate.") + else: + DaftarHadir.objects.create(jadwal=jadwal, tanggal=tanggal, pertemuan_ke=pertemuan, keterangan=ket) + messages.success(request, "Daftar Hadir berhasil ditambahkan.") + return redirect("daftar_hadir_list") + return render(request, "core/daftar_hadir_list.html", {"hadir_all": hadir_all, "jadwal_all": jadwal_all}) + +def daftar_hadir_delete(request, pk): + get_object_or_404(DaftarHadir, pk=pk).delete() + messages.success(request, "Daftar Hadir berhasil dihapus.") + return redirect("daftar_hadir_list") + +# --- JADWAL --- +def jadwal_list(request): + jadwal_all = Jadwal.objects.all().order_by('hari', 'jam') + pengampu_all = DosenPengampu.objects.all() + ruangan_all = Ruangan.objects.all() + hari_all = Hari.objects.all() + jam_all = Jam.objects.all() + kelas_all = Kelas.objects.all() + + if request.method == "POST": + if "delete_massal" in request.POST: + ids = request.POST.getlist("selected_ids") + Jadwal.objects.filter(id__in=ids).delete() + messages.success(request, f"Berhasil menghapus {len(ids)} Jadwal.") + else: + pk = request.POST.get("pk") + pengampu_id = request.POST.get("pengampu") + ruangan_id = request.POST.get("ruangan") + hari_id = request.POST.get("hari") + jam_id = request.POST.get("jam") + kelas_id = request.POST.get("kelas") + + pengampu = get_object_or_404(DosenPengampu, id=pengampu_id) + ruangan = get_object_or_404(Ruangan, id=ruangan_id) + hari = get_object_or_404(Hari, id=hari_id) + jam = get_object_or_404(Jam, id=jam_id) + kelas = get_object_or_404(Kelas, id=kelas_id) + + if pk: + obj = get_object_or_404(Jadwal, pk=pk) + obj.dosen_pengampu = pengampu + obj.ruangan = ruangan + obj.hari = hari + obj.jam = jam + obj.kelas = kelas + obj.save() + messages.success(request, "Jadwal berhasil diupdate.") + else: + Jadwal.objects.create( + dosen_pengampu=pengampu, ruangan=ruangan, + hari=hari, jam=jam, kelas=kelas, is_manual=True + ) + messages.success(request, "Jadwal berhasil ditambahkan secara manual.") + return redirect("jadwal_list") + + return render(request, "core/jadwal_list.html", { + "jadwal_all": jadwal_all, + "pengampu_all": pengampu_all, + "ruangan_all": ruangan_all, + "hari_all": hari_all, + "jam_all": jam_all, + "kelas_all": kelas_all, + }) + +def buat_jadwal(request): + if request.method == "POST": + pengampu_all = DosenPengampu.objects.all() + ruangan_all = Ruangan.objects.all() + hari_all = Hari.objects.all() + jam_all = Jam.objects.all() + + if not (pengampu_all and ruangan_all and hari_all and jam_all): + messages.error(request, "Data Master belum lengkap.") + return redirect("buat_jadwal") + + # Only delete schedules that were not manually created + Jadwal.objects.filter(is_manual=False).delete() + + # Get pengampu that don't have a schedule yet (including manual ones) + pengampu_with_jadwal = Jadwal.objects.values_list('dosen_pengampu_id', flat=True) + pending_pengampu = pengampu_all.exclude(id__in=pengampu_with_jadwal) + + for pengampu in pending_pengampu: + # We use the class assigned in DosenPengampu + kelas = pengampu.kelas + if not kelas: + # Fallback to random class if not assigned in Pengampu (for legacy data) + kelas_all = Kelas.objects.all() + kelas = random.choice(kelas_all) if kelas_all.exists() else None + + if not kelas: continue + + # Simple logic to find an available slot (avoiding collisions for manual ones) + # This is still a basic random generator but it now respects existing manual entries + max_tries = 50 + for _ in range(max_tries): + h = random.choice(hari_all) + j = random.choice(jam_all) + r = random.choice(ruangan_all) + + # Check for collisions in that room/time + if not Jadwal.objects.filter(hari=h, jam=j, ruangan=r).exists() and \ + not Jadwal.objects.filter(hari=h, jam=j, kelas=kelas).exists() and \ + not Jadwal.objects.filter(hari=h, jam=j, dosen_pengampu__dosen=pengampu.dosen).exists(): + + Jadwal.objects.create( + dosen_pengampu=pengampu, + ruangan=r, + hari=h, + jam=j, + kelas=kelas, + is_manual=False + ) + break + + messages.success(request, "Jadwal berhasil digenerate otomatis (menghindari jadwal manual).") + return redirect("jadwal_list") + return render(request, "core/buat_jadwal.html") + +def jadwal_delete(request, pk): + get_object_or_404(Jadwal, pk=pk).delete() + messages.success(request, "Jadwal berhasil dihapus.") + return redirect("jadwal_list") + +def jadwal_export_excel(request): + wb = Workbook() + ws = wb.active + ws.title = "Jadwal Kuliah" + + headers = [ + 'No', 'Hari', 'Jam', 'Kode MK', 'Mata Kuliah', 'SKS', 'Semester', + 'Dosen', 'Kelas', 'Ruangan', 'Program Studi', 'Tahun Akademik' + ] + ws.append(headers) + + jadwal_all = Jadwal.objects.all().select_related( + 'hari', 'jam', 'dosen_pengampu__matkul', 'dosen_pengampu__dosen', + 'kelas', 'ruangan', 'dosen_pengampu__matkul__prodi', 'dosen_pengampu__tahun_akademik' + ).order_by('hari', 'jam') + + for i, j in enumerate(jadwal_all, 1): + ws.append([ + i, + j.hari.nama, + j.jam.range_waktu, + j.dosen_pengampu.matkul.kode, + j.dosen_pengampu.matkul.nama, + j.dosen_pengampu.matkul.sks, + j.dosen_pengampu.matkul.semester, + j.dosen_pengampu.dosen.nama, + j.kelas.nama, + j.ruangan.nama, + j.dosen_pengampu.matkul.prodi.nama, + j.dosen_pengampu.tahun_akademik.nama + ]) + + response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + response['Content-Disposition'] = 'attachment; filename="jadwal_kuliah.xlsx"' + wb.save(response) + return response diff --git a/requirements.txt b/requirements.txt index e22994c..70ec869 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +openpyxl==3.1.5 diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..78856cd 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,152 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@400;600;700&display=swap'); + +:root { + --primary-color: #00B4D8; + --secondary-color: #FFB703; + --dark-bg: #212529; + --light-bg: #F8F9FA; + --sidebar-width: 280px; + --text-primary: #333; + --text-muted: #6c757d; } + +body { + font-family: 'Inter', sans-serif; + background-color: var(--light-bg); + color: var(--text-primary); + overflow-x: hidden; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Outfit', sans-serif; + font-weight: 600; +} + +/* Sidebar */ +#sidebar { + width: var(--sidebar-width); + height: 100vh; + position: fixed; + top: 0; + left: 0; + background-color: var(--dark-bg); + color: white; + z-index: 1000; + transition: all 0.3s; + padding-top: 20px; +} + +#sidebar .nav-link { + color: rgba(255,255,255,0.7); + padding: 12px 25px; + display: flex; + align-items: center; + border-radius: 0; + transition: 0.2s; +} + +#sidebar .nav-link i { + margin-right: 15px; + font-size: 1.1rem; +} + +#sidebar .nav-link:hover, #sidebar .nav-link.active { + color: white; + background-color: rgba(255,255,255,0.1); + border-left: 4px solid var(--primary-color); +} + +#sidebar .sidebar-header { + padding: 20px 25px; + margin-bottom: 20px; +} + +/* Main Content */ +#main-content { + margin-left: var(--sidebar-width); + padding: 30px; + min-height: 100vh; +} + +.top-navbar { + background: white; + padding: 15px 30px; + border-radius: 15px; + margin-bottom: 30px; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 6px rgba(0,0,0,0.02); +} + +/* Cards */ +.card { + border: none; + border-radius: 15px; + box-shadow: 0 4px 15px rgba(0,0,0,0.05); + transition: transform 0.3s; +} + +.card:hover { + transform: translateY(-5px); +} + +.card-stat { + padding: 25px; +} + +.card-stat i { + font-size: 2.5rem; + color: var(--primary-color); + margin-bottom: 15px; +} + +/* Buttons */ +.btn-primary { + background-color: var(--primary-color); + border: none; + border-radius: 10px; + padding: 10px 20px; + font-weight: 500; +} + +.btn-primary:hover { + background-color: #0096B4; +} + +/* Tables */ +.table { + background: white; + border-radius: 15px; + overflow: hidden; +} + +.table thead th { + background-color: #f1f3f5; + border: none; + padding: 15px; + font-weight: 600; +} + +.table tbody td { + padding: 15px; + vertical-align: middle; +} + +/* Profile Icon */ +.profile-section { + display: flex; + align-items: center; +} + +.profile-img { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + margin-right: 10px; +} \ No newline at end of file