Aplikasi manajemen tugas lahir dari satu pertanyaan sederhana: siapa mengerjakan apa, sampai kapan, dan sudah sampai mana. Pada bagian ini kita menurunkan pertanyaan itu menjadi skema tugas_db — empat tabel yang merekam orang, proyek, pekerjaan, dan jejak perubahan statusnya. Desainnya menuntut keputusan yang lebih halus dari proyek sebelumnya: satu tabel bisa dirujuk lewat dua foreign key berbeda, beberapa kolom sengaja dibiarkan NULL, dan satu tabel murni berperan sebagai buku harian.

Menganalisis Kebutuhan Aplikasi Manajemen Tugas #

Kita mulai dari perilaku tim yang memakai aplikasi papan tugas. Anggota tim melakukan lima hal berikut setiap minggu:

  • melihat daftar tugas per proyek beserta prioritas dan tenggatnya;
  • mengambil tugas yang belum dikerjakan siapa pun;
  • mengubah status tugas dari baru menjadi berjalan, lalu selesai;
  • menelusuri kapan dan mengapa sebuah status berubah — misalnya tugas yang tertunda;
  • mengetahui beban tiap anggota: berapa tugas dipikul dan berapa yang sudah tuntas.

Dari daftar tersebut muncul empat kata benda yang berulang: pengguna, proyek, tugas, dan perubahan status (kita sebut riwayat). Kata kerjanya memetakan relasi: proyek dimiliki satu pengguna, tugas dimuat satu proyek dan dikerjakan satu pengguna (atau belum siapa pun), dan tiap perubahan status menempel pada satu tugas.

EntitasAtribut kunciRelasi
penggunanama, email1 ke banyak dengan proyek dan tugas
proyeknama, pemilikmengacu pengguna lewat pemilik_id
tugasjudul, prioritas, tenggat, statusmengacu proyek dan pengguna (opsional)
riwayat_statusstatus lama, status baru, waktumengacu tugas, murni catatan

Entitas dan Relasi #

Diagram berikut merangkum struktur lengkapnya. Perhatikan bahwa pengguna menjadi induk bagi dua relasi berbeda sekaligus — pemilik proyek dan assignee tugas — lewat dua kolom foreign key yang terpisah:

Tiga keputusan desain layak ditegaskan sebelum menulis CREATE TABLE. Pertama, kolom assignee_id dibiarkan NULL-able: tugas yang baru dibuat memang belum diambil siapa pun, dan nilai NULL adalah cara relational yang jujur menyatakan “belum ada” — bukan angka 0 atau teks kosong yang bisa disalahartikan sebagai pengguna bernama kosong. Kedua, status dan prioritas memakai ENUM: daftar nilainya tertutup dan salah ketik seperti 'selasai' akan ditolak server, bukan tersimpan diam-diam. Ketiga, riwayat_status adalah tabel yang hanya menerima tambahan baris — aplikasi tidak pernah memperbarui atau menghapus riwayat, sehingga tabel ini menjadi sumber kebenaran historis.

Membangun Database dan Tabelnya #

Skema lengkap berikut adalah isi berkas database/skema.sql. Empat foreign key semuanya bernama eksplisit agar pesan galat mudah dibaca, dan kolom waktu memakai DATETIME karena perubahan status perlu dicatat sampai detik:

sql
DROP DATABASE IF EXISTS tugas_db;
CREATE DATABASE tugas_db;
USE tugas_db;

CREATE TABLE pengguna (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  email VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE proyek (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  pemilik_id INT NOT NULL,
  CONSTRAINT fk_proyek_pemilik
    FOREIGN KEY (pemilik_id) REFERENCES pengguna (id)
);

CREATE TABLE tugas (
  id INT AUTO_INCREMENT PRIMARY KEY,
  judul VARCHAR(80) NOT NULL,
  proyek_id INT NOT NULL,
  assignee_id INT NULL,
  prioritas ENUM('rendah', 'sedang', 'tinggi') NOT NULL,
  tenggat DATE NULL,
  status ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  CONSTRAINT fk_tugas_proyek
    FOREIGN KEY (proyek_id) REFERENCES proyek (id),
  CONSTRAINT fk_tugas_assignee
    FOREIGN KEY (assignee_id) REFERENCES pengguna (id)
);

CREATE TABLE riwayat_status (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tugas_id INT NOT NULL,
  status_lama ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  status_baru ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  waktu DATETIME NOT NULL,
  catatan VARCHAR(200),
  CONSTRAINT fk_riwayat_tugas
    FOREIGN KEY (tugas_id) REFERENCES tugas (id)
);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Mari kita bedah bagian-bagian pentingnya:

  1. email ... UNIQUE menolak dua pengguna ber-email sama — identitas yang paling sering dipakai aplikasi untuk login. Kolom ini sekaligus membuat index unik, jadi pencarian pengguna lewat email menjadi cepat.
  2. assignee_id INT NULL tanpa NOT NULL menjadi foreign key opsional. InnoDB tetap memvalidasi setiap nilai non-NULL terhadap pengguna(id), sementara banyak baris NULL hidup berdampingan tanpa masalah.
  3. ENUM('baru', 'berjalan', 'selesai') di tugas dan di kedua kolom riwayat_status memakai daftar yang sama. Nilai di luar daftar memicu galat saat INSERT/UPDATE — kita akan melihatnya bekerja di Bagian 2 dan 3.
  4. Foreign key bernama (fk_tugas_proyek, fk_tugas_assignee, fk_riwayat_tugas, fk_proyek_pemilik) membuat pesan galat menyebut nama relasi yang bermasalah, bukan hanya nama kolom.
  5. tenggat DATE NULL mengakui kenyataan bahwa tidak semua tugas punya batas waktu — tugas riset eksploratif bisa dikerjakan kapan saja.
  6. catatan VARCHAR(200) tanpa NOT NULL karena tidak semua perubahan status perlu penjelasan — perpindahan yang rutin boleh dicatat tanpa kata-kata.

Verifikasi Struktur #

Setelah skema dibuat, kita pastikan keempat tabel hadir dengan struktur yang diharapkan:

sql
SHOW TABLES;

Output yang Dihasilkan:

text
+--------------------+
| Tables_in_tugas_db |
+--------------------+
| pengguna           |
| proyek             |
| riwayat_status     |
| tugas              |
+--------------------+
4 rows in set (0.00 sec)
sql
DESC tugas;

Output yang Dihasilkan:

text
+-------------+-----------------------------------+------+-----+---------+----------------+
| Field       | Type                              | Null | Key | Default | Extra          |
+-------------+-----------------------------------+------+-----+---------+----------------+
| id          | int                               | NO   | PRI | NULL    | auto_increment |
| judul       | varchar(80)                       | NO   |     | NULL    |                |
| proyek_id   | int                               | NO   | MUL | NULL    |                |
| assignee_id | int                               | YES  | MUL | NULL    |                |
| prioritas   | enum('rendah','sedang','tinggi')  | NO   |     | NULL    |                |
| tenggat     | date                              | YES  |     | NULL    |                |
| status      | enum('baru','berjalan','selesai') | NO   |     | NULL    |                |
+-------------+-----------------------------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)
sql
DESC riwayat_status;

Output yang Dihasilkan:

text
+-------------+-----------------------------------+------+-----+---------+----------------+
| Field       | Type                              | Null | Key | Default | Extra          |
+-------------+-----------------------------------+------+-----+---------+----------------+
| id          | int                               | NO   | PRI | NULL    | auto_increment |
| tugas_id    | int                               | NO   | MUL | NULL    |                |
| status_lama | enum('baru','berjalan','selesai') | NO   |     | NULL    |                |
| status_baru | enum('baru','berjalan','selesai') | NO   |     | NULL    |                |
| waktu       | datetime                          | NO   |     | NULL    |                |
| catatan     | varchar(200)                      | YES  |     | NULL    |                |
+-------------+-----------------------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

Perhatikan kolom Null pada DESC tugas: assignee_id dan tenggat bernilai YES — keduanya kolom opsional hasil keputusan desain tadi — sedangkan proyek_id bernilai NO karena setiap tugas wajib tergabung dalam satu proyek. Baris Key pada proyek_id dan assignee_id bernilai MUL: InnoDB otomatis membuat index pendukung untuk tiap foreign key agar pemeriksaan referensi tetap cepat.

Mengisi Data Awal #

Tabel kosong tidak bisa diverifikasi lebih jauh, jadi kita muat data contoh — isi berkas database/data-awal.sql. Enam pengguna, tiga proyek, sepuluh tugas, dan sebelas baris riwayat perubahan status sepanjang Januari-Februari 2026:

sql
INSERT INTO pengguna (id, nama, email) VALUES
  (1, 'Budi Santoso', '[email protected]'),
  (2, 'Siti Rahma', '[email protected]'),
  (3, 'Andi Wijaya', '[email protected]'),
  (4, 'Dewi Lestari', '[email protected]'),
  (5, 'Tono Pratama', '[email protected]'),
  (6, 'Rina Kusuma', '[email protected]');

INSERT INTO proyek (id, nama, pemilik_id) VALUES
  (1, 'Rilis Aplikasi Mobile', 1),
  (2, 'Migrasi Data Gudang', 2),
  (3, 'Kampanye Pemasaran Q1', 4);

INSERT INTO tugas (id, judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES
  (1,  'Desain mockup halaman utama',  1, 3,    'tinggi', '2026-02-10', 'selesai'),
  (2,  'Implementasi API autentikasi', 1, 1,    'tinggi', '2026-02-10', 'berjalan'),
  (3,  'Uji coba beta di Play Store',  1, NULL, 'sedang', '2026-03-15', 'baru'),
  (4,  'Pemetaan tabel lama ke baru',  2, 2,    'tinggi', '2026-02-05', 'selesai'),
  (5,  'Skrip migrasi batch pertama', 2, 5,    'sedang', '2026-02-25', 'berjalan'),
  (6,  'Validasi hasil migrasi',      2, 4,    'rendah', '2026-03-01', 'baru'),
  (7,  'Menyusun materi iklan',       3, 4,    'sedang', '2026-02-12', 'selesai'),
  (8,  'Riset kata kunci iklan',      3, 3,    'rendah', '2026-02-08', 'selesai'),
  (9,  'Negosiasi anggaran media',    3, 1,    'tinggi', '2026-02-12', 'berjalan'),
  (10, 'Laporan performa kampanye',   3, NULL, 'sedang', '2026-04-10', 'baru');

INSERT INTO riwayat_status (id, tugas_id, status_lama, status_baru, waktu, catatan) VALUES
  (1,  1, 'baru',     'berjalan', '2026-01-06 10:15:00', 'Mulai kerjakan mockup'),
  (2,  1, 'berjalan', 'selesai',  '2026-02-09 16:40:00', 'Mockup disetujui tim'),
  (3,  2, 'baru',     'berjalan', '2026-01-20 11:00:00', 'Sprint 1 dimulai'),
  (4,  4, 'baru',     'berjalan', '2026-01-07 09:00:00', 'Pemetaan awal berjalan'),
  (5,  4, 'berjalan', 'selesai',  '2026-02-04 17:00:00', 'Pemetaan tervalidasi'),
  (6,  5, 'baru',     'berjalan', '2026-01-15 10:30:00', 'Skrip versi awal mulai dikerjakan'),
  (7,  7, 'baru',     'berjalan', '2026-01-12 10:00:00', 'Brief iklan diterima'),
  (8,  7, 'berjalan', 'selesai',  '2026-02-11 15:30:00', 'Materi disetujui manajemen'),
  (9,  8, 'baru',     'berjalan', '2026-01-13 09:10:00', 'Riset kata kunci dimulai'),
  (10, 8, 'berjalan', 'selesai',  '2026-02-06 12:00:00', 'Riset selesai, 40 kata kunci'),
  (11, 9, 'baru',     'berjalan', '2026-01-25 10:30:00', 'Rapat awal dengan vendor');

Output yang Dihasilkan:

text
Query OK, 6 rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0

Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0

Query OK, 11 rows affected (0.00 sec)
Records: 11  Duplicates: 0  Warnings: 0

Seluruh tanggal dan waktu ditulis sebagai literal tetap — kolom waktu memakai nilai seperti '2026-01-15 10:30:00', bukan NOW() — sehingga semua contoh pada materi ini bisa direproduksi persis. Tiga tugas berstatus baru (id 3, 6, 10) sengaja tidak punya baris riwayat: tabel riwayat hanya mencatat perubahan status, dan tugas yang belum pernah berubah memang belum punya sejarah. Rina Kusuma juga sengaja tidak memegang satu tugas pun — data itu akan menjadi bahan laporan beban kerja di Bagian 4.

Sekarang kita buktikan relasinya bekerja: satu query join menerjemahkan id menjadi nama yang terbaca:

sql
SELECT
  t.id,
  t.judul,
  p.nama AS proyek,
  u.nama AS penanggung_jawab,
  t.prioritas,
  t.tenggat,
  t.status
FROM tugas t
JOIN proyek p ON p.id = t.proyek_id
LEFT JOIN pengguna u ON u.id = t.assignee_id
ORDER BY t.id;

Output yang Dihasilkan:

text
+----+------------------------------+-----------------------+------------------+-----------+------------+----------+
| id | judul                        | proyek                | penanggung_jawab | prioritas | tenggat    | status   |
+----+------------------------------+-----------------------+------------------+-----------+------------+----------+
|  1 | Desain mockup halaman utama  | Rilis Aplikasi Mobile | Andi Wijaya      | tinggi    | 2026-02-10 | selesai  |
|  2 | Implementasi API autentikasi | Rilis Aplikasi Mobile | Budi Santoso     | tinggi    | 2026-02-10 | berjalan |
|  3 | Uji coba beta di Play Store  | Rilis Aplikasi Mobile | NULL             | sedang    | 2026-03-15 | baru     |
|  4 | Pemetaan tabel lama ke baru  | Migrasi Data Gudang   | Siti Rahma       | tinggi    | 2026-02-05 | selesai  |
|  5 | Skrip migrasi batch pertama  | Migrasi Data Gudang   | Tono Pratama     | sedang    | 2026-02-25 | berjalan |
|  6 | Validasi hasil migrasi       | Migrasi Data Gudang   | Dewi Lestari     | rendah    | 2026-03-01 | baru     |
|  7 | Menyusun materi iklan        | Kampanye Pemasaran Q1 | Dewi Lestari     | sedang    | 2026-02-12 | selesai  |
|  8 | Riset kata kunci iklan       | Kampanye Pemasaran Q1 | Andi Wijaya      | rendah    | 2026-02-08 | selesai  |
|  9 | Negosiasi anggaran media     | Kampanye Pemasaran Q1 | Budi Santoso     | tinggi    | 2026-02-12 | berjalan |
| 10 | Laporan performa kampanye    | Kampanye Pemasaran Q1 | NULL             | sedang    | 2026-04-10 | baru     |
+----+------------------------------+-----------------------+------------------+-----------+------------+----------+
10 rows in set (0.00 sec)

Dua baris menampilkan NULL pada kolom penanggung_jawab — tugas 3 dan 10 yang belum diambil. LEFT JOIN dipakai bukan JOIN karena INNER JOIN akan membuang baris dengan assignee_id NULL sama sekali, dan daftar tugas yang diam-diam kurang dua baris adalah jenis galat yang paling sulit disadari.

Membersihkan Database Latihan #

Bagian ini selesai dan tugas_db boleh kita bongkar agar bagian berikutnya mulai dari kondisi bersih:

sql
DROP DATABASE IF EXISTS tugas_db;

Output yang Dihasilkan:

text
Query OK, 4 rows affected (0.00 sec)

Perintah DROP DATABASE menghapus database beserta seluruh tabel di dalamnya tanpa konfirmasi. Karena tiap bagian proyek ini membuka bloknya dengan pembangunan ulang dari nol, kebiasaan membersihkan di akhir tidak pernah membuat kita kehilangan apa pun.

Referensi #

Skema sudah berdiri dan datanya terbukti terhubung. Bagian 2 melanjutkan ke denyut harian aplikasi: menambah pengguna baru, membuat tugas, mengalihkan penanggung jawab, dan menghapus tugas — serta melihat bagaimana UNIQUE dan foreign key menolak data yang salah bentuk.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

database/data-awal.sql

Unduh berkas
sql
-- data-awal.sql — Proyek: Database Manajemen Tugas
-- Data contoh deterministik (6 pengguna, 3 proyek, 10 tugas, riwayat awal).
-- Cara pakai: mysql -u root -p tugas_db < data-awal.sql
-- (jalankan skema.sql terlebih dahulu)

USE tugas_db;

INSERT INTO pengguna (id, nama, email) VALUES
  (1, 'Budi Santoso', '[email protected]'),
  (2, 'Siti Rahma', '[email protected]'),
  (3, 'Andi Wijaya', '[email protected]'),
  (4, 'Dewi Lestari', '[email protected]'),
  (5, 'Tono Pratama', '[email protected]'),
  (6, 'Rina Kusuma', '[email protected]');

INSERT INTO proyek (id, nama, pemilik_id) VALUES
  (1, 'Rilis Aplikasi Mobile', 1),
  (2, 'Migrasi Data Gudang', 2),
  (3, 'Kampanye Pemasaran Q1', 4);

INSERT INTO tugas (id, judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES
  (1,  'Desain mockup halaman utama',  1, 3,    'tinggi', '2026-02-10', 'selesai'),
  (2,  'Implementasi API autentikasi', 1, 1,    'tinggi', '2026-02-10', 'berjalan'),
  (3,  'Uji coba beta di Play Store',  1, NULL, 'sedang', '2026-03-15', 'baru'),
  (4,  'Pemetaan tabel lama ke baru',  2, 2,    'tinggi', '2026-02-05', 'selesai'),
  (5,  'Skrip migrasi batch pertama', 2, 5,    'sedang', '2026-02-25', 'berjalan'),
  (6,  'Validasi hasil migrasi',      2, 4,    'rendah', '2026-03-01', 'baru'),
  (7,  'Menyusun materi iklan',       3, 4,    'sedang', '2026-02-12', 'selesai'),
  (8,  'Riset kata kunci iklan',      3, 3,    'rendah', '2026-02-08', 'selesai'),
  (9,  'Negosiasi anggaran media',    3, 1,    'tinggi', '2026-02-12', 'berjalan'),
  (10, 'Laporan performa kampanye',   3, NULL, 'sedang', '2026-04-10', 'baru');

INSERT INTO riwayat_status (id, tugas_id, status_lama, status_baru, waktu, catatan) VALUES
  (1,  1, 'baru',     'berjalan', '2026-01-06 10:15:00', 'Mulai kerjakan mockup'),
  (2,  1, 'berjalan', 'selesai',  '2026-02-09 16:40:00', 'Mockup disetujui tim'),
  (3,  2, 'baru',     'berjalan', '2026-01-20 11:00:00', 'Sprint 1 dimulai'),
  (4,  4, 'baru',     'berjalan', '2026-01-07 09:00:00', 'Pemetaan awal berjalan'),
  (5,  4, 'berjalan', 'selesai',  '2026-02-04 17:00:00', 'Pemetaan tervalidasi'),
  (6,  5, 'baru',     'berjalan', '2026-01-15 10:30:00', 'Skrip versi awal mulai dikerjakan'),
  (7,  7, 'baru',     'berjalan', '2026-01-12 10:00:00', 'Brief iklan diterima'),
  (8,  7, 'berjalan', 'selesai',  '2026-02-11 15:30:00', 'Materi disetujui manajemen'),
  (9,  8, 'baru',     'berjalan', '2026-01-13 09:10:00', 'Riset kata kunci dimulai'),
  (10, 8, 'berjalan', 'selesai',  '2026-02-06 12:00:00', 'Riset selesai, 40 kata kunci'),
  (11, 9, 'baru',     'berjalan', '2026-01-25 10:30:00', 'Rapat awal dengan vendor');

database/skema.sql

Unduh berkas
sql
-- skema.sql — Proyek: Database Manajemen Tugas
-- Membangun database tugas_db beserta 4 tabel berelasi (pengguna, proyek,
-- tugas, riwayat_status) lengkap dengan foreign key bernama.
-- Cara pakai: mysql -u root -p < skema.sql

DROP DATABASE IF EXISTS tugas_db;
CREATE DATABASE tugas_db;
USE tugas_db;

CREATE TABLE pengguna (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  email VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE proyek (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  pemilik_id INT NOT NULL,
  CONSTRAINT fk_proyek_pemilik
    FOREIGN KEY (pemilik_id) REFERENCES pengguna (id)
);

CREATE TABLE tugas (
  id INT AUTO_INCREMENT PRIMARY KEY,
  judul VARCHAR(80) NOT NULL,
  proyek_id INT NOT NULL,
  assignee_id INT NULL,
  prioritas ENUM('rendah', 'sedang', 'tinggi') NOT NULL,
  tenggat DATE NULL,
  status ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  CONSTRAINT fk_tugas_proyek
    FOREIGN KEY (proyek_id) REFERENCES proyek (id),
  CONSTRAINT fk_tugas_assignee
    FOREIGN KEY (assignee_id) REFERENCES pengguna (id)
);

CREATE TABLE riwayat_status (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tugas_id INT NOT NULL,
  status_lama ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  status_baru ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  waktu DATETIME NOT NULL,
  catatan VARCHAR(200),
  CONSTRAINT fk_riwayat_tugas
    FOREIGN KEY (tugas_id) REFERENCES tugas (id)
);

queries/laporan-beban-kerja.sql

Unduh berkas
sql
-- laporan-beban-kerja.sql — Beban kerja tiap anggota (total/selesai/berjalan/baru).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-beban-kerja.sql
USE tugas_db;

SELECT
  p.nama,
  COUNT(t.id) AS total_tugas,
  SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
  SUM(CASE WHEN t.status = 'berjalan' THEN 1 ELSE 0 END) AS berjalan,
  SUM(CASE WHEN t.status = 'baru' THEN 1 ELSE 0 END) AS baru
FROM pengguna p
LEFT JOIN tugas t ON t.assignee_id = p.id
GROUP BY p.id, p.nama
ORDER BY total_tugas DESC, p.nama;

queries/laporan-overdue.sql

Unduh berkas
sql
-- laporan-overdue.sql — Tugas menembus tenggat namun belum selesai.
-- Ganti tanggal acuan sesuai kebutuhan (di aplikasi: CURRENT_DATE()).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-overdue.sql
USE tugas_db;

SELECT
  t.id,
  t.judul,
  pr.nama AS proyek,
  p.nama AS penanggung,
  t.tenggat,
  DATEDIFF('2026-02-15', t.tenggat) AS hari_terlambat
FROM tugas t
JOIN proyek pr ON pr.id = t.proyek_id
LEFT JOIN pengguna p ON p.id = t.assignee_id
WHERE t.status <> 'selesai'
  AND t.tenggat < '2026-02-15'
ORDER BY t.tenggat, t.id;

queries/laporan-status-proyek.sql

Unduh berkas
sql
-- laporan-status-proyek.sql — Distribusi status + persentase selesai per proyek.
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-status-proyek.sql
USE tugas_db;

SELECT
  pr.nama AS proyek,
  COUNT(t.id) AS total_tugas,
  SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
  ROUND(
    SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
    1
  ) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC, pr.nama;

tests/pemeriksaan.sql

Unduh berkas
sql
-- pemeriksaan.sql — Proyek: Database Manajemen Tugas
-- Rangkaian pemeriksaan otomatis: skema, jumlah data, hasil alur Bagian 3,
-- dan angka laporan Bagian 4. Ekspektasi tertulis di komentar tiap blok.
-- Cara pakai (urutan lengkap membangun kondisi akhir proyek):
--   mysql -u root -p < database/skema.sql
--   mysql -u root -p < database/data-awal.sql        (kondisi awal Bagian 2)
--   ...jalankan alur transaksi Bagian 3...           (atau lihat Bagian 4)
--   mysql -u root -p < tests/pemeriksaan.sql
-- Pemeriksaan ini mengasumsikan kondisi AKHIR (13 baris riwayat).
USE tugas_db;

-- [1] Struktur: 4 tabel harus ada
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'tugas_db'
ORDER BY table_name;
-- Ekspektasi: pengguna, proyek, riwayat_status, tugas (4 baris)

-- [2] Jumlah data kondisi akhir: 6 pengguna, 3 proyek, 10 tugas, 13 riwayat
SELECT
  (SELECT COUNT(*) FROM pengguna) AS pengguna,
  (SELECT COUNT(*) FROM proyek) AS proyek,
  (SELECT COUNT(*) FROM tugas) AS tugas,
  (SELECT COUNT(*) FROM riwayat_status) AS riwayat;
-- Ekspektasi: 6, 3, 10, 13

-- [3] Integritas ENUM: email unik dijaga (duplikat ditolak)
-- (dibuktikan di Bagian 2 dengan ERROR 1062; di sini cukup hitung unik)
SELECT COUNT(DISTINCT email) AS email_unik FROM pengguna;
-- Ekspektasi: 6

-- [4] Alur Bagian 3 terekam: tugas 3 kini milik Rina dan berjalan
SELECT judul, assignee_id, status FROM tugas WHERE id = 3;
-- Ekspektasi: Uji coba beta di Play Store | 6 | berjalan

-- [5] Riwayat tugas 3: tepat 1 baris baru → berjalan
SELECT COUNT(*) AS perubahan_tugas3 FROM riwayat_status WHERE tugas_id = 3;
-- Ekspektasi: 1

-- [6] Durasi siklus tugas 2: selesai 19 hari setelah mulai
SELECT
  TIMESTAMPDIFF(DAY, MIN(waktu), MAX(waktu)) AS durasi_hari
FROM riwayat_status
WHERE tugas_id = 2;
-- Ekspektasi: 19

-- [7] Laporan overdue (acuan 2026-02-15): 4 tugas belum selesai menembus tenggat
SELECT COUNT(*) AS tugas_terlambat
FROM tugas
WHERE status <> 'selesai' AND tenggat < '2026-02-15';
-- Ekspektasi: 4

-- [8] Laporan status proyek: Kampanye Pemasaran Q1 paling tinggi persentase selesainya
SELECT
  pr.nama AS proyek,
  ROUND(
    SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
    1
  ) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC;
-- Ekspektasi: Kampanye Pemasaran Q1 (83.3) teratas, lalu Migrasi Data Gudang, Rilis Aplikasi Mobile