diff --git a/docs/codex-handoff-2026-05-10.md b/docs/codex-handoff-2026-05-10.md deleted file mode 100644 index a384fa1..0000000 --- a/docs/codex-handoff-2026-05-10.md +++ /dev/null @@ -1,208 +0,0 @@ -# Codex Handoff - 2026-05-10 - -Dokumen ini menyimpan konteks kerja terakhir agar pengerjaan bisa langsung dilanjutkan saat project dibuka lagi. - -## Status Umum - -- App aktif dikembangkan di `Next.js + Prisma`. -- Banyak flow bisnis inti sudah tersambung end-to-end. -- Project belum dianggap production-ready; fokus saat ini masih penyelesaian fitur dan perapihan UX. - -## Perubahan Domain dan Terminologi - -- `Customer` sudah dirapikan menjadi `Buyer`. -- `Seller` sudah dirapikan menjadi `Sales`. -- `sellerCurrencyCode` sudah dirapikan menjadi `companyCurrencyCode`. -- `item type` dan `item grade` sudah dibersihkan dari kontrak aktif UI/API; flow aktif sekarang pakai `grade`. -- Struktur Prisma dan mapping database sudah disesuaikan ke arah penamaan yang lebih jelas. - -## Fitur yang Sudah Aktif - -### 1. Penjualan Reguler - -- Flow create dan close sudah aktif. -- Create: - - buyer - - mata uang buyer - - mata uang perusahaan - - rate buyer ke perusahaan bila beda currency - - kurir - - biaya kirim - - resi opsional - - multiple lot -- Line item: - - pilih lot - - berat jual - - harga jual - - info grade, kode lot, berat sekarang, harga MAL, bagi hasil agent -- Close: - - berat jual aktual - - berat retur - - harga jual aktual - - selisih otomatis masuk penyusutan -- Komisi agent: - - dihitung ulang saat close - - menambah saldo agent - -### 2. Titip Jual / Consignment - -- Flow create dan close sudah aktif. -- Close support: - - berat terjual - - berat kembali - - berat susut - - harga jual - - komisi sales manual -- Rumus komisi agent: - - `((berat terjual * (harga jual - harga MAL)) - komisi sales) * persentase share` -- Komisi agent: - - menambah saldo bagi hasil agent -- Komisi sales: - - punya histori sendiri di master `Sales` - -### 3. Penjualan Just In Time - -- Placeholder sudah diganti menjadi flow aktif. -- Header input sama arah umumnya dengan penjualan reguler. -- Barang dijual tanpa masuk gudang dan tanpa retur. -- Line item JIT: - - grade - - quantity - - harga MAL - - harga jual - - agent opsional - - skema bagi hasil opsional - - catatan -- Status create: - - `OPEN` -- Close: - - update `harga jual aktual` saja -- Komisi agent: - - dihitung saat close - - menambah saldo bagi hasil agent -- Banyak pass UI sudah dilakukan agar gaya JIT mendekati penjualan reguler. - -### 4. Purchases - -- Menu `Purchases` sudah dipecah: - - `Pembelian Reguler` - - `Pembelian Kantor / Buyout Agent` -- `Purchase Detail` sekarang dibuka sebagai popup/modal agar tidak merusak layout utama. -- Tombol `Cetak receipt` dipindah ke detail purchase, bukan lagi di halaman receipt. - -### 5. Pembelian Kantor / Buyout Agent - -- Sudah aktif sebagai submenu terpisah. -- Sumber hanya dari lot aktif yang sudah ada. -- Buyout bisa parsial atau full. -- Setiap buyout selalu membuat lot baru milik kantor. -- Lot lama dikurangi; jika habis bisa closed. -- Komisi agent saat buyout: - - `max(0, qty * (harga buyout - harga MAL)) * persen share` -- Jika harga buyout di bawah harga MAL, komisi agent default `0`. -- Komisi buyout menambah saldo bagi hasil agent dan masuk histori. -- Detail lot sudah menampilkan jejak buyout. - -### 6. Fund Request - -- Menu baru `/fund-requests` sudah aktif. -- Ada 2 tipe: - - `Dana modal` - - `Bagi hasil` -- Input: - - code generated - - no reff - - agent - - rekening agent - - rekening kantor - - nominal - - waktu transfer - - bukti transfer opsional -- Dampak saldo: - - `Dana modal`: kantor transfer ke agent, jadi menambah saldo modal agent - - `Bagi hasil`: pembayaran bagi hasil ke agent, jadi mengurangi saldo bagi hasil agent -- Histori mutasi agent ikut tercatat. - -### 7. Master Agent - -- Agent punya dua saldo: - - `saldo bagi hasil` - - `saldo modal` -- Histori mutasi saldo sudah aktif. -- Sumber histori mencakup: - - opening balance - - manual adjustment - - consignment commission - - regular sale commission - - JIT sale commission - - office buyout commission - - fund request profit share - - fund request capital -- Detail agent sudah bisa melihat histori. - -### 8. Master Sales - -- `Sales` punya `commission_balance`. -- Histori komisi sales sudah aktif. -- Detail sales sudah bisa menampilkan histori komisi. - -### 9. Settings / Profil Perusahaan - -- Profil perusahaan sekarang mendukung multiple rekening kantor. -- Bank kantor dipilih dari master bank. -- Fund Request memakai pilihan rekening kantor yang dipilih user. -- Bug sesudah save yang membuat list rekening kantor terlihat kosong tanpa refresh sudah diperbaiki. - -## Perapihan UI/UX yang Sudah Dilakukan - -- Banyak halaman master 2 kolom sekarang sudah punya: - - pagination - - search -- Picker grade besar sudah diganti ke searchable combobox di beberapa flow aktif. -- Sidebar bug auto-expand karena prefix path sudah diperbaiki. -- Icon bell di topbar di-hide. -- Tombol `?` diarahkan ke halaman bantuan `/help`. -- Beberapa halaman transaksi besar sudah dipadatkan dan disamakan skalanya. - -## Catatan Teknis Penting - -- Untuk banyak perubahan schema terakhir, `npm run prisma:generate`, `npm run db:push`, dan `npm run typecheck` sudah pernah lolos. -- Alur kerja saat ini masih banyak mengandalkan `db push`, belum migrasi Prisma versioned. -- Belum ada test suite otomatis yang matang. -- Project sebelumnya juga sudah dinilai belum production-ready, terutama pada aspek auth/bootstrap dev account, secret fallback, migration discipline, dan testing. - -## Isu yang Baru Saja Diperbaiki - -- `Settings`: - - setelah simpan, list rekening kantor sempat terlihat reset - - akar masalah: response `PUT /api/v1/settings` tidak mengembalikan relasi `companyBankAccounts` - - sudah diperbaiki dengan fetch ulang record lengkap sebelum serialize -- `Pembelian Reguler`: - - warning `lengkapi master data` sempat muncul terus saat buka halaman - - sudah diperbaiki agar hanya muncul setelah loading selesai dan hanya jika master wajib benar-benar kosong -- `Purchase Analysis`: - - chip jumlah purchase sudah dipaksa satu baris - -## Kandidat Lanjutan Paling Masuk Akal - -1. Lanjut desain dan implementasi flow `Pembelian Just In Time` jika memang masih ada versi purchase-side yang terpisah dari `Sales JIT`. -2. Sweep UI consistency lagi untuk: - - `Sales JIT` - - `Fund Request` - - `Office Buyout` -3. Tambah dokumentasi user flow untuk transaksi baru: - - regular sale - - JIT sale - - office buyout - - fund request -4. Hardening project sebelum production: - - hapus bootstrap user dev dari runtime login - - wajibkan `AUTH_SECRET` - - pindah ke Prisma migrations - - tambah test smoke/integration - - rate limiting auth endpoint - -## File Ini - -- Update file ini setiap kali ada keputusan bisnis besar atau modul baru selesai. -- Kalau mau lebih formal, file ini bisa diganti nanti menjadi `docs/project-status.md` permanen. diff --git a/docs/codex-handoff-2026-05-19.md b/docs/codex-handoff-2026-05-19.md new file mode 100644 index 0000000..f296469 --- /dev/null +++ b/docs/codex-handoff-2026-05-19.md @@ -0,0 +1,243 @@ +# Codex Handoff - 2026-05-19 + +Dokumen ini menyimpan konteks kerja terbaru agar pengerjaan bisa langsung dilanjutkan tanpa menggali ulang repo. + +## Status Umum + +- App aktif dikembangkan di `Next.js + Prisma + PostgreSQL`. +- Fitur operasional inti sudah cukup lengkap: pembelian, penerimaan, lot, aktivitas stok, penjualan, analisis pembelian, dan realisasi pembelian. +- Scope mobile API untuk fase operasional utama juga sudah disiapkan. +- Repo sudah dipush ke: + `https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git` +- Branch aktif: + `main` + +## Commit Penting Terakhir + +- `14bb9bf` `Initial import of AbelBirdNest Stock` +- `9141f99` `Fix grade seed path for production deploy` +- `1274f2b` `Harden login and refresh production deploy guide` +- `8e8912e` `Allow negative purchase moisture percentages` + +## Perubahan Besar yang Sudah Selesai + +### 1. Purchase Analysis + +- Snapshot analisis pembelian sudah dipersist agar saat edit nilainya kembali sama persis. +- Field seperti: + - `modal_barang` + - `modal_beli` + - `modal_masuk` + - `modal_jual` + - `total_modal_beli` + - `total_modal_mal` + - `average_price` + - `kadar akhir` + sudah tersimpan dan bisa dipakai ulang. +- Banyak rumus dashboard/analisis sudah disesuaikan: + - `kadar akhir` + - `laba total / kg` + - `laba/rugi agen` + - basis berat gram vs kg + +### 2. Purchase Realization + +- Modul realization sudah didesain dan diimplementasikan bertahap. +- Prisma model baru sudah ada: + - `LotPurchaseAllocation` + - `PurchaseRealizationEntry` + - `PurchaseRealizationSummary` +- Event yang sudah tersambung ke realization: + - purchase submit + - office buyout + - washing complete + - lot transformation / regrade / mix + - regular sale close + - consignment close +- Endpoint list/detail dan halaman UI `Purchase Realization` sudah ada. +- Filter/search dan drill-down dasar ke dokumen asal juga sudah ditambahkan. + +### 3. Mobile API + +- Namespace `/api/v1/mobile/**` sudah disiapkan untuk role: + - `WAREHOUSE` + - `QC` + - `SALES` + - `PURCHASING` + - `OWNER` +- Endpoint mobile yang sudah ada mencakup: + - bootstrap + - dashboard + - lots + - receipts + - stock adjustments + - washing + - lot transformations + - regular sales + - JIT sales + - consignments + - purchases + - fund requests + - purchase analyses + - purchase realizations +- Dokumen pendukung: + - [mobile-api-blueprint.md](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/mobile-api-blueprint.md) + - [abelbirdnest-mobile-api.postman_collection.json](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/postman/abelbirdnest-mobile-api.postman_collection.json) + +### 4. Bilingual UI dan Terminologi + +- Banyak area UI sudah dibersihkan dari campuran istilah Indonesia/Inggris. +- Sidebar, mobile nav, halaman transaksi utama, master data, dan modul analitis sudah jauh lebih konsisten. +- Banyak label dropdown dan placeholder sudah dibuat locale-aware. +- Istilah bisnis Indonesia sudah dirapikan, misalnya: + - `Agent` → `Agen` + - `Courier` → `Jasa Pengiriman` + - `Warehouse` → `Gudang` + - `Warehouse Location` → `Lokasi Gudang` + - `Contact Person` → `Kontak PIC` + +### 5. Dashboard + +- Dashboard atas sudah diubah: + - `Nilai Inventory` menjadi basis `Nilai Pembelian Bulan Ini` + - `Receipt Bulan Ini` menjadi `Pembelian Bulan Ini` + - grafik menjadi `Tren Pembelian vs Penjualan` + - kartu kanan menjadi `Tingkat Serap Penjualan` +- Widget lot kritis diperjelas menjadi `Lot Perlu Perhatian` + dengan alasan: + - `Stok Rendah` + - `Usia Tinggi` + - `Ditahan` + +### 6. Users + +- User sekarang sudah bisa diedit dari UI. +- Jika email diubah: + - `email_verified_at` di-reset + - token verifikasi lama dibuang + - email verifikasi dikirim ulang + +### 7. Regular Purchase Moisture Validation + +- Validasi pembelian reguler sekarang mengizinkan: + - `kadar beli` + - `kadar masuk` + - `kadar akhir` + bernilai negatif sampai `-100`. +- Alasan bisnis: + nilai minus dipakai untuk kasus penyusutan / penurunan berat pada alur pembelian dan analisis. +- Patch ini ada di commit: + - `8e8912e` `Allow negative purchase moisture percentages` +- File yang berubah: + - [src/features/purchases/schemas/purchase.schema.ts](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/src/features/purchases/schemas/purchase.schema.ts) +- Tidak perlu migration database untuk patch ini. +- Update server cukup: + - `git pull origin main` + - `npm run build` + - restart service `abelbirdnest-web` + +## Hardening Production yang Sudah Dilakukan + +- Bootstrap akun default sekarang dimatikan di production lewat: + - `AUTH_BOOTSTRAP=false` +- `AUTH_SECRET` dan `APP_URL` dipaksa aman via helper runtime. +- Health endpoint tersedia di: + - `/api/v1/health` +- Security header dasar ditambahkan di `next.config.ts`. +- Workflow Prisma production-ready sudah disiapkan: + - `npm run prisma:migrate:deploy` +- File deploy yang sudah ada: + - [deploy/nginx/abelbirdnest.id.conf](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/deploy/nginx/abelbirdnest.id.conf) + - [deploy/nginx/abelbirdnest.id.http.conf](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/deploy/nginx/abelbirdnest.id.http.conf) + - [deploy/systemd/abelbirdnest-web.service](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/deploy/systemd/abelbirdnest-web.service) + - [deploy-production.md](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/deploy-production.md) + +## Kondisi Deploy Server yang Perlu Diingat + +- Domain target production: + - `abelbirdnest.id` +- App dijalankan di: + - `127.0.0.1:3007` +- Reverse proxy: + - `nginx` +- Struktur repo aktual di server diasumsikan: + - `/var/www/abelbirdnest-web/AbelBirdNest-Stock` +- `systemd` service file repo sudah diarahkan ke struktur subfolder ini. +- Nginx sekarang disiapkan 2 tahap: + 1. HTTP-only dulu dengan `abelbirdnest.id.http.conf` + 2. setelah sertifikat ada, ganti ke `abelbirdnest.id.conf` + +## Catatan Produksi Penting yang Masih Relevan + +- `roles` tidak dibuat otomatis jika `AUTH_BOOTSTRAP=false`. + Jadi sebelum membuat user pertama di production, tabel `roles` harus diisi dulu secara manual. +- User pertama production sebaiknya dibuat langsung di PostgreSQL sebagai `SYSTEM_ADMIN`. +- Seed: + - `banks` dan `currencies` bisa langsung + - `grades` butuh file `Grade.xls` +- Script `seed:grades` sudah tidak lagi hardcoded ke path Mac lokal. + Default path sekarang: + - `scripts/data/Grade.xls` + +## Perubahan Login dan Footer + +- Halaman login sudah dibersihkan: + - tidak ada lagi autofill email/password default + - panel daftar akun dev dihapus +- Footer copyright sudah dipasang: + - `© 2026 AbelBirdnest` +- Copyright sudah muncul di: + - halaman login + - seluruh halaman app utama via `AppShell` + +## Dokumentasi User yang Sudah Dibuat + +Sudah ada dokumen manual Word di folder `docs/`: + +- [manual-dashboard-abelbirdnest.docx](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/manual-dashboard-abelbirdnest.docx) +- [manual-operasional-abelbirdnest-lengkap.docx](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/manual-operasional-abelbirdnest-lengkap.docx) +- [manual-operasional-abelbirdnest-formal-screenshot.docx](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/manual-operasional-abelbirdnest-formal-screenshot.docx) + +Dan asset screenshot pendukung: + +- [docs/manual-assets](/Users/wirabasalamah/Documents/Codex/abelbirdnest-web/docs/manual-assets) + +Catatan: + +- file manual dan asset screenshot saat ini masih `untracked` di git +- belum dipush ke remote + +## Status Git Saat Handoff Ini Dibuat + +Untracked: + +- `docs/manual-assets/` +- `docs/manual-dashboard-abelbirdnest.docx` +- `docs/manual-operasional-abelbirdnest-lengkap.docx` +- `docs/manual-operasional-abelbirdnest-formal-screenshot.docx` + +## Hal yang Masih Belum Ideal + +- Belum ada rate limiting auth/API sensitif. +- `SMTP_PASSWORD` masih bisa tersimpan di DB settings. +- `npm run lint` belum dijadikan gate CI non-interaktif yang rapi. +- Belum ada test suite otomatis yang matang. +- Sebagian validasi/error backend mungkin masih belum sepenuhnya locale-aware walaupun UI utama sudah jauh lebih bersih. + +## Langkah Lanjutan Paling Masuk Akal + +1. Putuskan apakah dokumen manual Word + screenshot perlu masuk repo dan dipush. +2. Jika iya, `git add` file manual dan asset screenshot lalu commit terpisah. +3. Tambahkan script/helper resmi untuk membuat `roles` dan user production pertama agar tidak lagi manual SQL. +4. Lanjut hardening production: + - rate limit + - cleanup SMTP secret strategy + - CI/lint/test +5. Jika mobile app mulai dikerjakan, pakai: + - `docs/mobile-api-blueprint.md` + - Postman collection mobile + +## Catatan Penutup + +- File handoff lama `docs/codex-handoff-2026-05-10.md` sudah tidak relevan lagi karena banyak perubahan besar setelah tanggal itu. +- Handoff terbaru ini menjadi sumber konteks utama sampai ada update berikutnya. diff --git a/docs/deploy-production.md b/docs/deploy-production.md index 58d134f..50b7f09 100644 --- a/docs/deploy-production.md +++ b/docs/deploy-production.md @@ -1,6 +1,6 @@ # Deploy Production -Panduan ini untuk deploy production dengan asumsi: +Dokumen ini menyiapkan deploy production untuk: - domain `abelbirdnest.id` - reverse proxy `nginx` @@ -8,26 +8,26 @@ Panduan ini untuk deploy production dengan asumsi: - database `PostgreSQL` - source code dari git `https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git` - user service khusus `abelbirdnest` -- repo berada di: - `/var/www/abelbirdnest-web/AbelBirdNest-Stock` ## 1. Persiapan Server -Install: +Siapkan: - Node.js LTS - npm - PostgreSQL - nginx -- certbot +- certbot / SSL Let’s Encrypt -Direktori aplikasi: +Direktori contoh: ```bash /var/www/abelbirdnest-web ``` -## 2. Buat User OS Khusus Aplikasi +## 2. Buat User Khusus Aplikasi + +Jalankan sebagai `root` atau dengan `sudo`: ```bash sudo useradd -r -m -d /var/www/abelbirdnest-web -s /bin/bash abelbirdnest @@ -35,44 +35,35 @@ sudo mkdir -p /var/www/abelbirdnest-web sudo chown -R abelbirdnest:abelbirdnest /var/www/abelbirdnest-web ``` +Catatan: + +- user `abelbirdnest` dipakai khusus untuk menjalankan service aplikasi +- jangan jalankan app production dengan user pribadi atau `root` + +## 3. Clone Repo dari Git + Masuk sebagai user aplikasi: ```bash sudo -u abelbirdnest -H bash cd /var/www/abelbirdnest-web +git clone https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git . ``` -## 3. Clone Repo +Kalau server butuh autentikasi git internal, siapkan credential sesuai kebijakan server Git Anda. -Clone normal: +## 4. Environment Production -```bash -git clone https://git.iptek.co/wirabasalamah/AbelBirdNest-Stock.git -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock -``` +Salin `.env.production.example` menjadi `.env.production`, lalu isi nilainya. -Catatan: - -- panduan ini mengikuti struktur clone normal di atas -- jadi `WorkingDirectory`, `.env.production`, dan semua perintah memakai path: - `/var/www/abelbirdnest-web/AbelBirdNest-Stock` - -## 4. Siapkan Environment Production - -Salin file contoh: - -```bash -cp .env.production.example .env.production -``` - -Isi minimal: +Yang wajib: ```env NODE_ENV=production PORT=3007 APP_URL=https://abelbirdnest.id -DATABASE_URL=postgresql://abelbirdnest_app:password@127.0.0.1:5432/abelbirdnest_prod?schema=public -AUTH_SECRET=ganti-dengan-random-string-panjang +DATABASE_URL=postgresql://... +AUTH_SECRET=... AUTH_BOOTSTRAP=false SMTP_HOST=... SMTP_PORT=465 @@ -84,13 +75,20 @@ SMTP_FROM=... Catatan: -- `AUTH_BOOTSTRAP=false` wajib di production -- akun default dev tidak akan aktif -- `AUTH_SECRET` harus random dan panjang +- `AUTH_SECRET` harus random panjang. +- `AUTH_BOOTSTRAP=false` wajib untuk production. +- `APP_URL` harus domain production final. -## 5. Inisialisasi PostgreSQL +## 5. Inisialisasi Database PostgreSQL -Masuk sebagai superuser PostgreSQL: +Contoh di bawah memakai: + +- database: `abelbirdnest_prod` +- database user: `abelbirdnest_app` +- host: `127.0.0.1` +- port: `5432` + +Masuk ke PostgreSQL sebagai superuser: ```bash sudo -u postgres psql @@ -99,19 +97,29 @@ sudo -u postgres psql Buat user database: ```sql -CREATE USER abelbirdnest_app WITH PASSWORD 'ganti-dengan-password-yang-kuat'; +CREATE USER abelbirdnest_app WITH PASSWORD '72ed04ddd2bb520eacae7f4b71a16b0d'; ``` -Buat database: +Buat database production: ```sql CREATE DATABASE abelbirdnest_prod OWNER abelbirdnest_app; +``` + +Pastikan owner database benar: + +```sql ALTER DATABASE abelbirdnest_prod OWNER TO abelbirdnest_app; +``` + +Opsional tapi disarankan, kunci privilege default: + +```sql REVOKE ALL ON DATABASE abelbirdnest_prod FROM PUBLIC; GRANT ALL PRIVILEGES ON DATABASE abelbirdnest_prod TO abelbirdnest_app; ``` -Keluar: +Keluar dari `psql`: ```sql \q @@ -120,295 +128,156 @@ Keluar: Tes koneksi: ```bash -psql "postgresql://abelbirdnest_app:ganti-dengan-password-yang-kuat@127.0.0.1:5432/abelbirdnest_prod" +psql "postgresql://abelbirdnest_app:72ed04ddd2bb520eacae7f4b71a16b0d@127.0.0.1:5432/abelbirdnest_prod?schema=public" +``` + +Jika koneksi berhasil, pakai URL itu di `.env.production`: + +```env +DATABASE_URL="postgresql://abelbirdnest_app:ganti-dengan-password-yang-kuat@127.0.0.1:5432/abelbirdnest_prod?schema=public" +``` + +## 6. Install Dependency, Database & Migration + +Repo ini sudah disiapkan memakai migration Prisma. + +Jalankan: + +```bash +cd /var/www/abelbirdnest-web +npm install +npm run prisma:generate +npm run prisma:migrate:deploy +``` + +Kalau perlu isi master awal: + +```bash +npm run seed:master +``` + +Data seed yang dibawa: + +- grade +- bank +- currency + +Urutan pertama kali untuk fresh database: + +```bash +cd /var/www/abelbirdnest-web +npm install +npm run prisma:generate +npm run prisma:migrate:deploy +npm run seed:master ``` Catatan: -- untuk `psql`, jangan pakai `?schema=public` -- untuk `DATABASE_URL` Prisma, tetap pakai `?schema=public` +- `prisma:migrate:deploy` akan membuat seluruh tabel dari migration yang ada di repo +- `seed:master` hanya mengisi data awal `grade`, `bank`, dan `currency` +- user login production tetap harus dibuat terpisah, jangan mengandalkan akun dev/default -## 6. Install Dependency dan Buat Tabel - -Masih sebagai user `abelbirdnest`: +## 7. Build Production ```bash -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock -npm install -npm run prisma:generate -``` - -Load env production ke shell saat menjalankan command manual: - -```bash -set -a -source .env.production -set +a -``` - -Jalankan migration: - -```bash -npm run prisma:migrate:deploy -``` - -Opsional cek status: - -```bash -npx prisma migrate status -``` - -## 7. Seed Data Awal - -Untuk fresh database: - -```bash -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock -set -a -source .env.production -set +a - -npm run seed:banks -npm run seed:currencies -``` - -### Khusus Grade - -`seed:grades` butuh file `Grade.xls`. - -Buat folder data: - -```bash -mkdir -p scripts/data -``` - -Upload file `Grade.xls` ke: - -```bash -/var/www/abelbirdnest-web/AbelBirdNest-Stock/scripts/data/Grade.xls -``` - -Lalu jalankan: - -```bash -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock -set -a -source .env.production -set +a - -npm run seed:grades -``` - -Alternatif jika file ada di lokasi lain: - -```bash -node scripts/seed-grades-from-xls.mjs /path/ke/Grade.xls -``` - -## 8. Buat User Pertama di PostgreSQL - -Karena `AUTH_BOOTSTRAP=false`, Anda harus buat user login pertama sendiri. - -### 8.1 Generate Hash Password - -Ganti password contoh ini: - -```bash -node -e 'const {randomBytes,scryptSync}=require("crypto"); const p="GantiPasswordKuat123!"; const salt=randomBytes(16).toString("hex"); const derived=scryptSync(p,salt,64).toString("hex"); console.log(`${salt}:${derived}`)' -``` - -Simpan output hash-nya. - -### 8.2 Masuk ke PostgreSQL - -```bash -psql "postgresql://abelbirdnest_app:password@127.0.0.1:5432/abelbirdnest_prod" -``` - -### 8.3 Pastikan Role `SYSTEM_ADMIN` Ada - -```sql -INSERT INTO roles (code, name, created_at, updated_at) -VALUES ('SYSTEM_ADMIN', 'System Admin', NOW(), NOW()) -ON CONFLICT (code) DO UPDATE -SET name = EXCLUDED.name, - updated_at = NOW(); -``` - -### 8.4 Buat User Pertama - -Ganti: - -- `Nama Anda` -- `superadmin` -- `superadmin@abelbirdnest.id` -- `HASH_HASIL_LANGKAH_8_1` - -```sql -INSERT INTO users ( - role_id, - name, - username, - email, - email_verified_at, - phone, - password_hash, - status, - created_at, - updated_at -) -SELECT - r.id, - 'Nama Anda', - 'superadmin', - 'superadmin@abelbirdnest.id', - NOW(), - NULL, - 'HASH_HASIL_LANGKAH_8_1', - 'ACTIVE', - NOW(), - NOW() -FROM roles r -WHERE r.code = 'SYSTEM_ADMIN' -ON CONFLICT (email) DO UPDATE -SET role_id = EXCLUDED.role_id, - name = EXCLUDED.name, - username = EXCLUDED.username, - email_verified_at = EXCLUDED.email_verified_at, - password_hash = EXCLUDED.password_hash, - status = EXCLUDED.status, - updated_at = NOW(); -``` - -Verifikasi: - -```sql -SELECT u.id, u.name, u.username, u.email, u.status, r.code AS role -FROM users u -JOIN roles r ON r.id = u.role_id -WHERE u.email = 'superadmin@abelbirdnest.id'; -``` - -## 9. Build Production - -```bash -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock -set -a -source .env.production -set +a - npm run build ``` -## 10. Jalankan App dengan systemd +## 8. Jalankan App di Port 3007 -File service repo sudah disiapkan untuk struktur subfolder ini: +Manual: + +```bash +PORT=3007 npm run start +``` + +Atau gunakan `systemd` dari: ```bash deploy/systemd/abelbirdnest-web.service ``` -Pasang: +Contoh setup: ```bash -sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/systemd/abelbirdnest-web.service /etc/systemd/system/ -sudo chown -R abelbirdnest:abelbirdnest /var/www/abelbirdnest-web/AbelBirdNest-Stock +sudo cp deploy/systemd/abelbirdnest-web.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable abelbirdnest-web -sudo systemctl restart abelbirdnest-web +sudo systemctl start abelbirdnest-web sudo systemctl status abelbirdnest-web ``` -Verifikasi port: +Autostart saat server restart terjadi karena service di-`enable`. + +Untuk verifikasi: ```bash -ss -ltnp | grep 3007 -curl http://127.0.0.1:3007/api/v1/health +sudo systemctl is-enabled abelbirdnest-web ``` -Kalau service gagal, cek: +## 9. Reverse Proxy Nginx + +Gunakan file: ```bash -sudo systemctl cat abelbirdnest-web -sudo journalctl -u abelbirdnest-web -n 100 --no-pager -which npm -``` - -## 11. Pasang Nginx Tahap 1: HTTP Dulu - -Jangan langsung pakai config HTTPS sebelum sertifikat ada. - -Pakai file ini dulu: - -```bash -deploy/nginx/abelbirdnest.id.http.conf +deploy/nginx/abelbirdnest.id.conf ``` Pasang: ```bash -sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/nginx/abelbirdnest.id.http.conf /etc/nginx/sites-available/abelbirdnest.id.conf -sudo ln -sf /etc/nginx/sites-available/abelbirdnest.id.conf /etc/nginx/sites-enabled/abelbirdnest.id.conf +sudo cp deploy/nginx/abelbirdnest.id.conf /etc/nginx/sites-available/abelbirdnest.id.conf +sudo ln -s /etc/nginx/sites-available/abelbirdnest.id.conf /etc/nginx/sites-enabled/abelbirdnest.id.conf sudo nginx -t sudo systemctl reload nginx ``` -Tes: +## 10. Health Check + +Endpoint health: ```bash -curl http://abelbirdnest.id/api/v1/health +GET /api/v1/health ``` -## 12. Buat Sertifikat SSL - -Setelah HTTP sudah hidup: - -```bash -sudo certbot --nginx -d abelbirdnest.id -d www.abelbirdnest.id -``` - -## 13. Ganti ke Config HTTPS Final - -Setelah sertifikat berhasil dibuat, baru pakai config final: - -```bash -sudo cp /var/www/abelbirdnest-web/AbelBirdNest-Stock/deploy/nginx/abelbirdnest.id.conf /etc/nginx/sites-available/abelbirdnest.id.conf -sudo nginx -t -sudo systemctl reload nginx -``` - -Verifikasi: +Contoh: ```bash curl https://abelbirdnest.id/api/v1/health ``` -## 14. Update Deployment Berikutnya +## 11. Update Deployment Berikutnya + +Jika aplikasi sudah live dan ada update dari git: ```bash -cd /var/www/abelbirdnest-web/AbelBirdNest-Stock +cd /var/www/abelbirdnest-web git pull origin main - -set -a -source .env.production -set +a - npm install -npm run prisma:generate npm run prisma:migrate:deploy npm run build sudo systemctl restart abelbirdnest-web -sudo systemctl status abelbirdnest-web ``` -## 15. Smoke Test Setelah Live +Jika branch utama nanti bukan `main`, sesuaikan perintah `git pull`. -Cek minimal: +## 12. Checklist Go-Live -- login dengan user `SYSTEM_ADMIN` -- dashboard terbuka -- health check OK -- buat master `bank`, `currency`, `grade` tampil -- buat transaksi sederhana -- logout/login ulang -- email reset/verifikasi jika SMTP sudah aktif +- `AUTH_BOOTSTRAP=false` +- `AUTH_SECRET` sudah production-grade +- `APP_URL=https://abelbirdnest.id` +- SSL aktif +- database backup aktif +- `npm run build` lulus +- `npm run prisma:migrate:deploy` lulus +- `npm run seed:master` selesai jika dibutuhkan +- login, reset password, dan email verifikasi sudah dites +- create purchase, receipt, lot, sale sudah dites + +## 13. Catatan Penting + +- Jangan pakai `npm run db:push` untuk production. +- Jangan pakai akun default development. +- Jangan simpan `.env.production` di repo. +- Pastikan ownership file tetap `abelbirdnest:abelbirdnest`. diff --git a/docs/manual-assets/dashboard-lot-perhatian.png b/docs/manual-assets/dashboard-lot-perhatian.png new file mode 100644 index 0000000..7137d5a Binary files /dev/null and b/docs/manual-assets/dashboard-lot-perhatian.png differ diff --git a/docs/manual-assets/login.png b/docs/manual-assets/login.png new file mode 100644 index 0000000..ae12e9e Binary files /dev/null and b/docs/manual-assets/login.png differ diff --git a/docs/manual-assets/purchase-analysis.png b/docs/manual-assets/purchase-analysis.png new file mode 100644 index 0000000..931b3e8 Binary files /dev/null and b/docs/manual-assets/purchase-analysis.png differ diff --git a/docs/manual-assets/sidebar-menu.png b/docs/manual-assets/sidebar-menu.png new file mode 100644 index 0000000..bf68f39 Binary files /dev/null and b/docs/manual-assets/sidebar-menu.png differ diff --git a/docs/manual-dashboard-abelbirdnest.docx b/docs/manual-dashboard-abelbirdnest.docx new file mode 100644 index 0000000..bad624a Binary files /dev/null and b/docs/manual-dashboard-abelbirdnest.docx differ diff --git a/docs/manual-operasional-abelbirdnest-formal-screenshot.docx b/docs/manual-operasional-abelbirdnest-formal-screenshot.docx new file mode 100644 index 0000000..c3440ef Binary files /dev/null and b/docs/manual-operasional-abelbirdnest-formal-screenshot.docx differ diff --git a/docs/manual-operasional-abelbirdnest-lengkap.docx b/docs/manual-operasional-abelbirdnest-lengkap.docx new file mode 100644 index 0000000..d219550 Binary files /dev/null and b/docs/manual-operasional-abelbirdnest-lengkap.docx differ diff --git a/docs/mobile-api-blueprint.md b/docs/mobile-api-blueprint.md index 265fe5d..dbdc856 100644 --- a/docs/mobile-api-blueprint.md +++ b/docs/mobile-api-blueprint.md @@ -46,8 +46,7 @@ Semua role mobile memakai: Tujuan: - scan lot -- buat receipt -- generate lot +- submit purchase yang otomatis membuat receipt dan lot - buat penyesuaian stok - kirim / selesaikan washing @@ -57,11 +56,9 @@ Endpoint: - `GET /api/v1/mobile/lots` - `GET /api/v1/mobile/lots/:id` - `GET /api/v1/mobile/lots/scan?code=...` -- `GET /api/v1/mobile/receipts/bootstrap` -- `GET /api/v1/mobile/receipts` -- `POST /api/v1/mobile/receipts` -- `GET /api/v1/mobile/receipts/:id` -- `POST /api/v1/mobile/receipts/:id/generate-lots` +- `GET /api/v1/mobile/purchases` +- `GET /api/v1/mobile/purchases/:id` +- `POST /api/v1/mobile/purchases/:id/submit` - `GET /api/v1/mobile/stock-adjustments/bootstrap` - `GET /api/v1/mobile/stock-adjustments` - `POST /api/v1/mobile/stock-adjustments` @@ -185,8 +182,8 @@ Layar minimum: 2. Dashboard ringkas 3. Scan lot 4. Detail lot -5. Receipt baru -6. Detail receipt + generate lot +5. Daftar pembelian siap submit +6. Submit pembelian yang otomatis membuat receipt + lot 7. Penyesuaian stok 8. Daftar washing 9. Buat washing diff --git a/src/app/api/v1/mobile/bootstrap/route.ts b/src/app/api/v1/mobile/bootstrap/route.ts index 4eda477..8a7d1c3 100644 --- a/src/app/api/v1/mobile/bootstrap/route.ts +++ b/src/app/api/v1/mobile/bootstrap/route.ts @@ -11,7 +11,6 @@ export async function GET(request: Request) { OWNER: [ "dashboard", "lots", - "receipts", "washing", "stock_adjustments", "lot_transformations", @@ -33,7 +32,6 @@ export async function GET(request: Request) { WAREHOUSE: [ "dashboard", "lots", - "receipts", "washing", "stock_adjustments" ], @@ -54,7 +52,6 @@ export async function GET(request: Request) { ADMIN: [ "dashboard", "lots", - "receipts", "washing", "stock_adjustments", "lot_transformations", @@ -69,7 +66,6 @@ export async function GET(request: Request) { SYSTEM_ADMIN: [ "dashboard", "lots", - "receipts", "washing", "stock_adjustments", "lot_transformations", diff --git a/src/app/api/v1/purchases/[id]/submit/route.ts b/src/app/api/v1/purchases/[id]/submit/route.ts index 00ccd52..5aa4e80 100644 --- a/src/app/api/v1/purchases/[id]/submit/route.ts +++ b/src/app/api/v1/purchases/[id]/submit/route.ts @@ -7,6 +7,7 @@ import { requireApiAccess } from "@/lib/authorization"; import { prisma } from "@/lib/prisma"; import { recalculatePurchaseRealizationSummary } from "@/features/purchase-realization/lib/recalculate-purchase-realization-summary"; import { generateLotCode } from "@/features/receipts/lib/generate-lot-code"; +import { generateReceiptNo } from "@/features/receipts/lib/generate-receipt-no"; type RouteContext = { params: Promise<{ id: string }> }; type SubmitTx = Prisma.TransactionClient & { @@ -36,6 +37,12 @@ export async function POST(request: Request, context: RouteContext) { include: { agent: { select: { id: true, name: true } }, profitShareScheme: { select: { id: true, shareAgent: true } }, + receipts: { + select: { + id: true, + receiptNo: true + } + }, lines: { include: { grade: true, @@ -52,6 +59,13 @@ export async function POST(request: Request, context: RouteContext) { return NextResponse.json({ message: "Purchase sudah disubmit" }, { status: 409 }); } + if (purchase.receipts.length > 0) { + return NextResponse.json( + { message: `Purchase sudah memiliki receipt ${purchase.receipts[0]?.receiptNo ?? ""}`.trim() }, + { status: 409 } + ); + } + if (!purchase.receivedByEmployeeId) { return NextResponse.json({ message: "Penerima belum dipilih" }, { status: 400 }); } @@ -61,6 +75,8 @@ export async function POST(request: Request, context: RouteContext) { } const receivedAt = purchase.receivedAt; + const receiptDate = new Date(receivedAt.toISOString().slice(0, 10)); + const receiptNo = await generateReceiptNo(receiptDate); const sourceCode = purchase.agent?.name || purchase.purchaseNo; const enteredQtyLines = purchase.lines.filter((line) => Number(line.qtyAccepted.toNumber()) > 0); if (enteredQtyLines.length === 0) { @@ -92,8 +108,45 @@ export async function POST(request: Request, context: RouteContext) { throw new Error("Lot untuk purchase ini sudah pernah digenerate"); } + const existingReceiptCount = await tx.receipt.count({ where: { purchaseId } }); + if (existingReceiptCount > 0) { + throw new Error("Receipt untuk purchase ini sudah pernah dibuat"); + } + + const receipt = await tx.receipt.create({ + data: { + receiptNo, + purchaseId: purchase.id, + receiptDate, + status: "FINALIZED", + notes: purchase.notes || null, + receivedById: BigInt(auth.user.id) + } + }); + + await tx.receiptLine.createMany({ + data: enteredQtyLines.map((line) => ({ + receiptId: receipt.id, + purchaseLineId: line.id, + gradeId: line.gradeId, + qtyReceived: line.qtyReceived, + qtyAccepted: line.qtyAccepted, + qtyRejected: line.qtyRejected, + unitId: line.unitId, + unitCost: line.unitCost, + warehouseId: line.warehouseId!, + warehouseLocationId: line.warehouseLocationId, + notes: line.notes || null + })) + }); + + const receiptLines = await tx.receiptLine.findMany({ + where: { receiptId: receipt.id }, + orderBy: { id: "asc" } + }); + const lots = []; - for (const line of enteredQtyLines) { + for (const line of receiptLines) { if (!line.warehouseId) { throw new Error("Warehouse belum diisi pada salah satu baris pembelian"); } @@ -107,7 +160,9 @@ export async function POST(request: Request, context: RouteContext) { sourceType: "PURCHASE", sourceRefId: purchaseId, purchaseId: purchase.id, - purchaseLineId: line.id, + purchaseLineId: line.purchaseLineId, + receiptId: receipt.id, + receiptLineId: line.id, gradeId: line.gradeId, warehouseId: line.warehouseId, warehouseLocationId: line.warehouseLocationId, @@ -158,7 +213,7 @@ export async function POST(request: Request, context: RouteContext) { amountProfit: new Prisma.Decimal(0), agentSharePercentSnapshot: purchase.profitShareScheme?.shareAgent ?? null, agentAmount: new Prisma.Decimal(0), - notes: `Opening realization dari purchase line ${line.id.toString()}` + notes: `Opening realization dari purchase line ${line.purchaseLineId.toString()}` } }); @@ -181,6 +236,10 @@ export async function POST(request: Request, context: RouteContext) { return { purchase: updated, + receipt: { + id: receipt.id.toString(), + receipt_no: receipt.receiptNo + }, lots }; }); @@ -193,10 +252,14 @@ export async function POST(request: Request, context: RouteContext) { method: request.method, pathname: new URL(request.url).pathname, statusCode: 200, - summary: `Purchase ${purchase.purchaseNo} disubmit + lot dibuat`, + summary: `Purchase ${purchase.purchaseNo} disubmit + receipt + lot dibuat`, metadata: buildAuditChangeMetadata( - { status: purchase.status, lot_count: 0 }, - { status: generated.purchase.status, lot_count: generated.lots.length } + { status: purchase.status, lot_count: 0, receipt_no: null }, + { + status: generated.purchase.status, + lot_count: generated.lots.length, + receipt_no: generated.receipt.receipt_no + } ) }); @@ -208,7 +271,8 @@ export async function POST(request: Request, context: RouteContext) { { success: true, status: generated.purchase.status, - lot_count: generated.lots.length + lot_count: generated.lots.length, + receipt: generated.receipt }, { status: 200 } ); diff --git a/src/app/sorting/page.tsx b/src/app/sorting/page.tsx index 5dee64b..58cd857 100644 --- a/src/app/sorting/page.tsx +++ b/src/app/sorting/page.tsx @@ -1,14 +1,17 @@ import { AppShell } from "@/components/layout/app-shell"; import { LotMixingClient } from "@/features/lot-transformations/components/lot-mixing-client"; +import { getAppSettings } from "@/lib/app-settings"; export default async function SortingPage() { + const settings = await getAppSettings(); + return ( - + ); } diff --git a/src/app/washing/page.tsx b/src/app/washing/page.tsx index 43a5332..8289feb 100644 --- a/src/app/washing/page.tsx +++ b/src/app/washing/page.tsx @@ -1,14 +1,17 @@ import { AppShell } from "@/components/layout/app-shell"; import { WashingClient } from "@/features/washing/components/washing-client"; +import { getAppSettings } from "@/lib/app-settings"; export default async function WashingPage() { + const settings = await getAppSettings(); + return ( - + ); } diff --git a/src/features/lot-transformations/components/lot-mixing-client.tsx b/src/features/lot-transformations/components/lot-mixing-client.tsx index b0ef969..038bc77 100644 --- a/src/features/lot-transformations/components/lot-mixing-client.tsx +++ b/src/features/lot-transformations/components/lot-mixing-client.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from "react"; import { useLocale } from "@/components/providers/locale-provider"; import { SearchableSelectField } from "@/components/shared/searchable-select-field"; +import { formatCurrencyAmount, formatDecimal, formatKilogram } from "@/lib/formatters"; import type { ApiErrorResponse, DetailResponse } from "@/types/api"; import type { GradeRecord, @@ -65,7 +66,7 @@ const createEmptyForm = (): TransformationForm => ({ outputs: [createEmptyOutput()] }); -export function LotMixingClient() { +export function LotMixingClient({ currencyCode }: { currencyCode: string }) { const { locale } = useLocale(); const [lots, setLots] = useState([]); const [transformations, setTransformations] = useState([]); @@ -478,8 +479,8 @@ export function LotMixingClient() {

{locale === "id" ? "Penanganan sisa regrade" : "Regrade remainder handling"}

{locale === "id" - ? <>Masih ada sisa {unusedSourceRemainderQty.toFixed(3)} kg dari lot sumber yang tidak dipakai untuk regrade. Pilih apakah sisa tetap berada di grade asal atau dicatat sebagai susut. - : <>There is still {unusedSourceRemainderQty.toFixed(3)} kg remaining from the source lot that was not used for regrade. Choose whether the remainder stays in the original grade or is recorded as shrinkage.} + ? <>Masih ada sisa {formatKilogram(unusedSourceRemainderQty, locale)} dari lot sumber yang tidak dipakai untuk regrade. Pilih apakah sisa tetap berada di grade asal atau dicatat sebagai susut. + : <>There is still {formatKilogram(unusedSourceRemainderQty, locale)} remaining from the source lot that was not used for regrade. Choose whether the remainder stays in the original grade or is recorded as shrinkage.}

@@ -521,8 +522,8 @@ export function LotMixingClient() {
{locale === "id" ? "Catat sebagai susut" : "Record as shrinkage"}
{locale === "id" - ? `Sisa ${unusedSourceRemainderQty.toFixed(3)} kg akan ditambahkan ke shrinkage lot sumber.` - : `The remaining ${unusedSourceRemainderQty.toFixed(3)} kg will be added to source-lot shrinkage.`} + ? `Sisa ${formatKilogram(unusedSourceRemainderQty, locale)} akan ditambahkan ke shrinkage lot sumber.` + : `The remaining ${formatKilogram(unusedSourceRemainderQty, locale)} will be added to source-lot shrinkage.`}
@@ -615,8 +616,8 @@ export function LotMixingClient() {

{locale === "id" ? "Penanganan selisih hasil ubah grade" : "Regrade output difference handling"}

{locale === "id" - ? <>Total output masih kurang {outputLossQty.toFixed(3)} kg dari qty yang dipakai untuk regrade. Anda tidak bisa menyimpan sebelum selisih ini ditangani. - : <>Total output is still short by {outputLossQty.toFixed(3)} kg compared with the qty used for regrade. You cannot save before this difference is handled.} + ? <>Total output masih kurang {formatKilogram(outputLossQty, locale)} dari qty yang dipakai untuk regrade. Anda tidak bisa menyimpan sebelum selisih ini ditangani. + : <>Total output is still short by {formatKilogram(outputLossQty, locale)} compared with the qty used for regrade. You cannot save before this difference is handled.}