fix final2
|
|
@ -0,0 +1,32 @@
|
||||||
|
BAB III
|
||||||
|
PERANCANGAN SISTEM
|
||||||
|
3.1 Use Case Diagram
|
||||||
|
Use case diagram menggambarkan interaksi antara aktor dengan sistem aplikasi Tobaku Sulastri. Aktor utama pada aplikasi ini adalah Admin atau Pemilik Toko, yaitu pengguna yang melakukan pengelolaan data bahan kue, pencatatan stok masuk, perhitungan kebutuhan produksi, prediksi permintaan, serta pemantauan laporan.Use case yang terdapat pada sistem meliputi login, melihat dashboard, mengelola produk dan stok bahan, mencatat stok masuk, melakukan prediksi kebutuhan produksi, menghitung kebutuhan bahan berdasarkan resep, submit produksi dan update stok, melihat laporan, serta mengatur akun/password. Pada proses prediksi dan produksi, sistem mengambil data resep, menghitung kebutuhan bahan, mengecek ketersediaan stok, mengurangi stok apabila produksi disubmit, dan menyimpan riwayat prediksi atau pemakaian bahan.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gambar 3.1 Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue
|
||||||
|
|
||||||
|
3.2 Flowchart Sistem
|
||||||
|
Flowchart sistem menjelaskan alur kerja aplikasi dari pengguna membuka aplikasi sampai proses selesai. Sistem dimulai dari splash screen, kemudian mengecek status sesi login. Jika sesi belum tersedia, pengguna harus memasukkan username/email dan password. Apabila validasi berhasil, pengguna diarahkan ke dashboard utama. Jika gagal, sistem menampilkan pesan kesalahan dan pengguna diminta login kembali.Setelah masuk ke dashboard, pengguna dapat memilih menu produk/stok, transaksi stok masuk, prediksi/produksi, laporan, atau pengaturan. Pada menu produk/stok, pengguna dapat melihat, memfilter, dan menambahkan data bahan. Pada menu transaksi stok masuk, pengguna memilih bahan, mengisi jumlah, tanggal, dan menyimpan transaksi sehingga stok bertambah. Pada menu prediksi/produksi, pengguna memilih resep dan jumlah produksi, lalu sistem menghitung kebutuhan bahan dan mengecek stok. Jika stok cukup, sistem mengurangi stok, menyimpan riwayat penggunaan bahan, dan menyimpan hasil prediksi. Jika stok tidak cukup, pengguna diarahkan untuk memperbarui stok terlebih dahulu. Menu laporan menampilkan data stok, riwayat stok masuk, hasil prediksi, dan bahan kritis secara realtime dari database.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gambar 3.2 Flowchart Sistem Aplikasi Tobaku Sulastri
|
||||||
|
|
||||||
|
3.3 Entity Relationship Diagram (ERD)
|
||||||
|
ERD menggambarkan rancangan basis data yang digunakan oleh aplikasi. Tabel utama pada sistem adalah products, transactions atau Stock In, predictions, recipes, recipe_ingredients, stock_usage_history, login, dan password_reset_otps. Tabel products menyimpan data bahan kue seperti nama, kategori, satuan, harga, stok saat ini, dan stok minimum. Tabel transactions atau Stock In menyimpan riwayat stok masuk. Tabel recipes menyimpan data resep produk, sedangkan recipe_ingredients menyimpan daftar bahan dan jumlah kebutuhan untuk setiap resep.Tabel stock_usage_history menyimpan riwayat pemakaian stok saat produksi disubmit. Tabel predictions atau prediksi menyimpan hasil prediksi jumlah produksi, estimasi kebutuhan bahan, tanggal prediksi, dan informasi akurasi model jika tersedia. Tabel login menyimpan data akun pengguna, sementara password_reset_otps menyimpan kode OTP untuk proses reset password. Relasi utama yang terbentuk adalah satu akun login dapat memiliki banyak OTP, satu resep memiliki banyak detail bahan, satu produk dapat muncul pada banyak riwayat stok masuk, riwayat pemakaian stok, dan hasil prediksi.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Gambar 3.3 Entity Relationship Diagram Database Aplikasi
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# BAB III - Perancangan Sistem
|
||||||
|
|
||||||
|
## 3.1 Use Case Diagram
|
||||||
|
Use case diagram menggambarkan interaksi antara aktor dengan sistem aplikasi Tobaku Sulastri. Aktor utama pada aplikasi ini adalah Admin/Pemilik Toko, yaitu pengguna yang melakukan pengelolaan data bahan kue, pencatatan stok masuk, perhitungan kebutuhan produksi, prediksi permintaan, serta pemantauan laporan.
|
||||||
|
|
||||||
|
Use case yang terdapat pada sistem meliputi login, melihat dashboard, mengelola produk dan stok bahan, mencatat stok masuk, melakukan prediksi kebutuhan produksi, menghitung kebutuhan bahan berdasarkan resep, submit produksi dan update stok, melihat laporan, serta mengatur akun/password. Pada proses prediksi dan produksi, sistem mengambil data resep, menghitung kebutuhan bahan, mengecek ketersediaan stok, mengurangi stok apabila produksi disubmit, dan menyimpan riwayat prediksi atau pemakaian bahan.
|
||||||
|
|
||||||
|
Gambar 3.1 Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue.
|
||||||
|
|
||||||
|
## 3.2 Flowchart Sistem
|
||||||
|
Flowchart sistem menjelaskan alur kerja aplikasi dari pengguna membuka aplikasi sampai proses selesai. Sistem dimulai dari splash screen, kemudian mengecek status sesi login. Jika sesi belum tersedia, pengguna harus memasukkan username/email dan password. Apabila validasi berhasil, pengguna diarahkan ke dashboard utama. Jika gagal, sistem menampilkan pesan kesalahan dan pengguna diminta login kembali.
|
||||||
|
|
||||||
|
Setelah masuk ke dashboard, pengguna dapat memilih menu produk/stok, transaksi stok masuk, prediksi/produksi, laporan, atau pengaturan. Pada menu produk/stok, pengguna dapat melihat, memfilter, dan menambahkan data bahan. Pada menu transaksi stok masuk, pengguna memilih bahan, mengisi jumlah, tanggal, dan menyimpan transaksi sehingga stok bertambah. Pada menu prediksi/produksi, pengguna memilih resep dan jumlah produksi, lalu sistem menghitung kebutuhan bahan dan mengecek stok. Jika stok cukup, sistem mengurangi stok, menyimpan riwayat penggunaan bahan, dan menyimpan hasil prediksi. Jika stok tidak cukup, pengguna diarahkan untuk memperbarui stok terlebih dahulu. Menu laporan menampilkan data stok, riwayat stok masuk, hasil prediksi, dan bahan kritis secara realtime dari database.
|
||||||
|
|
||||||
|
Gambar 3.2 Flowchart Sistem Aplikasi Tobaku Sulastri.
|
||||||
|
|
||||||
|
## 3.3 Entity Relationship Diagram (ERD)
|
||||||
|
ERD menggambarkan rancangan basis data yang digunakan oleh aplikasi. Tabel utama pada sistem adalah `products`, `transactions` atau `Stock In`, `predictions`, `recipes`, `recipe_ingredients`, `stock_usage_history`, `login`, dan `password_reset_otps`. Tabel `products` menyimpan data bahan kue seperti nama, kategori, satuan, harga, stok saat ini, dan stok minimum. Tabel `transactions` atau `Stock In` menyimpan riwayat stok masuk. Tabel `recipes` menyimpan data resep produk, sedangkan `recipe_ingredients` menyimpan daftar bahan dan jumlah kebutuhan untuk setiap resep.
|
||||||
|
|
||||||
|
Tabel `stock_usage_history` menyimpan riwayat pemakaian stok saat produksi disubmit. Tabel `predictions` atau `prediksi` menyimpan hasil prediksi jumlah produksi, estimasi kebutuhan bahan, tanggal prediksi, dan informasi akurasi model jika tersedia. Tabel `login` menyimpan data akun pengguna, sementara `password_reset_otps` menyimpan kode OTP untuk proses reset password. Relasi utama yang terbentuk adalah satu akun login dapat memiliki banyak OTP, satu resep memiliki banyak detail bahan, satu produk dapat muncul pada banyak riwayat stok masuk, riwayat pemakaian stok, dan hasil prediksi.
|
||||||
|
|
||||||
|
Gambar 3.3 Entity Relationship Diagram Database Aplikasi.
|
||||||
|
|
||||||
|
## Keterangan Diagram
|
||||||
|
- File sumber Draw.io Use Case: `docs/bab3_use_case.drawio`
|
||||||
|
- File gambar Use Case: `docs/bab3_use_case.svg`
|
||||||
|
- File sumber Draw.io Flowchart: `docs/bab3_flowchart_sistem.drawio`
|
||||||
|
- File gambar Flowchart: `docs/bab3_flowchart_sistem.svg`
|
||||||
|
- File sumber Draw.io ERD: `docs/bab3_erd.drawio`
|
||||||
|
- File gambar ERD: `docs/bab3_erd.svg`
|
||||||
|
|
@ -0,0 +1,759 @@
|
||||||
|
|
||||||
|
|
||||||
|
BAB IV
|
||||||
|
|
||||||
|
HASIL DAN PEMBAHASAN APLIKASI
|
||||||
|
|
||||||
|
Tobaku Sulastri - Aplikasi Prediksi dan Stok Bahan Kue
|
||||||
|
|
||||||
|
Dokumen pembahasan aplikasi berdasarkan implementasi Flutter, Flask API, MySQL, dan model prediksi.
|
||||||
|
|
||||||
|
|
||||||
|
4.1 Gambaran Umum Aplikasi
|
||||||
|
|
||||||
|
Aplikasi yang dibangun pada penelitian ini adalah aplikasi manajemen permintaan dan prediksi stok bahan kue untuk Tobaku Sulastri. Aplikasi ini dirancang untuk membantu proses pencatatan stok bahan, pemantauan produk, pencatatan stok masuk, perhitungan kebutuhan bahan berdasarkan resep, pengurangan stok setelah produksi, serta penyajian laporan stok dan prediksi.
|
||||||
|
|
||||||
|
Permasalahan utama yang diselesaikan aplikasi adalah kurang efisiennya pencatatan stok bahan secara manual, sulitnya mengetahui jumlah bahan yang perlu disiapkan untuk produksi, dan belum adanya peringatan stok berdasarkan batas minimum setiap bahan. Dengan aplikasi ini, pengguna dapat melihat stok bahan secara real-time, menjalankan kalkulasi kebutuhan bahan berdasarkan resep, menyimpan hasil produksi, serta melihat laporan yang terhubung langsung dengan data produk.
|
||||||
|
|
||||||
|
Aplikasi terdiri dari dua bagian utama, yaitu:
|
||||||
|
|
||||||
|
Aplikasi mobile berbasis Flutter sebagai antarmuka pengguna.
|
||||||
|
|
||||||
|
Backend berbasis Flask API yang menghubungkan aplikasi dengan database MySQL dan model prediksi.
|
||||||
|
|
||||||
|
Secara umum, aplikasi memiliki fitur utama sebagai berikut:
|
||||||
|
|
||||||
|
Login pengguna.
|
||||||
|
|
||||||
|
Dashboard ringkasan stok dan aktivitas.
|
||||||
|
|
||||||
|
Daftar produk dan bahan.
|
||||||
|
|
||||||
|
Input stok masuk.
|
||||||
|
|
||||||
|
Prediksi kebutuhan bahan berdasarkan resep dan jumlah produksi.
|
||||||
|
|
||||||
|
Pengurangan stok otomatis setelah produksi berhasil.
|
||||||
|
|
||||||
|
Laporan stok, stok masuk, prediksi, bahan kritis, dan penggunaan bahan.
|
||||||
|
|
||||||
|
Pengaturan akun dan reset password menggunakan OTP email.
|
||||||
|
|
||||||
|
4.2 Tujuan Implementasi Sistem
|
||||||
|
|
||||||
|
Implementasi aplikasi bertujuan untuk menghasilkan sistem yang dapat membantu pengelolaan stok bahan kue secara lebih terstruktur. Tujuan implementasi sistem ini meliputi:
|
||||||
|
|
||||||
|
Mempermudah pengguna dalam melihat data stok bahan yang tersedia.
|
||||||
|
|
||||||
|
Membantu pengguna menghitung kebutuhan bahan berdasarkan resep produk.
|
||||||
|
|
||||||
|
Mengurangi stok bahan secara otomatis setelah proses produksi dinyatakan berhasil.
|
||||||
|
|
||||||
|
Menentukan status stok berdasarkan stok minimum masing-masing produk.
|
||||||
|
|
||||||
|
Menyediakan laporan stok, stok masuk, prediksi, dan bahan kritis secara real-time.
|
||||||
|
|
||||||
|
Menyimpan riwayat prediksi dan penggunaan bahan agar dapat digunakan sebagai bahan evaluasi.
|
||||||
|
|
||||||
|
Menghubungkan data produk, transaksi, resep, dan prediksi dalam satu alur sistem.
|
||||||
|
|
||||||
|
4.3 Arsitektur Sistem
|
||||||
|
|
||||||
|
Arsitektur aplikasi menggunakan pola client-server. Flutter berperan sebagai client yang digunakan oleh pengguna, sedangkan Flask API berperan sebagai server yang memproses permintaan dari aplikasi, mengakses database MySQL, dan menjalankan proses prediksi.
|
||||||
|
|
||||||
|
Alur kerja sistem dapat dijelaskan sebagai berikut:
|
||||||
|
|
||||||
|
Pengguna membuka aplikasi Flutter.
|
||||||
|
|
||||||
|
Aplikasi mengirim permintaan ke Flask API melalui HTTP request.
|
||||||
|
|
||||||
|
Flask API memproses request dan mengambil atau menyimpan data ke MySQL.
|
||||||
|
|
||||||
|
Untuk fitur prediksi, API juga mengakses model machine learning yang telah disimpan.
|
||||||
|
|
||||||
|
API mengembalikan response dalam format JSON.
|
||||||
|
|
||||||
|
Flutter menampilkan hasil response kepada pengguna dalam bentuk halaman, kartu informasi, tabel, grafik, atau status.
|
||||||
|
|
||||||
|
Komponen sistem terdiri dari:
|
||||||
|
|
||||||
|
Komponen
|
||||||
|
Fungsi
|
||||||
|
Flutter
|
||||||
|
Menampilkan antarmuka aplikasi dan mengatur interaksi pengguna
|
||||||
|
Flask API
|
||||||
|
Menyediakan endpoint login, produk, transaksi, prediksi, resep, dan laporan
|
||||||
|
MySQL
|
||||||
|
Menyimpan data produk, transaksi, prediksi, resep, login, OTP, dan riwayat penggunaan stok
|
||||||
|
Model Prediksi
|
||||||
|
Menghasilkan prediksi permintaan berdasarkan input tanggal, harga, dan kategori produk
|
||||||
|
HTTP Service
|
||||||
|
Menghubungkan Flutter dengan backend melalui `MLService`
|
||||||
|
|
||||||
|
4.4 Struktur Navigasi Aplikasi
|
||||||
|
|
||||||
|
Navigasi aplikasi didefinisikan pada main.dart. Halaman awal aplikasi adalah splash screen, kemudian pengguna diarahkan ke halaman login. Setelah login berhasil, pengguna masuk ke halaman dashboard.
|
||||||
|
|
||||||
|
Daftar route utama aplikasi adalah sebagai berikut:
|
||||||
|
|
||||||
|
Route
|
||||||
|
Halaman
|
||||||
|
Fungsi
|
||||||
|
`/splash`
|
||||||
|
Splash Screen
|
||||||
|
Tampilan awal aplikasi
|
||||||
|
`/login`
|
||||||
|
Login
|
||||||
|
Autentikasi pengguna
|
||||||
|
`/dashboard`
|
||||||
|
Dashboard
|
||||||
|
Menampilkan ringkasan data stok dan aktivitas
|
||||||
|
`/products`
|
||||||
|
Produk
|
||||||
|
Menampilkan daftar produk dan stok
|
||||||
|
`/transaction`
|
||||||
|
Stok Masuk
|
||||||
|
Menambahkan stok produk/bahan
|
||||||
|
`/prediction`
|
||||||
|
Prediksi
|
||||||
|
Menghitung kebutuhan bahan dan mengurangi stok
|
||||||
|
`/reports`
|
||||||
|
Laporan
|
||||||
|
Menampilkan laporan stok, prediksi, dan bahan kritis
|
||||||
|
`/settings`
|
||||||
|
Pengaturan
|
||||||
|
Pengaturan akun dan logout
|
||||||
|
|
||||||
|
Navigasi bawah digunakan pada halaman utama agar pengguna dapat berpindah antarfitur dengan cepat. Halaman Produk, Prediksi, Transaksi, Laporan, dan Pengaturan dapat diakses langsung melalui menu navigasi.
|
||||||
|
|
||||||
|
4.5 Implementasi Halaman Login
|
||||||
|
|
||||||
|
Halaman login digunakan untuk membatasi akses aplikasi hanya kepada pengguna yang memiliki akun. Pengguna memasukkan email atau username beserta password. Data login dikirim ke endpoint /api/login.
|
||||||
|
|
||||||
|
Jika login berhasil, sistem akan mengarahkan pengguna ke halaman dashboard. Jika gagal, aplikasi menampilkan pesan kesalahan. Selain login, aplikasi juga menyediakan fitur lupa password dengan OTP email. Fitur ini melibatkan beberapa endpoint, yaitu:
|
||||||
|
|
||||||
|
/api/forgot-password/send-otp
|
||||||
|
|
||||||
|
/api/forgot-password/verify-otp
|
||||||
|
|
||||||
|
/api/forgot-password/reset
|
||||||
|
|
||||||
|
/api/change-password
|
||||||
|
|
||||||
|
Dengan adanya fitur OTP, pengguna dapat melakukan reset password secara mandiri selama email yang digunakan terdaftar di database.
|
||||||
|
|
||||||
|
4.6 Implementasi Dashboard
|
||||||
|
|
||||||
|
Dashboard berfungsi sebagai halaman ringkasan utama. Halaman ini menampilkan informasi penting yang dibutuhkan pengguna setelah login. Data dashboard berasal dari beberapa endpoint, seperti produk, ringkasan penggunaan bahan, dan bahan yang stoknya kritis.
|
||||||
|
|
||||||
|
Informasi yang ditampilkan pada dashboard meliputi:
|
||||||
|
|
||||||
|
Total produk.
|
||||||
|
|
||||||
|
Total bahan digunakan hari ini.
|
||||||
|
|
||||||
|
Ringkasan penggunaan bahan.
|
||||||
|
|
||||||
|
Daftar bahan dengan stok kritis.
|
||||||
|
|
||||||
|
Shortcut menuju halaman prediksi dan stok masuk.
|
||||||
|
|
||||||
|
Dashboard juga memanfaatkan status stok yang dihitung berdasarkan stok saat ini dan stok minimum. Jika stok suatu bahan berada di bawah atau sama dengan batas minimum, bahan tersebut masuk kategori kritis dan ditampilkan sebagai perhatian utama.
|
||||||
|
|
||||||
|
4.7 Implementasi Fitur Produk
|
||||||
|
|
||||||
|
Fitur Produk digunakan untuk menampilkan daftar bahan dan barang yang tersedia. Setiap produk memiliki informasi nama, kategori, harga, stok saat ini, satuan, stok minimum, dan status ketersediaan.
|
||||||
|
|
||||||
|
Data produk diambil dari endpoint /products. Response dari API kemudian diubah menjadi objek produk pada aplikasi Flutter. Pada implementasi terbaru, nilai stok dibuat dalam bentuk double agar stok desimal seperti 9.975 kg dapat ditampilkan dengan benar.
|
||||||
|
|
||||||
|
Informasi yang ditampilkan pada halaman Produk meliputi:
|
||||||
|
|
||||||
|
Nama produk.
|
||||||
|
|
||||||
|
Kategori produk.
|
||||||
|
|
||||||
|
Harga produk.
|
||||||
|
|
||||||
|
Stok tersedia.
|
||||||
|
|
||||||
|
Satuan stok.
|
||||||
|
|
||||||
|
Status stok.
|
||||||
|
|
||||||
|
Persentase kapasitas stok.
|
||||||
|
|
||||||
|
Detail produk.
|
||||||
|
|
||||||
|
Stok minimum produk.
|
||||||
|
|
||||||
|
Status stok tidak lagi hanya menggunakan batas umum, tetapi juga memperhatikan stok minimum masing-masing produk. Logika status yang digunakan adalah:
|
||||||
|
|
||||||
|
Kondisi
|
||||||
|
Status
|
||||||
|
Stok lebih kecil atau sama dengan stok minimum
|
||||||
|
Kritis
|
||||||
|
Stok lebih kecil atau sama dengan dua kali stok minimum
|
||||||
|
Sedang
|
||||||
|
Stok lebih besar dari dua kali stok minimum
|
||||||
|
Tersedia
|
||||||
|
|
||||||
|
Jika produk belum memiliki stok minimum atau nilai minimum masih 0, sistem tetap menyediakan fallback menggunakan batas umum, yaitu stok kurang dari 5 kg dianggap kritis dan stok kurang dari 20 kg dianggap sedang.
|
||||||
|
|
||||||
|
4.8 Implementasi Fitur Stok Masuk
|
||||||
|
|
||||||
|
Fitur Stok Masuk digunakan untuk mencatat penambahan stok produk. Pengguna dapat memilih produk, memasukkan jumlah stok masuk, memilih tanggal transaksi, lalu menyimpan transaksi. Setelah transaksi berhasil, stok produk akan bertambah otomatis.
|
||||||
|
|
||||||
|
Endpoint yang digunakan untuk stok masuk adalah /transaksi. Endpoint ini menyimpan transaksi dan memperbarui nilai current_stock pada tabel products.
|
||||||
|
|
||||||
|
Selain mencatat stok masuk untuk produk yang sudah ada, halaman ini juga menyediakan fitur Tambah Produk Baru. Form tambah produk berisi:
|
||||||
|
|
||||||
|
Nama produk.
|
||||||
|
|
||||||
|
Jenis kategori, yaitu Bahan atau Barang.
|
||||||
|
|
||||||
|
Satuan, yaitu kg untuk Bahan dan pcs untuk Barang.
|
||||||
|
|
||||||
|
Harga.
|
||||||
|
|
||||||
|
Stok awal.
|
||||||
|
|
||||||
|
Stok minimum.
|
||||||
|
|
||||||
|
Penambahan field stok minimum penting karena sistem dapat memberikan status stok yang lebih sesuai dengan kebutuhan masing-masing bahan. Misalnya Tepung Terigu membutuhkan stok minimum lebih besar dibanding Baking Powder.
|
||||||
|
|
||||||
|
4.9 Implementasi Fitur Prediksi Kebutuhan Bahan
|
||||||
|
|
||||||
|
Fitur Prediksi digunakan untuk menghitung kebutuhan bahan berdasarkan produk atau resep yang dipilih dan jumlah produksi yang dimasukkan. Pada halaman ini, pengguna memilih resep, memasukkan jumlah produksi, lalu aplikasi menampilkan kebutuhan bahan.
|
||||||
|
|
||||||
|
Data resep diambil dari endpoint /recipes. Setiap resep memiliki daftar bahan dan jumlah kebutuhan per satu kali produksi. Contoh resep yang digunakan antara lain:
|
||||||
|
|
||||||
|
Donat.
|
||||||
|
|
||||||
|
Roti Putih.
|
||||||
|
|
||||||
|
Kue Brownies.
|
||||||
|
|
||||||
|
Kue Tart.
|
||||||
|
|
||||||
|
Setelah pengguna memasukkan jumlah produksi, aplikasi menghitung kebutuhan bahan dengan rumus:
|
||||||
|
|
||||||
|
kebutuhan bahan = quantity_needed x jumlah produksi
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
|
||||||
|
Tepung Terigu untuk Donat = 500 gr
|
||||||
|
|
||||||
|
Jumlah produksi = 10
|
||||||
|
|
||||||
|
Kebutuhan tepung = 500 gr x 10 = 5000 gr = 5 kg
|
||||||
|
|
||||||
|
Aplikasi kemudian membandingkan kebutuhan bahan dengan stok saat ini. Jika stok mencukupi, status bahan ditampilkan sebagai cukup. Jika stok kurang, status bahan ditampilkan sebagai kurang.
|
||||||
|
|
||||||
|
4.10 Mekanisme Konversi Satuan
|
||||||
|
|
||||||
|
Karena resep dan stok dapat menggunakan satuan yang berbeda, aplikasi menerapkan konversi satuan sebelum menghitung ketersediaan stok. Contohnya resep dapat menggunakan gram, sedangkan stok produk tersimpan dalam kilogram.
|
||||||
|
|
||||||
|
Konversi yang digunakan meliputi:
|
||||||
|
|
||||||
|
Dari
|
||||||
|
Ke
|
||||||
|
Rumus
|
||||||
|
gr
|
||||||
|
kg
|
||||||
|
nilai / 1000
|
||||||
|
kg
|
||||||
|
gr
|
||||||
|
nilai x 1000
|
||||||
|
ml
|
||||||
|
l
|
||||||
|
nilai / 1000
|
||||||
|
l
|
||||||
|
ml
|
||||||
|
nilai x 1000
|
||||||
|
butir
|
||||||
|
kg
|
||||||
|
jumlah butir x 50 gr / 1000
|
||||||
|
butir
|
||||||
|
gr
|
||||||
|
jumlah butir x 50 gr
|
||||||
|
|
||||||
|
Untuk telur, sistem menggunakan asumsi:
|
||||||
|
|
||||||
|
1 butir telur = 50 gram
|
||||||
|
|
||||||
|
Konversi ini digunakan agar stok yang tampil pada halaman Prediksi sesuai dengan stok pada halaman Produk. Sebelumnya stok desimal dapat terbaca sebagai 0 kg apabila response dari database berbentuk string, sehingga parser stok diperbaiki agar dapat membaca angka maupun string.
|
||||||
|
|
||||||
|
4.11 Mekanisme Pengurangan Stok Setelah Produksi
|
||||||
|
|
||||||
|
Setelah kebutuhan bahan dihitung dan pengguna memilih proses produksi berhasil, aplikasi akan mengurangi stok bahan secara otomatis. Proses ini dilakukan melalui endpoint /stock/consume.
|
||||||
|
|
||||||
|
Alur pengurangan stok adalah sebagai berikut:
|
||||||
|
|
||||||
|
Pengguna memilih resep.
|
||||||
|
|
||||||
|
Pengguna memasukkan jumlah produksi.
|
||||||
|
|
||||||
|
Aplikasi menghitung kebutuhan bahan.
|
||||||
|
|
||||||
|
Aplikasi mengecek stok saat ini.
|
||||||
|
|
||||||
|
Jika stok mencukupi, pengguna dapat menyimpan produksi.
|
||||||
|
|
||||||
|
Aplikasi mengirim daftar bahan, jumlah yang digunakan, satuan, nama resep, dan jumlah produksi ke backend.
|
||||||
|
|
||||||
|
Backend mengurangi current_stock pada tabel products.
|
||||||
|
|
||||||
|
Backend menyimpan riwayat penggunaan bahan pada tabel stock_usage_history.
|
||||||
|
|
||||||
|
Aplikasi menyimpan data prediksi ke tabel predictions.
|
||||||
|
|
||||||
|
Aplikasi melakukan refresh stok agar halaman Produk dan Laporan menampilkan data terbaru.
|
||||||
|
|
||||||
|
Rumus pengurangan stok adalah:
|
||||||
|
|
||||||
|
stok akhir = stok awal - jumlah bahan yang digunakan
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
|
||||||
|
Stok Baking Powder awal = 10 kg
|
||||||
|
|
||||||
|
Kebutuhan Baking Powder = 0.025 kg
|
||||||
|
|
||||||
|
Stok akhir = 10 - 0.025 = 9.975 kg
|
||||||
|
|
||||||
|
Sistem tidak membulatkan pengurangan stok ke angka bulat. Pengurangan stok mengikuti nilai kebutuhan bahan yang ada pada ringkasan stok. Oleh karena itu, stok dapat tampil sebagai nilai desimal seperti 9.975 kg.
|
||||||
|
|
||||||
|
4.12 Implementasi Prediksi Permintaan
|
||||||
|
|
||||||
|
Backend menyediakan endpoint /prediksi dan /batch-prediksi untuk proses prediksi permintaan. Model prediksi yang digunakan disimpan dalam file model pada folder ml_model. Flask API memuat model, encoder, metadata, dan kolom fitur ketika server dijalankan.
|
||||||
|
|
||||||
|
Input prediksi meliputi:
|
||||||
|
|
||||||
|
Tahun.
|
||||||
|
|
||||||
|
Bulan.
|
||||||
|
|
||||||
|
Hari.
|
||||||
|
|
||||||
|
Hari dalam minggu.
|
||||||
|
|
||||||
|
Harga satuan.
|
||||||
|
|
||||||
|
Total harga.
|
||||||
|
|
||||||
|
Produk encoded.
|
||||||
|
|
||||||
|
Nama produk encoded.
|
||||||
|
|
||||||
|
Kategori produk encoded.
|
||||||
|
|
||||||
|
Output prediksi berupa jumlah unit yang diprediksi, nilai raw prediksi, dan informasi metadata model seperti nilai evaluasi. Hasil prediksi dapat disimpan ke database melalui endpoint /predictions.
|
||||||
|
|
||||||
|
Pada aplikasi, hasil prediksi juga dimanfaatkan untuk membuat ringkasan kebutuhan bahan. Ringkasan tersebut kemudian digunakan pada halaman laporan untuk menampilkan informasi prediksi dan penggunaan bahan.
|
||||||
|
|
||||||
|
4.13 Implementasi Fitur Laporan
|
||||||
|
|
||||||
|
Fitur Laporan digunakan untuk menampilkan data rekapitulasi dari aktivitas aplikasi. Data laporan diambil dari beberapa endpoint backend, yaitu:
|
||||||
|
|
||||||
|
/laporan/stok
|
||||||
|
|
||||||
|
/laporan/stok-masuk
|
||||||
|
|
||||||
|
/laporan/prediksi
|
||||||
|
|
||||||
|
/laporan/bahan-kritis
|
||||||
|
|
||||||
|
/api/dashboard/summary
|
||||||
|
|
||||||
|
Jenis laporan yang tersedia meliputi:
|
||||||
|
|
||||||
|
4.13.1 Laporan Stok Bahan
|
||||||
|
|
||||||
|
Laporan stok bahan menampilkan daftar bahan, stok saat ini, satuan, stok minimum, dan status stok. Data stok diambil dari tabel products. Apabila tabel memiliki kolom min_stock, nilai tersebut digunakan sebagai stok minimum.
|
||||||
|
|
||||||
|
Jika nilai stok minimum masih 0, backend akan melakukan pengisian default untuk bahan utama seperti Tepung Terigu, Telur, Gula Pasir, Susu Bubuk, Cokelat Bubuk, Mentega, Keju Parut, dan Baking Powder.
|
||||||
|
|
||||||
|
4.13.2 Laporan Stok Masuk
|
||||||
|
|
||||||
|
Laporan stok masuk menampilkan riwayat penambahan stok yang dilakukan pengguna. Data berasal dari transaksi stok masuk yang disimpan melalui endpoint /transaksi.
|
||||||
|
|
||||||
|
Informasi yang ditampilkan meliputi:
|
||||||
|
|
||||||
|
Tanggal transaksi.
|
||||||
|
|
||||||
|
Nama produk.
|
||||||
|
|
||||||
|
Jumlah stok masuk.
|
||||||
|
|
||||||
|
Satuan.
|
||||||
|
|
||||||
|
4.13.3 Laporan Prediksi
|
||||||
|
|
||||||
|
Laporan prediksi menampilkan riwayat prediksi yang pernah disimpan. Data berasal dari tabel predictions.
|
||||||
|
|
||||||
|
Informasi yang ditampilkan meliputi:
|
||||||
|
|
||||||
|
Tanggal prediksi.
|
||||||
|
|
||||||
|
Nama produk atau resep.
|
||||||
|
|
||||||
|
Jumlah prediksi.
|
||||||
|
|
||||||
|
Estimasi kebutuhan bahan.
|
||||||
|
|
||||||
|
4.13.4 Laporan Bahan Kritis
|
||||||
|
|
||||||
|
Laporan bahan kritis menampilkan bahan yang stoknya berada pada status kritis. Status kritis ditentukan berdasarkan perbandingan stok saat ini dengan stok minimum.
|
||||||
|
|
||||||
|
4.13.5 Grafik dan Ringkasan
|
||||||
|
|
||||||
|
Halaman laporan juga menampilkan ringkasan dan grafik, seperti:
|
||||||
|
|
||||||
|
Total bahan.
|
||||||
|
|
||||||
|
Total produk.
|
||||||
|
|
||||||
|
Total prediksi.
|
||||||
|
|
||||||
|
Total bahan kritis.
|
||||||
|
|
||||||
|
Tren permintaan produk.
|
||||||
|
|
||||||
|
Penggunaan bahan paling sering.
|
||||||
|
|
||||||
|
Laporan dapat diekspor dalam bentuk file sesuai format yang disediakan aplikasi.
|
||||||
|
|
||||||
|
4.14 Struktur Database
|
||||||
|
|
||||||
|
Database yang digunakan adalah MySQL. Tabel utama yang digunakan aplikasi adalah sebagai berikut:
|
||||||
|
|
||||||
|
Tabel
|
||||||
|
Fungsi
|
||||||
|
`products`
|
||||||
|
Menyimpan data produk, kategori, harga, stok, satuan, jenis produk, dan stok minimum
|
||||||
|
`transactions`
|
||||||
|
Menyimpan transaksi stok masuk versi umum
|
||||||
|
`predictions`
|
||||||
|
Menyimpan hasil prediksi dan estimasi kebutuhan bahan
|
||||||
|
`recipes`
|
||||||
|
Menyimpan daftar resep produk
|
||||||
|
`recipe_ingredients`
|
||||||
|
Menyimpan bahan dan jumlah kebutuhan pada setiap resep
|
||||||
|
`stock_usage_history`
|
||||||
|
Menyimpan riwayat penggunaan bahan setelah produksi
|
||||||
|
`stok_keluar`
|
||||||
|
Menyimpan data stok keluar untuk laporan
|
||||||
|
`login`
|
||||||
|
Menyimpan akun pengguna
|
||||||
|
`password_reset_otps`
|
||||||
|
Menyimpan kode OTP reset password
|
||||||
|
|
||||||
|
Struktur tabel products menjadi pusat data stok. Kolom penting pada tabel ini adalah:
|
||||||
|
|
||||||
|
Kolom
|
||||||
|
Fungsi
|
||||||
|
`id`
|
||||||
|
ID produk
|
||||||
|
`name`
|
||||||
|
Nama produk
|
||||||
|
`category`
|
||||||
|
Kategori produk
|
||||||
|
`product_type`
|
||||||
|
Jenis produk, misalnya Bahan atau Barang
|
||||||
|
`unit`
|
||||||
|
Satuan stok
|
||||||
|
`price`
|
||||||
|
Harga produk
|
||||||
|
`current_stock`
|
||||||
|
Jumlah stok saat ini
|
||||||
|
`min_stock`
|
||||||
|
Batas minimum stok
|
||||||
|
|
||||||
|
Kolom current_stock dan min_stock menggunakan tipe desimal agar dapat menyimpan stok pecahan seperti 9.975 kg.
|
||||||
|
|
||||||
|
4.15 Implementasi API Backend
|
||||||
|
|
||||||
|
Backend dikembangkan menggunakan Flask. API menerima request dari aplikasi Flutter, memproses data, lalu mengirim response JSON.
|
||||||
|
|
||||||
|
Endpoint utama yang digunakan aplikasi adalah:
|
||||||
|
|
||||||
|
Endpoint
|
||||||
|
Method
|
||||||
|
Fungsi
|
||||||
|
`/api/login`
|
||||||
|
POST
|
||||||
|
Login pengguna
|
||||||
|
`/api/forgot-password/send-otp`
|
||||||
|
POST
|
||||||
|
Mengirim OTP reset password
|
||||||
|
`/api/forgot-password/verify-otp`
|
||||||
|
POST
|
||||||
|
Memverifikasi OTP
|
||||||
|
`/api/forgot-password/reset`
|
||||||
|
POST
|
||||||
|
Reset password
|
||||||
|
`/api/change-password`
|
||||||
|
POST
|
||||||
|
Mengubah password
|
||||||
|
`/products`
|
||||||
|
GET
|
||||||
|
Mengambil daftar produk
|
||||||
|
`/products`
|
||||||
|
POST
|
||||||
|
Menambahkan produk baru
|
||||||
|
`/products/<id>`
|
||||||
|
GET
|
||||||
|
Mengambil detail produk
|
||||||
|
`/transaksi`
|
||||||
|
POST
|
||||||
|
Menyimpan stok masuk dan menambah stok
|
||||||
|
`/stock/consume`
|
||||||
|
POST
|
||||||
|
Mengurangi stok setelah produksi
|
||||||
|
`/recipes`
|
||||||
|
GET
|
||||||
|
Mengambil daftar resep
|
||||||
|
`/recipes/<id>`
|
||||||
|
GET
|
||||||
|
Mengambil detail resep
|
||||||
|
`/prediksi`
|
||||||
|
POST
|
||||||
|
Melakukan prediksi tunggal
|
||||||
|
`/batch-prediksi`
|
||||||
|
POST
|
||||||
|
Melakukan prediksi batch
|
||||||
|
`/predictions`
|
||||||
|
POST
|
||||||
|
Menyimpan hasil prediksi
|
||||||
|
`/laporan/stok`
|
||||||
|
GET
|
||||||
|
Mengambil laporan stok
|
||||||
|
`/laporan/stok-masuk`
|
||||||
|
GET
|
||||||
|
Mengambil laporan stok masuk
|
||||||
|
`/laporan/prediksi`
|
||||||
|
GET
|
||||||
|
Mengambil laporan prediksi
|
||||||
|
`/laporan/bahan-kritis`
|
||||||
|
GET
|
||||||
|
Mengambil laporan bahan kritis
|
||||||
|
`/api/dashboard/summary`
|
||||||
|
GET
|
||||||
|
Mengambil ringkasan dashboard
|
||||||
|
|
||||||
|
Backend juga memiliki fungsi kompatibilitas untuk memastikan kolom database yang dibutuhkan tersedia. Contohnya backend dapat menambahkan kolom min_stock dan mengubah current_stock menjadi desimal jika database lama belum sesuai.
|
||||||
|
|
||||||
|
4.16 Implementasi Service pada Flutter
|
||||||
|
|
||||||
|
Pada aplikasi Flutter, komunikasi dengan backend dipusatkan pada file MLService. Service ini berisi fungsi-fungsi untuk mengakses endpoint backend.
|
||||||
|
|
||||||
|
Beberapa fungsi penting pada MLService adalah:
|
||||||
|
|
||||||
|
login() untuk autentikasi pengguna.
|
||||||
|
|
||||||
|
getProducts() untuk mengambil data produk.
|
||||||
|
|
||||||
|
createProduct() untuk menambahkan produk.
|
||||||
|
|
||||||
|
addTransactionWithStockUpdate() untuk menyimpan stok masuk.
|
||||||
|
|
||||||
|
consumeStock() untuk mengurangi stok bahan setelah produksi.
|
||||||
|
|
||||||
|
getRecipes() untuk mengambil data resep.
|
||||||
|
|
||||||
|
savePrediction() untuk menyimpan hasil prediksi.
|
||||||
|
|
||||||
|
getReportStock() untuk mengambil laporan stok.
|
||||||
|
|
||||||
|
getReportStockIn() untuk mengambil laporan stok masuk.
|
||||||
|
|
||||||
|
getReportPredictions() untuk mengambil laporan prediksi.
|
||||||
|
|
||||||
|
getReportCritical() untuk mengambil laporan bahan kritis.
|
||||||
|
|
||||||
|
Dengan adanya service khusus, setiap halaman tidak perlu langsung menulis request HTTP secara berulang. Halaman cukup memanggil controller, lalu controller memanggil service yang sesuai.
|
||||||
|
|
||||||
|
4.17 Pembahasan Keterhubungan Antarfitur
|
||||||
|
|
||||||
|
Kelebihan utama aplikasi ini adalah keterhubungan data antarfitur. Setiap fitur tidak berdiri sendiri, melainkan saling memengaruhi data yang sama.
|
||||||
|
|
||||||
|
Contoh keterhubungan antarfitur adalah:
|
||||||
|
|
||||||
|
Produk yang ditambahkan pada Stok Masuk muncul pada halaman Produk.
|
||||||
|
|
||||||
|
Stok awal dan stok masuk memengaruhi stok yang tampil pada halaman Prediksi.
|
||||||
|
|
||||||
|
Ketika produksi berhasil, stok bahan pada halaman Produk berkurang.
|
||||||
|
|
||||||
|
Pengurangan stok masuk ke riwayat penggunaan bahan.
|
||||||
|
|
||||||
|
Hasil prediksi tersimpan dan muncul pada halaman Laporan.
|
||||||
|
|
||||||
|
Stok minimum pada produk digunakan oleh halaman Produk, Dashboard, dan Laporan.
|
||||||
|
|
||||||
|
Dengan alur tersebut, aplikasi dapat memberikan informasi stok yang lebih konsisten dan mengurangi risiko selisih data antarhalaman.
|
||||||
|
|
||||||
|
4.18 Pembahasan Stok Minimum
|
||||||
|
|
||||||
|
Stok minimum merupakan batas aman yang digunakan untuk menentukan apakah stok suatu bahan perlu segera ditambah. Fitur ini penting karena setiap bahan memiliki kebutuhan dan karakteristik berbeda.
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
|
||||||
|
Tepung Terigu digunakan hampir di semua resep, sehingga stok minimumnya perlu lebih tinggi.
|
||||||
|
|
||||||
|
Baking Powder digunakan dalam jumlah kecil, sehingga stok minimumnya dapat lebih rendah.
|
||||||
|
|
||||||
|
Telur digunakan dalam satuan butir atau kilogram, sehingga sistem perlu mengelola konversi satuan dengan tepat.
|
||||||
|
|
||||||
|
Tanpa stok minimum, sistem hanya dapat menggunakan batas umum, misalnya semua bahan kurang dari 5 kg dianggap kritis. Hal ini kurang tepat karena kebutuhan minimum Tepung Terigu tidak sama dengan Baking Powder. Oleh karena itu, stok minimum dibuat per produk agar status lebih realistis.
|
||||||
|
|
||||||
|
4.19 Pembahasan Keakuratan Data Stok
|
||||||
|
|
||||||
|
Keakuratan stok dijaga melalui beberapa mekanisme:
|
||||||
|
|
||||||
|
Stok produk disimpan dalam tipe desimal.
|
||||||
|
|
||||||
|
Satuan bahan dikonversi sebelum stok dikurangi.
|
||||||
|
|
||||||
|
Stok tidak dikurangi jika bahan tidak mencukupi.
|
||||||
|
|
||||||
|
Pengurangan stok dilakukan pada backend agar data tersimpan langsung ke database.
|
||||||
|
|
||||||
|
Setelah pengurangan berhasil, aplikasi melakukan refresh data stok.
|
||||||
|
|
||||||
|
Riwayat penggunaan bahan disimpan untuk mendukung laporan.
|
||||||
|
|
||||||
|
Dengan mekanisme tersebut, stok pada halaman Prediksi, Produk, Dashboard, dan Laporan dapat saling sinkron.
|
||||||
|
|
||||||
|
4.20 Pengujian Sistem
|
||||||
|
|
||||||
|
Pengujian dilakukan untuk memastikan fitur utama aplikasi berjalan sesuai kebutuhan. Jenis pengujian yang digunakan adalah pengujian fungsional atau black box testing.
|
||||||
|
|
||||||
|
Contoh skenario pengujian adalah:
|
||||||
|
|
||||||
|
No
|
||||||
|
Fitur
|
||||||
|
Skenario
|
||||||
|
Hasil yang Diharapkan
|
||||||
|
1
|
||||||
|
Login
|
||||||
|
Pengguna memasukkan akun benar
|
||||||
|
Sistem masuk ke dashboard
|
||||||
|
2
|
||||||
|
Login
|
||||||
|
Pengguna memasukkan password salah
|
||||||
|
Sistem menampilkan pesan gagal login
|
||||||
|
3
|
||||||
|
Produk
|
||||||
|
Pengguna membuka halaman produk
|
||||||
|
Sistem menampilkan daftar produk dan stok
|
||||||
|
4
|
||||||
|
Tambah Produk
|
||||||
|
Pengguna mengisi data produk lengkap
|
||||||
|
Produk tersimpan dan tampil di daftar produk
|
||||||
|
5
|
||||||
|
Stok Masuk
|
||||||
|
Pengguna menambah stok produk
|
||||||
|
Stok produk bertambah
|
||||||
|
6
|
||||||
|
Prediksi
|
||||||
|
Pengguna memilih resep dan jumlah produksi
|
||||||
|
Sistem menampilkan kebutuhan bahan
|
||||||
|
7
|
||||||
|
Prediksi
|
||||||
|
Stok bahan mencukupi lalu produksi disimpan
|
||||||
|
Stok bahan berkurang sesuai ringkasan stok
|
||||||
|
8
|
||||||
|
Prediksi
|
||||||
|
Stok bahan tidak mencukupi
|
||||||
|
Sistem menampilkan status kurang
|
||||||
|
9
|
||||||
|
Laporan Stok
|
||||||
|
Pengguna membuka laporan stok
|
||||||
|
Sistem menampilkan stok, minimum, dan status
|
||||||
|
10
|
||||||
|
Laporan Prediksi
|
||||||
|
Pengguna membuka laporan prediksi
|
||||||
|
Sistem menampilkan riwayat prediksi
|
||||||
|
11
|
||||||
|
Lupa Password
|
||||||
|
Pengguna meminta OTP
|
||||||
|
Sistem mengirim OTP ke email
|
||||||
|
12
|
||||||
|
Reset Password
|
||||||
|
OTP valid dan password baru diisi
|
||||||
|
Password berhasil diperbarui
|
||||||
|
|
||||||
|
Berdasarkan pengujian, aplikasi dapat menjalankan proses utama, yaitu pengelolaan produk, stok masuk, prediksi kebutuhan bahan, pengurangan stok, dan laporan.
|
||||||
|
|
||||||
|
4.21 Kelebihan Aplikasi
|
||||||
|
|
||||||
|
Aplikasi memiliki beberapa kelebihan, yaitu:
|
||||||
|
|
||||||
|
Data stok produk dapat dipantau secara real-time.
|
||||||
|
|
||||||
|
Perhitungan kebutuhan bahan dilakukan otomatis berdasarkan resep.
|
||||||
|
|
||||||
|
Stok bahan berkurang otomatis setelah produksi berhasil.
|
||||||
|
|
||||||
|
Status stok menggunakan batas minimum per produk.
|
||||||
|
|
||||||
|
Laporan menampilkan data stok, stok masuk, prediksi, dan bahan kritis.
|
||||||
|
|
||||||
|
Sistem mendukung stok desimal sehingga penggunaan bahan kecil tetap tercatat.
|
||||||
|
|
||||||
|
Fitur reset password menggunakan OTP email meningkatkan keamanan akun.
|
||||||
|
|
||||||
|
Backend menyediakan endpoint yang terpisah sehingga pengembangan fitur lebih mudah.
|
||||||
|
|
||||||
|
4.22 Keterbatasan Aplikasi
|
||||||
|
|
||||||
|
Walaupun aplikasi telah memenuhi kebutuhan utama, masih terdapat beberapa keterbatasan, yaitu:
|
||||||
|
|
||||||
|
Aplikasi masih bergantung pada koneksi ke server Flask lokal atau jaringan yang sama.
|
||||||
|
|
||||||
|
Pengaturan IP API masih ditulis pada MLService, sehingga perlu disesuaikan jika alamat server berubah.
|
||||||
|
|
||||||
|
Data resep masih bergantung pada data yang sudah tersimpan di database.
|
||||||
|
|
||||||
|
Sistem belum menyediakan manajemen resep langsung dari aplikasi.
|
||||||
|
|
||||||
|
Hak akses pengguna masih sederhana dan belum dipisahkan berdasarkan role.
|
||||||
|
|
||||||
|
Model prediksi masih bergantung pada dataset dan fitur yang telah disiapkan sebelumnya.
|
||||||
|
|
||||||
|
4.23 Saran Pengembangan
|
||||||
|
|
||||||
|
Beberapa pengembangan yang dapat dilakukan pada tahap berikutnya adalah:
|
||||||
|
|
||||||
|
Menambahkan fitur CRUD resep dari aplikasi.
|
||||||
|
|
||||||
|
Menambahkan role pengguna, misalnya admin dan staf.
|
||||||
|
|
||||||
|
Menambahkan notifikasi ketika stok bahan mendekati minimum.
|
||||||
|
|
||||||
|
Menambahkan export laporan dengan format yang lebih lengkap.
|
||||||
|
|
||||||
|
Menambahkan grafik prediksi per produk.
|
||||||
|
|
||||||
|
Menyediakan konfigurasi alamat API dari halaman pengaturan.
|
||||||
|
|
||||||
|
Menambahkan backup dan restore database.
|
||||||
|
|
||||||
|
Mengembangkan model prediksi dengan data transaksi yang lebih besar.
|
||||||
|
|
||||||
|
4.24 Kesimpulan Pembahasan
|
||||||
|
|
||||||
|
Berdasarkan hasil implementasi, aplikasi Tobaku Sulastri berhasil dibangun sebagai sistem manajemen stok dan prediksi kebutuhan bahan kue. Aplikasi mampu mengelola produk, mencatat stok masuk, menghitung kebutuhan bahan berdasarkan resep, mengurangi stok setelah produksi, menyimpan hasil prediksi, serta menyajikan laporan stok dan bahan kritis.
|
||||||
|
|
||||||
|
Penerapan stok minimum per produk membuat status stok menjadi lebih sesuai dengan kebutuhan masing-masing bahan. Selain itu, dukungan stok desimal membuat pencatatan penggunaan bahan lebih akurat, terutama untuk bahan dengan jumlah penggunaan kecil seperti Baking Powder.
|
||||||
|
|
||||||
|
Secara keseluruhan, aplikasi ini dapat membantu proses pengelolaan stok bahan kue menjadi lebih terstruktur, cepat, dan informatif.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-05-29T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
|
||||||
|
<diagram id="bab3" name="Page-1">
|
||||||
|
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1000" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="etitle" value="Entity Relationship Diagram Database Aplikasi" style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=24;fontStyle=1;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="430" y="20" width="740" height="40" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_login" value="login
|
||||||
|
PK id
|
||||||
|
name
|
||||||
|
email (unique)
|
||||||
|
username (unique)
|
||||||
|
password
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="80" y="110" width="245" height="170" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_otp" value="password_reset_otps
|
||||||
|
PK id
|
||||||
|
FK login_id
|
||||||
|
email
|
||||||
|
otp_code
|
||||||
|
expires_at
|
||||||
|
is_used
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="80" y="360" width="245" height="190" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_products" value="products
|
||||||
|
PK id
|
||||||
|
name/product_name
|
||||||
|
category
|
||||||
|
product_type
|
||||||
|
unit
|
||||||
|
price
|
||||||
|
current_stock
|
||||||
|
min_stock
|
||||||
|
created_at
|
||||||
|
updated_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="430" y="90" width="270" height="240" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_trans" value="transactions / Stock In
|
||||||
|
PK id
|
||||||
|
product_name
|
||||||
|
category
|
||||||
|
quantity
|
||||||
|
unit_price
|
||||||
|
total_price
|
||||||
|
transaction_date
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="430" y="405" width="270" height="205" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_recipes" value="recipes
|
||||||
|
PK id
|
||||||
|
recipe_name
|
||||||
|
description
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="805" y="95" width="245" height="135" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_ing" value="recipe_ingredients
|
||||||
|
PK id
|
||||||
|
FK recipe_id
|
||||||
|
product_name
|
||||||
|
quantity_needed
|
||||||
|
unit" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="805" y="300" width="245" height="155" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_usage" value="stock_usage_history
|
||||||
|
PK id
|
||||||
|
recipe_name
|
||||||
|
production_quantity
|
||||||
|
FK product_id
|
||||||
|
product_name
|
||||||
|
quantity_used
|
||||||
|
unit
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="805" y="535" width="270" height="205" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_pred" value="predictions / prediksi
|
||||||
|
PK id
|
||||||
|
product_name/nama_produk
|
||||||
|
category
|
||||||
|
unit_price
|
||||||
|
prediction_date
|
||||||
|
predicted_quantity
|
||||||
|
estimated_needs
|
||||||
|
accuracy_r2
|
||||||
|
error_mae
|
||||||
|
created_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1160" y="230" width="300" height="265" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e_bahan" value="bahan / stok_masuk / stok_keluar
|
||||||
|
PK id
|
||||||
|
FK product_id / bahan_id
|
||||||
|
nama_bahan
|
||||||
|
stok / jumlah
|
||||||
|
unit / satuan
|
||||||
|
tanggal
|
||||||
|
created_at
|
||||||
|
updated_at" style="shape=swimlane;whiteSpace=wrap;html=1;startSize=28;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1160" y="565" width="300" height="235" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee0" value="1 : N" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_login" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee1" value="1 : N" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_recipes" target="e_ing"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee2" value="1 : N" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_products" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee3" value="1 : N" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_products" target="e_bahan"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee4" value="1 : N (berdasarkan nama/id)" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_products" target="e_trans"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee5" value="1 : N (produk diprediksi)" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_products" target="e_pred"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee6" value="N : 1 (product_name)" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_ing" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="ee7" value="1 : N (recipe_name)" style="endArrow=ERmany;startArrow=ERone;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="e_recipes" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
|
After Width: | Height: | Size: 114 KiB |
|
|
@ -0,0 +1,47 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="850" viewBox="0 0 1500 850"><rect width="1500" height="850" fill="#F7F3EE"/>
|
||||||
|
<defs><style>.title{font-family:Arial,sans-serif;font-size:28px;font-weight:700;fill:#2D2520}.label{font-family:Arial,sans-serif;font-size:16px;fill:#2D2520}.small{font-family:Arial,sans-serif;font-size:13px;fill:#4B5563}.box{fill:#fff;stroke:#8B5E3C;stroke-width:2}.note{fill:#EAF4FF;stroke:#2B6CB0;stroke-width:2}.decision{fill:#FFF7D6;stroke:#B7791F;stroke-width:2}.term{fill:#EAF8EF;stroke:#2E7D32;stroke-width:2}.db{fill:#EFF2F5;stroke:#4B5563;stroke-width:2}.line{stroke:#4B3A31;stroke-width:2;fill:none}.dash{stroke:#6B7280;stroke-width:1.8;stroke-dasharray:7 5;fill:none}</style><marker id="arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#4B3A31"/></marker><marker id="arrow2" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#6B7280"/></marker></defs>
|
||||||
|
<text x="750" y="45" text-anchor="middle" class="title">Entity Relationship Diagram Database Aplikasi</text>
|
||||||
|
<rect x="80" y="110" width="245" height="170" rx="8" class="box"/>
|
||||||
|
<text x="202.5" y="134" text-anchor="middle" class="label">login</text>
|
||||||
|
<text x="94" y="162" text-anchor="start" class="small">PK id</text><text x="94" y="182" text-anchor="start" class="small">name</text><text x="94" y="202" text-anchor="start" class="small">email (unique)</text><text x="94" y="222" text-anchor="start" class="small">username (unique)</text><text x="94" y="242" text-anchor="start" class="small">password</text><text x="94" y="262" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="80" y="360" width="245" height="190" rx="8" class="box"/>
|
||||||
|
<text x="202.5" y="384" text-anchor="middle" class="label">password_reset_otps</text>
|
||||||
|
<text x="94" y="412" text-anchor="start" class="small">PK id</text><text x="94" y="432" text-anchor="start" class="small">FK login_id</text><text x="94" y="452" text-anchor="start" class="small">email</text><text x="94" y="472" text-anchor="start" class="small">otp_code</text><text x="94" y="492" text-anchor="start" class="small">expires_at</text><text x="94" y="512" text-anchor="start" class="small">is_used</text><text x="94" y="532" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="430" y="90" width="270" height="240" rx="8" class="box"/>
|
||||||
|
<text x="565.0" y="114" text-anchor="middle" class="label">products</text>
|
||||||
|
<text x="444" y="142" text-anchor="start" class="small">PK id</text><text x="444" y="162" text-anchor="start" class="small">name/product_name</text><text x="444" y="182" text-anchor="start" class="small">category</text><text x="444" y="202" text-anchor="start" class="small">product_type</text><text x="444" y="222" text-anchor="start" class="small">unit</text><text x="444" y="242" text-anchor="start" class="small">price</text><text x="444" y="262" text-anchor="start" class="small">current_stock</text><text x="444" y="282" text-anchor="start" class="small">min_stock</text><text x="444" y="302" text-anchor="start" class="small">created_at</text><text x="444" y="322" text-anchor="start" class="small">updated_at</text>
|
||||||
|
<rect x="430" y="405" width="270" height="205" rx="8" class="box"/>
|
||||||
|
<text x="565.0" y="429" text-anchor="middle" class="label">transactions / Stock In</text>
|
||||||
|
<text x="444" y="457" text-anchor="start" class="small">PK id</text><text x="444" y="477" text-anchor="start" class="small">product_name</text><text x="444" y="497" text-anchor="start" class="small">category</text><text x="444" y="517" text-anchor="start" class="small">quantity</text><text x="444" y="537" text-anchor="start" class="small">unit_price</text><text x="444" y="557" text-anchor="start" class="small">total_price</text><text x="444" y="577" text-anchor="start" class="small">transaction_date</text><text x="444" y="597" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="805" y="95" width="245" height="135" rx="8" class="box"/>
|
||||||
|
<text x="927.5" y="119" text-anchor="middle" class="label">recipes</text>
|
||||||
|
<text x="819" y="147" text-anchor="start" class="small">PK id</text><text x="819" y="167" text-anchor="start" class="small">recipe_name</text><text x="819" y="187" text-anchor="start" class="small">description</text><text x="819" y="207" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="805" y="300" width="245" height="155" rx="8" class="box"/>
|
||||||
|
<text x="927.5" y="324" text-anchor="middle" class="label">recipe_ingredients</text>
|
||||||
|
<text x="819" y="352" text-anchor="start" class="small">PK id</text><text x="819" y="372" text-anchor="start" class="small">FK recipe_id</text><text x="819" y="392" text-anchor="start" class="small">product_name</text><text x="819" y="412" text-anchor="start" class="small">quantity_needed</text><text x="819" y="432" text-anchor="start" class="small">unit</text>
|
||||||
|
<rect x="805" y="535" width="270" height="205" rx="8" class="box"/>
|
||||||
|
<text x="940.0" y="559" text-anchor="middle" class="label">stock_usage_history</text>
|
||||||
|
<text x="819" y="587" text-anchor="start" class="small">PK id</text><text x="819" y="607" text-anchor="start" class="small">recipe_name</text><text x="819" y="627" text-anchor="start" class="small">production_quantity</text><text x="819" y="647" text-anchor="start" class="small">FK product_id</text><text x="819" y="667" text-anchor="start" class="small">product_name</text><text x="819" y="687" text-anchor="start" class="small">quantity_used</text><text x="819" y="707" text-anchor="start" class="small">unit</text><text x="819" y="727" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="1160" y="230" width="300" height="265" rx="8" class="box"/>
|
||||||
|
<text x="1310.0" y="254" text-anchor="middle" class="label">predictions / prediksi</text>
|
||||||
|
<text x="1174" y="282" text-anchor="start" class="small">PK id</text><text x="1174" y="302" text-anchor="start" class="small">product_name/nama_produk</text><text x="1174" y="322" text-anchor="start" class="small">category</text><text x="1174" y="342" text-anchor="start" class="small">unit_price</text><text x="1174" y="362" text-anchor="start" class="small">prediction_date</text><text x="1174" y="382" text-anchor="start" class="small">predicted_quantity</text><text x="1174" y="402" text-anchor="start" class="small">estimated_needs</text><text x="1174" y="422" text-anchor="start" class="small">accuracy_r2</text><text x="1174" y="442" text-anchor="start" class="small">error_mae</text><text x="1174" y="462" text-anchor="start" class="small">created_at</text>
|
||||||
|
<rect x="1160" y="565" width="300" height="235" rx="8" class="box"/>
|
||||||
|
<text x="1310.0" y="589" text-anchor="middle" class="label">bahan / stok_masuk / stok_keluar</text>
|
||||||
|
<text x="1174" y="617" text-anchor="start" class="small">PK id</text><text x="1174" y="637" text-anchor="start" class="small">FK product_id / bahan_id</text><text x="1174" y="657" text-anchor="start" class="small">nama_bahan</text><text x="1174" y="677" text-anchor="start" class="small">stok / jumlah</text><text x="1174" y="697" text-anchor="start" class="small">unit / satuan</text><text x="1174" y="717" text-anchor="start" class="small">tanggal</text><text x="1174" y="737" text-anchor="start" class="small">created_at</text><text x="1174" y="757" text-anchor="start" class="small">updated_at</text>
|
||||||
|
<path d="M325,445 L430,180" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="377.5" y="307.5" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M1050,165 L805,345" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="927.5" y="250.0" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M700,210 L805,378" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="752.5" y="289.0" text-anchor="middle" class="small">N:1</text>
|
||||||
|
<path d="M700,210 L805,635" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="752.5" y="417.5" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M700,210 L430,510" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="565.0" y="355.0" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M700,210 L1160,360" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="930.0" y="280.0" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M700,210 L1160,680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="930.0" y="440.0" text-anchor="middle" class="small">1:N</text>
|
||||||
|
<path d="M1050,165 L805,635" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="927.5" y="395.0" text-anchor="middle" class="small">1:N</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -0,0 +1,80 @@
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-05-29T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
|
||||||
|
<diagram id="flow-auth-otp" name="Flowchart Login OTP Detail">
|
||||||
|
<mxGraphModel dx="1900" dy="1200" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1700" pageHeight="1750" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="title" value="Flowchart Detail Login dan Lupa Password OTP Email" style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=24;fontStyle=1;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="360" y="20" width="980" height="42" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="lane_login" value="Alur Login Normal" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontStyle=1;fontColor=#2D2520;arcSize=4;" vertex="1" parent="1"><mxGeometry x="80" y="420" width="650" height="1030" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="lane_otp" value="Alur Lupa Password dengan OTP Email" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontStyle=1;fontColor=#2D2520;arcSize=4;" vertex="1" parent="1"><mxGeometry x="820" y="420" width="760" height="1030" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="start" value="Mulai" style="ellipse;whiteSpace=wrap;html=1;fillColor=#EAF8EF;strokeColor=#2E7D32;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;fontColor=#1F3D2A;" vertex="1" parent="1"><mxGeometry x="760" y="90" width="180" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="splash" value="Splash Screen" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="755" y="165" width="190" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="session" value="Apakah sesi login masih aktif?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="720" y="250" width="260" height="90" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="dashboard" value="Masuk ke Dashboard Utama" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1060" y="260" width="250" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="end" value="Selesai" style="ellipse;whiteSpace=wrap;html=1;fillColor=#EAF8EF;strokeColor=#2E7D32;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;fontColor=#1F3D2A;" vertex="1" parent="1"><mxGeometry x="1095" y="350" width="180" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="login_page" value="Tampilkan Halaman Login" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="725" y="525" width="250" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="choice" value="Pilih aksi pada halaman login" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="720" y="620" width="260" height="85" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="input_login" value="Input username/email dan password" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#EAF4FF;strokeColor=#2B6CB0;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#1A365D;" vertex="1" parent="1"><mxGeometry x="245" y="760" width="310" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="api_login" value="Kirim data login ke API /api/login" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="255" y="855" width="290" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="db_login" value="Validasi akun pada tabel login" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="260" y="940" width="280" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="valid_login" value="Apakah data login valid?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="270" y="1040" width="260" height="85" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="login_success" value="Arahkan pengguna ke dashboard" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="250" y="1170" width="300" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="login_fail" value="Tampilkan pesan login gagal" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFECEC;strokeColor=#DC2626;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#7F1D1D;arcSize=10;" vertex="1" parent="1"><mxGeometry x="250" y="1290" width="300" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="login_back" value="Pengguna memperbaiki input login" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="250" y="1380" width="300" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="input_email" value="Input username/email akun" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#EAF4FF;strokeColor=#2B6CB0;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#1A365D;" vertex="1" parent="1"><mxGeometry x="1040" y="760" width="310" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="send_otp" value="Kirim request OTP ke API /api/forgot-password/send-otp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1010" y="855" width="370" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="db_account" value="Cek akun pada tabel login" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1055" y="950" width="280" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="found" value="Apakah email/username terdaftar?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1035" y="1050" width="320" height="90" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="not_found" value="Tampilkan pesan akun tidak ditemukan" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFECEC;strokeColor=#DC2626;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#7F1D1D;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1360" y="1065" width="300" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="gen_otp" value="Generate kode OTP 6 digit dan masa berlaku 10 menit" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1015" y="1180" width="360" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="save_otp" value="Simpan OTP ke tabel password_reset_otps" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="660" y="1180" width="320" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="email_otp" value="Kirim OTP ke email pengguna melalui SMTP" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F0FDF4;strokeColor=#16A34A;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#14532D;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1015" y="1280" width="360" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="input_otp" value="Input kode OTP dari email" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#EAF4FF;strokeColor=#2B6CB0;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#1A365D;" vertex="1" parent="1"><mxGeometry x="1045" y="1380" width="310" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="verify_otp" value="Kirim OTP ke API /api/forgot-password/verify-otp" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1010" y="1475" width="370" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="db_otp" value="Cek OTP: sesuai, belum dipakai, belum kedaluwarsa" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1010" y="1570" width="370" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="otp_valid" value="Apakah OTP valid?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1065" y="1670" width="260" height="85" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="otp_fail" value="Tampilkan pesan OTP salah/kedaluwarsa" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFECEC;strokeColor=#DC2626;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#7F1D1D;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1360" y="1680" width="300" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="new_pass" value="Input password baru" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#EAF4FF;strokeColor=#2B6CB0;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#1A365D;" vertex="1" parent="1"><mxGeometry x="650" y="1680" width="300" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="reset_api" value="Kirim reset password ke API /api/forgot-password/reset" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="650" y="1780" width="370" height="65" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="update_pass" value="Update password tabel login dan tandai OTP sudah digunakan" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1020" y="1780" width="390" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="reset_ok" value="Reset password berhasil, kembali ke halaman login" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1025" y="1890" width="390" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e0" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="start" target="splash"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="splash" target="session"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e2" value="Ya" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="session" target="dashboard"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="dashboard" target="end"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e4" value="Tidak" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="session" target="login_page"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="login_page" target="choice"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e6" value="Login" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="choice" target="input_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e7" value="Lupa password" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="choice" target="input_email"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="input_login" target="api_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="api_login" target="db_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="db_login" target="valid_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e11" value="Ya" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="valid_login" target="login_success"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="login_success" target="dashboard"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e13" value="Tidak" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="valid_login" target="login_fail"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e14" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="login_fail" target="login_back"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e15" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="login_back" target="input_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e16" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="input_email" target="send_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e17" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="send_otp" target="db_account"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e18" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="db_account" target="found"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e19" value="Ya" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="found" target="gen_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e20" value="Tidak" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="found" target="not_found"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e21" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="not_found" target="input_email"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e22" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="gen_otp" target="save_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e23" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="save_otp" target="email_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e24" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="email_otp" target="input_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e25" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="input_otp" target="verify_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e26" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="verify_otp" target="db_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e27" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="db_otp" target="otp_valid"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e28" value="Ya" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="otp_valid" target="new_pass"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e29" value="Tidak" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="otp_valid" target="otp_fail"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e30" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="otp_fail" target="input_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e31" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="new_pass" target="reset_api"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e32" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="reset_api" target="update_pass"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="update_pass" target="reset_ok"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="e34" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;fontColor=#2D2520;" edge="1" parent="1" source="reset_ok" target="login_page"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
|
After Width: | Height: | Size: 196 KiB |
|
|
@ -0,0 +1,113 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1700" height="2420" viewBox="0 0 1700 2420"><rect width="1700" height="2420" fill="#F7F3EE"/>
|
||||||
|
<defs><style>.title{font-family:Arial,sans-serif;font-size:28px;font-weight:700;fill:#2D2520}.label{font-family:Arial,sans-serif;font-size:16px;fill:#2D2520}.small{font-family:Arial,sans-serif;font-size:13px;fill:#4B5563}.box{fill:#fff;stroke:#8B5E3C;stroke-width:2}.data{fill:#EAF4FF;stroke:#2B6CB0;stroke-width:2}.decision{fill:#FFF7D6;stroke:#B7791F;stroke-width:2}.term{fill:#EAF8EF;stroke:#2E7D32;stroke-width:2}.db{fill:#EFF2F5;stroke:#4B5563;stroke-width:2}.mail{fill:#F0FDF4;stroke:#16A34A;stroke-width:2}.err{fill:#FFECEC;stroke:#DC2626;stroke-width:2}.lane{fill:#fff;stroke:#8B5E3C;stroke-width:2}.line{stroke:#4B3A31;stroke-width:2;fill:none}</style><marker id="arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#4B3A31"/></marker></defs>
|
||||||
|
<text x="850" y="50" text-anchor="middle" class="title">Flowchart Detail Login dan Lupa Password OTP Email</text>
|
||||||
|
<ellipse cx="850.0" cy="117.5" rx="90.0" ry="27.5" class="term"/>
|
||||||
|
<text x="850.0" y="109.5" text-anchor="middle" class="label">Mulai</text>
|
||||||
|
<rect x="755" y="170" width="190" height="55" rx="10" class="box"/>
|
||||||
|
<text x="850.0" y="189.5" text-anchor="middle" class="label">Splash Screen</text>
|
||||||
|
<polygon points="850.0,260 980,305.0 850.0,350 720,305.0" class="decision"/>
|
||||||
|
<text x="850.0" y="297.0" text-anchor="middle" class="label">Apakah sesi login</text><text x="850.0" y="317.0" text-anchor="middle" class="label">masih aktif?</text>
|
||||||
|
<ellipse cx="1240.0" cy="307.5" rx="130.0" ry="32.5" class="term"/>
|
||||||
|
<text x="1240.0" y="299.5" text-anchor="middle" class="label">Masuk ke</text><text x="1240.0" y="319.5" text-anchor="middle" class="label">Dashboard Utama</text>
|
||||||
|
<rect x="725" y="450" width="250" height="65" rx="10" class="box"/>
|
||||||
|
<text x="850.0" y="474.5" text-anchor="middle" class="label">Tampilkan</text><text x="850.0" y="494.5" text-anchor="middle" class="label">Halaman Login</text>
|
||||||
|
<polygon points="850.0,550 980,592.5 850.0,635 720,592.5" class="decision"/>
|
||||||
|
<text x="850.0" y="584.5" text-anchor="middle" class="label">Pilih aksi pada</text><text x="850.0" y="604.5" text-anchor="middle" class="label">halaman login</text>
|
||||||
|
<rect x="60" y="705" width="680" height="1125" rx="14" class="lane"/>
|
||||||
|
<text x="400" y="740" text-anchor="middle" class="label">Alur Login Normal</text>
|
||||||
|
<rect x="820" y="705" width="820" height="1270" rx="14" class="lane"/>
|
||||||
|
<text x="1230" y="740" text-anchor="middle" class="label">Alur Lupa Password dengan OTP Email</text>
|
||||||
|
<polygon points="270,815 555,815 530,885 245,885" class="data"/>
|
||||||
|
<text x="400.0" y="842.0" text-anchor="middle" class="label">Input username/email</text><text x="400.0" y="862.0" text-anchor="middle" class="label">dan password</text>
|
||||||
|
<rect x="255" y="930" width="290" height="65" rx="10" class="box"/>
|
||||||
|
<text x="400.0" y="954.5" text-anchor="middle" class="label">Kirim data login ke API</text><text x="400.0" y="974.5" text-anchor="middle" class="label">/api/login</text>
|
||||||
|
<rect x="260" y="1040" width="280" height="65" rx="24" class="db"/>
|
||||||
|
<text x="400.0" y="1064.5" text-anchor="middle" class="label">Validasi akun pada</text><text x="400.0" y="1084.5" text-anchor="middle" class="label">tabel login</text>
|
||||||
|
<polygon points="400.0,1160 530,1205.0 400.0,1250 270,1205.0" class="decision"/>
|
||||||
|
<text x="400.0" y="1197.0" text-anchor="middle" class="label">Apakah data</text><text x="400.0" y="1217.0" text-anchor="middle" class="label">login valid?</text>
|
||||||
|
<ellipse cx="400.0" cy="1350.0" rx="150.0" ry="40.0" class="term"/>
|
||||||
|
<text x="400.0" y="1342.0" text-anchor="middle" class="label">Login berhasil,</text><text x="400.0" y="1362.0" text-anchor="middle" class="label">masuk dashboard</text>
|
||||||
|
<rect x="105" y="1435" width="270" height="65" rx="10" class="err"/>
|
||||||
|
<text x="240.0" y="1459.5" text-anchor="middle" class="label">Tampilkan pesan</text><text x="240.0" y="1479.5" text-anchor="middle" class="label">login gagal</text>
|
||||||
|
<rect x="105" y="1545" width="270" height="65" rx="10" class="box"/>
|
||||||
|
<text x="240.0" y="1569.5" text-anchor="middle" class="label">Ulangi input username/email</text><text x="240.0" y="1589.5" text-anchor="middle" class="label">dan password</text>
|
||||||
|
<polygon points="1100,815 1385,815 1360,885 1075,885" class="data"/>
|
||||||
|
<text x="1230.0" y="842.0" text-anchor="middle" class="label">Input username/email</text><text x="1230.0" y="862.0" text-anchor="middle" class="label">akun</text>
|
||||||
|
<rect x="1045" y="930" width="370" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1230.0" y="957.0" text-anchor="middle" class="label">Request OTP ke API</text><text x="1230.0" y="977.0" text-anchor="middle" class="label">/api/forgot-password/send-otp</text>
|
||||||
|
<rect x="1090" y="1045" width="280" height="65" rx="24" class="db"/>
|
||||||
|
<text x="1230.0" y="1069.5" text-anchor="middle" class="label">Cek akun pada</text><text x="1230.0" y="1089.5" text-anchor="middle" class="label">tabel login</text>
|
||||||
|
<polygon points="1230.0,1160 1390,1205.0 1230.0,1250 1070,1205.0" class="decision"/>
|
||||||
|
<text x="1230.0" y="1197.0" text-anchor="middle" class="label">Apakah email/username</text><text x="1230.0" y="1217.0" text-anchor="middle" class="label">terdaftar?</text>
|
||||||
|
<rect x="1390" y="1170" width="300" height="65" rx="10" class="err"/>
|
||||||
|
<text x="1540.0" y="1194.5" text-anchor="middle" class="label">Tampilkan pesan akun</text><text x="1540.0" y="1214.5" text-anchor="middle" class="label">tidak ditemukan</text>
|
||||||
|
<rect x="1390" y="1270" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="1540.0" y="1294.5" text-anchor="middle" class="label">Ulangi input username/email</text><text x="1540.0" y="1314.5" text-anchor="middle" class="label">yang terdaftar</text>
|
||||||
|
<rect x="1050" y="1305" width="360" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1230.0" y="1332.0" text-anchor="middle" class="label">Generate kode OTP 6 digit</text><text x="1230.0" y="1352.0" text-anchor="middle" class="label">masa berlaku 10 menit</text>
|
||||||
|
<rect x="1050" y="1415" width="360" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1230.0" y="1442.0" text-anchor="middle" class="label">Simpan OTP ke tabel</text><text x="1230.0" y="1462.0" text-anchor="middle" class="label">password_reset_otps</text>
|
||||||
|
<rect x="1050" y="1525" width="360" height="70" rx="10" class="mail"/>
|
||||||
|
<text x="1230.0" y="1552.0" text-anchor="middle" class="label">Kirim OTP ke email pengguna</text><text x="1230.0" y="1572.0" text-anchor="middle" class="label">melalui SMTP</text>
|
||||||
|
<polygon points="1100,1635 1385,1635 1360,1705 1075,1705" class="data"/>
|
||||||
|
<text x="1230.0" y="1662.0" text-anchor="middle" class="label">Input kode OTP</text><text x="1230.0" y="1682.0" text-anchor="middle" class="label">dari email</text>
|
||||||
|
<rect x="1045" y="1745" width="370" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1230.0" y="1772.0" text-anchor="middle" class="label">Verifikasi OTP ke API</text><text x="1230.0" y="1792.0" text-anchor="middle" class="label">/api/forgot-password/verify-otp</text>
|
||||||
|
<rect x="1045" y="1855" width="370" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1230.0" y="1882.0" text-anchor="middle" class="label">Cek OTP: sesuai, belum dipakai,</text><text x="1230.0" y="1902.0" text-anchor="middle" class="label">dan belum kedaluwarsa</text>
|
||||||
|
<polygon points="1230.0,1960 1360,2005.0 1230.0,2050 1100,2005.0" class="decision"/>
|
||||||
|
<text x="1230.0" y="1997.0" text-anchor="middle" class="label">Apakah OTP</text><text x="1230.0" y="2017.0" text-anchor="middle" class="label">valid?</text>
|
||||||
|
<rect x="1390" y="1975" width="300" height="65" rx="10" class="err"/>
|
||||||
|
<text x="1540.0" y="1999.5" text-anchor="middle" class="label">Tampilkan pesan OTP</text><text x="1540.0" y="2019.5" text-anchor="middle" class="label">salah/kedaluwarsa</text>
|
||||||
|
<rect x="1390" y="2075" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="1540.0" y="2099.5" text-anchor="middle" class="label">Ulangi input OTP</text><text x="1540.0" y="2119.5" text-anchor="middle" class="label">atau kirim OTP baru</text>
|
||||||
|
<polygon points="685,2075 960,2075 935,2145 660,2145" class="data"/>
|
||||||
|
<text x="810.0" y="2102.0" text-anchor="middle" class="label">Input password</text><text x="810.0" y="2122.0" text-anchor="middle" class="label">baru</text>
|
||||||
|
<rect x="660" y="2185" width="380" height="70" rx="10" class="box"/>
|
||||||
|
<text x="850.0" y="2212.0" text-anchor="middle" class="label">Request reset password ke API</text><text x="850.0" y="2232.0" text-anchor="middle" class="label">/api/forgot-password/reset</text>
|
||||||
|
<rect x="1080" y="2185" width="390" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1275.0" y="2212.0" text-anchor="middle" class="label">Update password tabel login</text><text x="1275.0" y="2232.0" text-anchor="middle" class="label">dan tandai OTP digunakan</text>
|
||||||
|
<ellipse cx="1275.0" cy="2335.0" rx="155.0" ry="35.0" class="term"/>
|
||||||
|
<text x="1275.0" y="2327.0" text-anchor="middle" class="label">Reset berhasil,</text><text x="1275.0" y="2347.0" text-anchor="middle" class="label">kembali ke halaman login</text>
|
||||||
|
<path d="M850.0,145 L850.0,170" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M850.0,225 L850.0,260" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M850.0,515 L850.0,550" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M400.0,885 L400.0,930" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M400.0,995 L400.0,1040" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M400.0,1105 L400.0,1160" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M240.0,1500 L240.0,1545" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,885 L1230.0,930" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1000 L1230.0,1045" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1110 L1230.0,1160" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1540.0,1235 L1540.0,1270" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1375 L1230.0,1415" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1485 L1230.0,1525" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1595 L1230.0,1635" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1705 L1230.0,1745" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1815 L1230.0,1855" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230.0,1925 L1230.0,1960" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1540.0,2040 L1540.0,2075" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M810.0,2145 L850.0,2185" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1275.0,2255 L1275.0,2300" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M980,305.0 L1110,307" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1050" y="296" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M850.0,350 L850,450" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="878" y="405" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M720,592.5 L400,592 L400,815" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="550" y="590" text-anchor="middle" class="small">Login</text>
|
||||||
|
<path d="M980,592.5 L1230,592 L1230,815" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1080" y="590" text-anchor="middle" class="small">Lupa password</text>
|
||||||
|
<path d="M400.0,1250 L400,1310" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="430" y="1290" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M270,1205.0 L190,1205 L190,1468 L375,1468" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="220" y="1218" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1230.0,1250 L1230,1305" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1258" y="1285" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M1390,1205.0 L1390,1202" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1408" y="1188" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1100,2005.0 L810,2005 L810,2075 L960,2110" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1005" y="1995" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M1360,2005.0 L1390,2007" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1375" y="1992" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1040,2220.0 L1080,2220" class="line" marker-end="url(#arrow)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,52 @@
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-05-29T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
|
||||||
|
<diagram id="bab3" name="Page-1">
|
||||||
|
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1000" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="ftitle" value="Flowchart Sistem Aplikasi Tobaku Sulastri" style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=24;fontStyle=1;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="450" y="25" width="700" height="40" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="start" value="Mulai" style="ellipse;whiteSpace=wrap;html=1;fillColor=#EAF8EF;strokeColor=#2E7D32;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;fontColor=#1F3D2A;" vertex="1" parent="1"><mxGeometry x="710" y="90" width="160" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="splash" value="Splash Screen" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="700" y="170" width="180" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="sess" value="Sesi login tersedia?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="685" y="255" width="210" height="80" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="login" value="Input username/email dan password" style="shape=parallelogram;perimeter=parallelogramPerimeter;whiteSpace=wrap;html=1;fillColor=#EAF4FF;strokeColor=#2B6CB0;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#1A365D;" vertex="1" parent="1"><mxGeometry x="420" y="375" width="240" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="valid" value="Login valid?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="435" y="475" width="210" height="80" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="err" value="Tampilkan pesan gagal login" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="170" y="485" width="210" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="dash" value="Dashboard utama" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="705" y="385" width="190" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="menu" value="Pilih menu aplikasi" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="695" y="475" width="210" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="produk" value="Data Produk/Stok: lihat, filter, tambah bahan" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="280" y="620" width="250" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="trans" value="Transaksi Stok Masuk: pilih bahan, jumlah, tanggal" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="560" y="620" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="pred" value="Prediksi/Produksi: pilih resep dan jumlah produksi" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="850" y="620" width="270" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="calc" value="Hitung kebutuhan bahan dan cek stok" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="860" y="735" width="250" height="60" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="cukup" value="Stok cukup?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFF7D6;strokeColor=#B7791F;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="885" y="825" width="200" height="75" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="lap" value="Laporan: stok, stok masuk, prediksi, kritis" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1160" y="620" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="settings" value="Pengaturan akun, ubah/reset password" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;arcSize=10;" vertex="1" parent="1"><mxGeometry x="1160" y="735" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="db" value="Database MySQL + Model Random Forest" style="shape=cylinder3d;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#EFF2F5;strokeColor=#4B5563;strokeWidth=2;fontFamily=Arial;fontSize=13;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="630" y="870" width="330" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="end" value="Selesai / Logout" style="ellipse;whiteSpace=wrap;html=1;fillColor=#EAF8EF;strokeColor=#2E7D32;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;fontColor=#1F3D2A;" vertex="1" parent="1"><mxGeometry x="1165" y="870" width="210" height="55" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe0" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="start" target="splash"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe1" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="splash" target="sess"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe2" value="Ya" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="sess" target="dash"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe3" value="Tidak" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="sess" target="login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe4" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="login" target="valid"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe5" value="Ya" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="valid" target="dash"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe6" value="Tidak" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="valid" target="err"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe7" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="err" target="login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe8" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="dash" target="menu"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe9" value="Produk" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="menu" target="produk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe10" value="Transaksi" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="menu" target="trans"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe11" value="Prediksi" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="menu" target="pred"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe12" value="Laporan" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="menu" target="lap"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe13" value="Setting" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="menu" target="settings"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe14" value="Simpan/Ambil data" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="produk" target="db"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe15" value="Tambah stok" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="trans" target="db"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe16" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="pred" target="calc"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe17" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="calc" target="cukup"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe18" value="Ya, kurangi stok + simpan prediksi" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="cukup" target="db"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe19" value="Tidak, update stok" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="cukup" target="produk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe20" value="Ambil data laporan" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="lap" target="db"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe21" value="Update akun/OTP" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="settings" target="db"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="fe22" value="" style="endArrow=block;html=1;rounded=0;strokeColor=#4B3A31;fontFamily=Arial;fontSize=12;" edge="1" parent="1" source="db" target="end"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
|
After Width: | Height: | Size: 116 KiB |
|
|
@ -0,0 +1,60 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="980" viewBox="0 0 1500 980"><rect width="1500" height="980" fill="#F7F3EE"/>
|
||||||
|
<defs><style>.title{font-family:Arial,sans-serif;font-size:28px;font-weight:700;fill:#2D2520}.label{font-family:Arial,sans-serif;font-size:16px;fill:#2D2520}.small{font-family:Arial,sans-serif;font-size:13px;fill:#4B5563}.box{fill:#fff;stroke:#8B5E3C;stroke-width:2}.note{fill:#EAF4FF;stroke:#2B6CB0;stroke-width:2}.decision{fill:#FFF7D6;stroke:#B7791F;stroke-width:2}.term{fill:#EAF8EF;stroke:#2E7D32;stroke-width:2}.db{fill:#EFF2F5;stroke:#4B5563;stroke-width:2}.line{stroke:#4B3A31;stroke-width:2;fill:none}.dash{stroke:#6B7280;stroke-width:1.8;stroke-dasharray:7 5;fill:none}</style><marker id="arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#4B3A31"/></marker><marker id="arrow2" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#6B7280"/></marker></defs>
|
||||||
|
<text x="750" y="50" text-anchor="middle" class="title">Flowchart Sistem Aplikasi Tobaku Sulastri</text>
|
||||||
|
<ellipse cx="750.0" cy="112.5" rx="80.0" ry="27.5" class="term"/>
|
||||||
|
<text x="750.0" y="107.5" text-anchor="middle" class="label">Mulai</text>
|
||||||
|
<rect x="660" y="165" width="180" height="55" rx="10" class="box"/>
|
||||||
|
<text x="750.0" y="187.5" text-anchor="middle" class="label">Splash Screen</text>
|
||||||
|
<polygon points="750.0,250.0 855.0,290.0 750.0,330.0 645.0,290.0" class="decision"/>
|
||||||
|
<text x="750.0" y="285.0" text-anchor="middle" class="label">Sesi login</text><text x="750.0" y="305.0" text-anchor="middle" class="label">tersedia?</text>
|
||||||
|
<polygon points="400,365 625,365 600,430 375,430" class="note"/>
|
||||||
|
<text x="500.0" y="392.5" text-anchor="middle" class="label">Input username/email</text><text x="500.0" y="412.5" text-anchor="middle" class="label">dan password</text>
|
||||||
|
<polygon points="500.0,465.0 605.0,505.0 500.0,545.0 395.0,505.0" class="decision"/>
|
||||||
|
<text x="500.0" y="500.0" text-anchor="middle" class="label">Login valid?</text>
|
||||||
|
<rect x="150" y="480" width="210" height="60" rx="10" class="box"/>
|
||||||
|
<text x="255.0" y="505.0" text-anchor="middle" class="label">Tampilkan pesan</text><text x="255.0" y="525.0" text-anchor="middle" class="label">gagal login</text>
|
||||||
|
<rect x="670" y="375" width="190" height="60" rx="10" class="box"/>
|
||||||
|
<text x="765.0" y="400.0" text-anchor="middle" class="label">Dashboard utama</text>
|
||||||
|
<polygon points="765.0,465.0 870.0,502.5 765.0,540.0 660.0,502.5" class="decision"/>
|
||||||
|
<text x="765.0" y="497.5" text-anchor="middle" class="label">Pilih menu</text><text x="765.0" y="517.5" text-anchor="middle" class="label">aplikasi</text>
|
||||||
|
<rect x="250" y="620" width="260" height="75" rx="10" class="box"/>
|
||||||
|
<text x="380.0" y="652.5" text-anchor="middle" class="label">Data Produk/Stok:</text><text x="380.0" y="672.5" text-anchor="middle" class="label">lihat, filter, tambah bahan</text>
|
||||||
|
<rect x="540" y="620" width="270" height="75" rx="10" class="box"/>
|
||||||
|
<text x="675.0" y="652.5" text-anchor="middle" class="label">Transaksi Stok Masuk:</text><text x="675.0" y="672.5" text-anchor="middle" class="label">pilih bahan, jumlah, tanggal</text>
|
||||||
|
<rect x="840" y="620" width="270" height="75" rx="10" class="box"/>
|
||||||
|
<text x="975.0" y="652.5" text-anchor="middle" class="label">Prediksi/Produksi:</text><text x="975.0" y="672.5" text-anchor="middle" class="label">pilih resep dan jumlah</text>
|
||||||
|
<rect x="850" y="735" width="250" height="65" rx="10" class="box"/>
|
||||||
|
<text x="975.0" y="762.5" text-anchor="middle" class="label">Hitung kebutuhan</text><text x="975.0" y="782.5" text-anchor="middle" class="label">dan cek stok</text>
|
||||||
|
<polygon points="975.0,825.0 1075.0,862.5 975.0,900.0 875.0,862.5" class="decision"/>
|
||||||
|
<text x="975.0" y="857.5" text-anchor="middle" class="label">Stok cukup?</text>
|
||||||
|
<rect x="1150" y="620" width="270" height="75" rx="10" class="box"/>
|
||||||
|
<text x="1285.0" y="652.5" text-anchor="middle" class="label">Laporan stok, stok masuk,</text><text x="1285.0" y="672.5" text-anchor="middle" class="label">prediksi, bahan kritis</text>
|
||||||
|
<rect x="1150" y="735" width="270" height="75" rx="10" class="box"/>
|
||||||
|
<text x="1285.0" y="767.5" text-anchor="middle" class="label">Pengaturan akun,</text><text x="1285.0" y="787.5" text-anchor="middle" class="label">ubah/reset password</text>
|
||||||
|
<rect x="610" y="875" width="330" height="70" rx="25" class="db"/>
|
||||||
|
<text x="775.0" y="905.0" text-anchor="middle" class="label">Database MySQL +</text><text x="775.0" y="925.0" text-anchor="middle" class="label">Model Random Forest</text>
|
||||||
|
<ellipse cx="1265.0" cy="902.5" rx="105.0" ry="27.5" class="term"/>
|
||||||
|
<text x="1265.0" y="897.5" text-anchor="middle" class="label">Selesai / Logout</text>
|
||||||
|
<path d="M750,140 L750,165" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M750,220 L750,250" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M750,330 L765,375" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M645,290 L625,395" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M500,430 L500,465" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M395,505 L360,510" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M150,510 L375,395" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M765,435 L765,465" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M660,505 L510,620" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M765,540 L675,620" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M870,505 L975,620" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M870,505 L1150,657" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M870,505 L1150,772" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M975,695 L975,735" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M975,800 L975,825" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M875,862 L510,657" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M975,900 L940,910" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1285,695 L1285,735" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1285,810 L1285,875" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1285,657 L940,910" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M675,695 L675,875" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M380,695 L610,910" class="line" marker-end="url(#arrow)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
|
@ -0,0 +1,215 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="2200" height="3600" viewBox="0 0 2200 3600"><rect width="2200" height="3600" fill="#F7F3EE"/>
|
||||||
|
<defs><style>.title{font-family:Arial,sans-serif;font-size:30px;font-weight:700;fill:#2D2520}.label{font-family:Arial,sans-serif;font-size:16px;fill:#2D2520}.small{font-family:Arial,sans-serif;font-size:13px;fill:#4B5563}.lane-title{font-family:Arial,sans-serif;font-size:18px;font-weight:700;fill:#2D2520}.box{fill:#fff;stroke:#8B5E3C;stroke-width:2}.data{fill:#EAF4FF;stroke:#2B6CB0;stroke-width:2}.decision{fill:#FFF7D6;stroke:#B7791F;stroke-width:2}.term{fill:#EAF8EF;stroke:#2E7D32;stroke-width:2}.db{fill:#EFF2F5;stroke:#4B5563;stroke-width:2}.mail{fill:#F0FDF4;stroke:#16A34A;stroke-width:2}.err{fill:#FFECEC;stroke:#DC2626;stroke-width:2}.lane{fill:#fff;stroke:#8B5E3C;stroke-width:2}.line{stroke:#4B3A31;stroke-width:2;fill:none}</style><marker id="arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#4B3A31"/></marker></defs>
|
||||||
|
<text x="1100.0" y="50" text-anchor="middle" class="title">Flowchart Sistem Lengkap Aplikasi Tobaku Sulastri</text>
|
||||||
|
<rect x="70" y="90" width="2060" height="2180" rx="14" class="lane"/>
|
||||||
|
<text x="1100" y="130" text-anchor="middle" class="lane-title">A. Alur Autentikasi Pengguna</text>
|
||||||
|
<rect x="110" y="650" width="760" height="620" rx="14" class="lane"/>
|
||||||
|
<text x="490" y="690" text-anchor="middle" class="lane-title">Login Normal</text>
|
||||||
|
<rect x="950" y="650" width="1140" height="1520" rx="14" class="lane"/>
|
||||||
|
<text x="1520" y="690" text-anchor="middle" class="lane-title">Lupa Password dengan OTP Email</text>
|
||||||
|
<rect x="70" y="2300" width="2060" height="1120" rx="14" class="lane"/>
|
||||||
|
<text x="1100" y="2340" text-anchor="middle" class="lane-title">B. Alur Menu Utama Setelah Masuk Dashboard</text>
|
||||||
|
<ellipse cx="1100.0" cy="157.5" rx="90.0" ry="27.5" class="term"/>
|
||||||
|
<text x="1100.0" y="149.5" text-anchor="middle" class="label">Mulai</text>
|
||||||
|
<rect x="1005" y="210" width="190" height="55" rx="10" class="box"/>
|
||||||
|
<text x="1100.0" y="229.5" text-anchor="middle" class="label">Splash Screen</text>
|
||||||
|
<polygon points="1100.0,310 1230,355.0 1100.0,400 970,355.0" class="decision"/>
|
||||||
|
<text x="1100.0" y="347.0" text-anchor="middle" class="label">Apakah sesi login</text><text x="1100.0" y="367.0" text-anchor="middle" class="label">masih aktif?</text>
|
||||||
|
<ellipse cx="1465.0" cy="357.5" rx="135.0" ry="32.5" class="term"/>
|
||||||
|
<text x="1465.0" y="349.5" text-anchor="middle" class="label">Sesi aktif, lanjut</text><text x="1465.0" y="369.5" text-anchor="middle" class="label">ke aplikasi</text>
|
||||||
|
<rect x="985" y="490" width="230" height="60" rx="10" class="box"/>
|
||||||
|
<text x="1100.0" y="512.0" text-anchor="middle" class="label">Halaman Login</text>
|
||||||
|
<polygon points="1100.0,590 1240,632.5 1100.0,675 960,632.5" class="decision"/>
|
||||||
|
<text x="1100.0" y="624.5" text-anchor="middle" class="label">Pilih aksi pada</text><text x="1100.0" y="644.5" text-anchor="middle" class="label">halaman login</text>
|
||||||
|
<polygon points="324,740 640,740 616,810 300,810" class="data"/>
|
||||||
|
<text x="470.0" y="767.0" text-anchor="middle" class="label">Input username/email</text><text x="470.0" y="787.0" text-anchor="middle" class="label">dan password</text>
|
||||||
|
<rect x="325" y="850" width="290" height="65" rx="10" class="box"/>
|
||||||
|
<text x="470.0" y="874.5" text-anchor="middle" class="label">Kirim data ke API</text><text x="470.0" y="894.5" text-anchor="middle" class="label">/api/login</text>
|
||||||
|
<rect x="325" y="950" width="290" height="65" rx="24" class="db"/>
|
||||||
|
<text x="470.0" y="974.5" text-anchor="middle" class="label">Validasi akun pada</text><text x="470.0" y="994.5" text-anchor="middle" class="label">tabel login</text>
|
||||||
|
<polygon points="470.0,1060 600,1102.5 470.0,1145 340,1102.5" class="decision"/>
|
||||||
|
<text x="470.0" y="1094.5" text-anchor="middle" class="label">Apakah data</text><text x="470.0" y="1114.5" text-anchor="middle" class="label">login valid?</text>
|
||||||
|
<ellipse cx="470.0" cy="1222.5" rx="160.0" ry="32.5" class="term"/>
|
||||||
|
<text x="470.0" y="1214.5" text-anchor="middle" class="label">Login berhasil</text><text x="470.0" y="1234.5" text-anchor="middle" class="label">dan simpan sesi</text>
|
||||||
|
<rect x="90" y="1190" width="270" height="65" rx="10" class="err"/>
|
||||||
|
<text x="225.0" y="1214.5" text-anchor="middle" class="label">Tampilkan pesan</text><text x="225.0" y="1234.5" text-anchor="middle" class="label">login gagal</text>
|
||||||
|
<rect x="90" y="1285" width="270" height="55" rx="10" class="box"/>
|
||||||
|
<text x="225.0" y="1304.5" text-anchor="middle" class="label">Ulangi input</text><text x="225.0" y="1324.5" text-anchor="middle" class="label">data login</text>
|
||||||
|
<polygon points="1424,740 1730,740 1706,810 1400,810" class="data"/>
|
||||||
|
<text x="1565.0" y="767.0" text-anchor="middle" class="label">Input username/email</text><text x="1565.0" y="787.0" text-anchor="middle" class="label">akun</text>
|
||||||
|
<rect x="1365" y="850" width="390" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1560.0" y="877.0" text-anchor="middle" class="label">Request OTP ke API</text><text x="1560.0" y="897.0" text-anchor="middle" class="label">/api/forgot-password/send-otp</text>
|
||||||
|
<rect x="1410" y="960" width="300" height="65" rx="24" class="db"/>
|
||||||
|
<text x="1560.0" y="984.5" text-anchor="middle" class="label">Cek akun pada</text><text x="1560.0" y="1004.5" text-anchor="middle" class="label">tabel login</text>
|
||||||
|
<polygon points="1560.0,1070 1690,1112.5 1560.0,1155 1430,1112.5" class="decision"/>
|
||||||
|
<text x="1560.0" y="1104.5" text-anchor="middle" class="label">Apakah akun</text><text x="1560.0" y="1124.5" text-anchor="middle" class="label">ditemukan?</text>
|
||||||
|
<rect x="1790" y="1078" width="300" height="65" rx="10" class="err"/>
|
||||||
|
<text x="1940.0" y="1102.5" text-anchor="middle" class="label">Tampilkan pesan akun</text><text x="1940.0" y="1122.5" text-anchor="middle" class="label">tidak ditemukan</text>
|
||||||
|
<rect x="1790" y="1175" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="1940.0" y="1199.5" text-anchor="middle" class="label">Ulangi input email/username</text><text x="1940.0" y="1219.5" text-anchor="middle" class="label">yang terdaftar</text>
|
||||||
|
<rect x="1375" y="1195" width="370" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1560.0" y="1222.0" text-anchor="middle" class="label">Generate OTP 6 digit</text><text x="1560.0" y="1242.0" text-anchor="middle" class="label">masa berlaku 10 menit</text>
|
||||||
|
<rect x="1375" y="1305" width="370" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1560.0" y="1332.0" text-anchor="middle" class="label">Simpan OTP ke tabel</text><text x="1560.0" y="1352.0" text-anchor="middle" class="label">password_reset_otps</text>
|
||||||
|
<rect x="1375" y="1415" width="370" height="70" rx="10" class="mail"/>
|
||||||
|
<text x="1560.0" y="1442.0" text-anchor="middle" class="label">Kirim OTP ke email</text><text x="1560.0" y="1462.0" text-anchor="middle" class="label">pengguna via SMTP</text>
|
||||||
|
<polygon points="1419,1525 1725,1525 1701,1595 1395,1595" class="data"/>
|
||||||
|
<text x="1560.0" y="1552.0" text-anchor="middle" class="label">Input kode OTP</text><text x="1560.0" y="1572.0" text-anchor="middle" class="label">dari email</text>
|
||||||
|
<rect x="1365" y="1635" width="390" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1560.0" y="1662.0" text-anchor="middle" class="label">Verifikasi OTP ke API</text><text x="1560.0" y="1682.0" text-anchor="middle" class="label">/api/forgot-password/verify-otp</text>
|
||||||
|
<rect x="1365" y="1745" width="390" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1560.0" y="1772.0" text-anchor="middle" class="label">Cek OTP: sesuai, belum dipakai,</text><text x="1560.0" y="1792.0" text-anchor="middle" class="label">dan belum kedaluwarsa</text>
|
||||||
|
<polygon points="1560.0,1855 1690,1897.5 1560.0,1940 1430,1897.5" class="decision"/>
|
||||||
|
<text x="1560.0" y="1889.5" text-anchor="middle" class="label">Apakah OTP</text><text x="1560.0" y="1909.5" text-anchor="middle" class="label">valid?</text>
|
||||||
|
<rect x="1790" y="1863" width="300" height="65" rx="10" class="err"/>
|
||||||
|
<text x="1940.0" y="1887.5" text-anchor="middle" class="label">Tampilkan pesan OTP</text><text x="1940.0" y="1907.5" text-anchor="middle" class="label">salah/kedaluwarsa</text>
|
||||||
|
<rect x="1790" y="1960" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="1940.0" y="1984.5" text-anchor="middle" class="label">Ulangi OTP atau</text><text x="1940.0" y="2004.5" text-anchor="middle" class="label">kirim OTP baru</text>
|
||||||
|
<polygon points="1024,1960 1320,1960 1296,2030 1000,2030" class="data"/>
|
||||||
|
<text x="1160.0" y="1987.0" text-anchor="middle" class="label">Input password baru</text>
|
||||||
|
<rect x="995" y="2070" width="350" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1170.0" y="2097.0" text-anchor="middle" class="label">Request reset password</text><text x="1170.0" y="2117.0" text-anchor="middle" class="label">/api/forgot-password/reset</text>
|
||||||
|
<rect x="1380" y="2070" width="390" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1575.0" y="2097.0" text-anchor="middle" class="label">Update password tabel login</text><text x="1575.0" y="2117.0" text-anchor="middle" class="label">dan tandai OTP digunakan</text>
|
||||||
|
<ellipse cx="1570.0" cy="2212.5" rx="165.0" ry="32.5" class="term"/>
|
||||||
|
<text x="1570.0" y="2204.5" text-anchor="middle" class="label">Reset berhasil,</text><text x="1570.0" y="2224.5" text-anchor="middle" class="label">kembali ke halaman login</text>
|
||||||
|
<ellipse cx="1100.0" cy="2215.0" rx="170.0" ry="35.0" class="term"/>
|
||||||
|
<text x="1100.0" y="2207.0" text-anchor="middle" class="label">Autentikasi berhasil</text><text x="1100.0" y="2227.0" text-anchor="middle" class="label">atau sesi masih aktif</text>
|
||||||
|
<rect x="800" y="2410" width="600" height="75" rx="10" class="box"/>
|
||||||
|
<text x="1100.0" y="2439.5" text-anchor="middle" class="label">Dashboard Utama</text><text x="1100.0" y="2459.5" text-anchor="middle" class="label">Ringkasan stok, bahan digunakan hari ini, stok kritis</text>
|
||||||
|
<polygon points="1100.0,2530 1240,2572.5 1100.0,2615 960,2572.5" class="decision"/>
|
||||||
|
<text x="1100.0" y="2564.5" text-anchor="middle" class="label">Pilih menu aplikasi</text>
|
||||||
|
<rect x="110" y="2680" width="300" height="60" rx="10" class="box"/>
|
||||||
|
<text x="260.0" y="2702.0" text-anchor="middle" class="label">Menu Produk/Stok</text>
|
||||||
|
<rect x="110" y="2775" width="300" height="65" rx="24" class="db"/>
|
||||||
|
<text x="260.0" y="2799.5" text-anchor="middle" class="label">Ambil data produk</text><text x="260.0" y="2819.5" text-anchor="middle" class="label">GET /products</text>
|
||||||
|
<rect x="90" y="2880" width="340" height="80" rx="10" class="box"/>
|
||||||
|
<text x="260.0" y="2912.0" text-anchor="middle" class="label">Lihat/filter stok, cek status kritis,</text><text x="260.0" y="2932.0" text-anchor="middle" class="label">dan tambah bahan</text>
|
||||||
|
<rect x="105" y="3000" width="310" height="70" rx="24" class="db"/>
|
||||||
|
<text x="260.0" y="3027.0" text-anchor="middle" class="label">Jika tambah bahan:</text><text x="260.0" y="3047.0" text-anchor="middle" class="label">simpan ke tabel products</text>
|
||||||
|
<ellipse cx="260.0" cy="3145.0" rx="155.0" ry="30.0" class="term"/>
|
||||||
|
<text x="260.0" y="3137.0" text-anchor="middle" class="label">Data stok diperbarui</text><text x="260.0" y="3157.0" text-anchor="middle" class="label">dan dashboard bisa direfresh</text>
|
||||||
|
<rect x="520" y="2680" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="670.0" y="2704.5" text-anchor="middle" class="label">Menu Transaksi</text><text x="670.0" y="2724.5" text-anchor="middle" class="label">Stok Masuk</text>
|
||||||
|
<polygon points="544,2780 820,2780 796,2850 520,2850" class="data"/>
|
||||||
|
<text x="670.0" y="2807.0" text-anchor="middle" class="label">Pilih bahan, jumlah,</text><text x="670.0" y="2827.0" text-anchor="middle" class="label">harga, dan tanggal</text>
|
||||||
|
<rect x="520" y="2890" width="300" height="65" rx="10" class="box"/>
|
||||||
|
<text x="670.0" y="2914.5" text-anchor="middle" class="label">Simpan transaksi</text><text x="670.0" y="2934.5" text-anchor="middle" class="label">POST /transaksi</text>
|
||||||
|
<rect x="500" y="3000" width="340" height="75" rx="24" class="db"/>
|
||||||
|
<text x="670.0" y="3029.5" text-anchor="middle" class="label">Update current_stock</text><text x="670.0" y="3049.5" text-anchor="middle" class="label">dan tabel Stock In/transactions</text>
|
||||||
|
<ellipse cx="670.0" cy="3155.0" rx="170.0" ry="30.0" class="term"/>
|
||||||
|
<text x="670.0" y="3147.0" text-anchor="middle" class="label">Stok masuk tersimpan</text><text x="670.0" y="3167.0" text-anchor="middle" class="label">dan stok bahan bertambah</text>
|
||||||
|
<rect x="930" y="2680" width="330" height="60" rx="10" class="box"/>
|
||||||
|
<text x="1095.0" y="2702.0" text-anchor="middle" class="label">Menu Prediksi/Produksi</text>
|
||||||
|
<rect x="930" y="2775" width="330" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1095.0" y="2802.0" text-anchor="middle" class="label">Ambil data resep dan produk</text><text x="1095.0" y="2822.0" text-anchor="middle" class="label">GET /recipes, GET /products</text>
|
||||||
|
<polygon points="949,2885 1265,2885 1241,2960 925,2960" class="data"/>
|
||||||
|
<text x="1095.0" y="2914.5" text-anchor="middle" class="label">Pilih resep, jumlah produksi,</text><text x="1095.0" y="2934.5" text-anchor="middle" class="label">dan bahan yang digunakan</text>
|
||||||
|
<rect x="920" y="3000" width="350" height="70" rx="10" class="box"/>
|
||||||
|
<text x="1095.0" y="3027.0" text-anchor="middle" class="label">Hitung kebutuhan bahan</text><text x="1095.0" y="3047.0" text-anchor="middle" class="label">berdasarkan recipe_ingredients</text>
|
||||||
|
<polygon points="1095.0,3110 1225,3152.5 1095.0,3195 965,3152.5" class="decision"/>
|
||||||
|
<text x="1095.0" y="3144.5" text-anchor="middle" class="label">Apakah stok</text><text x="1095.0" y="3164.5" text-anchor="middle" class="label">cukup?</text>
|
||||||
|
<rect x="690" y="3130" width="280" height="65" rx="10" class="err"/>
|
||||||
|
<text x="830.0" y="3154.5" text-anchor="middle" class="label">Tampilkan bahan kurang</text><text x="830.0" y="3174.5" text-anchor="middle" class="label">dan arahkan update stok</text>
|
||||||
|
<rect x="995" y="3230" width="320" height="65" rx="10" class="box"/>
|
||||||
|
<text x="1155.0" y="3254.5" text-anchor="middle" class="label">Submit produksi</text><text x="1155.0" y="3274.5" text-anchor="middle" class="label">POST /stock/consume</text>
|
||||||
|
<rect x="965" y="3330" width="380" height="75" rx="24" class="db"/>
|
||||||
|
<text x="1155.0" y="3359.5" text-anchor="middle" class="label">Kurangi stok products, simpan</text><text x="1155.0" y="3379.5" text-anchor="middle" class="label">stock_usage_history dan predictions</text>
|
||||||
|
<ellipse cx="1155.0" cy="3475.0" rx="175.0" ry="30.0" class="term"/>
|
||||||
|
<text x="1155.0" y="3467.0" text-anchor="middle" class="label">Produksi tersimpan</text><text x="1155.0" y="3487.0" text-anchor="middle" class="label">laporan prediksi terbarui</text>
|
||||||
|
<rect x="1400" y="2680" width="300" height="60" rx="10" class="box"/>
|
||||||
|
<text x="1550.0" y="2702.0" text-anchor="middle" class="label">Menu Laporan</text>
|
||||||
|
<rect x="1380" y="2775" width="340" height="75" rx="24" class="db"/>
|
||||||
|
<text x="1550.0" y="2804.5" text-anchor="middle" class="label">Ambil laporan stok, stok masuk,</text><text x="1550.0" y="2824.5" text-anchor="middle" class="label">prediksi, dan bahan kritis</text>
|
||||||
|
<rect x="1385" y="2895" width="330" height="75" rx="10" class="box"/>
|
||||||
|
<text x="1550.0" y="2924.5" text-anchor="middle" class="label">Tampilkan grafik/tren, tabel laporan,</text><text x="1550.0" y="2944.5" text-anchor="middle" class="label">dan ringkasan</text>
|
||||||
|
<ellipse cx="1550.0" cy="3040.0" rx="155.0" ry="30.0" class="term"/>
|
||||||
|
<text x="1550.0" y="3032.0" text-anchor="middle" class="label">Laporan berhasil</text><text x="1550.0" y="3052.0" text-anchor="middle" class="label">ditampilkan</text>
|
||||||
|
<rect x="1780" y="2680" width="300" height="60" rx="10" class="box"/>
|
||||||
|
<text x="1930.0" y="2702.0" text-anchor="middle" class="label">Menu Pengaturan</text>
|
||||||
|
<rect x="1760" y="2780" width="340" height="80" rx="10" class="box"/>
|
||||||
|
<text x="1930.0" y="2812.0" text-anchor="middle" class="label">Lihat akun, ubah password, logout,</text><text x="1930.0" y="2832.0" text-anchor="middle" class="label">atau reset password OTP</text>
|
||||||
|
<rect x="1770" y="2900" width="320" height="70" rx="24" class="db"/>
|
||||||
|
<text x="1930.0" y="2927.0" text-anchor="middle" class="label">Jika ubah password:</text><text x="1930.0" y="2947.0" text-anchor="middle" class="label">POST /api/change-password</text>
|
||||||
|
<ellipse cx="1930.0" cy="3040.0" rx="145.0" ry="30.0" class="term"/>
|
||||||
|
<text x="1930.0" y="3032.0" text-anchor="middle" class="label">Logout dan hapus sesi</text>
|
||||||
|
<path d="M1100.0,185 L1100.0,210" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1100.0,265 L1100.0,310" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1100.0,550 L1100.0,590" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M470.0,810 L470.0,850" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M470.0,915 L470.0,950" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M470.0,1015 L470.0,1060" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M225.0,1255 L225.0,1285" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1565.0,810 L1560.0,850" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,920 L1560.0,960" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1025 L1560.0,1070" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1940.0,1143 L1940.0,1175" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1265 L1560.0,1305" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1375 L1560.0,1415" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1485 L1560.0,1525" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1595 L1560.0,1635" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1705 L1560.0,1745" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1815 L1560.0,1855" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1940.0,1928 L1940.0,1960" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1160.0,2030 L1170.0,2070" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1170.0,2140 L1575.0,2070" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1575.0,2140 L1570.0,2180" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1100.0,2250 L1100.0,2410" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1100.0,2485 L1100.0,2530" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M260.0,2740 L260.0,2775" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M260.0,2840 L260.0,2880" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M260.0,2960 L260.0,3000" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M260.0,3070 L260.0,3115" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M670.0,2745 L670.0,2780" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M670.0,2850 L670.0,2890" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M670.0,2955 L670.0,3000" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M670.0,3075 L670.0,3125" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1095.0,2740 L1095.0,2775" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1095.0,2845 L1095.0,2885" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1095.0,2960 L1095.0,3000" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1095.0,3070 L1095.0,3110" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1155.0,3295 L1155.0,3330" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1155.0,3405 L1155.0,3445" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1550.0,2740 L1550.0,2775" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1550.0,2850 L1550.0,2895" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1550.0,2970 L1550.0,3010" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1930.0,2740 L1930.0,2780" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1930.0,2860 L1930.0,2900" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1930.0,2970 L1930.0,3010" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1230,355.0 L1330,357.5" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1290" y="338" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M1100.0,400 L1100.0,490" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1130" y="430" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1465.0,390 L1465,2145 L1270,2215.0" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M960,632.5 L480,632.5 L470.0,740" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="675" y="632" text-anchor="middle" class="small">Login</text>
|
||||||
|
<path d="M1240,632.5 L1565,632.5 L1565.0,740" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1360" y="632" text-anchor="middle" class="small">Lupa password</text>
|
||||||
|
<path d="M470.0,1145 L470.0,1190" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="510" y="1170" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M340,1102.5 L225,1102 L225,1222 L360,1222.5" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="250" y="1120" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M470.0,1255 L470,2145 L930,2215.0" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<path d="M1560.0,1155 L1560.0,1195" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1588" y="1178" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M1690,1112.5 L1790,1110.5" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1770" y="1095" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1430,1897.5 L1160,1897.5 L1160.0,1960" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1320" y="1892" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M1690,1897.5 L1790,1895.5" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1768" y="1880" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
<path d="M1100.0,2615 L1100.0,2635 L260,2635 L260.0,2680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="260" y="2625" text-anchor="middle" class="small">Produk/Stok</text>
|
||||||
|
<path d="M1100.0,2615 L1100.0,2635 L670,2635 L670.0,2680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="670" y="2625" text-anchor="middle" class="small">Transaksi</text>
|
||||||
|
<path d="M1100.0,2615 L1100.0,2635 L1095,2635 L1095.0,2680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1095" y="2625" text-anchor="middle" class="small">Prediksi</text>
|
||||||
|
<path d="M1100.0,2615 L1100.0,2635 L1550,2635 L1550.0,2680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1550" y="2625" text-anchor="middle" class="small">Laporan</text>
|
||||||
|
<path d="M1100.0,2615 L1100.0,2635 L1930,2635 L1930.0,2680" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1930" y="2625" text-anchor="middle" class="small">Pengaturan</text>
|
||||||
|
<path d="M1095.0,3195 L1155.0,3230" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="1180" y="3210" text-anchor="middle" class="small">Ya</text>
|
||||||
|
<path d="M965,3152.5 L830,3152.5 L970,3162.5" class="line" marker-end="url(#arrow)"/>
|
||||||
|
<text x="870" y="3135" text-anchor="middle" class="small">Tidak</text>
|
||||||
|
</svg>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-05-29T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
|
||||||
|
<diagram id="bab3" name="Page-1">
|
||||||
|
<mxGraphModel dx="1600" dy="1000" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1600" pageHeight="1000" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="title" value="Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue" style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=24;fontStyle=1;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="330" y="30" width="900" height="40" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="actor" value="Admin / Pemilik Toko" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fillColor=#ffffff;strokeColor=#2D2520;fontFamily=Arial;fontSize=14;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="70" y="250" width="120" height="180" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="sys" value="Sistem Aplikasi Tobaku Sulastri" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF8EF;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=18;fontStyle=1;fontColor=#2D2520;arcSize=8;" vertex="1" parent="1"><mxGeometry x="250" y="100" width="1180" height="760" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_login" value="Login" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="330" y="180" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_dashboard" value="Melihat Dashboard" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="330" y="300" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_produk" value="Mengelola Produk dan Stok Bahan" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="330" y="420" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_transaksi" value="Mencatat Stok Masuk" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="330" y="540" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_prediksi" value="Prediksi Kebutuhan Produksi" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="670" y="190" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_hitung" value="Menghitung Kebutuhan Bahan dari Resep" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="670" y="330" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_submit" value="Submit Produksi dan Update Stok" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="670" y="470" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_laporan" value="Melihat Laporan Stok, Prediksi, dan Bahan Kritis" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="670" y="610" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_settings" value="Mengatur Akun dan Password" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="670" y="740" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_resep" value="Mengambil Data Resep" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1060" y="250" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_cekstok" value="Mengecek Ketersediaan Stok" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1060" y="390" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_simpan" value="Menyimpan Riwayat Prediksi/Pemakaian" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1060" y="540" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="uc_otp" value="Reset Password dengan OTP" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#8B5E3C;strokeWidth=2;fontFamily=Arial;fontSize=15;fontColor=#2D2520;" vertex="1" parent="1"><mxGeometry x="1060" y="700" width="260" height="70" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a0" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a1" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_dashboard"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a2" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_produk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a3" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_transaksi"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a4" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_prediksi"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a5" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_submit"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a6" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_laporan"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a7" value="" style="endArrow=none;html=1;rounded=0;strokeColor=#4B3A31;" edge="1" parent="1" source="actor" target="uc_settings"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i0" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_prediksi" target="uc_resep"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i1" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_prediksi" target="uc_hitung"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i2" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_hitung" target="uc_cekstok"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i3" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_submit" target="uc_cekstok"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i4" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_submit" target="uc_simpan"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i5" value="<<include>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_laporan" target="uc_simpan"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="i6" value="<<extend>>" style="endArrow=open;html=1;rounded=0;dashed=1;strokeColor=#6B7280;fontFamily=Arial;fontSize=12;fontColor=#6B7280;" edge="1" parent="1" source="uc_settings" target="uc_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
|
After Width: | Height: | Size: 154 KiB |
|
|
@ -0,0 +1,50 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1500" height="920" viewBox="0 0 1500 920"><rect width="1500" height="920" fill="#F7F3EE"/>
|
||||||
|
<defs><style>.title{font-family:Arial,sans-serif;font-size:28px;font-weight:700;fill:#2D2520}.label{font-family:Arial,sans-serif;font-size:16px;fill:#2D2520}.small{font-family:Arial,sans-serif;font-size:13px;fill:#4B5563}.box{fill:#fff;stroke:#8B5E3C;stroke-width:2}.note{fill:#EAF4FF;stroke:#2B6CB0;stroke-width:2}.decision{fill:#FFF7D6;stroke:#B7791F;stroke-width:2}.term{fill:#EAF8EF;stroke:#2E7D32;stroke-width:2}.db{fill:#EFF2F5;stroke:#4B5563;stroke-width:2}.line{stroke:#4B3A31;stroke-width:2;fill:none}.dash{stroke:#6B7280;stroke-width:1.8;stroke-dasharray:7 5;fill:none}</style><marker id="arrow" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#4B3A31"/></marker><marker id="arrow2" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto"><path d="M0,0 L10,4 L0,8 z" fill="#6B7280"/></marker></defs>
|
||||||
|
<text x="750" y="50" text-anchor="middle" class="title">Use Case Diagram Aplikasi Prediksi dan Pengelolaan Stok Bahan Kue</text>
|
||||||
|
<rect x="250" y="95" width="1180" height="760" rx="18" class="box"/>
|
||||||
|
<text x="840" y="130" text-anchor="middle" class="label">Sistem Aplikasi Tobaku Sulastri</text>
|
||||||
|
<circle cx="120" cy="250" r="28" fill="#fff" stroke="#2D2520" stroke-width="3"/><path d="M120 278 L120 365 M70 310 L170 310 M120 365 L80 440 M120 365 L160 440" stroke="#2D2520" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||||
|
<text x="120" y="480" text-anchor="middle" class="label">Admin /</text><text x="120" y="500" text-anchor="middle" class="label">Pemilik Toko</text>
|
||||||
|
<ellipse cx="460.0" cy="205.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="460" y="202" text-anchor="middle" class="label">Login</text>
|
||||||
|
<ellipse cx="460.0" cy="325.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="460" y="322" text-anchor="middle" class="label">Melihat</text><text x="460" y="342" text-anchor="middle" class="label">Dashboard</text>
|
||||||
|
<ellipse cx="460.0" cy="445.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="460" y="442" text-anchor="middle" class="label">Mengelola Produk</text><text x="460" y="462" text-anchor="middle" class="label">dan Stok Bahan</text>
|
||||||
|
<ellipse cx="460.0" cy="565.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="460" y="562" text-anchor="middle" class="label">Mencatat</text><text x="460" y="582" text-anchor="middle" class="label">Stok Masuk</text>
|
||||||
|
<ellipse cx="800.0" cy="215.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="800" y="212" text-anchor="middle" class="label">Prediksi Kebutuhan</text><text x="800" y="232" text-anchor="middle" class="label">Produksi</text>
|
||||||
|
<ellipse cx="800.0" cy="355.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="800" y="352" text-anchor="middle" class="label">Menghitung Kebutuhan</text><text x="800" y="372" text-anchor="middle" class="label">Bahan dari Resep</text>
|
||||||
|
<ellipse cx="800.0" cy="495.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="800" y="492" text-anchor="middle" class="label">Submit Produksi</text><text x="800" y="512" text-anchor="middle" class="label">dan Update Stok</text>
|
||||||
|
<ellipse cx="800.0" cy="635.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="800" y="632" text-anchor="middle" class="label">Melihat Laporan Stok,</text><text x="800" y="652" text-anchor="middle" class="label">Prediksi, dan Bahan Kritis</text>
|
||||||
|
<ellipse cx="800.0" cy="765.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="800" y="762" text-anchor="middle" class="label">Mengatur Akun</text><text x="800" y="782" text-anchor="middle" class="label">dan Password</text>
|
||||||
|
<ellipse cx="1190.0" cy="275.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="1190" y="272" text-anchor="middle" class="label">Mengambil</text><text x="1190" y="292" text-anchor="middle" class="label">Data Resep</text>
|
||||||
|
<ellipse cx="1190.0" cy="415.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="1190" y="412" text-anchor="middle" class="label">Mengecek</text><text x="1190" y="432" text-anchor="middle" class="label">Ketersediaan Stok</text>
|
||||||
|
<ellipse cx="1190.0" cy="555.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="1190" y="552" text-anchor="middle" class="label">Menyimpan Riwayat</text><text x="1190" y="572" text-anchor="middle" class="label">Prediksi/Pemakaian</text>
|
||||||
|
<ellipse cx="1190.0" cy="725.0" rx="130.0" ry="35.0" class="box"/>
|
||||||
|
<text x="1190" y="722" text-anchor="middle" class="label">Reset Password</text><text x="1190" y="742" text-anchor="middle" class="label">dengan OTP</text>
|
||||||
|
<path d="M170,350 L330,205" class="line"/>
|
||||||
|
<path d="M170,350 L330,325" class="line"/>
|
||||||
|
<path d="M170,350 L330,445" class="line"/>
|
||||||
|
<path d="M170,350 L330,565" class="line"/>
|
||||||
|
<path d="M170,350 L670,215" class="line"/>
|
||||||
|
<path d="M170,350 L670,355" class="line"/>
|
||||||
|
<path d="M170,350 L670,495" class="line"/>
|
||||||
|
<path d="M170,350 L670,635" class="line"/>
|
||||||
|
<path d="M170,350 L670,765" class="line"/>
|
||||||
|
<path d="M930,215 L1060,275" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,215 L670,355" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,355 L1060,415" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,495 L1060,415" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,495 L1060,555" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,635 L1060,555" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
<path d="M930,765 L1060,725" class="dash" marker-end="url(#arrow2)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -0,0 +1,438 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
OUT_DIR = ROOT / "wireframe_mobile_requested"
|
||||||
|
COMBINED = ROOT / "wireframe-mobile-full.png"
|
||||||
|
ZIP_PATH = ROOT / "wireframe-mobile-screens.zip"
|
||||||
|
|
||||||
|
PHONE_W = 430
|
||||||
|
PHONE_H = 860
|
||||||
|
CANVAS_W = 520
|
||||||
|
CANVAS_H = 1010
|
||||||
|
MARGIN = 32
|
||||||
|
|
||||||
|
COLS = 3
|
||||||
|
ROWS = 5
|
||||||
|
|
||||||
|
|
||||||
|
def font(size: int, bold: bool = False, italic: bool = False) -> ImageFont.FreeTypeFont:
|
||||||
|
names = []
|
||||||
|
if bold and italic:
|
||||||
|
names = ["arialbi.ttf", "Arial Bold Italic.ttf"]
|
||||||
|
elif bold:
|
||||||
|
names = ["arialbd.ttf", "Arial Bold.ttf"]
|
||||||
|
elif italic:
|
||||||
|
names = ["ariali.ttf", "Arial Italic.ttf"]
|
||||||
|
else:
|
||||||
|
names = ["arial.ttf", "Arial.ttf"]
|
||||||
|
for name in names:
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype(name, size)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
F = {
|
||||||
|
"title": font(23, True),
|
||||||
|
"subtitle": font(16, True),
|
||||||
|
"body": font(14),
|
||||||
|
"small": font(12),
|
||||||
|
"tiny": font(10),
|
||||||
|
"button": font(14, True),
|
||||||
|
"label": font(13, True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Screen:
|
||||||
|
def __init__(self, title: str):
|
||||||
|
self.title = title
|
||||||
|
self.img = Image.new("RGB", (CANVAS_W, CANVAS_H), "white")
|
||||||
|
self.d = ImageDraw.Draw(self.img)
|
||||||
|
self.phone_x = (CANVAS_W - PHONE_W) // 2
|
||||||
|
self.phone_y = 82
|
||||||
|
self.d.text((CANVAS_W // 2, 36), title, font=F["title"], fill="#111111", anchor="mm")
|
||||||
|
self.rounded(self.phone_x, self.phone_y, PHONE_W, PHONE_H, 36, "#ffffff", "#777777", 3)
|
||||||
|
self.rounded(self.phone_x + 155, self.phone_y + 12, 120, 18, 9, "#eeeeee", "#bbbbbb", 1)
|
||||||
|
self.x0 = self.phone_x + 28
|
||||||
|
self.y0 = self.phone_y + 54
|
||||||
|
self.w = PHONE_W - 56
|
||||||
|
self.bottom_y = self.phone_y + PHONE_H - 86
|
||||||
|
|
||||||
|
def rounded(self, x, y, w, h, r=10, fill="#f3f3f3", outline="#9a9a9a", width=2):
|
||||||
|
self.d.rounded_rectangle([x, y, x + w, y + h], radius=r, fill=fill, outline=outline, width=width)
|
||||||
|
|
||||||
|
def rect(self, x, y, w, h, fill="#f3f3f3", outline="#9a9a9a", width=2):
|
||||||
|
self.d.rectangle([x, y, x + w, y + h], fill=fill, outline=outline, width=width)
|
||||||
|
|
||||||
|
def line(self, x1, y1, x2, y2, fill="#9a9a9a", width=2):
|
||||||
|
self.d.line([x1, y1, x2, y2], fill=fill, width=width)
|
||||||
|
|
||||||
|
def text(self, x, y, txt, style="body", fill="#111111", anchor="la", max_width=None):
|
||||||
|
txt = str(txt)
|
||||||
|
if max_width is None:
|
||||||
|
self.d.text((x, y), txt, font=F[style], fill=fill, anchor=anchor)
|
||||||
|
return
|
||||||
|
words = txt.split()
|
||||||
|
lines = []
|
||||||
|
cur = ""
|
||||||
|
for word in words:
|
||||||
|
test = f"{cur} {word}".strip()
|
||||||
|
if self.d.textlength(test, font=F[style]) <= max_width:
|
||||||
|
cur = test
|
||||||
|
else:
|
||||||
|
if cur:
|
||||||
|
lines.append(cur)
|
||||||
|
cur = word
|
||||||
|
if cur:
|
||||||
|
lines.append(cur)
|
||||||
|
for i, line in enumerate(lines[:3]):
|
||||||
|
self.d.text((x, y + i * 17), line, font=F[style], fill=fill, anchor=anchor)
|
||||||
|
|
||||||
|
def header(self, title, sub=None, back=False):
|
||||||
|
self.rounded(self.phone_x + 1, self.phone_y + 34, PHONE_W - 2, 92, 22, "#dedede", "#999999", 2)
|
||||||
|
label = f"< {title}" if back else title
|
||||||
|
self.text(self.x0, self.phone_y + 78, label, "subtitle")
|
||||||
|
if sub:
|
||||||
|
self.text(self.x0, self.phone_y + 103, sub, "small", "#555555")
|
||||||
|
|
||||||
|
def input(self, x, y, label, placeholder="", w=None, eye=False):
|
||||||
|
w = w or self.w
|
||||||
|
self.text(x, y, label, "label")
|
||||||
|
self.rounded(x, y + 22, w, 44, 10, "#f7f7f7", "#999999", 2)
|
||||||
|
self.text(x + 14, y + 51, placeholder, "small", "#666666", anchor="lm")
|
||||||
|
if eye:
|
||||||
|
self.text(x + w - 25, y + 51, "eye", "tiny", "#555555", anchor="mm")
|
||||||
|
|
||||||
|
def button(self, x, y, label, w=None, dark=True):
|
||||||
|
w = w or self.w
|
||||||
|
fill = "#111111" if dark else "#f7f7f7"
|
||||||
|
outline = "#111111" if dark else "#999999"
|
||||||
|
text = "#ffffff" if dark else "#111111"
|
||||||
|
self.rounded(x, y, w, 46, 12, fill, outline, 2)
|
||||||
|
self.text(x + w / 2, y + 24, label, "button", text, anchor="mm")
|
||||||
|
|
||||||
|
def card(self, x, y, w, h, title=None, sub=None, right=None):
|
||||||
|
self.rounded(x, y, w, h, 14, "#f7f7f7", "#9a9a9a", 2)
|
||||||
|
if title:
|
||||||
|
self.text(x + 14, y + 18, title, "label", max_width=w - 28)
|
||||||
|
if sub:
|
||||||
|
self.text(x + 14, y + 42, sub, "small", "#555555", max_width=w - 28)
|
||||||
|
if right:
|
||||||
|
self.text(x + w - 14, y + 24, right, "small", "#111111", anchor="ra")
|
||||||
|
|
||||||
|
def bottom_nav(self, active="Home"):
|
||||||
|
y = self.phone_y + PHONE_H - 62
|
||||||
|
labels = ["Home", "Produk", "Stok", "Pred", "Lapor"]
|
||||||
|
item_w = self.w / len(labels)
|
||||||
|
for i, label in enumerate(labels):
|
||||||
|
x = self.x0 + i * item_w
|
||||||
|
if label == active:
|
||||||
|
self.rounded(x + 3, y, item_w - 6, 38, 8, "#111111", "#111111", 1)
|
||||||
|
self.text(x + item_w / 2, y + 22, label, "tiny", "#ffffff", anchor="mm")
|
||||||
|
else:
|
||||||
|
self.text(x + item_w / 2, y + 22, label, "tiny", "#555555", anchor="mm")
|
||||||
|
|
||||||
|
def plus(self, x, y):
|
||||||
|
self.rounded(x, y, 46, 46, 23, "#111111", "#111111", 2)
|
||||||
|
self.text(x + 23, y + 25, "+", "title", "#ffffff", anchor="mm")
|
||||||
|
|
||||||
|
def save(self, filename: str):
|
||||||
|
OUT_DIR.mkdir(exist_ok=True)
|
||||||
|
path = OUT_DIR / filename
|
||||||
|
self.img.save(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def splash():
|
||||||
|
s = Screen("1. Splash Screen")
|
||||||
|
x, y = s.x0, s.phone_y + 220
|
||||||
|
s.rounded(x + 112, y, 150, 150, 28, "#e0e0e0", "#999999", 2)
|
||||||
|
s.text(x + 187, y + 65, "IKON", "label", "#555555", anchor="mm")
|
||||||
|
s.text(x + 187, y + 90, "BAHAN KUE", "tiny", "#555555", anchor="mm")
|
||||||
|
s.text(x + 187, y + 205, "Prediksi Stok", "title", anchor="mm")
|
||||||
|
s.text(x + 187, y + 235, "Bahan Kue", "title", anchor="mm")
|
||||||
|
s.text(x + 187, y + 315, "Memuat aplikasi...", "small", "#555555", anchor="mm")
|
||||||
|
s.text(x + 187, y + 350, ". . .", "subtitle", "#777777", anchor="mm")
|
||||||
|
return s.save("01_splash_screen.png")
|
||||||
|
|
||||||
|
|
||||||
|
def login():
|
||||||
|
s = Screen("2. Halaman Login")
|
||||||
|
x, y = s.x0, s.y0 + 35
|
||||||
|
s.rounded(x + 137, y, 100, 100, 22, "#dedede", "#999999", 2)
|
||||||
|
s.text(x + 187, y + 53, "LOGO", "label", "#555555", anchor="mm")
|
||||||
|
s.text(x + 187, y + 145, "Prediksi Stok Bahan Kue", "subtitle", anchor="mm")
|
||||||
|
s.input(x, y + 190, "Email", "Masukkan email")
|
||||||
|
s.input(x, y + 275, "Password", "Masukkan password", eye=True)
|
||||||
|
s.button(x, y + 380, "Masuk / Login")
|
||||||
|
s.text(x + s.w, y + 450, "Lupa Password", "small", "#555555", anchor="ra")
|
||||||
|
s.text(x + 187, y + 500, "Belum punya akun? Daftar", "small", "#111111", anchor="mm")
|
||||||
|
return s.save("02_login.png")
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
s = Screen("3. Halaman Daftar / Register")
|
||||||
|
s.header("Daftar Akun", "Buat akun baru", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 165
|
||||||
|
s.input(x, y, "Nama Pengguna", "Masukkan nama")
|
||||||
|
s.input(x, y + 82, "Email", "Masukkan email")
|
||||||
|
s.input(x, y + 164, "Password", "Masukkan password")
|
||||||
|
s.input(x, y + 246, "Konfirmasi Password", "Ulangi password")
|
||||||
|
s.button(x, y + 355, "Daftar")
|
||||||
|
s.text(x + 187, y + 425, "Sudah punya akun? Login", "small", "#111111", anchor="mm")
|
||||||
|
return s.save("03_register.png")
|
||||||
|
|
||||||
|
|
||||||
|
def dashboard():
|
||||||
|
s = Screen("4. Halaman Dashboard")
|
||||||
|
s.header("Prediksi Stok Bahan Kue", "Tobaku Sulastri")
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.card(x, y, 176, 76, "Jumlah Bahan", "24 item")
|
||||||
|
s.card(x + 198, y, 176, 76, "Stok Kritis", "3 bahan")
|
||||||
|
s.card(x, y + 92, 374, 70, "Prediksi Terbaru", "Tepung: siapkan 12 kg", "RF")
|
||||||
|
s.text(x, y + 195, "Menu Utama", "label")
|
||||||
|
menus = [("Produk/Stok", "Bahan"), ("Stok Masuk", "Tambah"), ("Prediksi", "Kebutuhan"), ("Laporan", "Rekap")]
|
||||||
|
for i, (a, b) in enumerate(menus):
|
||||||
|
cx = x + (i % 2) * 198
|
||||||
|
cy = y + 225 + (i // 2) * 98
|
||||||
|
s.card(cx, cy, 176, 78, a, b)
|
||||||
|
s.bottom_nav("Home")
|
||||||
|
return s.save("04_dashboard.png")
|
||||||
|
|
||||||
|
|
||||||
|
def products():
|
||||||
|
s = Screen("5. Data Produk / Stok Bahan")
|
||||||
|
s.header("Data Produk / Stok", "Daftar bahan kue", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
items = [
|
||||||
|
("Tepung Terigu", "Bahan | Stok 20 kg | Min 5 kg"),
|
||||||
|
("Gula Pasir", "Bahan | Stok 2 kg | Min 5 kg"),
|
||||||
|
("Telur", "Bahan | Stok 10 kg | Min 3 kg"),
|
||||||
|
("Mentega", "Bahan | Stok 4 kg | Min 2 kg"),
|
||||||
|
]
|
||||||
|
for i, (name, sub) in enumerate(items):
|
||||||
|
s.card(x, y + i * 88, s.w, 74, name, sub)
|
||||||
|
s.text(x + s.w - 70, y + i * 88 + 52, "Edit", "tiny", "#555555")
|
||||||
|
s.text(x + s.w - 30, y + i * 88 + 52, "Hapus", "tiny", "#555555", anchor="ra")
|
||||||
|
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
|
||||||
|
s.bottom_nav("Produk")
|
||||||
|
return s.save("05_data_produk_stok.png")
|
||||||
|
|
||||||
|
|
||||||
|
def product_form():
|
||||||
|
s = Screen("6. Tambah / Edit Produk")
|
||||||
|
s.header("Tambah / Edit Produk", "Input data bahan", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.input(x, y, "Nama Bahan", "Contoh: Tepung")
|
||||||
|
s.input(x, y + 82, "Kategori", "Bahan / Barang")
|
||||||
|
s.input(x, y + 164, "Jumlah Stok", "0")
|
||||||
|
s.input(x, y + 246, "Satuan", "kg / pcs")
|
||||||
|
s.input(x, y + 328, "Stok Minimum", "0")
|
||||||
|
s.button(x, y + 435, "Simpan")
|
||||||
|
return s.save("06_tambah_edit_produk.png")
|
||||||
|
|
||||||
|
|
||||||
|
def stock_in():
|
||||||
|
s = Screen("7. Halaman Stok Masuk")
|
||||||
|
s.header("Stok Masuk", "Riwayat penambahan stok", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
rows = [
|
||||||
|
("Tepung Terigu", "+10 kg | 01/06/2026", "Pembelian"),
|
||||||
|
("Gula Pasir", "+5 kg | 01/06/2026", "Restock"),
|
||||||
|
("Mentega", "+2 kg | 31/05/2026", "Supplier"),
|
||||||
|
("Telur", "+6 kg | 30/05/2026", "Pembelian"),
|
||||||
|
]
|
||||||
|
for i, (a, b, c) in enumerate(rows):
|
||||||
|
s.card(x, y + i * 88, s.w, 74, a, b)
|
||||||
|
s.text(x + 14, y + i * 88 + 59, c, "tiny", "#777777")
|
||||||
|
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
|
||||||
|
s.bottom_nav("Stok")
|
||||||
|
return s.save("07_stok_masuk.png")
|
||||||
|
|
||||||
|
|
||||||
|
def add_stock_in():
|
||||||
|
s = Screen("8. Tambah Stok Masuk")
|
||||||
|
s.header("Tambah Stok Masuk", "Stok bertambah otomatis", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.input(x, y, "Pilih Bahan", "Dropdown bahan")
|
||||||
|
s.input(x, y + 82, "Jumlah Stok Masuk", "0")
|
||||||
|
s.input(x, y + 164, "Tanggal", "Pilih tanggal")
|
||||||
|
s.input(x, y + 246, "Keterangan", "Opsional")
|
||||||
|
s.card(x, y + 335, s.w, 72, "Catatan", "Setelah disimpan, stok bahan bertambah otomatis.")
|
||||||
|
s.button(x, y + 435, "Simpan")
|
||||||
|
return s.save("08_tambah_stok_masuk.png")
|
||||||
|
|
||||||
|
|
||||||
|
def prediction():
|
||||||
|
s = Screen("9. Prediksi Kebutuhan Bahan")
|
||||||
|
s.header("Prediksi Kebutuhan", "Model utama: Random Forest", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.input(x, y, "Pilih Bahan / Produk", "Dropdown produk")
|
||||||
|
s.input(x, y + 82, "Periode Prediksi", "Harian / Mingguan / Bulanan")
|
||||||
|
s.input(x, y + 164, "Parameter Kebutuhan", "Jumlah produksi / input lain")
|
||||||
|
s.card(x, y + 255, s.w, 76, "Model Prediksi", "Random Forest digunakan sebagai model utama.")
|
||||||
|
s.button(x, y + 365, "Proses Prediksi")
|
||||||
|
s.bottom_nav("Pred")
|
||||||
|
return s.save("09_prediksi_kebutuhan.png")
|
||||||
|
|
||||||
|
|
||||||
|
def prediction_result():
|
||||||
|
s = Screen("10. Hasil Prediksi")
|
||||||
|
s.header("Hasil Prediksi", "Rekomendasi stok", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.card(x, y, s.w, 76, "Nama Bahan", "Tepung Terigu")
|
||||||
|
s.card(x, y + 92, s.w, 78, "Hasil Prediksi", "Permintaan: 12 kg")
|
||||||
|
s.card(x, y + 184, s.w, 78, "Rekomendasi", "Siapkan stok minimal 15 kg")
|
||||||
|
s.card(x, y + 276, s.w, 70, "Status Stok", "Aman / Kritis")
|
||||||
|
s.card(x, y + 360, s.w, 88, "Perbandingan Model", "Random Forest: 92% | Linear Regression: 78%")
|
||||||
|
s.button(x, y + 475, "Simpan Hasil")
|
||||||
|
s.button(x, y + 535, "Gunakan untuk Produksi", dark=False)
|
||||||
|
s.text(x, y + 600, "Jika digunakan, stok bahan berkurang otomatis.", "tiny", "#555555")
|
||||||
|
return s.save("10_hasil_prediksi.png")
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
s = Screen("11. Penggunaan Bahan")
|
||||||
|
s.header("Penggunaan Bahan", "Pengurangan stok otomatis", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
rows = [
|
||||||
|
("Tepung Terigu", "Terpakai 3 kg | Sisa 17 kg"),
|
||||||
|
("Gula Pasir", "Terpakai 1 kg | Sisa 4 kg"),
|
||||||
|
("Telur", "Terpakai 2 kg | Sisa 8 kg"),
|
||||||
|
("Mentega", "Terpakai 0.5 kg | Sisa 3.5 kg"),
|
||||||
|
]
|
||||||
|
for i, (a, b) in enumerate(rows):
|
||||||
|
s.card(x, y + i * 88, s.w, 74, a, b)
|
||||||
|
s.text(x + 14, y + i * 88 + 59, "Tanggal: 01/06/2026", "tiny", "#777777")
|
||||||
|
s.card(x, y + 370, s.w, 64, "Info", "Stok bahan dikurangi otomatis saat produksi.")
|
||||||
|
s.plus(s.phone_x + PHONE_W - 88, s.bottom_y - 16)
|
||||||
|
return s.save("11_penggunaan_bahan.png")
|
||||||
|
|
||||||
|
|
||||||
|
def reports():
|
||||||
|
s = Screen("12. Halaman Laporan")
|
||||||
|
s.header("Laporan", "Rekap data sistem", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
cards = ["Laporan Stok Bahan", "Laporan Stok Masuk", "Laporan Penggunaan", "Laporan Hasil Prediksi"]
|
||||||
|
for i, title in enumerate(cards):
|
||||||
|
s.card(x, y + i * 76, s.w, 62, title, "Lihat detail")
|
||||||
|
s.rounded(x, y + 330, s.w, 150, 14, "#f7f7f7", "#999999", 2)
|
||||||
|
s.text(x + s.w / 2, y + 400, "Placeholder Grafik", "label", "#555555", anchor="mm")
|
||||||
|
s.line(x + 35, y + 445, x + s.w - 35, y + 355, "#aaaaaa", 2)
|
||||||
|
s.button(x, y + 515, "Export Laporan")
|
||||||
|
s.bottom_nav("Lapor")
|
||||||
|
return s.save("12_laporan.png")
|
||||||
|
|
||||||
|
|
||||||
|
def report_detail():
|
||||||
|
s = Screen("13. Detail Laporan")
|
||||||
|
s.header("Detail Laporan", "Data laporan terpilih", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
s.card(x, y, s.w, 78, "Ringkasan", "Total bahan: 24 | Kritis: 3")
|
||||||
|
s.text(x, y + 110, "Tabel Data", "label")
|
||||||
|
s.rounded(x, y + 135, s.w, 260, 12, "#f7f7f7", "#999999", 2)
|
||||||
|
headers = ["Nama", "Jumlah", "Status"]
|
||||||
|
for i, h in enumerate(headers):
|
||||||
|
s.text(x + 18 + i * 118, y + 168, h, "label")
|
||||||
|
for r in range(4):
|
||||||
|
yy = y + 205 + r * 42
|
||||||
|
s.line(x + 12, yy - 18, x + s.w - 12, yy - 18, "#d0d0d0", 1)
|
||||||
|
s.text(x + 18, yy, f"Data {r+1}", "small", "#555555")
|
||||||
|
s.text(x + 138, yy, "10 kg", "small", "#555555")
|
||||||
|
s.text(x + 258, yy, "Aman", "small", "#555555")
|
||||||
|
s.button(x, y + 440, "Export / Cetak Laporan")
|
||||||
|
return s.save("13_detail_laporan.png")
|
||||||
|
|
||||||
|
|
||||||
|
def settings():
|
||||||
|
s = Screen("14. Halaman Pengaturan")
|
||||||
|
s.header("Pengaturan", "Menu aplikasi", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 150
|
||||||
|
rows = ["Profil Pengguna", "Pengaturan Akun", "Tentang Aplikasi", "Bantuan"]
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
s.card(x, y + i * 82, s.w, 64, row, "Buka menu", ">")
|
||||||
|
s.button(x, y + 380, "Logout")
|
||||||
|
s.bottom_nav("Home")
|
||||||
|
return s.save("14_pengaturan.png")
|
||||||
|
|
||||||
|
|
||||||
|
def profile():
|
||||||
|
s = Screen("15. Halaman Profil")
|
||||||
|
s.header("Profil", "Informasi pengguna", back=True)
|
||||||
|
x, y = s.x0, s.phone_y + 155
|
||||||
|
s.rounded(x + 137, y, 100, 100, 50, "#dedede", "#999999", 2)
|
||||||
|
s.text(x + 187, y + 55, "Avatar", "tiny", "#555555", anchor="mm")
|
||||||
|
s.text(x + 187, y + 145, "Admin Sulastri", "subtitle", anchor="mm")
|
||||||
|
s.text(x + 187, y + 174, "admin@sulastri.com", "small", "#555555", anchor="mm")
|
||||||
|
s.card(x, y + 225, s.w, 72, "Nama Pengguna/Admin", "Admin Sulastri")
|
||||||
|
s.card(x, y + 313, s.w, 72, "Email", "admin@sulastri.com")
|
||||||
|
s.button(x, y + 420, "Edit Profil")
|
||||||
|
s.button(x, y + 480, "Logout", dark=False)
|
||||||
|
return s.save("15_profil.png")
|
||||||
|
|
||||||
|
|
||||||
|
BUILDERS = [
|
||||||
|
splash,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
dashboard,
|
||||||
|
products,
|
||||||
|
product_form,
|
||||||
|
stock_in,
|
||||||
|
add_stock_in,
|
||||||
|
prediction,
|
||||||
|
prediction_result,
|
||||||
|
usage,
|
||||||
|
reports,
|
||||||
|
report_detail,
|
||||||
|
settings,
|
||||||
|
profile,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def combine(paths: list[Path]):
|
||||||
|
sheet_w = COLS * CANVAS_W + (COLS + 1) * MARGIN
|
||||||
|
sheet_h = ROWS * CANVAS_H + (ROWS + 1) * MARGIN + 72
|
||||||
|
sheet = Image.new("RGB", (sheet_w, sheet_h), "#f2f2f2")
|
||||||
|
draw = ImageDraw.Draw(sheet)
|
||||||
|
draw.text((sheet_w // 2, 45), "Wireframe Mobile Aplikasi Prediksi Stok Bahan Kue", font=font(30, True), fill="#111111", anchor="mm")
|
||||||
|
for idx, path in enumerate(paths):
|
||||||
|
img = Image.open(path)
|
||||||
|
row = idx // COLS
|
||||||
|
col = idx % COLS
|
||||||
|
x = MARGIN + col * (CANVAS_W + MARGIN)
|
||||||
|
y = 84 + MARGIN + row * (CANVAS_H + MARGIN)
|
||||||
|
draw.rounded_rectangle([x - 8, y - 8, x + CANVAS_W + 8, y + CANVAS_H + 8], radius=18, fill="#ffffff", outline="#d0d0d0", width=2)
|
||||||
|
sheet.paste(img, (x, y))
|
||||||
|
sheet.save(COMBINED)
|
||||||
|
|
||||||
|
|
||||||
|
def zip_outputs(paths: list[Path]):
|
||||||
|
if ZIP_PATH.exists():
|
||||||
|
ZIP_PATH.unlink()
|
||||||
|
with zipfile.ZipFile(ZIP_PATH, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for path in paths:
|
||||||
|
z.write(path, path.name)
|
||||||
|
z.write(COMBINED, COMBINED.name)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
OUT_DIR.mkdir(exist_ok=True)
|
||||||
|
for old in OUT_DIR.glob("*.png"):
|
||||||
|
old.unlink()
|
||||||
|
paths = [builder() for builder in BUILDERS]
|
||||||
|
combine(paths)
|
||||||
|
zip_outputs(paths)
|
||||||
|
print(f"Combined wireframe: {COMBINED}")
|
||||||
|
print(f"Individual screens: {OUT_DIR}")
|
||||||
|
print(f"ZIP: {ZIP_PATH}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,800 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from zipfile import ZIP_DEFLATED, ZipFile
|
||||||
|
import html
|
||||||
|
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent / "wireframe-mobile-project.docx"
|
||||||
|
|
||||||
|
|
||||||
|
def esc(text: str) -> str:
|
||||||
|
return html.escape(text, quote=False)
|
||||||
|
|
||||||
|
|
||||||
|
def run(
|
||||||
|
text: str,
|
||||||
|
size: int = 22,
|
||||||
|
bold: bool = False,
|
||||||
|
italic: bool = False,
|
||||||
|
font: str = "Arial",
|
||||||
|
color: str = "000000",
|
||||||
|
) -> str:
|
||||||
|
props = [
|
||||||
|
f'<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"/>',
|
||||||
|
f'<w:sz w:val="{size}"/>',
|
||||||
|
f'<w:szCs w:val="{size}"/>',
|
||||||
|
f'<w:color w:val="{color}"/>',
|
||||||
|
]
|
||||||
|
if bold:
|
||||||
|
props.insert(0, "<w:b/>")
|
||||||
|
if italic:
|
||||||
|
props.insert(0, "<w:i/>")
|
||||||
|
lines = text.split("\n")
|
||||||
|
parts = [f"<w:t xml:space=\"preserve\">{esc(lines[0])}</w:t>"]
|
||||||
|
for line in lines[1:]:
|
||||||
|
parts.append("<w:br/>")
|
||||||
|
parts.append(f"<w:t xml:space=\"preserve\">{esc(line)}</w:t>")
|
||||||
|
return f"<w:r><w:rPr>{''.join(props)}</w:rPr>{''.join(parts)}</w:r>"
|
||||||
|
|
||||||
|
|
||||||
|
def p(
|
||||||
|
text: str = "",
|
||||||
|
style: str | None = None,
|
||||||
|
runs: list[str] | None = None,
|
||||||
|
jc: str | None = None,
|
||||||
|
before: int = 0,
|
||||||
|
after: int = 140,
|
||||||
|
line: int = 276,
|
||||||
|
keep_next: bool = False,
|
||||||
|
) -> str:
|
||||||
|
ppr = []
|
||||||
|
if style:
|
||||||
|
ppr.append(f'<w:pStyle w:val="{style}"/>')
|
||||||
|
if jc:
|
||||||
|
ppr.append(f'<w:jc w:val="{jc}"/>')
|
||||||
|
if keep_next:
|
||||||
|
ppr.append("<w:keepNext/>")
|
||||||
|
ppr.append(f'<w:spacing w:before="{before}" w:after="{after}" w:line="{line}" w:lineRule="auto"/>')
|
||||||
|
content = "".join(runs) if runs is not None else run(text)
|
||||||
|
return f"<w:p><w:pPr>{''.join(ppr)}</w:pPr>{content}</w:p>"
|
||||||
|
|
||||||
|
|
||||||
|
def heading(text: str, level: int = 1) -> str:
|
||||||
|
if level == 1:
|
||||||
|
return p(text, style="Heading1", keep_next=True, before=180, after=120)
|
||||||
|
if level == 2:
|
||||||
|
return p(text, style="Heading2", keep_next=True, before=140, after=90)
|
||||||
|
return p(text, style="Heading3", keep_next=True, before=100, after=70)
|
||||||
|
|
||||||
|
|
||||||
|
def bullet(text: str) -> str:
|
||||||
|
return p(runs=[run("- ", 22), run(text, 22)], after=70)
|
||||||
|
|
||||||
|
|
||||||
|
def numbered(n: int, text: str) -> str:
|
||||||
|
return p(runs=[run(f"{n}. ", 22), run(text, 22)], after=70)
|
||||||
|
|
||||||
|
|
||||||
|
def ascii_block(text: str) -> str:
|
||||||
|
return p(
|
||||||
|
runs=[run(text.strip("\n"), size=17, font="Courier New", color="202020")],
|
||||||
|
before=60,
|
||||||
|
after=180,
|
||||||
|
line=220,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def page_break() -> str:
|
||||||
|
return '<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
|
||||||
|
|
||||||
|
|
||||||
|
def table(rows: list[list[str]], widths: list[int]) -> str:
|
||||||
|
grid = "".join(f'<w:gridCol w:w="{w}"/>' for w in widths)
|
||||||
|
body = []
|
||||||
|
for r_idx, row in enumerate(rows):
|
||||||
|
cells = []
|
||||||
|
for c_idx, cell in enumerate(row):
|
||||||
|
fill = "EDEDED" if r_idx == 0 else "FFFFFF"
|
||||||
|
cells.append(
|
||||||
|
f"""
|
||||||
|
<w:tc>
|
||||||
|
<w:tcPr>
|
||||||
|
<w:tcW w:w="{widths[c_idx]}" w:type="dxa"/>
|
||||||
|
<w:shd w:fill="{fill}"/>
|
||||||
|
<w:tcMar><w:top w:w="90" w:type="dxa"/><w:left w:w="110" w:type="dxa"/><w:bottom w:w="90" w:type="dxa"/><w:right w:w="110" w:type="dxa"/></w:tcMar>
|
||||||
|
</w:tcPr>
|
||||||
|
{p(runs=[run(cell, 18 if r_idx == 0 else 17, bold=(r_idx == 0))], after=0, line=230)}
|
||||||
|
</w:tc>"""
|
||||||
|
)
|
||||||
|
body.append(f"<w:tr>{''.join(cells)}</w:tr>")
|
||||||
|
return f"""
|
||||||
|
<w:tbl>
|
||||||
|
<w:tblPr>
|
||||||
|
<w:tblW w:w="{sum(widths)}" w:type="dxa"/>
|
||||||
|
<w:tblBorders>
|
||||||
|
<w:top w:val="single" w:sz="6" w:color="B8B8B8"/>
|
||||||
|
<w:left w:val="single" w:sz="6" w:color="B8B8B8"/>
|
||||||
|
<w:bottom w:val="single" w:sz="6" w:color="B8B8B8"/>
|
||||||
|
<w:right w:val="single" w:sz="6" w:color="B8B8B8"/>
|
||||||
|
<w:insideH w:val="single" w:sz="4" w:color="D6D6D6"/>
|
||||||
|
<w:insideV w:val="single" w:sz="4" w:color="D6D6D6"/>
|
||||||
|
</w:tblBorders>
|
||||||
|
</w:tblPr>
|
||||||
|
<w:tblGrid>{grid}</w:tblGrid>
|
||||||
|
{''.join(body)}
|
||||||
|
</w:tbl>"""
|
||||||
|
|
||||||
|
|
||||||
|
PAGES = [
|
||||||
|
{
|
||||||
|
"name": "Splash Screen",
|
||||||
|
"purpose": "Menampilkan identitas aplikasi, status inisialisasi, dan menentukan tujuan navigasi berdasarkan status login.",
|
||||||
|
"elements": "Logo aplikasi, nama Tobaku Sulastri, tagline, loading indicator, status koneksi, versi aplikasi.",
|
||||||
|
"nav": "Jika sesi aktif menuju Dashboard, jika belum login menuju Login.",
|
||||||
|
"notes": "Gunakan komposisi vertikal tengah, status singkat, dan durasi loading yang tidak terlalu lama.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| |
|
||||||
|
| [ APP LOGO ] |
|
||||||
|
| |
|
||||||
|
| Tobaku Sulastri |
|
||||||
|
| Toko Bahan Kue |
|
||||||
|
| Smart Inventory Management |
|
||||||
|
| |
|
||||||
|
| . . . |
|
||||||
|
| |
|
||||||
|
| Menghubungkan ke server... |
|
||||||
|
| Version 1.0.0 |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Login",
|
||||||
|
"purpose": "Memvalidasi akun admin sebelum mengakses fitur pengelolaan stok dan prediksi.",
|
||||||
|
"elements": "Logo, judul selamat datang, input email/username, input password, tombol visibility password, lupa password, tombol masuk.",
|
||||||
|
"nav": "Login berhasil menuju Dashboard. Lupa Password membuka dialog OTP.",
|
||||||
|
"notes": "Form dibuat satu kolom agar nyaman di layar kecil. Error login ditampilkan melalui snackbar.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| |
|
||||||
|
| [ ICON ] |
|
||||||
|
| Selamat Datang |
|
||||||
|
| Masuk ke akun Anda |
|
||||||
|
| |
|
||||||
|
| Email / Username |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Masukkan email/username | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Password |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | ******** [eye]| |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Lupa Password? |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | MASUK | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reset Password / OTP",
|
||||||
|
"purpose": "Membantu pengguna membuat password baru melalui verifikasi kode OTP.",
|
||||||
|
"elements": "Field akun, tombol kirim OTP, field OTP, field password baru, tombol batal, tombol reset.",
|
||||||
|
"nav": "Reset berhasil kembali ke Login atau menutup dialog jika dibuka dari Pengaturan.",
|
||||||
|
"notes": "Gunakan dialog/bottom sheet agar tidak memutus konteks halaman sebelumnya.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Reset Password X |
|
||||||
|
| |
|
||||||
|
| Email / Username |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | akun pengguna | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | KIRIM OTP | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Kode OTP |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | 6 digit OTP | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Password Baru |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Password baru | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| [ Batal ] [ Reset ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dashboard / Home",
|
||||||
|
"purpose": "Memberi ringkasan cepat kondisi stok, pemakaian bahan, dan peringatan bahan kritis.",
|
||||||
|
"elements": "Header admin, notifikasi stok menipis, kartu bahan digunakan hari ini, kartu total produk, grafik penggunaan bahan, daftar stok menipis, tombol prediksi, tombol tambah stok, bottom navigation.",
|
||||||
|
"nav": "Menu bawah menuju Produk, Stok Masuk, Prediksi, Laporan, Pengaturan.",
|
||||||
|
"notes": "Prioritaskan informasi yang perlu tindakan: stok menipis dan akses cepat ke prediksi/stok masuk.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Selamat Datang [ ! ] |
|
||||||
|
| Admin Sulastri |
|
||||||
|
| +-------------+ +------------+ |
|
||||||
|
| | Bahan Hari | | Produk | |
|
||||||
|
| | 24 kg | | 8 Aktif | |
|
||||||
|
| +-------------+ +------------+ |
|
||||||
|
| |
|
||||||
|
| Penggunaan Bahan |
|
||||||
|
| Tepung [===========-----] |
|
||||||
|
| Telur [=========-------] |
|
||||||
|
| Gula [======----------] |
|
||||||
|
| |
|
||||||
|
| Stok Menipis |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Gula Pasir 2 kg Kritis | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| [ Lihat Prediksi ][Tambah Stok]|
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Data Produk",
|
||||||
|
"purpose": "Menampilkan seluruh bahan/barang beserta kategori, harga, stok, status, dan kapasitas.",
|
||||||
|
"elements": "Header, jumlah produk, search field, filter chip, kartu produk, badge status, progress kapasitas, tombol info.",
|
||||||
|
"nav": "Tap kartu/info membuka Detail Produk. Bottom navigation menuju menu lain.",
|
||||||
|
"notes": "Filter status penting untuk mobile karena admin perlu cepat menemukan stok kritis.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| < Data Produk |
|
||||||
|
| 8 Produk |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Cari produk... Q | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| [Semua][Tersedia][Sedang] |
|
||||||
|
| |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [ ] Tepung Terigu Aman | |
|
||||||
|
| | Bahan | Rp 12.000 | |
|
||||||
|
| | Stok 20 kg | |
|
||||||
|
| | Kapasitas [==========--] i | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [ ] Gula Pasir Kritis | |
|
||||||
|
| | Stok 2 kg [==----------] i | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Detail Produk",
|
||||||
|
"purpose": "Menampilkan informasi lengkap satu produk/bahan dan rekomendasi tindakan berdasarkan status stok.",
|
||||||
|
"elements": "Nama produk, ID, badge status, grid informasi kategori/harga/stok/minimum/satuan/kapasitas, progress kapasitas, rekomendasi stok, tombol tutup, tombol tambah stok.",
|
||||||
|
"nav": "Tutup kembali ke Data Produk. Tambah Stok menuju Stok Masuk.",
|
||||||
|
"notes": "Pada mobile cocok sebagai bottom sheet agar pengguna tetap merasa berada di daftar produk.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Detail Produk |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [ ] Tepung Terigu Aman | |
|
||||||
|
| | ID Produk: 001 | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| [Kategori] Bahan [Harga] Rp |
|
||||||
|
| [Stok] 20 kg [Min] 5 kg |
|
||||||
|
| [Satuan] kg [Kapas] 80% |
|
||||||
|
| |
|
||||||
|
| Kapasitas stok 80% |
|
||||||
|
| [====================----] |
|
||||||
|
| |
|
||||||
|
| Stok aman untuk produksi. |
|
||||||
|
| |
|
||||||
|
| [ Tutup ] [ Tambah Stok ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Stok Masuk",
|
||||||
|
"purpose": "Mencatat penambahan stok bahan/barang dan memperbarui stok di database.",
|
||||||
|
"elements": "Dropdown produk, input jumlah, harga satuan otomatis/terisi, tombol tambah ke keranjang, daftar keranjang, kontrol qty, hapus item, total pembayaran, tanggal, tombol simpan, riwayat stok masuk.",
|
||||||
|
"nav": "Tambah Produk membuka dialog tambah produk. Simpan sukses tetap di halaman dan memperbarui riwayat.",
|
||||||
|
"notes": "Keranjang membantu input beberapa bahan sekaligus tanpa berpindah halaman.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| < Stok Masuk |
|
||||||
|
| Form Stok Masuk |
|
||||||
|
| Produk |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Pilih produk v | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Jumlah |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | 0 | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | TAMBAH KE KERANJANG | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Keranjang |
|
||||||
|
| | Produk A [-] 2 [+] Del | |
|
||||||
|
| Total Pembayaran Rp xxx |
|
||||||
|
| Tanggal [calendar]|
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | SIMPAN STOK MASUK | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Riwayat Stok Masuk |
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tambah Produk Baru",
|
||||||
|
"purpose": "Menambahkan produk/bahan baru yang belum tersedia dalam daftar.",
|
||||||
|
"elements": "Nama produk, jenis kategori, satuan, harga, stok awal, stok minimum, tombol batal, tombol simpan.",
|
||||||
|
"nav": "Simpan berhasil menambahkan produk ke pilihan Stok Masuk.",
|
||||||
|
"notes": "Gunakan validasi langsung untuk harga, stok awal, dan stok minimum agar kesalahan input cepat diketahui.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Tambah Produk Baru |
|
||||||
|
| Nama Produk |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Masukkan nama produk | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Jenis Kategori [v] |
|
||||||
|
| Satuan [v] |
|
||||||
|
| Harga |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Rp | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Stok Awal |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | 0 | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Stok Minimum |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | 0 | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| [ Batal ] [ Simpan ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Prediksi Kebutuhan Bahan",
|
||||||
|
"purpose": "Menghitung kebutuhan bahan berdasarkan resep dan jumlah produksi, lalu memeriksa kecukupan stok.",
|
||||||
|
"elements": "Header, info kalkulasi otomatis, dropdown produk/resep, input jumlah produksi, tombol hitung, daftar bahan, checkbox bahan, kebutuhan, stok tersedia, status cukup/kurang, ringkasan pengurangan stok, rekomendasi tambah stok, tombol submit produksi.",
|
||||||
|
"nav": "Submit sukses membuka dialog produksi berhasil. Jika stok kurang, pengguna diarahkan menambah stok.",
|
||||||
|
"notes": "Ini halaman inti sistem pakar; hasil kalkulasi dan rekomendasi harus terlihat tanpa banyak scroll.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| < Prediksi Kebutuhan Bahan |
|
||||||
|
| Kalkulasi berdasarkan produksi |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [Calc] Kalkulasi Otomatis | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Rencana Produksi |
|
||||||
|
| Produk [v] |
|
||||||
|
| Jumlah Produksi |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | 0 | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | HITUNG BAHAN | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Kebutuhan Bahan |
|
||||||
|
| | Bahan | Butuh | Stok | Stat | |
|
||||||
|
| | Tepung| 3 kg |10 kg |Cukup | |
|
||||||
|
| | Telur | 2 kg |1 kg |Kurang| |
|
||||||
|
| Ringkasan Pengurangan Stok |
|
||||||
|
| Rekomendasi: Telur +1 kg |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | SUBMIT PRODUKSI | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Produksi Berhasil",
|
||||||
|
"purpose": "Memberi konfirmasi bahwa produksi tersimpan dan stok bahan telah diperbarui.",
|
||||||
|
"elements": "Icon sukses, judul, pesan singkat, tombol sukses.",
|
||||||
|
"nav": "Menutup dialog dan kembali ke halaman Prediksi dengan form reset.",
|
||||||
|
"notes": "Gunakan dialog sederhana agar pengguna memahami aksi sudah selesai.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| |
|
||||||
|
| [ OK ] |
|
||||||
|
| |
|
||||||
|
| Produksi Berhasil |
|
||||||
|
| Stok bahan sudah diperbarui |
|
||||||
|
| sesuai produksi. |
|
||||||
|
| |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | SUKSES | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Laporan & Analitik",
|
||||||
|
"purpose": "Menampilkan rekap stok, riwayat stok masuk, prediksi, grafik penggunaan, bahan kritis, dan export laporan.",
|
||||||
|
"elements": "Kartu total produk, total bahan, stok kritis, total prediksi, daftar stok bahan, riwayat stok masuk, daftar prediksi, grafik, bahan kritis, tombol export CSV/PDF.",
|
||||||
|
"nav": "Export membuka pilihan periode dan format. Bottom navigation menuju menu lain.",
|
||||||
|
"notes": "Konten laporan panjang, jadi section perlu heading jelas dan tinggi list dibatasi agar mudah discroll.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Laporan & Analitik |
|
||||||
|
| +-------------+ +------------+ |
|
||||||
|
| | Total Prod | | Total Bhn | |
|
||||||
|
| | 8 | | 6 | |
|
||||||
|
| +-------------+ +------------+ |
|
||||||
|
| | Stok Kritis | | Prediksi | |
|
||||||
|
| | 2 | | 10 | |
|
||||||
|
| +-------------+ +------------+ |
|
||||||
|
| Laporan Stok Bahan |
|
||||||
|
| | Nama | Stok | Status | |
|
||||||
|
| Riwayat Stok Masuk |
|
||||||
|
| | Bahan | Tgl | +Jumlah | |
|
||||||
|
| Laporan Prediksi |
|
||||||
|
| | Produk | Tgl | Prediksi | |
|
||||||
|
| Grafik Penggunaan Bahan |
|
||||||
|
| [Bar Chart] [Pie Chart] |
|
||||||
|
| [ Export CSV ] [ Export PDF ] |
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Export Laporan",
|
||||||
|
"purpose": "Memilih periode dan format export laporan.",
|
||||||
|
"elements": "Dialog periode harian/2 mingguan/bulanan, dialog format CSV/PDF, snackbar hasil export, aksi buka/share file.",
|
||||||
|
"nav": "Setelah export selesai tetap di halaman Laporan.",
|
||||||
|
"notes": "Gunakan dialog bertahap agar pilihan tidak memenuhi layar kecil.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Pilih Periode Export |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | Harian | |
|
||||||
|
| | 2 Mingguan | |
|
||||||
|
| | Bulanan | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| |
|
||||||
|
| Pilih Format Export |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | CSV | |
|
||||||
|
| | PDF | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| |
|
||||||
|
| Snackbar: Laporan diekspor |
|
||||||
|
| [ Buka ] [ Share ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pengaturan / Profil",
|
||||||
|
"purpose": "Menampilkan informasi akun, akses perubahan password, informasi aplikasi, dan logout.",
|
||||||
|
"elements": "Kartu akun, nama, email, menu ubah password, tombol logout, info versi aplikasi.",
|
||||||
|
"nav": "Ubah password membuka dialog OTP. Logout membuka dialog konfirmasi lalu kembali ke Login.",
|
||||||
|
"notes": "Profil dan keamanan dipisahkan agar hierarki informasi jelas.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Pengaturan |
|
||||||
|
| Akun |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [Avatar] Ibu Sulastri | |
|
||||||
|
| | sulastri...@gmail.com | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Keamanan |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | [Lock] Ubah Password > | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| | LOGOUT | |
|
||||||
|
| +----------------------------+ |
|
||||||
|
| Tobaku Sulastri |
|
||||||
|
| Version 1.0.0 |
|
||||||
|
|--------------------------------|
|
||||||
|
| Home Produk Stok Pred Laporan |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Logout Confirmation",
|
||||||
|
"purpose": "Mencegah pengguna keluar tanpa sengaja.",
|
||||||
|
"elements": "Judul logout, pesan konfirmasi, tombol batal, tombol logout.",
|
||||||
|
"nav": "Batal menutup dialog. Logout membersihkan sesi dan menuju Login.",
|
||||||
|
"notes": "Tombol logout diberi warna bahaya agar konsekuensi aksi jelas.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Logout |
|
||||||
|
| Apakah Anda yakin ingin keluar |
|
||||||
|
| dari aplikasi? |
|
||||||
|
| |
|
||||||
|
| [ Batal ] [ Logout ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "State Loading / Empty / Error",
|
||||||
|
"purpose": "Memberi respons visual saat data sedang dimuat, kosong, atau gagal diambil dari server.",
|
||||||
|
"elements": "Circular progress, pesan kosong, icon error, tombol coba lagi, snackbar error.",
|
||||||
|
"nav": "Coba Lagi memuat ulang data. Empty state tetap di halaman terkait.",
|
||||||
|
"notes": "State ini penting karena aplikasi bergantung pada API Flask dan data MySQL.",
|
||||||
|
"wire": """
|
||||||
|
+--------------------------------+
|
||||||
|
| Loading |
|
||||||
|
| [ spinner ] |
|
||||||
|
| Memuat data... |
|
||||||
|
|--------------------------------|
|
||||||
|
| Empty |
|
||||||
|
| [ icon ] |
|
||||||
|
| Data belum tersedia |
|
||||||
|
|--------------------------------|
|
||||||
|
| Error |
|
||||||
|
| [ ! ] |
|
||||||
|
| Gagal memuat data |
|
||||||
|
| [ Coba Lagi ] |
|
||||||
|
+--------------------------------+
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_document() -> str:
|
||||||
|
body: list[str] = []
|
||||||
|
body.append(p("Wireframe Mobile Project", style="Title", jc="center", after=60))
|
||||||
|
body.append(p("Aplikasi Sistem Prediksi dan Pengelolaan Stok Bahan Kue Tobaku Sulastri", style="Subtitle", jc="center", after=420))
|
||||||
|
|
||||||
|
body.append(heading("Ringkasan Aplikasi", 1))
|
||||||
|
body.append(
|
||||||
|
p(
|
||||||
|
"Aplikasi Tobaku Sulastri adalah aplikasi mobile berbasis Flutter untuk membantu admin toko bahan kue dalam memantau stok, mencatat stok masuk, menghitung kebutuhan bahan berdasarkan rencana produksi, dan membuat laporan stok serta prediksi. Aplikasi terhubung ke API Flask dan menyimpan sesi login menggunakan SharedPreferences."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
body.append(heading("Analisis Kebutuhan Berdasarkan Repository", 1))
|
||||||
|
analysis_rows = [
|
||||||
|
["Area", "Temuan dari Project", "Kebutuhan Wireframe"],
|
||||||
|
["Autentikasi", "Splash mengecek sesi login; Login memakai email/username dan password; OTP tersedia untuk reset password.", "Splash, Login, Reset Password/OTP, Logout."],
|
||||||
|
["Navigasi", "Route utama: splash, login, dashboard, prediction, transaction, products, reports, settings.", "Bottom navigation mobile dengan menu Dashboard, Produk, Stok Masuk, Prediksi, Laporan, Pengaturan."],
|
||||||
|
["Produk/Stok", "Product model memuat nama, kategori, harga, stok, stok minimum, satuan, status.", "Daftar produk, filter status, detail produk, rekomendasi tambah stok."],
|
||||||
|
["Stok Masuk", "TransactionController memakai dropdown produk, jumlah, keranjang, tanggal, dan submit transaksi.", "Form stok masuk, keranjang, total pembayaran, riwayat stok masuk."],
|
||||||
|
["Prediksi", "PredictionController mengambil resep, menghitung bahan, cek stok cukup/kurang, submit produksi, simpan prediksi.", "Halaman sistem pakar/prediksi dengan hasil bahan, status, rekomendasi, dan submit produksi."],
|
||||||
|
["Laporan", "ReportController memuat stok, stok masuk, prediksi, bahan kritis, grafik, export CSV/PDF.", "Halaman laporan panjang dengan ringkasan, list, chart, export."],
|
||||||
|
["Pengaturan", "Settings menampilkan akun, ubah password OTP, logout, info aplikasi.", "Profil/pengaturan dan dialog konfirmasi logout."],
|
||||||
|
]
|
||||||
|
body.append(table(analysis_rows, [1700, 3900, 3760]))
|
||||||
|
|
||||||
|
body.append(heading("Asumsi Perancangan", 1))
|
||||||
|
assumptions = [
|
||||||
|
"Tidak ditemukan halaman Register pada route aplikasi, sehingga dokumen hanya memasukkan Login dan Reset Password/OTP.",
|
||||||
|
"Aplikasi digunakan oleh admin/pemilik toko, bukan pelanggan umum.",
|
||||||
|
"Prediksi yang dimaksud pada UI saat ini berfokus pada kalkulasi kebutuhan bahan berdasarkan resep dan jumlah produksi.",
|
||||||
|
"Halaman profil digabung dengan Pengaturan karena struktur project hanya memiliki SettingsScreen.",
|
||||||
|
"Semua rancangan difokuskan pada layar mobile portrait dengan navigasi bawah.",
|
||||||
|
]
|
||||||
|
for item in assumptions:
|
||||||
|
body.append(bullet(item))
|
||||||
|
|
||||||
|
body.append(heading("Daftar Halaman", 1))
|
||||||
|
page_rows = [["No", "Halaman", "Fungsi Utama"]]
|
||||||
|
for idx, item in enumerate(PAGES, start=1):
|
||||||
|
page_rows.append([str(idx), item["name"], item["purpose"]])
|
||||||
|
body.append(table(page_rows, [700, 2600, 6060]))
|
||||||
|
|
||||||
|
body.append(page_break())
|
||||||
|
body.append(heading("User Flow Dari Awal Hingga Akhir", 1))
|
||||||
|
flows = [
|
||||||
|
"Pengguna membuka aplikasi dan melihat Splash Screen.",
|
||||||
|
"Sistem mengecek sesi login yang tersimpan.",
|
||||||
|
"Jika belum login, pengguna diarahkan ke Login.",
|
||||||
|
"Pengguna memasukkan email/username dan password.",
|
||||||
|
"Jika login berhasil, pengguna masuk ke Dashboard.",
|
||||||
|
"Dari Dashboard, pengguna dapat memantau bahan digunakan hari ini, produk aktif, penggunaan bahan, dan stok menipis.",
|
||||||
|
"Pengguna membuka Data Produk untuk melihat detail stok dan status bahan.",
|
||||||
|
"Jika stok kurang, pengguna dapat membuka Stok Masuk untuk menambah stok bahan.",
|
||||||
|
"Pengguna membuka Prediksi Kebutuhan Bahan, memilih produk/resep, mengisi jumlah produksi, lalu menghitung kebutuhan bahan.",
|
||||||
|
"Sistem menampilkan kebutuhan bahan, status cukup/kurang, dan rekomendasi penambahan stok.",
|
||||||
|
"Jika stok cukup, pengguna submit produksi dan sistem mengurangi stok bahan.",
|
||||||
|
"Pengguna melihat Laporan & Analitik untuk mengecek rekap stok, prediksi, grafik, bahan kritis, dan export CSV/PDF.",
|
||||||
|
"Pengguna dapat membuka Pengaturan untuk melihat akun, mengubah password, atau logout.",
|
||||||
|
]
|
||||||
|
for idx, item in enumerate(flows, start=1):
|
||||||
|
body.append(numbered(idx, item))
|
||||||
|
|
||||||
|
body.append(ascii_block("""
|
||||||
|
Splash
|
||||||
|
|
|
||||||
|
+-- Sesi aktif ----> Dashboard
|
||||||
|
|
|
||||||
|
+-- Belum login --> Login -- berhasil --> Dashboard
|
||||||
|
|
|
||||||
|
+-- Lupa Password --> OTP Reset
|
||||||
|
|
||||||
|
Dashboard
|
||||||
|
|
|
||||||
|
+-- Produk ----> Detail Produk ----> Tambah Stok
|
||||||
|
|
|
||||||
|
+-- Stok Masuk ----> Keranjang ----> Simpan Stok
|
||||||
|
|
|
||||||
|
+-- Prediksi ----> Hitung Bahan ----> Submit Produksi ----> Sukses
|
||||||
|
|
|
||||||
|
+-- Laporan ----> Export CSV/PDF
|
||||||
|
|
|
||||||
|
+-- Pengaturan ----> Ubah Password / Logout
|
||||||
|
"""))
|
||||||
|
|
||||||
|
body.append(page_break())
|
||||||
|
body.append(heading("Wireframe Mobile Per Halaman", 1))
|
||||||
|
for idx, item in enumerate(PAGES, start=1):
|
||||||
|
body.append(heading(f"{idx}. {item['name']}", 2))
|
||||||
|
body.append(p(runs=[run("Tujuan: ", bold=True), run(item["purpose"])]))
|
||||||
|
body.append(p(runs=[run("Elemen UI: ", bold=True), run(item["elements"])]))
|
||||||
|
body.append(p(runs=[run("Navigasi: ", bold=True), run(item["nav"])]))
|
||||||
|
body.append(p(runs=[run("Catatan desain mobile: ", bold=True), run(item["notes"])]))
|
||||||
|
body.append(ascii_block(item["wire"]))
|
||||||
|
if idx in {3, 6, 10, 13}:
|
||||||
|
body.append(page_break())
|
||||||
|
|
||||||
|
body.append(page_break())
|
||||||
|
body.append(heading("Penjelasan Komponen UI Utama", 1))
|
||||||
|
component_rows = [
|
||||||
|
["Komponen", "Fungsi", "Catatan Mobile"],
|
||||||
|
["Bottom Navigation", "Akses cepat ke enam halaman utama.", "Label harus singkat; gunakan ikon konsisten."],
|
||||||
|
["Header", "Menampilkan konteks halaman dan tombol kembali jika bukan dashboard.", "Jaga tinggi header agar konten utama tetap terlihat."],
|
||||||
|
["Search dan Filter Chip", "Mempercepat pencarian produk berdasarkan nama/status.", "Chip horizontal scroll cocok untuk layar kecil."],
|
||||||
|
["Card Produk", "Menampilkan informasi bahan secara ringkas.", "Gunakan badge status dan progress kapasitas."],
|
||||||
|
["Form Input", "Mengambil data login, stok masuk, produksi, dan produk baru.", "Validasi langsung dan keyboard numerik untuk angka."],
|
||||||
|
["Keranjang Stok Masuk", "Menampung beberapa stok masuk sebelum disimpan.", "Kontrol tambah/kurang harus mudah disentuh."],
|
||||||
|
["Tabel Kebutuhan Bahan", "Menampilkan bahan, kebutuhan, stok, dan status.", "Gunakan row/card jika tabel terlalu sempit."],
|
||||||
|
["Dialog/Bottom Sheet", "Digunakan untuk OTP, detail produk, tambah produk, sukses, logout.", "Tidak memenuhi layar penuh kecuali form panjang."],
|
||||||
|
["Snackbar", "Memberi feedback error/sukses singkat.", "Pesan harus pendek dan jelas."],
|
||||||
|
]
|
||||||
|
body.append(table(component_rows, [2100, 3900, 3360]))
|
||||||
|
|
||||||
|
body.append(heading("Catatan UX/UI", 1))
|
||||||
|
ux_notes = [
|
||||||
|
"Fokus utama aplikasi adalah efisiensi admin saat memantau stok dan menghitung kebutuhan bahan.",
|
||||||
|
"Halaman Prediksi harus menjadi alur paling jelas karena berisi proses sistem pakar: pilih resep, input jumlah, hitung, cek stok, rekomendasi, submit.",
|
||||||
|
"Gunakan warna status secara konsisten: hijau untuk cukup/aman, oranye untuk sedang/peringatan, merah untuk kritis/kurang.",
|
||||||
|
"Semua tombol aksi utama perlu ukuran sentuh minimal sekitar 44 px agar nyaman di mobile.",
|
||||||
|
"Data yang panjang seperti laporan sebaiknya dibagi menjadi section dengan heading dan list yang bisa discroll.",
|
||||||
|
"Empty state dan error state wajib terlihat karena aplikasi bergantung pada koneksi API.",
|
||||||
|
]
|
||||||
|
for item in ux_notes:
|
||||||
|
body.append(bullet(item))
|
||||||
|
|
||||||
|
body.append(heading("Rekomendasi Pengembangan Desain Berikutnya", 1))
|
||||||
|
recs = [
|
||||||
|
"Buat desain high-fidelity di Figma berdasarkan wireframe ini sebelum implementasi UI final.",
|
||||||
|
"Pisahkan komponen reusable seperti AppHeader, StatCard, ProductCard, FormField, StatusBadge, dan EmptyState.",
|
||||||
|
"Pertimbangkan tab atau collapsible section pada Laporan agar layar tidak terlalu panjang.",
|
||||||
|
"Tambahkan indikator koneksi/server pada Splash atau Dashboard bila API Flask tidak aktif.",
|
||||||
|
"Tambahkan konfirmasi sebelum submit produksi karena aksi tersebut mengurangi stok bahan.",
|
||||||
|
"Untuk halaman Prediksi, pertimbangkan tampilan hasil berbentuk card per bahan agar lebih mudah dibaca di layar kecil.",
|
||||||
|
"Lakukan pengujian mobile pada ukuran layar kecil agar bottom navigation enam item tetap terbaca.",
|
||||||
|
]
|
||||||
|
for item in recs:
|
||||||
|
body.append(bullet(item))
|
||||||
|
|
||||||
|
sect = """
|
||||||
|
<w:sectPr>
|
||||||
|
<w:pgSz w:w="12240" w:h="15840"/>
|
||||||
|
<w:pgMar w:top="1080" w:right="1080" w:bottom="1080" w:left="1080" w:header="720" w:footer="720" w:gutter="0"/>
|
||||||
|
</w:sectPr>"""
|
||||||
|
return f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
|
||||||
|
xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
|
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
|
||||||
|
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||||
|
xmlns:w10="urn:schemas-microsoft-com:office:word"
|
||||||
|
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||||
|
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
|
||||||
|
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
|
||||||
|
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
|
||||||
|
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
|
||||||
|
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
|
||||||
|
mc:Ignorable="w14 wp14">
|
||||||
|
<w:body>{''.join(body)}{sect}</w:body>
|
||||||
|
</w:document>"""
|
||||||
|
|
||||||
|
|
||||||
|
def styles_xml() -> str:
|
||||||
|
return """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||||
|
<w:style w:type="paragraph" w:default="1" w:styleId="Normal">
|
||||||
|
<w:name w:val="Normal"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:pPr><w:spacing w:after="140" w:line="276" w:lineRule="auto"/></w:pPr>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:sz w:val="22"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
<w:style w:type="paragraph" w:styleId="Title">
|
||||||
|
<w:name w:val="Title"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:pPr><w:spacing w:after="120"/></w:pPr>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:b/><w:sz w:val="34"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
<w:style w:type="paragraph" w:styleId="Subtitle">
|
||||||
|
<w:name w:val="Subtitle"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:color w:val="666666"/><w:sz w:val="22"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
<w:style w:type="paragraph" w:styleId="Heading1">
|
||||||
|
<w:name w:val="heading 1"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:pPr><w:spacing w:before="220" w:after="120"/></w:pPr>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:b/><w:color w:val="5B4436"/><w:sz w:val="28"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
<w:style w:type="paragraph" w:styleId="Heading2">
|
||||||
|
<w:name w:val="heading 2"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:pPr><w:spacing w:before="160" w:after="90"/></w:pPr>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:b/><w:color w:val="111111"/><w:sz w:val="24"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
<w:style w:type="paragraph" w:styleId="Heading3">
|
||||||
|
<w:name w:val="heading 3"/>
|
||||||
|
<w:qFormat/>
|
||||||
|
<w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial"/><w:b/><w:sz w:val="22"/></w:rPr>
|
||||||
|
</w:style>
|
||||||
|
</w:styles>"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_docx() -> None:
|
||||||
|
content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||||
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||||
|
<Default Extension="xml" ContentType="application/xml"/>
|
||||||
|
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||||
|
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
|
||||||
|
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
||||||
|
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
|
||||||
|
</Types>"""
|
||||||
|
root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||||
|
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
||||||
|
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
||||||
|
</Relationships>"""
|
||||||
|
document_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||||
|
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||||
|
</Relationships>"""
|
||||||
|
now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||||
|
core = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:dcterms="http://purl.org/dc/terms/"
|
||||||
|
xmlns:dcmitype="http://purl.org/dc/dcmitype/"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<dc:title>Wireframe Mobile Project</dc:title>
|
||||||
|
<dc:creator>Codex</dc:creator>
|
||||||
|
<cp:lastModifiedBy>Codex</cp:lastModifiedBy>
|
||||||
|
<dcterms:created xsi:type="dcterms:W3CDTF">{now}</dcterms:created>
|
||||||
|
<dcterms:modified xsi:type="dcterms:W3CDTF">{now}</dcterms:modified>
|
||||||
|
</cp:coreProperties>"""
|
||||||
|
app = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
|
||||||
|
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
|
||||||
|
<Application>Microsoft Word</Application>
|
||||||
|
</Properties>"""
|
||||||
|
if OUT.exists():
|
||||||
|
OUT.unlink()
|
||||||
|
with ZipFile(OUT, "w", ZIP_DEFLATED) as z:
|
||||||
|
z.writestr("[Content_Types].xml", content_types)
|
||||||
|
z.writestr("_rels/.rels", root_rels)
|
||||||
|
z.writestr("word/document.xml", build_document())
|
||||||
|
z.writestr("word/styles.xml", styles_xml())
|
||||||
|
z.writestr("word/_rels/document.xml.rels", document_rels)
|
||||||
|
z.writestr("docProps/core.xml", core)
|
||||||
|
z.writestr("docProps/app.xml", app)
|
||||||
|
print(OUT)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build_docx()
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
<mxfile host="app.diagrams.net" modified="2026-06-03T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
|
||||||
|
<diagram id="erd-project-detail" name="ERD Project Detail">
|
||||||
|
<mxGraphModel dx="2200" dy="1500" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2200" pageHeight="1500" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="title" value="ERD Aplikasi Permintaan dan Prediksi Stok Bahan Kue" style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=26;fontStyle=1;fontColor=#222222;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="540" y="30" width="1120" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="note" value="Gaya Chen: entitas = kotak, atribut = oval, relasi = belah ketupat. Atribut bergaris bawah adalah primary key. Beberapa label memakai alias tabel sesuai kode runtime: transactions / Stock In, predictions / prediksi, products.name / product_name." style="text;html=1;strokeColor=none;fillColor=none;fontFamily=Arial;fontSize=13;fontColor=#555555;whiteSpace=wrap;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="485" y="75" width="1170" height="45" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Entities -->
|
||||||
|
<mxCell id="e_login" value="Login" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=14;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="170" y="250" width="130" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_otp" value="Password_Reset_OTP" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="520" width="160" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_products" value="Products / Bahan" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=14;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="700" y="250" width="155" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_stockin" value="Transactions / Stock In" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="690" y="580" width="180" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_predictions" value="Predictions / Prediksi" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1260" y="250" width="175" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_recipes" value="Recipes" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=14;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1160" y="590" width="140" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_ingredients" value="Recipe_Ingredients" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1595" y="590" width="170" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_usage" value="Stock_Usage_History" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1125" y="1015" width="190" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_stokkeluar" value="Stok_Keluar" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="710" y="1015" width="145" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_stokmasuk" value="Stok_Masuk" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="780" width="145" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_bahan_report" value="Bahan (Laporan)" style="whiteSpace=wrap;html=1;rounded=0;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=13;fontStyle=1;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="420" y="1015" width="145" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Relationships -->
|
||||||
|
<mxCell id="r_login_otp" value="memiliki" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="210" y="400" width="90" height="65" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_login_product" value="mengelola" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="255" width="105" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_stockin_product" value="menambah stok" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="725" y="420" width="115" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_product_prediction" value="diprediksi" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1010" y="255" width="105" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_recipe_detail" value="memiliki" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1395" y="590" width="95" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_ing_product" value="mengacu bahan" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1515" y="345" width="115" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_recipe_usage" value="produksi memakai" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1180" y="805" width="115" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_usage_product" value="mengurangi stok" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="915" y="760" width="125" height="75" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_bahan_product" value="sinkron" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="940" width="95" height="70" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="r_stokmasuk_bahan" value="dicatat" style="rhombus;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#666666;strokeWidth=2;fontFamily=Arial;fontSize=12;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="462" y="910" width="90" height="65" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<!-- Login attributes -->
|
||||||
|
<mxCell id="a_login_id" value="<u>id_login</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="45" y="130" width="95" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_login_nama" value="nama" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="145" y="145" width="85" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_login_email" value="email" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="245" y="145" width="85" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_login_username" value="username" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="325" y="170" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_login_password" value="password" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="55" y="335" width="100" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_login_created" value="created_at" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="305" y="335" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- OTP attributes -->
|
||||||
|
<mxCell id="a_otp_id" value="<u>id_otp</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="55" y="665" width="90" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_otp_login" value="login_id (FK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="155" y="690" width="115" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_otp_code" value="otp_code" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="280" y="690" width="100" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_otp_exp" value="expires_at" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="360" y="630" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_otp_used" value="is_used" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="30" y="580" width="90" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Product attributes -->
|
||||||
|
<mxCell id="a_prod_id" value="<u>id_product</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="610" y="110" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_name" value="name / product_name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="740" y="105" width="150" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_cat" value="category" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="900" y="130" width="100" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_type" value="product_type" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="560" y="180" width="115" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_unit" value="unit" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="890" y="210" width="80" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_price" value="price" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="565" y="345" width="85" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_stock" value="current_stock" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="690" y="360" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_prod_min" value="min_stock" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="825" y="350" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Stock in attributes -->
|
||||||
|
<mxCell id="a_stockin_id" value="<u>id_transaction</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="570" y="510" width="135" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_stockin_pname" value="product_name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="870" y="515" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_stockin_qty" value="quantity / jumlah" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="560" y="680" width="135" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_stockin_price" value="unit_price" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="710" y="715" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_stockin_total" value="total_price" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="820" y="690" width="110" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_stockin_date" value="transaction_date" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="910" y="620" width="140" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Prediction attributes -->
|
||||||
|
<mxCell id="a_pred_id" value="<u>id_prediction</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1180" y="110" width="130" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_pred_product" value="product_name / nama_produk" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1320" y="110" width="180" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_pred_date" value="prediction_date / tanggal_prediksi" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1515" y="145" width="205" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_pred_qty" value="predicted_quantity / hasil_prediksi" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1495" y="300" width="210" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_pred_needs" value="estimated_needs / estimasi_kebutuhan_bahan" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1195" y="380" width="255" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_pred_metric" value="accuracy_r2, error_mae" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1460" y="390" width="155" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Recipe attributes -->
|
||||||
|
<mxCell id="a_recipe_id" value="<u>id_recipe</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1090" y="500" width="110" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_recipe_name" value="recipe_name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1240" y="500" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_recipe_desc" value="description" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1060" y="690" width="115" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_recipe_created" value="created_at" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1250" y="700" width="110" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Ingredient attributes -->
|
||||||
|
<mxCell id="a_ing_id" value="<u>id_recipe_ingredient</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1545" y="500" width="170" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_ing_recipe" value="recipe_id (FK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1740" y="505" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_ing_product" value="product_name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1785" y="610" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_ing_qty" value="quantity_needed" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1545" y="710" width="140" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_ing_unit" value="unit" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1710" y="710" width="80" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Usage attributes -->
|
||||||
|
<mxCell id="a_usage_id" value="<u>id_usage</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1005" y="930" width="105" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_usage_recipe" value="recipe_name" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1115" y="930" width="115" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_usage_prod" value="product_id (FK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1240" y="930" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_usage_qty" value="quantity_used" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1025" y="1125" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_usage_prodqty" value="production_quantity" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1165" y="1135" width="150" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_usage_unit" value="unit" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="1330" y="1125" width="80" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Stok keluar attributes -->
|
||||||
|
<mxCell id="a_skel_id" value="<u>id_stok_keluar</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="600" y="1125" width="135" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_skel_bahan" value="bahan_id" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="750" y="1135" width="95" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_skel_qty" value="jumlah_keluar" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="860" y="1125" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_skel_date" value="tanggal_keluar" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="680" y="930" width="130" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Bahan report attributes -->
|
||||||
|
<mxCell id="a_bahan_id" value="<u>id_bahan</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="255" y="930" width="100" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_bahan_prod" value="product_id (FK)" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="365" y="930" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_bahan_nama" value="nama_bahan" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="250" y="1115" width="120" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_bahan_stok" value="stok" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="380" y="1135" width="80" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_bahan_min" value="stok_minimum" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="470" y="1130" width="125" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Stok masuk report attributes -->
|
||||||
|
<mxCell id="a_smasuk_id" value="<u>id_stok_masuk</u>" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="250" y="780" width="140" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_smasuk_bahan" value="bahan_id / product_id" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="300" y="705" width="155" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_smasuk_jumlah" value="jumlah" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="575" y="775" width="90" height="45" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="a_smasuk_tanggal" value="tanggal" style="ellipse;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#999999;fontFamily=Arial;fontSize=12;" vertex="1" parent="1"><mxGeometry x="560" y="850" width="90" height="45" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Connect attributes to entities -->
|
||||||
|
<mxCell id="edge_a_login_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_id" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_login_nama" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_nama" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_login_email" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_email" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_login_username" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_username" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_login_password" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_password" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_login_created" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_login_created" target="e_login"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_otp_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_otp_id" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_otp_login" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_otp_login" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_otp_code" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_otp_code" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_otp_exp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_otp_exp" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_otp_used" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_otp_used" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_prod_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_id" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_name" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_name" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_cat" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_cat" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_type" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_type" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_unit" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_unit" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_price" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_price" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_stock" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_stock" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_prod_min" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_prod_min" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_stockin_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_id" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_stockin_pname" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_pname" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_stockin_qty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_qty" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_stockin_price" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_price" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_stockin_total" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_total" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_stockin_date" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_stockin_date" target="e_stockin"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_pred_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_id" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_pred_product" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_product" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_pred_date" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_date" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_pred_qty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_qty" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_pred_needs" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_needs" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_pred_metric" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_pred_metric" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_recipe_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_recipe_id" target="e_recipes"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_recipe_name" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_recipe_name" target="e_recipes"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_recipe_desc" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_recipe_desc" target="e_recipes"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_recipe_created" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_recipe_created" target="e_recipes"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_ing_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_ing_id" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_ing_recipe" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_ing_recipe" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_ing_product" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_ing_product" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_ing_qty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_ing_qty" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_ing_unit" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_ing_unit" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_usage_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_id" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_usage_recipe" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_recipe" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_usage_prod" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_prod" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_usage_qty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_qty" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_usage_prodqty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_prodqty" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_usage_unit" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_usage_unit" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_skel_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_skel_id" target="e_stokkeluar"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_skel_bahan" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_skel_bahan" target="e_stokkeluar"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_skel_qty" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_skel_qty" target="e_stokkeluar"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_skel_date" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_skel_date" target="e_stokkeluar"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_bahan_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_bahan_id" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_bahan_prod" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_bahan_prod" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_bahan_nama" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_bahan_nama" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_bahan_stok" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_bahan_stok" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_bahan_min" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_bahan_min" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_a_smasuk_id" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_smasuk_id" target="e_stokmasuk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_smasuk_bahan" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_smasuk_bahan" target="e_stokmasuk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_smasuk_jumlah" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_smasuk_jumlah" target="e_stokmasuk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_a_smasuk_tanggal" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#777777;" edge="1" parent="1" source="a_smasuk_tanggal" target="e_stokmasuk"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<!-- Relationship edges with cardinalities -->
|
||||||
|
<mxCell id="edge_login_r_otp" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_login" target="r_login_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_otp_r_login" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_login_otp" target="e_otp"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_login_r_product" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_login" target="r_login_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_product_r_login" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_login_product" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_stockin_r_prod" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_stockin" target="r_stockin_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_prod_r_stockin" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_stockin_product" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_prod_r_pred" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_products" target="r_product_prediction"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_pred_r_prod" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_product_prediction" target="e_predictions"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_recipe_r_detail" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_recipes" target="r_recipe_detail"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_detail_r_recipe" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_recipe_detail" target="e_ingredients"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_ing_r_prod" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_ingredients" target="r_ing_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_prod_r_ing" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_ing_product" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_recipe_r_usage" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_recipes" target="r_recipe_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_usage_r_recipe" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_recipe_usage" target="e_usage"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_usage_r_prod" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_usage" target="r_usage_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_prod_r_usage" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_usage_product" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
|
||||||
|
<mxCell id="edge_bahan_r_prod" value="0..1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_bahan_report" target="r_bahan_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_prod_r_bahan" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_bahan_product" target="e_products"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_stokmasuk_r_bahan" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_stokmasuk" target="r_stokmasuk_bahan"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_bahan_r_stokmasuk" value="1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="r_stokmasuk_bahan" target="e_bahan_report"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
<mxCell id="edge_stokkeluar_r_bahan" value="N" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=none;strokeColor=#555555;fontSize=12;" edge="1" parent="1" source="e_stokkeluar" target="r_bahan_product"><mxGeometry relative="1" as="geometry" /></mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
|
After Width: | Height: | Size: 324 KiB |
|
|
@ -711,36 +711,139 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
message:
|
message:
|
||||||
'Belum ada data bahan dari prediksi untuk menghitung bahan yang paling sering digunakan.',
|
'Belum ada data bahan dari prediksi untuk menghitung bahan yang paling sering digunakan.',
|
||||||
)
|
)
|
||||||
: SizedBox(
|
: _buildUsagePieChart(usagePie),
|
||||||
height: 200,
|
|
||||||
child: PieChart(
|
|
||||||
PieChartData(
|
|
||||||
sectionsSpace: 2,
|
|
||||||
centerSpaceRadius: 40,
|
|
||||||
sections:
|
|
||||||
usagePie.map((entry) {
|
|
||||||
final value = entry['value'] as double;
|
|
||||||
|
|
||||||
return PieChartSectionData(
|
|
||||||
value: value,
|
|
||||||
color: entry['color'] as Color,
|
|
||||||
radius: 50,
|
|
||||||
showTitle: value >= 5,
|
|
||||||
title: '${value.toStringAsFixed(1)}%',
|
|
||||||
titleStyle: AppTextStyles.labelSmall.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildUsagePieChart(List<Map<String, dynamic>> usagePie) {
|
||||||
|
final mostUsed = usagePie.reduce(
|
||||||
|
(current, next) =>
|
||||||
|
(current['value'] as double) >= (next['value'] as double)
|
||||||
|
? current
|
||||||
|
: next,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: PieChart(
|
||||||
|
PieChartData(
|
||||||
|
sectionsSpace: 2,
|
||||||
|
centerSpaceRadius: 40,
|
||||||
|
sections:
|
||||||
|
usagePie.map((entry) {
|
||||||
|
final value = entry['value'] as double;
|
||||||
|
|
||||||
|
return PieChartSectionData(
|
||||||
|
value: value,
|
||||||
|
color: entry['color'] as Color,
|
||||||
|
radius: 50,
|
||||||
|
showTitle: value >= 5,
|
||||||
|
title: '${value.toStringAsFixed(1)}%',
|
||||||
|
titleStyle: AppTextStyles.labelSmall.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.bgLight,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: AppColors.grey200),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Cara membaca pie chart',
|
||||||
|
style: AppTextStyles.labelLarge.copyWith(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(
|
||||||
|
'Setiap warna mewakili satu bahan. Semakin besar bagian lingkaran, semakin besar penggunaan bahan tersebut dibandingkan bahan lainnya.',
|
||||||
|
style: AppTextStyles.bodySmall.copyWith(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'${mostUsed['label']} merupakan bahan yang paling sering digunakan, yaitu ${_formatQuantity(mostUsed['amount'])} ${mostUsed['unit']} (${(mostUsed['value'] as double).toStringAsFixed(1)}% dari total penggunaan).',
|
||||||
|
style: AppTextStyles.bodySmall.copyWith(
|
||||||
|
color: AppColors.primaryBrownDark,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'Keterangan warna',
|
||||||
|
style: AppTextStyles.labelLarge.copyWith(
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...usagePie.map(_buildUsagePieLegendItem),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUsagePieLegendItem(Map<String, dynamic> entry) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.only(top: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: entry['color'] as Color,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'${entry['label']}: ${_formatQuantity(entry['amount'])} ${entry['unit']}',
|
||||||
|
style: AppTextStyles.bodySmall.copyWith(
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${(entry['value'] as double).toStringAsFixed(1)}%',
|
||||||
|
style: AppTextStyles.labelSmall.copyWith(
|
||||||
|
color: entry['color'] as Color,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildUsageBarChart(List<Map<String, dynamic>> usageBars) {
|
Widget _buildUsageBarChart(List<Map<String, dynamic>> usageBars) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|
@ -1843,11 +1946,19 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
final colors = [
|
final colors = <Color>[
|
||||||
AppColors.primaryBrown,
|
AppColors.primaryBrown,
|
||||||
AppColors.secondaryOrange,
|
AppColors.secondaryOrange,
|
||||||
AppColors.secondaryBlue,
|
AppColors.secondaryBlue,
|
||||||
AppColors.secondaryGreen,
|
AppColors.secondaryGreen,
|
||||||
|
AppColors.secondaryRed,
|
||||||
|
const Color(0xFF7E57C2),
|
||||||
|
const Color(0xFF00ACC1),
|
||||||
|
const Color(0xFFF06292),
|
||||||
|
const Color(0xFF3949AB),
|
||||||
|
const Color(0xFF00897B),
|
||||||
|
const Color(0xFFFDD835),
|
||||||
|
const Color(0xFF6D4C41),
|
||||||
];
|
];
|
||||||
|
|
||||||
return usageSummary.asMap().entries.map((entry) {
|
return usageSummary.asMap().entries.map((entry) {
|
||||||
|
|
@ -1857,7 +1968,15 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
'label': item['label'] as String,
|
'label': item['label'] as String,
|
||||||
'value': item['value'] as double,
|
'value': item['value'] as double,
|
||||||
'unit': item['unit']?.toString() ?? 'kg',
|
'unit': item['unit']?.toString() ?? 'kg',
|
||||||
'color': colors[index % colors.length],
|
'color':
|
||||||
|
index < colors.length
|
||||||
|
? colors[index]
|
||||||
|
: HSVColor.fromAHSV(
|
||||||
|
1,
|
||||||
|
(index * 137.508) % 360,
|
||||||
|
0.72,
|
||||||
|
0.78,
|
||||||
|
).toColor(),
|
||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
@ -1870,7 +1989,7 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
(sum, item) => sum + (item['value'] as double),
|
(sum, item) => sum + (item['value'] as double),
|
||||||
);
|
);
|
||||||
if (total == 0) {
|
if (total == 0) {
|
||||||
return usageBars;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return usageBars
|
return usageBars
|
||||||
|
|
@ -1878,6 +1997,8 @@ class _ReportScreenState extends State<ReportScreen> {
|
||||||
(entry) => {
|
(entry) => {
|
||||||
'label': entry['label'],
|
'label': entry['label'],
|
||||||
'value': ((entry['value'] as double) / total) * 100,
|
'value': ((entry['value'] as double) / total) * 100,
|
||||||
|
'amount': entry['value'],
|
||||||
|
'unit': entry['unit'],
|
||||||
'color': entry['color'],
|
'color': entry['color'],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class MLService {
|
||||||
// API URL - Change based on environment
|
// API URL - Change based on environment
|
||||||
// Untuk emulator Android: 10.0.2.2
|
// Untuk emulator Android: 10.0.2.2
|
||||||
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
// Untuk device fisik: 192.168.x.x atau 127.0.0.1 kalau local
|
||||||
static const String baseUrl = 'http://192.168.1.2:5000';
|
static const String baseUrl = 'http://10.0.2.2:5000';
|
||||||
|
|
||||||
static const int timeoutSeconds = 30;
|
static const int timeoutSeconds = 30;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ Future<bool> showPasswordResetDialog(
|
||||||
int step = 0;
|
int step = 0;
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
bool resetSuccess = false;
|
bool resetSuccess = false;
|
||||||
|
bool isNewPasswordVisible = false;
|
||||||
|
bool isConfirmPasswordVisible = false;
|
||||||
String? maskedEmail;
|
String? maskedEmail;
|
||||||
|
|
||||||
void showMessage(String message, {bool isError = false}) {
|
void showMessage(String message, {bool isError = false}) {
|
||||||
|
|
@ -185,20 +187,58 @@ Future<bool> showPasswordResetDialog(
|
||||||
TextField(
|
TextField(
|
||||||
controller: newPasswordController,
|
controller: newPasswordController,
|
||||||
enabled: !isLoading,
|
enabled: !isLoading,
|
||||||
obscureText: true,
|
obscureText: !isNewPasswordVisible,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Password baru',
|
labelText: 'Password baru',
|
||||||
prefixIcon: Icon(Icons.lock_outline),
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip:
|
||||||
|
isNewPasswordVisible
|
||||||
|
? 'Sembunyikan password baru'
|
||||||
|
: 'Tampilkan password baru',
|
||||||
|
icon: Icon(
|
||||||
|
isNewPasswordVisible
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
isLoading
|
||||||
|
? null
|
||||||
|
: () => setDialogState(
|
||||||
|
() =>
|
||||||
|
isNewPasswordVisible =
|
||||||
|
!isNewPasswordVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
TextField(
|
||||||
controller: confirmPasswordController,
|
controller: confirmPasswordController,
|
||||||
enabled: !isLoading,
|
enabled: !isLoading,
|
||||||
obscureText: true,
|
obscureText: !isConfirmPasswordVisible,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Konfirmasi password',
|
labelText: 'Konfirmasi password',
|
||||||
prefixIcon: Icon(Icons.lock_reset_outlined),
|
prefixIcon: const Icon(Icons.lock_reset_outlined),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip:
|
||||||
|
isConfirmPasswordVisible
|
||||||
|
? 'Sembunyikan konfirmasi password'
|
||||||
|
: 'Tampilkan konfirmasi password',
|
||||||
|
icon: Icon(
|
||||||
|
isConfirmPasswordVisible
|
||||||
|
? Icons.visibility
|
||||||
|
: Icons.visibility_off,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
isLoading
|
||||||
|
? null
|
||||||
|
: () => setDialogState(
|
||||||
|
() =>
|
||||||
|
isConfirmPasswordVisible =
|
||||||
|
!isConfirmPasswordVisible,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
PANDUAN PENGUJIAN MODEL
|
||||||
|
Linear Regression dan Random Forest
|
||||||
|
Studi Kasus Prediksi Permintaan Stok Bahan Kue
|
||||||
|
Informasi
|
||||||
|
Keterangan
|
||||||
|
Jumlah data
|
||||||
|
6.742 transaksi
|
||||||
|
Pembagian data
|
||||||
|
80% training dan 20% testing
|
||||||
|
Target
|
||||||
|
jumlah_permintaan_bahan
|
||||||
|
Model yang dibandingkan
|
||||||
|
Linear Regression dan Random Forest Regressor
|
||||||
|
Tujuan dokumen
|
||||||
|
Bahan belajar, pembahasan laporan, dan persiapan sidang
|
||||||
|
|
||||||
|
Disusun berdasarkan hasil pengujian model pada proyek tugas akhir
|
||||||
|
Ringkasan Eksekutif
|
||||||
|
Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi dan dibagi menjadi 5.393 data training serta 1.349 data testing menggunakan rasio 80:20.
|
||||||
|
Model
|
||||||
|
R² Testing
|
||||||
|
MAE
|
||||||
|
RMSE
|
||||||
|
Interpretasi Awal
|
||||||
|
Linear Regression
|
||||||
|
0,7813
|
||||||
|
0,63
|
||||||
|
0,91
|
||||||
|
Cukup baik pada test set
|
||||||
|
Random Forest
|
||||||
|
0,9964
|
||||||
|
0,03
|
||||||
|
0,12
|
||||||
|
Sangat tinggi pada test set
|
||||||
|
|
||||||
|
Temuan Kritis
|
||||||
|
Nilai Random Forest yang sangat tinggi bukan sepenuhnya menunjukkan kemampuan meramalkan permintaan masa depan. Kolom total_harga_update memiliki hubungan langsung dengan target: total_harga_update = harga_satuan_update x jumlah_permintaan_bahan. Kondisi ini disebut data leakage.
|
||||||
|
|
||||||
|
Dokumen ini menjelaskan dua sudut pandang secara terpisah: pertama, bagaimana hasil testing yang ada dihitung; kedua, apakah hasil tersebut valid sebagai ukuran kemampuan prediksi nyata.
|
||||||
|
Daftar Isi
|
||||||
|
1. Gambaran Umum Pengujian Model
|
||||||
|
2. Persiapan dan Pembagian Data
|
||||||
|
3. Cara Kerja Linear Regression
|
||||||
|
4. Cara Kerja Random Forest
|
||||||
|
5. Metrik Evaluasi Model
|
||||||
|
6. Perhitungan Hasil Testing Proyek
|
||||||
|
7. Perbandingan dan Interpretasi Model
|
||||||
|
8. Analisis Data Leakage
|
||||||
|
9. Pengujian Ulang Tanpa Data Leakage
|
||||||
|
10. Rekomendasi Perbaikan Metodologi
|
||||||
|
11. Contoh Penjelasan untuk Laporan
|
||||||
|
12. Contoh Jawaban Saat Sidang
|
||||||
|
Lampiran: Ringkasan Rumus dan Istilah
|
||||||
|
1. Gambaran Umum Pengujian Model
|
||||||
|
Machine learning tidak cukup hanya dilatih. Model harus diuji menggunakan data yang tidak digunakan selama proses pelatihan. Tujuannya adalah mengukur apakah pola yang dipelajari model dapat diterapkan pada data lain, bukan hanya menghafal data training.
|
||||||
|
1.1 Tujuan Testing
|
||||||
|
Menilai kedekatan hasil prediksi dengan nilai permintaan aktual.
|
||||||
|
Membandingkan performa Linear Regression dan Random Forest.
|
||||||
|
Mendeteksi overfitting, underfitting, dan kesalahan metodologi.
|
||||||
|
Memilih model yang paling sesuai untuk digunakan pada aplikasi.
|
||||||
|
1.2 Alur Pengujian
|
||||||
|
Membaca dataset transaksi.
|
||||||
|
Mengidentifikasi target dan fitur.
|
||||||
|
Mengubah data kategori menjadi angka.
|
||||||
|
Membagi data menjadi training set dan testing set.
|
||||||
|
Melatih masing-masing model menggunakan training set.
|
||||||
|
Menghasilkan prediksi pada testing set.
|
||||||
|
Menghitung R², MAE, dan RMSE.
|
||||||
|
Membandingkan serta mengevaluasi validitas hasil.
|
||||||
|
2. Persiapan dan Pembagian Data
|
||||||
|
2.1 Struktur Dataset
|
||||||
|
Komponen
|
||||||
|
Nilai
|
||||||
|
Total data
|
||||||
|
6.742 transaksi
|
||||||
|
Training set
|
||||||
|
5.393 transaksi atau 80%
|
||||||
|
Testing set
|
||||||
|
1.349 transaksi atau 20%
|
||||||
|
Target
|
||||||
|
jumlah_permintaan_bahan
|
||||||
|
Jumlah fitur model tersimpan
|
||||||
|
10 fitur
|
||||||
|
|
||||||
|
2.2 Fitur Model
|
||||||
|
No.
|
||||||
|
Fitur
|
||||||
|
Makna
|
||||||
|
1
|
||||||
|
produk_encoded
|
||||||
|
Kode angka produk
|
||||||
|
2
|
||||||
|
tahun
|
||||||
|
Tahun transaksi
|
||||||
|
3
|
||||||
|
bulan
|
||||||
|
Bulan transaksi
|
||||||
|
4
|
||||||
|
hari
|
||||||
|
Tanggal dalam bulan
|
||||||
|
5
|
||||||
|
hari_dalam_minggu
|
||||||
|
Kode hari dalam minggu
|
||||||
|
6
|
||||||
|
harga_satuan_update
|
||||||
|
Harga satuan bahan
|
||||||
|
7
|
||||||
|
total_harga_update
|
||||||
|
Total harga transaksi
|
||||||
|
8
|
||||||
|
hari_minggu
|
||||||
|
Duplikasi informasi hari dalam minggu
|
||||||
|
9
|
||||||
|
nama_produk_encoded
|
||||||
|
Nama produk yang telah diubah menjadi angka
|
||||||
|
10
|
||||||
|
kategori_produk_encoded
|
||||||
|
Kategori produk yang telah diubah menjadi angka
|
||||||
|
|
||||||
|
2.3 Pembagian Training dan Testing
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(
|
||||||
|
X, y, test_size=0.2, random_state=42
|
||||||
|
)
|
||||||
|
|
||||||
|
Parameter test_size=0.2 berarti 20% data digunakan untuk testing. Parameter random_state=42 membuat pembagian acak selalu sama ketika program dijalankan ulang, sehingga hasil pengujian dapat direproduksi.
|
||||||
|
Catatan untuk Data Berdasarkan Waktu
|
||||||
|
Karena tujuan sistem adalah memprediksi masa depan, pembagian data secara kronologis lebih kuat daripada pembagian acak. Data lama sebaiknya digunakan sebagai training dan data terbaru sebagai testing.
|
||||||
|
|
||||||
|
3. Cara Kerja Linear Regression
|
||||||
|
Linear Regression mencari hubungan berbentuk garis lurus antara fitur masukan dan target. Setiap fitur memperoleh koefisien yang menunjukkan arah serta besarnya pengaruh terhadap hasil prediksi.
|
||||||
|
ŷ = β₀ + β₁x₁ + β₂x₂ + ... + βₚxₚ
|
||||||
|
Rumus umum prediksi Linear Regression
|
||||||
|
Simbol
|
||||||
|
Arti
|
||||||
|
ŷ
|
||||||
|
Nilai jumlah permintaan hasil prediksi
|
||||||
|
β₀
|
||||||
|
Intercept atau nilai awal model
|
||||||
|
βᵢ
|
||||||
|
Koefisien untuk fitur ke-i
|
||||||
|
xᵢ
|
||||||
|
Nilai fitur ke-i
|
||||||
|
p
|
||||||
|
Jumlah fitur yang digunakan
|
||||||
|
|
||||||
|
3.1 Proses Pembelajaran
|
||||||
|
Koefisien dicari dengan metode Ordinary Least Squares. Model memilih koefisien yang membuat jumlah kuadrat selisih antara nilai aktual dan prediksi menjadi sekecil mungkin.
|
||||||
|
SSE = Σ(yᵢ - ŷᵢ)²
|
||||||
|
SSE adalah jumlah kuadrat error prediksi
|
||||||
|
β = (XᵀX)⁻¹Xᵀy
|
||||||
|
Bentuk matriks solusi Ordinary Least Squares
|
||||||
|
3.2 Persamaan yang Dihasilkan pada Proyek
|
||||||
|
ŷ ≈ 25,704 - 0,00306(produk) - 0,01028(tahun) + 0,00289(bulan) - 0,00276(hari) - 0,000208(harga satuan) + 0,0000421(total harga) + ...
|
||||||
|
Linear Regression memperoleh R² testing sebesar 0,7813. Model ini terbantu oleh total harga, tetapi tidak dapat menangkap hubungan pembagian antara total harga dan harga satuan sebaik Random Forest.
|
||||||
|
3.3 Kelebihan dan Keterbatasan
|
||||||
|
Kelebihan
|
||||||
|
Keterbatasan
|
||||||
|
Mudah dijelaskan dan diinterpretasikan
|
||||||
|
Mengasumsikan hubungan cenderung linear
|
||||||
|
Proses training cepat
|
||||||
|
Kurang baik untuk pola kompleks dan non-linear
|
||||||
|
Koefisien menunjukkan arah hubungan
|
||||||
|
Sensitif terhadap fitur yang tidak relevan dan outlier
|
||||||
|
|
||||||
|
4. Cara Kerja Random Forest
|
||||||
|
Random Forest Regressor adalah metode ensemble yang membangun banyak Decision Tree. Setiap pohon mempelajari sampel dan kombinasi fitur yang berbeda. Prediksi akhir diperoleh dari rata-rata hasil seluruh pohon.
|
||||||
|
RandomForestRegressor(
|
||||||
|
n_estimators=100,
|
||||||
|
random_state=42,
|
||||||
|
n_jobs=-1
|
||||||
|
)
|
||||||
|
|
||||||
|
ŷRF = (ŷ₁ + ŷ₂ + ... + ŷT) / T
|
||||||
|
Prediksi Random Forest adalah rata-rata prediksi T pohon
|
||||||
|
Karena n_estimators=100, nilai T pada proyek ini adalah 100 pohon.
|
||||||
|
4.1 Proses pada Setiap Pohon
|
||||||
|
Mengambil sampel data training secara acak dengan pengembalian atau bootstrap.
|
||||||
|
Memilih sebagian fitur secara acak pada setiap kandidat percabangan.
|
||||||
|
Mencari batas pembagian data yang paling mengurangi error.
|
||||||
|
Menghasilkan prediksi dari nilai rata-rata target pada leaf node.
|
||||||
|
Menggabungkan prediksi seluruh pohon dengan operasi rata-rata.
|
||||||
|
4.2 Pemilihan Percabangan
|
||||||
|
Untuk regresi, Decision Tree memilih percabangan yang menurunkan Mean Squared Error. Percabangan yang membuat nilai target dalam setiap kelompok semakin seragam akan dipilih.
|
||||||
|
MSE = (1/n) Σ(yᵢ - ŷᵢ)²
|
||||||
|
4.3 Mengapa Random Forest Lebih Fleksibel
|
||||||
|
Berbeda dari Linear Regression, Random Forest tidak mengharuskan hubungan berbentuk garis lurus. Model dapat membentuk banyak aturan kondisi sehingga mampu mempelajari interaksi kompleks antara fitur.
|
||||||
|
5. Metrik Evaluasi Model
|
||||||
|
5.1 R² atau Koefisien Determinasi
|
||||||
|
R² = 1 - [Σ(yᵢ - ŷᵢ)² / Σ(yᵢ - ȳ)²]
|
||||||
|
R² mengukur seberapa besar variasi target yang dapat dijelaskan model. Nilai mendekati 1 menunjukkan prediksi semakin dekat dengan data aktual. Nilai 0 berarti performa setara dengan selalu menebak rata-rata. Nilai negatif berarti model lebih buruk daripada tebakan rata-rata.
|
||||||
|
Istilah yang Tepat
|
||||||
|
R² sering disebut sebagai akurasi pada laporan regresi. Istilah akademik yang lebih tepat adalah koefisien determinasi atau kemampuan model menjelaskan variasi target.
|
||||||
|
|
||||||
|
5.2 Mean Absolute Error
|
||||||
|
MAE = (1/n) Σ|yᵢ - ŷᵢ|
|
||||||
|
MAE menunjukkan rata-rata besar kesalahan prediksi tanpa memperhatikan arah kesalahan. Jika MAE sebesar 0,63 unit, prediksi rata-rata meleset sekitar 0,63 unit dari nilai aktual.
|
||||||
|
5.3 Root Mean Squared Error
|
||||||
|
RMSE = √[(1/n) Σ(yᵢ - ŷᵢ)²]
|
||||||
|
RMSE memberikan penalti lebih besar pada kesalahan yang besar karena error dikuadratkan sebelum dirata-ratakan. Semakin kecil nilai MAE dan RMSE, semakin baik kedekatan prediksi.
|
||||||
|
5.4 Ringkasan Interpretasi
|
||||||
|
Metrik
|
||||||
|
Arah Nilai yang Baik
|
||||||
|
Makna
|
||||||
|
R²
|
||||||
|
Semakin mendekati 1
|
||||||
|
Besarnya variasi target yang dijelaskan model
|
||||||
|
MAE
|
||||||
|
Semakin mendekati 0
|
||||||
|
Rata-rata kesalahan absolut
|
||||||
|
RMSE
|
||||||
|
Semakin mendekati 0
|
||||||
|
Kesalahan dengan penalti lebih besar untuk error besar
|
||||||
|
|
||||||
|
6. Perhitungan Hasil Testing Proyek
|
||||||
|
Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 4,9622 unit, sedangkan total variasi aktual atau SST adalah 5.133,0719.
|
||||||
|
6.1 Linear Regression
|
||||||
|
Komponen
|
||||||
|
Nilai
|
||||||
|
Jumlah absolute error atau SAE
|
||||||
|
848,4592
|
||||||
|
Jumlah squared error atau SSE
|
||||||
|
1.122,3551
|
||||||
|
Jumlah data testing
|
||||||
|
1.349
|
||||||
|
Total variasi aktual atau SST
|
||||||
|
5.133,0719
|
||||||
|
|
||||||
|
MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63
|
||||||
|
RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91
|
||||||
|
R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813
|
||||||
|
Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set dan rata-rata prediksinya meleset sekitar 0,63 unit.
|
||||||
|
6.2 Random Forest
|
||||||
|
Komponen
|
||||||
|
Nilai
|
||||||
|
Jumlah absolute error atau SAE
|
||||||
|
39,86
|
||||||
|
Jumlah squared error atau SSE
|
||||||
|
18,7142
|
||||||
|
Jumlah data testing
|
||||||
|
1.349
|
||||||
|
Total variasi aktual atau SST
|
||||||
|
5.133,0719
|
||||||
|
|
||||||
|
MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03
|
||||||
|
RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12
|
||||||
|
R² = 1 - (18,7142 / 5.133,0719) = 0,9964
|
||||||
|
Interpretasinya, Random Forest menjelaskan sekitar 99,64% variasi target pada testing set dan rata-rata prediksinya meleset sekitar 0,03 unit. Angka ini harus dibaca bersama analisis data leakage pada bagian berikutnya.
|
||||||
|
6.3 Contoh Hasil Prediksi Individual
|
||||||
|
Aktual
|
||||||
|
Prediksi Linear Regression
|
||||||
|
Prediksi Random Forest
|
||||||
|
Error Absolut RF
|
||||||
|
4
|
||||||
|
3,5472
|
||||||
|
4,01
|
||||||
|
0,01
|
||||||
|
3
|
||||||
|
2,5369
|
||||||
|
3,00
|
||||||
|
0,00
|
||||||
|
4
|
||||||
|
4,4879
|
||||||
|
4,00
|
||||||
|
0,00
|
||||||
|
2
|
||||||
|
0,7336
|
||||||
|
2,00
|
||||||
|
0,00
|
||||||
|
7
|
||||||
|
7,2195
|
||||||
|
7,00
|
||||||
|
0,00
|
||||||
|
|
||||||
|
7. Perbandingan dan Interpretasi Model
|
||||||
|
Aspek
|
||||||
|
Linear Regression
|
||||||
|
Random Forest
|
||||||
|
R² testing
|
||||||
|
0,7813
|
||||||
|
0,9964
|
||||||
|
MAE testing
|
||||||
|
0,63 unit
|
||||||
|
0,03 unit
|
||||||
|
RMSE testing
|
||||||
|
0,91 unit
|
||||||
|
0,12 unit
|
||||||
|
Kemampuan pola non-linear
|
||||||
|
Terbatas
|
||||||
|
Baik
|
||||||
|
Kemudahan interpretasi
|
||||||
|
Sangat mudah
|
||||||
|
Lebih kompleks
|
||||||
|
Risiko pada hasil proyek
|
||||||
|
Terpengaruh leakage
|
||||||
|
Sangat terpengaruh leakage
|
||||||
|
|
||||||
|
7.1 Perbandingan MAE
|
||||||
|
Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3%
|
||||||
|
Pernyataan yang tepat adalah Random Forest mengurangi MAE sekitar 95,3% dibandingkan Linear Regression pada test set saat ini. Pernyataan tersebut tidak sama dengan mengatakan model 95,3% lebih akurat.
|
||||||
|
7.2 Indikasi Overfitting
|
||||||
|
Model
|
||||||
|
R² Training
|
||||||
|
R² Testing
|
||||||
|
Selisih
|
||||||
|
Linear Regression
|
||||||
|
0,7902
|
||||||
|
0,7813
|
||||||
|
0,0089
|
||||||
|
Random Forest
|
||||||
|
0,9995
|
||||||
|
0,9964
|
||||||
|
0,0031
|
||||||
|
|
||||||
|
Selisih training dan testing terlihat kecil sehingga secara angka awal model tampak stabil. Namun, kestabilan ini tidak membatalkan masalah data leakage karena informasi target tersedia pada kedua kelompok data melalui total harga.
|
||||||
|
8. Analisis Data Leakage
|
||||||
|
Data leakage adalah kondisi ketika fitur model mengandung informasi yang seharusnya belum tersedia pada saat prediksi dilakukan atau secara langsung dibentuk dari target. Leakage membuat hasil testing terlihat sangat baik tetapi performa tersebut sulit dipertahankan pada penggunaan nyata.
|
||||||
|
total_harga_update = harga_satuan_update x jumlah_permintaan_bahan
|
||||||
|
jumlah_permintaan_bahan = total_harga_update / harga_satuan_update
|
||||||
|
Hasil Pemeriksaan Dataset
|
||||||
|
Hubungan total_harga_update = harga_satuan_update x jumlah_permintaan_bahan berlaku tepat pada seluruh 6.742 baris atau 100% data.
|
||||||
|
|
||||||
|
8.1 Bukti dari Feature Importance
|
||||||
|
Fitur
|
||||||
|
Feature Importance Random Forest
|
||||||
|
total_harga_update
|
||||||
|
62,97%
|
||||||
|
harga_satuan_update
|
||||||
|
36,18%
|
||||||
|
Gabungan kedua fitur harga
|
||||||
|
99,15%
|
||||||
|
Seluruh fitur lain
|
||||||
|
Kurang dari 1% secara individual
|
||||||
|
|
||||||
|
Feature importance menunjukkan bahwa Random Forest hampir sepenuhnya menggunakan total harga dan harga satuan. Model terutama mempelajari cara memperoleh kembali jumlah permintaan, bukan mempelajari pola permintaan berdasarkan waktu, produk, atau kategori.
|
||||||
|
8.2 Mengapa Leakage Berbahaya
|
||||||
|
Total harga transaksi biasanya baru diketahui setelah jumlah permintaan diketahui.
|
||||||
|
Pada saat memprediksi permintaan masa depan, total harga belum tersedia.
|
||||||
|
Nilai R² yang tinggi dapat memberikan kesimpulan keliru bahwa model siap digunakan.
|
||||||
|
Model berpotensi gagal ketika menerima data nyata tanpa informasi target terselubung.
|
||||||
|
9. Pengujian Ulang Tanpa Data Leakage
|
||||||
|
Untuk mengukur kemampuan prediksi yang lebih realistis, total_harga_update dihapus dari fitur. Pengujian dilakukan menggunakan random split dan chronological split.
|
||||||
|
Skenario
|
||||||
|
Model
|
||||||
|
R²
|
||||||
|
MAE
|
||||||
|
RMSE
|
||||||
|
Random split tanpa total harga
|
||||||
|
Linear Regression
|
||||||
|
-0,0045
|
||||||
|
1,53
|
||||||
|
1,96
|
||||||
|
Random split tanpa total harga
|
||||||
|
Random Forest
|
||||||
|
-0,1124
|
||||||
|
1,64
|
||||||
|
2,06
|
||||||
|
Chronological split tanpa total harga
|
||||||
|
Linear Regression
|
||||||
|
-0,0025
|
||||||
|
1,57
|
||||||
|
1,99
|
||||||
|
Chronological split tanpa total harga
|
||||||
|
Random Forest
|
||||||
|
-0,0831
|
||||||
|
1,65
|
||||||
|
2,06
|
||||||
|
|
||||||
|
Makna R² Negatif
|
||||||
|
R² negatif berarti model lebih buruk dibandingkan metode sederhana yang selalu memprediksi nilai rata-rata permintaan. Hasil ini menunjukkan fitur yang tersedia belum mengandung pola permintaan yang cukup kuat setelah total harga dihapus.
|
||||||
|
|
||||||
|
9.1 Kesimpulan Validitas
|
||||||
|
Angka R² sebesar 0,9964 tetap merupakan hasil perhitungan yang benar untuk konfigurasi testing saat ini. Akan tetapi, angka tersebut tidak valid jika digunakan untuk menyatakan kemampuan memprediksi permintaan masa depan secara murni karena terdapat data leakage.
|
||||||
|
10. Rekomendasi Perbaikan Metodologi
|
||||||
|
10.1 Perbaikan Fitur
|
||||||
|
Hapus total_harga_update dari fitur prediksi.
|
||||||
|
Hapus salah satu dari hari_dalam_minggu atau hari_minggu karena keduanya duplikat.
|
||||||
|
Gunakan lag permintaan, misalnya jumlah permintaan hari sebelumnya atau minggu sebelumnya.
|
||||||
|
Gunakan moving average permintaan dalam 7, 14, atau 30 hari terakhir.
|
||||||
|
Tambahkan informasi hari libur, musim, promosi, dan periode ramai.
|
||||||
|
Tambahkan stok sebelumnya serta histori pemakaian bahan.
|
||||||
|
10.2 Perbaikan Proses Testing
|
||||||
|
Gunakan chronological split: data lama untuk training dan data terbaru untuk testing.
|
||||||
|
Gunakan TimeSeriesSplit atau walk-forward validation untuk evaluasi yang lebih kuat.
|
||||||
|
Bandingkan model dengan baseline sederhana seperti rata-rata atau nilai periode sebelumnya.
|
||||||
|
Lakukan tuning parameter Random Forest hanya menggunakan data training.
|
||||||
|
Laporkan R², MAE, dan RMSE secara bersamaan.
|
||||||
|
10.3 Konsistensi Aplikasi Flutter
|
||||||
|
Python menggunakan kode hari 0 sampai 6, sedangkan DateTime.weekday pada Flutter menggunakan 1 sampai 7. Nilai harus disamakan sebelum dikirim ke model. Selain itu, pemetaan encoded produk dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training.
|
||||||
|
11. Contoh Penjelasan untuk Laporan
|
||||||
|
11.1 Paragraf Metode Pengujian
|
||||||
|
Contoh Penulisan
|
||||||
|
Pengujian model dilakukan menggunakan metode hold-out dengan membagi dataset menjadi 80% data training dan 20% data testing. Dari total 6.742 data transaksi, sebanyak 5.393 data digunakan untuk melatih model dan 1.349 data digunakan untuk menguji performa model. Pembagian data menggunakan random_state sebesar 42 agar hasil pembagian dapat direproduksi. Evaluasi dilakukan menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.
|
||||||
|
|
||||||
|
11.2 Paragraf Hasil Perbandingan
|
||||||
|
Contoh Penulisan
|
||||||
|
Berdasarkan pengujian awal, Linear Regression menghasilkan R² sebesar 0,7813, MAE sebesar 0,63, dan RMSE sebesar 0,91. Random Forest menghasilkan R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE sebesar 0,12. Secara numerik, Random Forest memberikan hasil prediksi yang lebih dekat terhadap nilai aktual pada testing set dibandingkan Linear Regression.
|
||||||
|
|
||||||
|
11.3 Paragraf Keterbatasan
|
||||||
|
Contoh Penulisan
|
||||||
|
Evaluasi lanjutan menunjukkan bahwa nilai Random Forest yang sangat tinggi dipengaruhi oleh penggunaan fitur total_harga_update. Fitur tersebut dihitung dari harga_satuan_update dikalikan jumlah_permintaan_bahan yang menjadi target model. Kondisi ini menimbulkan data leakage karena fitur mengandung informasi langsung mengenai target. Oleh karena itu, hasil R² sebesar 0,9964 harus dipahami sebagai performa pada konfigurasi dataset awal dan belum sepenuhnya menggambarkan kemampuan prediksi permintaan masa depan.
|
||||||
|
|
||||||
|
11.4 Paragraf Rekomendasi Pengembangan
|
||||||
|
Contoh Penulisan
|
||||||
|
Pengembangan berikutnya perlu menghapus fitur total harga dan menggunakan fitur yang telah tersedia sebelum transaksi terjadi, seperti histori permintaan, lag permintaan, moving average, tren bulanan, hari libur, promosi, dan stok sebelumnya. Pengujian juga perlu dilakukan secara kronologis agar lebih sesuai dengan tujuan prediksi masa depan.
|
||||||
|
|
||||||
|
12. Contoh Jawaban Saat Sidang
|
||||||
|
12.1 Bagaimana Proses Testing Dilakukan?
|
||||||
|
Jawaban: Proses testing dilakukan dengan membagi 6.742 data menjadi 80% data training dan 20% data testing. Model mempelajari pola dari 5.393 data training, kemudian menghasilkan prediksi untuk 1.349 data testing yang tidak digunakan saat pelatihan. Hasil prediksi dibandingkan dengan nilai aktual menggunakan R², MAE, dan RMSE.
|
||||||
|
12.2 Mengapa Menggunakan Tiga Metrik?
|
||||||
|
Jawaban: R² digunakan untuk mengukur seberapa besar variasi target dapat dijelaskan model. MAE menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap.
|
||||||
|
12.3 Mengapa Random Forest Lebih Baik?
|
||||||
|
Jawaban: Pada konfigurasi testing awal, Random Forest lebih baik karena mampu menangkap hubungan non-linear dan menggabungkan prediksi dari 100 Decision Tree. Namun, performa yang sangat tinggi juga disebabkan total harga yang memiliki hubungan langsung dengan target.
|
||||||
|
12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%?
|
||||||
|
Jawaban: Tidak sepenuhnya. R² sebesar 0,9964 berarti model menjelaskan sekitar 99,64% variasi target pada testing set. R² bukan akurasi klasifikasi. Selain itu, nilai tersebut dipengaruhi data leakage sehingga belum mewakili kemampuan prediksi masa depan secara valid.
|
||||||
|
12.5 Apakah Model Mengalami Overfitting?
|
||||||
|
Jawaban: Selisih R² training dan testing kecil, sehingga secara angka awal tidak menunjukkan overfitting yang besar. Akan tetapi, terdapat masalah yang lebih penting, yaitu data leakage. Leakage membuat training dan testing sama-sama mudah diprediksi karena keduanya mengandung informasi target.
|
||||||
|
12.6 Apa Perbaikan yang Harus Dilakukan?
|
||||||
|
Jawaban: Total harga harus dihapus dari fitur, kemudian model dilatih menggunakan fitur yang benar-benar tersedia sebelum transaksi terjadi. Testing sebaiknya menggunakan pembagian berdasarkan waktu dan dibandingkan dengan baseline. Fitur histori seperti lag dan moving average juga perlu ditambahkan.
|
||||||
|
Lampiran: Ringkasan Rumus dan Istilah
|
||||||
|
Konsep
|
||||||
|
Rumus atau Definisi Ringkas
|
||||||
|
Linear Regression
|
||||||
|
ŷ = β₀ + β₁x₁ + ... + βₚxₚ
|
||||||
|
Random Forest
|
||||||
|
Rata-rata prediksi dari banyak Decision Tree
|
||||||
|
SSE
|
||||||
|
Σ(yᵢ - ŷᵢ)²
|
||||||
|
MAE
|
||||||
|
(1/n) Σ|yᵢ - ŷᵢ|
|
||||||
|
RMSE
|
||||||
|
√[(1/n) Σ(yᵢ - ŷᵢ)²]
|
||||||
|
R²
|
||||||
|
1 - SSE/SST
|
||||||
|
Overfitting
|
||||||
|
Model sangat baik pada training tetapi buruk pada data baru
|
||||||
|
Underfitting
|
||||||
|
Model terlalu sederhana dan gagal mempelajari pola
|
||||||
|
Data leakage
|
||||||
|
Fitur mengandung informasi target atau informasi masa depan
|
||||||
|
Chronological split
|
||||||
|
Training memakai data lama, testing memakai data terbaru
|
||||||
|
Baseline
|
||||||
|
Metode sederhana sebagai pembanding minimum
|
||||||
|
|
||||||
|
Kesimpulan Akhir
|
||||||
|
Random Forest menghasilkan metrik terbaik pada testing awal, tetapi performa tersebut sangat dipengaruhi data leakage dari total_harga_update. Untuk laporan yang bertanggung jawab secara akademik, tampilkan hasil awal, jelaskan keterbatasannya, dan cantumkan rekomendasi pengujian ulang tanpa fitur yang dibentuk dari target.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,924 @@
|
||||||
|
from docx import Document
|
||||||
|
from docx.enum.section import WD_SECTION
|
||||||
|
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
from docx.shared import Inches, Pt, RGBColor
|
||||||
|
|
||||||
|
|
||||||
|
OUTPUT = "ml_model/Panduan_Pengujian_Linear_Regression_dan_Random_Forest.docx"
|
||||||
|
|
||||||
|
NAVY = "17365D"
|
||||||
|
BLUE = "2E74B5"
|
||||||
|
LIGHT_BLUE = "E8EEF5"
|
||||||
|
LIGHT_GRAY = "F2F4F7"
|
||||||
|
GOLD = "7A5A00"
|
||||||
|
LIGHT_GOLD = "FFF4CC"
|
||||||
|
RED = "9B1C1C"
|
||||||
|
LIGHT_RED = "FDECEC"
|
||||||
|
GREEN = "1E6B3A"
|
||||||
|
WHITE = "FFFFFF"
|
||||||
|
BLACK = "000000"
|
||||||
|
MUTED = "666666"
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_shading(cell, fill):
|
||||||
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = tc_pr.find(qn("w:shd"))
|
||||||
|
if shd is None:
|
||||||
|
shd = OxmlElement("w:shd")
|
||||||
|
tc_pr.append(shd)
|
||||||
|
shd.set(qn("w:fill"), fill)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_margins(cell, top=100, start=120, bottom=100, end=120):
|
||||||
|
tc = cell._tc
|
||||||
|
tc_pr = tc.get_or_add_tcPr()
|
||||||
|
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||||
|
if tc_mar is None:
|
||||||
|
tc_mar = OxmlElement("w:tcMar")
|
||||||
|
tc_pr.append(tc_mar)
|
||||||
|
for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
||||||
|
node = tc_mar.find(qn(f"w:{margin}"))
|
||||||
|
if node is None:
|
||||||
|
node = OxmlElement(f"w:{margin}")
|
||||||
|
tc_mar.append(node)
|
||||||
|
node.set(qn("w:w"), str(value))
|
||||||
|
node.set(qn("w:type"), "dxa")
|
||||||
|
|
||||||
|
|
||||||
|
def set_repeat_table_header(row):
|
||||||
|
tr_pr = row._tr.get_or_add_trPr()
|
||||||
|
tbl_header = OxmlElement("w:tblHeader")
|
||||||
|
tbl_header.set(qn("w:val"), "true")
|
||||||
|
tr_pr.append(tbl_header)
|
||||||
|
|
||||||
|
|
||||||
|
def set_table_borders(table, color="B7C9DC", size="6"):
|
||||||
|
tbl = table._tbl
|
||||||
|
tbl_pr = tbl.tblPr
|
||||||
|
borders = tbl_pr.first_child_found_in("w:tblBorders")
|
||||||
|
if borders is None:
|
||||||
|
borders = OxmlElement("w:tblBorders")
|
||||||
|
tbl_pr.append(borders)
|
||||||
|
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
|
||||||
|
element = borders.find(qn(f"w:{edge}"))
|
||||||
|
if element is None:
|
||||||
|
element = OxmlElement(f"w:{edge}")
|
||||||
|
borders.append(element)
|
||||||
|
element.set(qn("w:val"), "single")
|
||||||
|
element.set(qn("w:sz"), size)
|
||||||
|
element.set(qn("w:space"), "0")
|
||||||
|
element.set(qn("w:color"), color)
|
||||||
|
|
||||||
|
|
||||||
|
def set_font(run, name="Calibri", size=11, bold=False, italic=False, color=BLACK):
|
||||||
|
run.font.name = name
|
||||||
|
run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name)
|
||||||
|
run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name)
|
||||||
|
run.font.size = Pt(size)
|
||||||
|
run.bold = bold
|
||||||
|
run.italic = italic
|
||||||
|
run.font.color.rgb = RGBColor.from_string(color)
|
||||||
|
|
||||||
|
|
||||||
|
def add_page_number(paragraph):
|
||||||
|
run = paragraph.add_run()
|
||||||
|
fld_char1 = OxmlElement("w:fldChar")
|
||||||
|
fld_char1.set(qn("w:fldCharType"), "begin")
|
||||||
|
instr_text = OxmlElement("w:instrText")
|
||||||
|
instr_text.set(qn("xml:space"), "preserve")
|
||||||
|
instr_text.text = "PAGE"
|
||||||
|
fld_char2 = OxmlElement("w:fldChar")
|
||||||
|
fld_char2.set(qn("w:fldCharType"), "end")
|
||||||
|
run._r.append(fld_char1)
|
||||||
|
run._r.append(instr_text)
|
||||||
|
run._r.append(fld_char2)
|
||||||
|
set_font(run, size=9, color=MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
def add_heading(doc, text, level=1):
|
||||||
|
p = doc.add_paragraph(style=f"Heading {level}")
|
||||||
|
p.add_run(text)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def add_body(doc, text, bold_prefix=None):
|
||||||
|
p = doc.add_paragraph(style="Normal")
|
||||||
|
if bold_prefix and text.startswith(bold_prefix):
|
||||||
|
r1 = p.add_run(bold_prefix)
|
||||||
|
set_font(r1, bold=True)
|
||||||
|
r2 = p.add_run(text[len(bold_prefix):])
|
||||||
|
set_font(r2)
|
||||||
|
else:
|
||||||
|
r = p.add_run(text)
|
||||||
|
set_font(r)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def add_bullet(doc, text, level=0):
|
||||||
|
style = "List Bullet" if level == 0 else "List Bullet 2"
|
||||||
|
p = doc.add_paragraph(style=style)
|
||||||
|
r = p.add_run(text)
|
||||||
|
set_font(r)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def add_number(doc, text):
|
||||||
|
p = doc.add_paragraph(style="List Number")
|
||||||
|
r = p.add_run(text)
|
||||||
|
set_font(r)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def add_formula(doc, formula, explanation=None):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_before = Pt(5)
|
||||||
|
p.paragraph_format.space_after = Pt(5)
|
||||||
|
r = p.add_run(formula)
|
||||||
|
set_font(r, name="Cambria Math", size=11.5, bold=True, color=NAVY)
|
||||||
|
if explanation:
|
||||||
|
p2 = doc.add_paragraph()
|
||||||
|
p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p2.paragraph_format.space_after = Pt(6)
|
||||||
|
r2 = p2.add_run(explanation)
|
||||||
|
set_font(r2, size=9.5, italic=True, color=MUTED)
|
||||||
|
|
||||||
|
|
||||||
|
def add_callout(doc, title, body, fill=LIGHT_BLUE, title_color=NAVY):
|
||||||
|
table = doc.add_table(rows=1, cols=1)
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
table.autofit = False
|
||||||
|
table.columns[0].width = Inches(6.3)
|
||||||
|
cell = table.cell(0, 0)
|
||||||
|
set_cell_shading(cell, fill)
|
||||||
|
set_cell_margins(cell, top=150, start=180, bottom=150, end=180)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.paragraph_format.space_after = Pt(4)
|
||||||
|
r = p.add_run(title)
|
||||||
|
set_font(r, bold=True, color=title_color)
|
||||||
|
p2 = cell.add_paragraph()
|
||||||
|
p2.paragraph_format.space_after = Pt(0)
|
||||||
|
r2 = p2.add_run(body)
|
||||||
|
set_font(r2, size=10.5)
|
||||||
|
doc.add_paragraph().paragraph_format.space_after = Pt(1)
|
||||||
|
|
||||||
|
|
||||||
|
def add_table(doc, headers, rows, widths=None, font_size=9.5):
|
||||||
|
table = doc.add_table(rows=1, cols=len(headers))
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
table.autofit = False
|
||||||
|
set_table_borders(table)
|
||||||
|
hdr = table.rows[0]
|
||||||
|
set_repeat_table_header(hdr)
|
||||||
|
for i, header in enumerate(headers):
|
||||||
|
cell = hdr.cells[i]
|
||||||
|
set_cell_shading(cell, NAVY)
|
||||||
|
set_cell_margins(cell)
|
||||||
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
r = p.add_run(str(header))
|
||||||
|
set_font(r, size=font_size, bold=True, color=WHITE)
|
||||||
|
for row_idx, row_data in enumerate(rows):
|
||||||
|
cells = table.add_row().cells
|
||||||
|
for i, value in enumerate(row_data):
|
||||||
|
cell = cells[i]
|
||||||
|
set_cell_margins(cell)
|
||||||
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||||
|
if row_idx % 2 == 1:
|
||||||
|
set_cell_shading(cell, LIGHT_GRAY)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.LEFT if i == 0 else WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
r = p.add_run(str(value))
|
||||||
|
set_font(r, size=font_size)
|
||||||
|
if widths:
|
||||||
|
for row in table.rows:
|
||||||
|
for idx, width in enumerate(widths):
|
||||||
|
row.cells[idx].width = Inches(width)
|
||||||
|
doc.add_paragraph().paragraph_format.space_after = Pt(1)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def add_code(doc, text):
|
||||||
|
table = doc.add_table(rows=1, cols=1)
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
cell = table.cell(0, 0)
|
||||||
|
set_cell_shading(cell, "F6F8FA")
|
||||||
|
set_cell_margins(cell, top=120, start=160, bottom=120, end=160)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
for idx, line in enumerate(text.splitlines()):
|
||||||
|
if idx:
|
||||||
|
p.add_run("\n")
|
||||||
|
r = p.add_run(line)
|
||||||
|
set_font(r, name="Consolas", size=8.5, color="24292F")
|
||||||
|
doc.add_paragraph().paragraph_format.space_after = Pt(1)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_document(doc):
|
||||||
|
section = doc.sections[0]
|
||||||
|
section.top_margin = Inches(0.85)
|
||||||
|
section.bottom_margin = Inches(0.8)
|
||||||
|
section.left_margin = Inches(1.0)
|
||||||
|
section.right_margin = Inches(1.0)
|
||||||
|
section.header_distance = Inches(0.4)
|
||||||
|
section.footer_distance = Inches(0.4)
|
||||||
|
|
||||||
|
styles = doc.styles
|
||||||
|
normal = styles["Normal"]
|
||||||
|
normal.font.name = "Calibri"
|
||||||
|
normal._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
|
||||||
|
normal._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
|
||||||
|
normal.font.size = Pt(11)
|
||||||
|
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||||
|
normal.paragraph_format.space_after = Pt(7)
|
||||||
|
normal.paragraph_format.line_spacing = 1.25
|
||||||
|
|
||||||
|
for name, size, color, before, after in (
|
||||||
|
("Heading 1", 16, BLUE, 16, 8),
|
||||||
|
("Heading 2", 13, BLUE, 12, 6),
|
||||||
|
("Heading 3", 11.5, NAVY, 9, 4),
|
||||||
|
):
|
||||||
|
style = styles[name]
|
||||||
|
style.font.name = "Calibri"
|
||||||
|
style._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
|
||||||
|
style._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
|
||||||
|
style.font.size = Pt(size)
|
||||||
|
style.font.bold = True
|
||||||
|
style.font.color.rgb = RGBColor.from_string(color)
|
||||||
|
style.paragraph_format.space_before = Pt(before)
|
||||||
|
style.paragraph_format.space_after = Pt(after)
|
||||||
|
style.paragraph_format.keep_with_next = True
|
||||||
|
|
||||||
|
for list_name in ("List Bullet", "List Bullet 2", "List Number"):
|
||||||
|
style = styles[list_name]
|
||||||
|
style.font.name = "Calibri"
|
||||||
|
style.font.size = Pt(11)
|
||||||
|
style.paragraph_format.space_after = Pt(4)
|
||||||
|
style.paragraph_format.line_spacing = 1.2
|
||||||
|
|
||||||
|
header = section.header
|
||||||
|
hp = header.paragraphs[0]
|
||||||
|
hp.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
r = hp.add_run("Panduan Pengujian Model Prediksi Permintaan Bahan Kue")
|
||||||
|
set_font(r, size=8.5, color=MUTED)
|
||||||
|
|
||||||
|
footer = section.footer
|
||||||
|
fp = footer.paragraphs[0]
|
||||||
|
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = fp.add_run("Tugas Akhir | Halaman ")
|
||||||
|
set_font(r, size=9, color=MUTED)
|
||||||
|
add_page_number(fp)
|
||||||
|
|
||||||
|
|
||||||
|
def add_cover(doc):
|
||||||
|
for _ in range(5):
|
||||||
|
doc.add_paragraph()
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = p.add_run("PANDUAN PENGUJIAN MODEL")
|
||||||
|
set_font(r, size=25, bold=True, color=NAVY)
|
||||||
|
p.paragraph_format.space_after = Pt(6)
|
||||||
|
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = p.add_run("Linear Regression dan Random Forest")
|
||||||
|
set_font(r, size=17, bold=True, color=BLUE)
|
||||||
|
p.paragraph_format.space_after = Pt(8)
|
||||||
|
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = p.add_run("Studi Kasus Prediksi Permintaan Stok Bahan Kue")
|
||||||
|
set_font(r, size=13, italic=True, color=MUTED)
|
||||||
|
p.paragraph_format.space_after = Pt(34)
|
||||||
|
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Informasi", "Keterangan"],
|
||||||
|
[
|
||||||
|
["Jumlah data", "6.742 transaksi"],
|
||||||
|
["Pembagian data", "80% training dan 20% testing"],
|
||||||
|
["Target", "jumlah_permintaan_bahan"],
|
||||||
|
["Model yang dibandingkan", "Linear Regression dan Random Forest Regressor"],
|
||||||
|
["Tujuan dokumen", "Bahan belajar, pembahasan laporan, dan persiapan sidang"],
|
||||||
|
],
|
||||||
|
widths=[1.8, 4.5],
|
||||||
|
font_size=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in range(5):
|
||||||
|
doc.add_paragraph()
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
r = p.add_run("Disusun berdasarkan hasil pengujian model pada proyek tugas akhir")
|
||||||
|
set_font(r, size=10, italic=True, color=MUTED)
|
||||||
|
p.add_run().add_break(WD_BREAK.PAGE)
|
||||||
|
|
||||||
|
|
||||||
|
def build_document():
|
||||||
|
doc = Document()
|
||||||
|
configure_document(doc)
|
||||||
|
add_cover(doc)
|
||||||
|
|
||||||
|
add_heading(doc, "Ringkasan Eksekutif", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Pengujian model dilakukan untuk mengetahui kemampuan Linear Regression dan Random Forest "
|
||||||
|
"dalam memprediksi jumlah permintaan bahan kue. Dataset berisi 6.742 transaksi dan dibagi "
|
||||||
|
"menjadi 5.393 data training serta 1.349 data testing menggunakan rasio 80:20."
|
||||||
|
)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Model", "R² Testing", "MAE", "RMSE", "Interpretasi Awal"],
|
||||||
|
[
|
||||||
|
["Linear Regression", "0,7813", "0,63", "0,91", "Cukup baik pada test set"],
|
||||||
|
["Random Forest", "0,9964", "0,03", "0,12", "Sangat tinggi pada test set"],
|
||||||
|
],
|
||||||
|
widths=[1.6, 1.0, 0.8, 0.8, 2.1],
|
||||||
|
)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Temuan Kritis",
|
||||||
|
"Nilai Random Forest yang sangat tinggi bukan sepenuhnya menunjukkan kemampuan meramalkan "
|
||||||
|
"permintaan masa depan. Kolom total_harga_update memiliki hubungan langsung dengan target: "
|
||||||
|
"total_harga_update = harga_satuan_update x jumlah_permintaan_bahan. Kondisi ini disebut data leakage.",
|
||||||
|
fill=LIGHT_RED,
|
||||||
|
title_color=RED,
|
||||||
|
)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Dokumen ini menjelaskan dua sudut pandang secara terpisah: pertama, bagaimana hasil testing "
|
||||||
|
"yang ada dihitung; kedua, apakah hasil tersebut valid sebagai ukuran kemampuan prediksi nyata."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "Daftar Isi", 1)
|
||||||
|
for item in (
|
||||||
|
"1. Gambaran Umum Pengujian Model",
|
||||||
|
"2. Persiapan dan Pembagian Data",
|
||||||
|
"3. Cara Kerja Linear Regression",
|
||||||
|
"4. Cara Kerja Random Forest",
|
||||||
|
"5. Metrik Evaluasi Model",
|
||||||
|
"6. Perhitungan Hasil Testing Proyek",
|
||||||
|
"7. Perbandingan dan Interpretasi Model",
|
||||||
|
"8. Analisis Data Leakage",
|
||||||
|
"9. Pengujian Ulang Tanpa Data Leakage",
|
||||||
|
"10. Rekomendasi Perbaikan Metodologi",
|
||||||
|
"11. Contoh Penjelasan untuk Laporan",
|
||||||
|
"12. Contoh Jawaban Saat Sidang",
|
||||||
|
"Lampiran: Ringkasan Rumus dan Istilah",
|
||||||
|
):
|
||||||
|
add_body(doc, item)
|
||||||
|
doc.add_paragraph().add_run().add_break(WD_BREAK.PAGE)
|
||||||
|
|
||||||
|
add_heading(doc, "1. Gambaran Umum Pengujian Model", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Machine learning tidak cukup hanya dilatih. Model harus diuji menggunakan data yang tidak "
|
||||||
|
"digunakan selama proses pelatihan. Tujuannya adalah mengukur apakah pola yang dipelajari model "
|
||||||
|
"dapat diterapkan pada data lain, bukan hanya menghafal data training."
|
||||||
|
)
|
||||||
|
add_heading(doc, "1.1 Tujuan Testing", 2)
|
||||||
|
for text in (
|
||||||
|
"Menilai kedekatan hasil prediksi dengan nilai permintaan aktual.",
|
||||||
|
"Membandingkan performa Linear Regression dan Random Forest.",
|
||||||
|
"Mendeteksi overfitting, underfitting, dan kesalahan metodologi.",
|
||||||
|
"Memilih model yang paling sesuai untuk digunakan pada aplikasi.",
|
||||||
|
):
|
||||||
|
add_bullet(doc, text)
|
||||||
|
add_heading(doc, "1.2 Alur Pengujian", 2)
|
||||||
|
for text in (
|
||||||
|
"Membaca dataset transaksi.",
|
||||||
|
"Mengidentifikasi target dan fitur.",
|
||||||
|
"Mengubah data kategori menjadi angka.",
|
||||||
|
"Membagi data menjadi training set dan testing set.",
|
||||||
|
"Melatih masing-masing model menggunakan training set.",
|
||||||
|
"Menghasilkan prediksi pada testing set.",
|
||||||
|
"Menghitung R², MAE, dan RMSE.",
|
||||||
|
"Membandingkan serta mengevaluasi validitas hasil.",
|
||||||
|
):
|
||||||
|
add_number(doc, text)
|
||||||
|
|
||||||
|
add_heading(doc, "2. Persiapan dan Pembagian Data", 1)
|
||||||
|
add_heading(doc, "2.1 Struktur Dataset", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Komponen", "Nilai"],
|
||||||
|
[
|
||||||
|
["Total data", "6.742 transaksi"],
|
||||||
|
["Training set", "5.393 transaksi atau 80%"],
|
||||||
|
["Testing set", "1.349 transaksi atau 20%"],
|
||||||
|
["Target", "jumlah_permintaan_bahan"],
|
||||||
|
["Jumlah fitur model tersimpan", "10 fitur"],
|
||||||
|
],
|
||||||
|
widths=[2.3, 4.0],
|
||||||
|
)
|
||||||
|
add_heading(doc, "2.2 Fitur Model", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["No.", "Fitur", "Makna"],
|
||||||
|
[
|
||||||
|
["1", "produk_encoded", "Kode angka produk"],
|
||||||
|
["2", "tahun", "Tahun transaksi"],
|
||||||
|
["3", "bulan", "Bulan transaksi"],
|
||||||
|
["4", "hari", "Tanggal dalam bulan"],
|
||||||
|
["5", "hari_dalam_minggu", "Kode hari dalam minggu"],
|
||||||
|
["6", "harga_satuan_update", "Harga satuan bahan"],
|
||||||
|
["7", "total_harga_update", "Total harga transaksi"],
|
||||||
|
["8", "hari_minggu", "Duplikasi informasi hari dalam minggu"],
|
||||||
|
["9", "nama_produk_encoded", "Nama produk yang telah diubah menjadi angka"],
|
||||||
|
["10", "kategori_produk_encoded", "Kategori produk yang telah diubah menjadi angka"],
|
||||||
|
],
|
||||||
|
widths=[0.6, 2.3, 3.4],
|
||||||
|
)
|
||||||
|
add_heading(doc, "2.3 Pembagian Training dan Testing", 2)
|
||||||
|
add_code(
|
||||||
|
doc,
|
||||||
|
"X_train, X_test, y_train, y_test = train_test_split(\n"
|
||||||
|
" X, y, test_size=0.2, random_state=42\n"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Parameter test_size=0.2 berarti 20% data digunakan untuk testing. Parameter random_state=42 "
|
||||||
|
"membuat pembagian acak selalu sama ketika program dijalankan ulang, sehingga hasil pengujian "
|
||||||
|
"dapat direproduksi."
|
||||||
|
)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Catatan untuk Data Berdasarkan Waktu",
|
||||||
|
"Karena tujuan sistem adalah memprediksi masa depan, pembagian data secara kronologis lebih kuat "
|
||||||
|
"daripada pembagian acak. Data lama sebaiknya digunakan sebagai training dan data terbaru sebagai testing.",
|
||||||
|
fill=LIGHT_GOLD,
|
||||||
|
title_color=GOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "3. Cara Kerja Linear Regression", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Linear Regression mencari hubungan berbentuk garis lurus antara fitur masukan dan target. "
|
||||||
|
"Setiap fitur memperoleh koefisien yang menunjukkan arah serta besarnya pengaruh terhadap hasil prediksi."
|
||||||
|
)
|
||||||
|
add_formula(doc, "ŷ = β₀ + β₁x₁ + β₂x₂ + ... + βₚxₚ", "Rumus umum prediksi Linear Regression")
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Simbol", "Arti"],
|
||||||
|
[
|
||||||
|
["ŷ", "Nilai jumlah permintaan hasil prediksi"],
|
||||||
|
["β₀", "Intercept atau nilai awal model"],
|
||||||
|
["βᵢ", "Koefisien untuk fitur ke-i"],
|
||||||
|
["xᵢ", "Nilai fitur ke-i"],
|
||||||
|
["p", "Jumlah fitur yang digunakan"],
|
||||||
|
],
|
||||||
|
widths=[1.1, 5.2],
|
||||||
|
)
|
||||||
|
add_heading(doc, "3.1 Proses Pembelajaran", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Koefisien dicari dengan metode Ordinary Least Squares. Model memilih koefisien yang membuat "
|
||||||
|
"jumlah kuadrat selisih antara nilai aktual dan prediksi menjadi sekecil mungkin."
|
||||||
|
)
|
||||||
|
add_formula(doc, "SSE = Σ(yᵢ - ŷᵢ)²", "SSE adalah jumlah kuadrat error prediksi")
|
||||||
|
add_formula(doc, "β = (XᵀX)⁻¹Xᵀy", "Bentuk matriks solusi Ordinary Least Squares")
|
||||||
|
add_heading(doc, "3.2 Persamaan yang Dihasilkan pada Proyek", 2)
|
||||||
|
add_formula(
|
||||||
|
doc,
|
||||||
|
"ŷ ≈ 25,704 - 0,00306(produk) - 0,01028(tahun) + 0,00289(bulan) "
|
||||||
|
"- 0,00276(hari) - 0,000208(harga satuan) + 0,0000421(total harga) + ...",
|
||||||
|
)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Linear Regression memperoleh R² testing sebesar 0,7813. Model ini terbantu oleh total harga, "
|
||||||
|
"tetapi tidak dapat menangkap hubungan pembagian antara total harga dan harga satuan sebaik Random Forest."
|
||||||
|
)
|
||||||
|
add_heading(doc, "3.3 Kelebihan dan Keterbatasan", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Kelebihan", "Keterbatasan"],
|
||||||
|
[
|
||||||
|
["Mudah dijelaskan dan diinterpretasikan", "Mengasumsikan hubungan cenderung linear"],
|
||||||
|
["Proses training cepat", "Kurang baik untuk pola kompleks dan non-linear"],
|
||||||
|
["Koefisien menunjukkan arah hubungan", "Sensitif terhadap fitur yang tidak relevan dan outlier"],
|
||||||
|
],
|
||||||
|
widths=[3.15, 3.15],
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "4. Cara Kerja Random Forest", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Random Forest Regressor adalah metode ensemble yang membangun banyak Decision Tree. Setiap pohon "
|
||||||
|
"mempelajari sampel dan kombinasi fitur yang berbeda. Prediksi akhir diperoleh dari rata-rata hasil seluruh pohon."
|
||||||
|
)
|
||||||
|
add_code(
|
||||||
|
doc,
|
||||||
|
"RandomForestRegressor(\n"
|
||||||
|
" n_estimators=100,\n"
|
||||||
|
" random_state=42,\n"
|
||||||
|
" n_jobs=-1\n"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
add_formula(doc, "ŷRF = (ŷ₁ + ŷ₂ + ... + ŷT) / T", "Prediksi Random Forest adalah rata-rata prediksi T pohon")
|
||||||
|
add_body(doc, "Karena n_estimators=100, nilai T pada proyek ini adalah 100 pohon.")
|
||||||
|
add_heading(doc, "4.1 Proses pada Setiap Pohon", 2)
|
||||||
|
for text in (
|
||||||
|
"Mengambil sampel data training secara acak dengan pengembalian atau bootstrap.",
|
||||||
|
"Memilih sebagian fitur secara acak pada setiap kandidat percabangan.",
|
||||||
|
"Mencari batas pembagian data yang paling mengurangi error.",
|
||||||
|
"Menghasilkan prediksi dari nilai rata-rata target pada leaf node.",
|
||||||
|
"Menggabungkan prediksi seluruh pohon dengan operasi rata-rata.",
|
||||||
|
):
|
||||||
|
add_number(doc, text)
|
||||||
|
add_heading(doc, "4.2 Pemilihan Percabangan", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Untuk regresi, Decision Tree memilih percabangan yang menurunkan Mean Squared Error. "
|
||||||
|
"Percabangan yang membuat nilai target dalam setiap kelompok semakin seragam akan dipilih."
|
||||||
|
)
|
||||||
|
add_formula(doc, "MSE = (1/n) Σ(yᵢ - ŷᵢ)²")
|
||||||
|
add_heading(doc, "4.3 Mengapa Random Forest Lebih Fleksibel", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Berbeda dari Linear Regression, Random Forest tidak mengharuskan hubungan berbentuk garis lurus. "
|
||||||
|
"Model dapat membentuk banyak aturan kondisi sehingga mampu mempelajari interaksi kompleks antara fitur."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "5. Metrik Evaluasi Model", 1)
|
||||||
|
add_heading(doc, "5.1 R² atau Koefisien Determinasi", 2)
|
||||||
|
add_formula(doc, "R² = 1 - [Σ(yᵢ - ŷᵢ)² / Σ(yᵢ - ȳ)²]")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"R² mengukur seberapa besar variasi target yang dapat dijelaskan model. Nilai mendekati 1 "
|
||||||
|
"menunjukkan prediksi semakin dekat dengan data aktual. Nilai 0 berarti performa setara dengan "
|
||||||
|
"selalu menebak rata-rata. Nilai negatif berarti model lebih buruk daripada tebakan rata-rata."
|
||||||
|
)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Istilah yang Tepat",
|
||||||
|
"R² sering disebut sebagai akurasi pada laporan regresi. Istilah akademik yang lebih tepat adalah "
|
||||||
|
"koefisien determinasi atau kemampuan model menjelaskan variasi target.",
|
||||||
|
)
|
||||||
|
add_heading(doc, "5.2 Mean Absolute Error", 2)
|
||||||
|
add_formula(doc, "MAE = (1/n) Σ|yᵢ - ŷᵢ|")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"MAE menunjukkan rata-rata besar kesalahan prediksi tanpa memperhatikan arah kesalahan. "
|
||||||
|
"Jika MAE sebesar 0,63 unit, prediksi rata-rata meleset sekitar 0,63 unit dari nilai aktual."
|
||||||
|
)
|
||||||
|
add_heading(doc, "5.3 Root Mean Squared Error", 2)
|
||||||
|
add_formula(doc, "RMSE = √[(1/n) Σ(yᵢ - ŷᵢ)²]")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"RMSE memberikan penalti lebih besar pada kesalahan yang besar karena error dikuadratkan sebelum "
|
||||||
|
"dirata-ratakan. Semakin kecil nilai MAE dan RMSE, semakin baik kedekatan prediksi."
|
||||||
|
)
|
||||||
|
add_heading(doc, "5.4 Ringkasan Interpretasi", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Metrik", "Arah Nilai yang Baik", "Makna"],
|
||||||
|
[
|
||||||
|
["R²", "Semakin mendekati 1", "Besarnya variasi target yang dijelaskan model"],
|
||||||
|
["MAE", "Semakin mendekati 0", "Rata-rata kesalahan absolut"],
|
||||||
|
["RMSE", "Semakin mendekati 0", "Kesalahan dengan penalti lebih besar untuk error besar"],
|
||||||
|
],
|
||||||
|
widths=[1.0, 1.8, 3.5],
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "6. Perhitungan Hasil Testing Proyek", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Pada testing set terdapat 1.349 data. Rata-rata target testing adalah sekitar 4,9622 unit, "
|
||||||
|
"sedangkan total variasi aktual atau SST adalah 5.133,0719."
|
||||||
|
)
|
||||||
|
add_heading(doc, "6.1 Linear Regression", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Komponen", "Nilai"],
|
||||||
|
[
|
||||||
|
["Jumlah absolute error atau SAE", "848,4592"],
|
||||||
|
["Jumlah squared error atau SSE", "1.122,3551"],
|
||||||
|
["Jumlah data testing", "1.349"],
|
||||||
|
["Total variasi aktual atau SST", "5.133,0719"],
|
||||||
|
],
|
||||||
|
widths=[3.4, 2.9],
|
||||||
|
)
|
||||||
|
add_formula(doc, "MAE = 848,4592 / 1.349 = 0,6290 ≈ 0,63")
|
||||||
|
add_formula(doc, "RMSE = √(1.122,3551 / 1.349) = 0,9121 ≈ 0,91")
|
||||||
|
add_formula(doc, "R² = 1 - (1.122,3551 / 5.133,0719) = 0,7813")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Interpretasinya, Linear Regression menjelaskan sekitar 78,13% variasi target pada testing set "
|
||||||
|
"dan rata-rata prediksinya meleset sekitar 0,63 unit."
|
||||||
|
)
|
||||||
|
add_heading(doc, "6.2 Random Forest", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Komponen", "Nilai"],
|
||||||
|
[
|
||||||
|
["Jumlah absolute error atau SAE", "39,86"],
|
||||||
|
["Jumlah squared error atau SSE", "18,7142"],
|
||||||
|
["Jumlah data testing", "1.349"],
|
||||||
|
["Total variasi aktual atau SST", "5.133,0719"],
|
||||||
|
],
|
||||||
|
widths=[3.4, 2.9],
|
||||||
|
)
|
||||||
|
add_formula(doc, "MAE = 39,86 / 1.349 = 0,0295 ≈ 0,03")
|
||||||
|
add_formula(doc, "RMSE = √(18,7142 / 1.349) = 0,1178 ≈ 0,12")
|
||||||
|
add_formula(doc, "R² = 1 - (18,7142 / 5.133,0719) = 0,9964")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Interpretasinya, Random Forest menjelaskan sekitar 99,64% variasi target pada testing set "
|
||||||
|
"dan rata-rata prediksinya meleset sekitar 0,03 unit. Angka ini harus dibaca bersama analisis "
|
||||||
|
"data leakage pada bagian berikutnya."
|
||||||
|
)
|
||||||
|
add_heading(doc, "6.3 Contoh Hasil Prediksi Individual", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Aktual", "Prediksi Linear Regression", "Prediksi Random Forest", "Error Absolut RF"],
|
||||||
|
[
|
||||||
|
["4", "3,5472", "4,01", "0,01"],
|
||||||
|
["3", "2,5369", "3,00", "0,00"],
|
||||||
|
["4", "4,4879", "4,00", "0,00"],
|
||||||
|
["2", "0,7336", "2,00", "0,00"],
|
||||||
|
["7", "7,2195", "7,00", "0,00"],
|
||||||
|
],
|
||||||
|
widths=[0.9, 2.0, 2.0, 1.4],
|
||||||
|
font_size=9,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "7. Perbandingan dan Interpretasi Model", 1)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Aspek", "Linear Regression", "Random Forest"],
|
||||||
|
[
|
||||||
|
["R² testing", "0,7813", "0,9964"],
|
||||||
|
["MAE testing", "0,63 unit", "0,03 unit"],
|
||||||
|
["RMSE testing", "0,91 unit", "0,12 unit"],
|
||||||
|
["Kemampuan pola non-linear", "Terbatas", "Baik"],
|
||||||
|
["Kemudahan interpretasi", "Sangat mudah", "Lebih kompleks"],
|
||||||
|
["Risiko pada hasil proyek", "Terpengaruh leakage", "Sangat terpengaruh leakage"],
|
||||||
|
],
|
||||||
|
widths=[2.2, 2.05, 2.05],
|
||||||
|
)
|
||||||
|
add_heading(doc, "7.1 Perbandingan MAE", 2)
|
||||||
|
add_formula(doc, "Penurunan MAE = [(0,6290 - 0,0295) / 0,6290] x 100% = 95,3%")
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Pernyataan yang tepat adalah Random Forest mengurangi MAE sekitar 95,3% dibandingkan Linear "
|
||||||
|
"Regression pada test set saat ini. Pernyataan tersebut tidak sama dengan mengatakan model "
|
||||||
|
"95,3% lebih akurat."
|
||||||
|
)
|
||||||
|
add_heading(doc, "7.2 Indikasi Overfitting", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Model", "R² Training", "R² Testing", "Selisih"],
|
||||||
|
[
|
||||||
|
["Linear Regression", "0,7902", "0,7813", "0,0089"],
|
||||||
|
["Random Forest", "0,9995", "0,9964", "0,0031"],
|
||||||
|
],
|
||||||
|
widths=[2.0, 1.4, 1.4, 1.4],
|
||||||
|
)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Selisih training dan testing terlihat kecil sehingga secara angka awal model tampak stabil. "
|
||||||
|
"Namun, kestabilan ini tidak membatalkan masalah data leakage karena informasi target tersedia "
|
||||||
|
"pada kedua kelompok data melalui total harga."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "8. Analisis Data Leakage", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Data leakage adalah kondisi ketika fitur model mengandung informasi yang seharusnya belum "
|
||||||
|
"tersedia pada saat prediksi dilakukan atau secara langsung dibentuk dari target. Leakage membuat "
|
||||||
|
"hasil testing terlihat sangat baik tetapi performa tersebut sulit dipertahankan pada penggunaan nyata."
|
||||||
|
)
|
||||||
|
add_formula(doc, "total_harga_update = harga_satuan_update x jumlah_permintaan_bahan")
|
||||||
|
add_formula(doc, "jumlah_permintaan_bahan = total_harga_update / harga_satuan_update")
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Hasil Pemeriksaan Dataset",
|
||||||
|
"Hubungan total_harga_update = harga_satuan_update x jumlah_permintaan_bahan berlaku tepat pada "
|
||||||
|
"seluruh 6.742 baris atau 100% data.",
|
||||||
|
fill=LIGHT_RED,
|
||||||
|
title_color=RED,
|
||||||
|
)
|
||||||
|
add_heading(doc, "8.1 Bukti dari Feature Importance", 2)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Fitur", "Feature Importance Random Forest"],
|
||||||
|
[
|
||||||
|
["total_harga_update", "62,97%"],
|
||||||
|
["harga_satuan_update", "36,18%"],
|
||||||
|
["Gabungan kedua fitur harga", "99,15%"],
|
||||||
|
["Seluruh fitur lain", "Kurang dari 1% secara individual"],
|
||||||
|
],
|
||||||
|
widths=[3.7, 2.6],
|
||||||
|
)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Feature importance menunjukkan bahwa Random Forest hampir sepenuhnya menggunakan total harga "
|
||||||
|
"dan harga satuan. Model terutama mempelajari cara memperoleh kembali jumlah permintaan, bukan "
|
||||||
|
"mempelajari pola permintaan berdasarkan waktu, produk, atau kategori."
|
||||||
|
)
|
||||||
|
add_heading(doc, "8.2 Mengapa Leakage Berbahaya", 2)
|
||||||
|
for text in (
|
||||||
|
"Total harga transaksi biasanya baru diketahui setelah jumlah permintaan diketahui.",
|
||||||
|
"Pada saat memprediksi permintaan masa depan, total harga belum tersedia.",
|
||||||
|
"Nilai R² yang tinggi dapat memberikan kesimpulan keliru bahwa model siap digunakan.",
|
||||||
|
"Model berpotensi gagal ketika menerima data nyata tanpa informasi target terselubung.",
|
||||||
|
):
|
||||||
|
add_bullet(doc, text)
|
||||||
|
|
||||||
|
add_heading(doc, "9. Pengujian Ulang Tanpa Data Leakage", 1)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Untuk mengukur kemampuan prediksi yang lebih realistis, total_harga_update dihapus dari fitur. "
|
||||||
|
"Pengujian dilakukan menggunakan random split dan chronological split."
|
||||||
|
)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Skenario", "Model", "R²", "MAE", "RMSE"],
|
||||||
|
[
|
||||||
|
["Random split tanpa total harga", "Linear Regression", "-0,0045", "1,53", "1,96"],
|
||||||
|
["Random split tanpa total harga", "Random Forest", "-0,1124", "1,64", "2,06"],
|
||||||
|
["Chronological split tanpa total harga", "Linear Regression", "-0,0025", "1,57", "1,99"],
|
||||||
|
["Chronological split tanpa total harga", "Random Forest", "-0,0831", "1,65", "2,06"],
|
||||||
|
],
|
||||||
|
widths=[2.3, 1.6, 0.8, 0.8, 0.8],
|
||||||
|
font_size=8.8,
|
||||||
|
)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Makna R² Negatif",
|
||||||
|
"R² negatif berarti model lebih buruk dibandingkan metode sederhana yang selalu memprediksi "
|
||||||
|
"nilai rata-rata permintaan. Hasil ini menunjukkan fitur yang tersedia belum mengandung pola "
|
||||||
|
"permintaan yang cukup kuat setelah total harga dihapus.",
|
||||||
|
fill=LIGHT_GOLD,
|
||||||
|
title_color=GOLD,
|
||||||
|
)
|
||||||
|
add_heading(doc, "9.1 Kesimpulan Validitas", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Angka R² sebesar 0,9964 tetap merupakan hasil perhitungan yang benar untuk konfigurasi testing "
|
||||||
|
"saat ini. Akan tetapi, angka tersebut tidak valid jika digunakan untuk menyatakan kemampuan "
|
||||||
|
"memprediksi permintaan masa depan secara murni karena terdapat data leakage."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "10. Rekomendasi Perbaikan Metodologi", 1)
|
||||||
|
add_heading(doc, "10.1 Perbaikan Fitur", 2)
|
||||||
|
for text in (
|
||||||
|
"Hapus total_harga_update dari fitur prediksi.",
|
||||||
|
"Hapus salah satu dari hari_dalam_minggu atau hari_minggu karena keduanya duplikat.",
|
||||||
|
"Gunakan lag permintaan, misalnya jumlah permintaan hari sebelumnya atau minggu sebelumnya.",
|
||||||
|
"Gunakan moving average permintaan dalam 7, 14, atau 30 hari terakhir.",
|
||||||
|
"Tambahkan informasi hari libur, musim, promosi, dan periode ramai.",
|
||||||
|
"Tambahkan stok sebelumnya serta histori pemakaian bahan.",
|
||||||
|
):
|
||||||
|
add_bullet(doc, text)
|
||||||
|
add_heading(doc, "10.2 Perbaikan Proses Testing", 2)
|
||||||
|
for text in (
|
||||||
|
"Gunakan chronological split: data lama untuk training dan data terbaru untuk testing.",
|
||||||
|
"Gunakan TimeSeriesSplit atau walk-forward validation untuk evaluasi yang lebih kuat.",
|
||||||
|
"Bandingkan model dengan baseline sederhana seperti rata-rata atau nilai periode sebelumnya.",
|
||||||
|
"Lakukan tuning parameter Random Forest hanya menggunakan data training.",
|
||||||
|
"Laporkan R², MAE, dan RMSE secara bersamaan.",
|
||||||
|
):
|
||||||
|
add_bullet(doc, text)
|
||||||
|
add_heading(doc, "10.3 Konsistensi Aplikasi Flutter", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Python menggunakan kode hari 0 sampai 6, sedangkan DateTime.weekday pada Flutter menggunakan "
|
||||||
|
"1 sampai 7. Nilai harus disamakan sebelum dikirim ke model. Selain itu, pemetaan encoded produk "
|
||||||
|
"dan kategori pada Flutter harus sama persis dengan LabelEncoder saat training."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "11. Contoh Penjelasan untuk Laporan", 1)
|
||||||
|
add_heading(doc, "11.1 Paragraf Metode Pengujian", 2)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Contoh Penulisan",
|
||||||
|
"Pengujian model dilakukan menggunakan metode hold-out dengan membagi dataset menjadi 80% data "
|
||||||
|
"training dan 20% data testing. Dari total 6.742 data transaksi, sebanyak 5.393 data digunakan "
|
||||||
|
"untuk melatih model dan 1.349 data digunakan untuk menguji performa model. Pembagian data "
|
||||||
|
"menggunakan random_state sebesar 42 agar hasil pembagian dapat direproduksi. Evaluasi dilakukan "
|
||||||
|
"menggunakan koefisien determinasi R², Mean Absolute Error, dan Root Mean Squared Error.",
|
||||||
|
)
|
||||||
|
add_heading(doc, "11.2 Paragraf Hasil Perbandingan", 2)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Contoh Penulisan",
|
||||||
|
"Berdasarkan pengujian awal, Linear Regression menghasilkan R² sebesar 0,7813, MAE sebesar 0,63, "
|
||||||
|
"dan RMSE sebesar 0,91. Random Forest menghasilkan R² sebesar 0,9964, MAE sebesar 0,03, dan RMSE "
|
||||||
|
"sebesar 0,12. Secara numerik, Random Forest memberikan hasil prediksi yang lebih dekat terhadap "
|
||||||
|
"nilai aktual pada testing set dibandingkan Linear Regression.",
|
||||||
|
)
|
||||||
|
add_heading(doc, "11.3 Paragraf Keterbatasan", 2)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Contoh Penulisan",
|
||||||
|
"Evaluasi lanjutan menunjukkan bahwa nilai Random Forest yang sangat tinggi dipengaruhi oleh "
|
||||||
|
"penggunaan fitur total_harga_update. Fitur tersebut dihitung dari harga_satuan_update dikalikan "
|
||||||
|
"jumlah_permintaan_bahan yang menjadi target model. Kondisi ini menimbulkan data leakage karena "
|
||||||
|
"fitur mengandung informasi langsung mengenai target. Oleh karena itu, hasil R² sebesar 0,9964 "
|
||||||
|
"harus dipahami sebagai performa pada konfigurasi dataset awal dan belum sepenuhnya menggambarkan "
|
||||||
|
"kemampuan prediksi permintaan masa depan.",
|
||||||
|
fill=LIGHT_GOLD,
|
||||||
|
title_color=GOLD,
|
||||||
|
)
|
||||||
|
add_heading(doc, "11.4 Paragraf Rekomendasi Pengembangan", 2)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Contoh Penulisan",
|
||||||
|
"Pengembangan berikutnya perlu menghapus fitur total harga dan menggunakan fitur yang telah tersedia "
|
||||||
|
"sebelum transaksi terjadi, seperti histori permintaan, lag permintaan, moving average, tren bulanan, "
|
||||||
|
"hari libur, promosi, dan stok sebelumnya. Pengujian juga perlu dilakukan secara kronologis agar "
|
||||||
|
"lebih sesuai dengan tujuan prediksi masa depan.",
|
||||||
|
fill="ECF7EF",
|
||||||
|
title_color=GREEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "12. Contoh Jawaban Saat Sidang", 1)
|
||||||
|
add_heading(doc, "12.1 Bagaimana Proses Testing Dilakukan?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: Proses testing dilakukan dengan membagi 6.742 data menjadi 80% data training dan 20% "
|
||||||
|
"data testing. Model mempelajari pola dari 5.393 data training, kemudian menghasilkan prediksi "
|
||||||
|
"untuk 1.349 data testing yang tidak digunakan saat pelatihan. Hasil prediksi dibandingkan dengan "
|
||||||
|
"nilai aktual menggunakan R², MAE, dan RMSE."
|
||||||
|
)
|
||||||
|
add_heading(doc, "12.2 Mengapa Menggunakan Tiga Metrik?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: R² digunakan untuk mengukur seberapa besar variasi target dapat dijelaskan model. MAE "
|
||||||
|
"menunjukkan rata-rata kesalahan dalam satuan permintaan sehingga mudah dipahami. RMSE memberikan "
|
||||||
|
"penalti lebih besar untuk kesalahan yang besar. Ketiganya memberikan penilaian yang lebih lengkap."
|
||||||
|
)
|
||||||
|
add_heading(doc, "12.3 Mengapa Random Forest Lebih Baik?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: Pada konfigurasi testing awal, Random Forest lebih baik karena mampu menangkap hubungan "
|
||||||
|
"non-linear dan menggabungkan prediksi dari 100 Decision Tree. Namun, performa yang sangat tinggi "
|
||||||
|
"juga disebabkan total harga yang memiliki hubungan langsung dengan target."
|
||||||
|
)
|
||||||
|
add_heading(doc, "12.4 Apakah R² 0,9964 Berarti Akurasi 99,64%?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: Tidak sepenuhnya. R² sebesar 0,9964 berarti model menjelaskan sekitar 99,64% variasi "
|
||||||
|
"target pada testing set. R² bukan akurasi klasifikasi. Selain itu, nilai tersebut dipengaruhi "
|
||||||
|
"data leakage sehingga belum mewakili kemampuan prediksi masa depan secara valid."
|
||||||
|
)
|
||||||
|
add_heading(doc, "12.5 Apakah Model Mengalami Overfitting?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: Selisih R² training dan testing kecil, sehingga secara angka awal tidak menunjukkan "
|
||||||
|
"overfitting yang besar. Akan tetapi, terdapat masalah yang lebih penting, yaitu data leakage. "
|
||||||
|
"Leakage membuat training dan testing sama-sama mudah diprediksi karena keduanya mengandung informasi target."
|
||||||
|
)
|
||||||
|
add_heading(doc, "12.6 Apa Perbaikan yang Harus Dilakukan?", 2)
|
||||||
|
add_body(
|
||||||
|
doc,
|
||||||
|
"Jawaban: Total harga harus dihapus dari fitur, kemudian model dilatih menggunakan fitur yang benar-benar "
|
||||||
|
"tersedia sebelum transaksi terjadi. Testing sebaiknya menggunakan pembagian berdasarkan waktu dan "
|
||||||
|
"dibandingkan dengan baseline. Fitur histori seperti lag dan moving average juga perlu ditambahkan."
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(doc, "Lampiran: Ringkasan Rumus dan Istilah", 1)
|
||||||
|
add_table(
|
||||||
|
doc,
|
||||||
|
["Konsep", "Rumus atau Definisi Ringkas"],
|
||||||
|
[
|
||||||
|
["Linear Regression", "ŷ = β₀ + β₁x₁ + ... + βₚxₚ"],
|
||||||
|
["Random Forest", "Rata-rata prediksi dari banyak Decision Tree"],
|
||||||
|
["SSE", "Σ(yᵢ - ŷᵢ)²"],
|
||||||
|
["MAE", "(1/n) Σ|yᵢ - ŷᵢ|"],
|
||||||
|
["RMSE", "√[(1/n) Σ(yᵢ - ŷᵢ)²]"],
|
||||||
|
["R²", "1 - SSE/SST"],
|
||||||
|
["Overfitting", "Model sangat baik pada training tetapi buruk pada data baru"],
|
||||||
|
["Underfitting", "Model terlalu sederhana dan gagal mempelajari pola"],
|
||||||
|
["Data leakage", "Fitur mengandung informasi target atau informasi masa depan"],
|
||||||
|
["Chronological split", "Training memakai data lama, testing memakai data terbaru"],
|
||||||
|
["Baseline", "Metode sederhana sebagai pembanding minimum"],
|
||||||
|
],
|
||||||
|
widths=[2.0, 4.3],
|
||||||
|
font_size=9.3,
|
||||||
|
)
|
||||||
|
add_callout(
|
||||||
|
doc,
|
||||||
|
"Kesimpulan Akhir",
|
||||||
|
"Random Forest menghasilkan metrik terbaik pada testing awal, tetapi performa tersebut sangat "
|
||||||
|
"dipengaruhi data leakage dari total_harga_update. Untuk laporan yang bertanggung jawab secara "
|
||||||
|
"akademik, tampilkan hasil awal, jelaskan keterbatasannya, dan cantumkan rekomendasi pengujian ulang "
|
||||||
|
"tanpa fitur yang dibentuk dari target.",
|
||||||
|
fill="ECF7EF",
|
||||||
|
title_color=GREEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.save(OUTPUT)
|
||||||
|
print(OUTPUT)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build_document()
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
roihan | ||||||