# PRD - Dashboard Manajemen Gudang Kaca Mobil Multi-Cabang

## 1. Overview
**Background**: Bisnis distribusi dan penyimpanan kaca mobil (windshield, side glass, rear glass, dan aksesori terkait) memiliki kompleksitas inventori yang tinggi karena setiap produk bersifat spesifik terhadap merek, model, dan tahun kendaraan. Dengan rencana ekspansi hingga 10 cabang gudang, pengelolaan stok secara manual atau dengan spreadsheet tidak lagi efisien dan berpotensi menimbulkan kesalahan data yang berdampak langsung pada operasional bisnis.

**Problem Statement**: Saat ini tidak ada sistem terpusat yang memungkinkan owner untuk memantau stok barang di seluruh cabang secara real-time. Karyawan di masing-masing cabang mengelola inventori secara terpisah sehingga terjadi silo data, duplikasi pencatatan, ketidaksesuaian stok fisik vs digital, kesulitan transfer stok antar cabang, dan owner tidak dapat membuat keputusan bisnis berbasis data yang akurat.

**Goals**:
- Menyediakan satu platform terpusat untuk manajemen stok kaca mobil di seluruh cabang (hingga 10 cabang)
- Memberikan visibilitas real-time kepada owner tentang kondisi stok, pergerakan barang, dan laporan di setiap cabang
- Mempercepat proses pencatatan stok masuk, stok keluar, transfer antar cabang, dan penyesuaian stok oleh karyawan
- Mengimplementasikan RBAC (Role-Based Access Control) dengan 2 role: Owner (akses penuh lintas cabang) dan Karyawan (akses terbatas per cabang)
- Menyediakan laporan dan analitik yang actionable untuk mendukung keputusan bisnis owner
- Membangun fondasi arsitektur yang scalable, clean, dan mudah di-maintain oleh tim developer

**Recommended Names**: GlassHub — Warehouse Management System, KacaTrack — Multi-Branch Inventory Dashboard, AutoGlass IMS (Inventory Management System), ClearStock — Dashboard Gudang Kaca Otomotif, GlassWMS — Warehouse & Stock Control

## 2. Requirements
### Functional Requirements
- **FR-01**: Sistem autentikasi dan manajemen akun pengguna dengan role-based access control (RBAC). Owner dapat melihat dan mengelola data seluruh cabang, sedangkan Karyawan hanya dapat mengakses data cabang yang ditugaskan.
  - [ ] Owner dapat login dan mendapatkan akses ke semua fitur lintas cabang tanpa batasan
  - [ ] Karyawan yang login hanya dapat melihat data (stok, transaksi, laporan) milik cabang yang terasosiasi dengan akun mereka
  - [ ] Sistem menggunakan Laravel Sanctum untuk API token authentication
  - [ ] Password di-hash menggunakan bcrypt dan tidak pernah disimpan plaintext
  - [ ] Sesi login expired setelah 8 jam tidak aktif
  - [ ] Owner dapat membuat, menonaktifkan, dan mereset password akun karyawan
- **FR-02**: Manajemen cabang: Owner dapat mendaftarkan, mengedit, mengaktifkan/menonaktifkan cabang gudang. Setiap cabang memiliki informasi lengkap dan terhubung dengan karyawan serta stok masing-masing.
  - [ ] Owner dapat membuat cabang baru dengan mengisi: nama cabang, kode cabang (unik), alamat, kota, nomor telepon, dan nama penanggung jawab
  - [ ] Cabang yang dinonaktifkan tidak dapat menerima transaksi baru namun data historisnya tetap tersimpan
  - [ ] Daftar cabang ditampilkan beserta ringkasan jumlah karyawan aktif dan total SKU yang tersimpan
  - [ ] Owner dapat mengassign dan unassign karyawan ke cabang tertentu
- **FR-03**: Manajemen produk (master data): Pengelolaan katalog produk kaca mobil yang spesifik terhadap merek kendaraan, model, dan tahun produksi. Produk bersifat global (tidak per cabang) namun stoknya per cabang.
  - [ ] Setiap produk memiliki: SKU (auto-generated dan unique), nama produk, kategori, merek kendaraan, model kendaraan, rentang tahun kendaraan, tipe kaca (depan/belakang/samping kiri/samping kanan/quarter), merek produk, satuan, deskripsi, dan foto
  - [ ] Sistem mendukung filter dan search produk berdasarkan merek kendaraan, model, tahun, tipe kaca, dan kategori
  - [ ] Owner dan karyawan yang diberi izin dapat menambah produk baru; penghapusan produk menggunakan soft delete
  - [ ] Produk dengan stok aktif tidak dapat di-harddelete
- **FR-04**: Manajemen stok per cabang: Setiap cabang memiliki record stok sendiri untuk setiap produk. Sistem mencatat stok minimum dan maksimum sebagai batas peringatan.
  - [ ] Sistem menampilkan stok setiap produk per cabang secara real-time
  - [ ] Owner dapat melihat stok aggregat (total lintas cabang) dan stok per cabang dalam satu tampilan
  - [ ] Sistem memberikan notifikasi/indikator visual (badge merah) ketika stok suatu produk di cabang tertentu berada di bawah minimum stock level
  - [ ] Karyawan hanya dapat melihat stok cabang mereka sendiri
- **FR-05**: Pencatatan stok masuk (inbound): Karyawan dapat mencatat penerimaan barang dari supplier melalui mekanisme Purchase Order atau penerimaan langsung.
  - [ ] Karyawan dapat membuat Purchase Order dengan memilih supplier, menambahkan item produk beserta jumlah dan harga beli
  - [ ] PO memiliki alur status: Draft → Approved (oleh Owner atau karyawan yang diberi izin) → Received → Cancelled
  - [ ] Ketika PO berstatus Received, stok cabang otomatis bertambah sejumlah quantity_received
  - [ ] Sistem mencatat selisih antara quantity_ordered dan quantity_received (partial receive diperbolehkan)
  - [ ] Setiap transaksi masuk dicatat dalam tabel stock_transactions dengan referensi ke PO
- **FR-06**: Pencatatan stok keluar (outbound/penjualan): Karyawan mencatat penjualan atau pengeluaran barang dari gudang ke pelanggan.
  - [ ] Karyawan dapat membuat transaksi penjualan dengan memilih produk, memasukkan jumlah, harga jual, dan informasi pelanggan (opsional)
  - [ ] Sistem memvalidasi bahwa jumlah barang yang dijual tidak melebihi stok tersedia; jika tidak cukup, transaksi ditolak dengan pesan error yang jelas
  - [ ] Stok cabang otomatis berkurang setelah transaksi penjualan dikonfirmasi
  - [ ] Nomor transaksi di-generate otomatis dengan format: TRX-{KODE_CABANG}-{YYYYMMDD}-{SEQUENCE}
- **FR-07**: Transfer stok antar cabang: Karyawan atau Owner dapat mengajukan transfer stok dari satu cabang ke cabang lain dengan alur persetujuan.
  - [ ] Karyawan dari cabang asal dapat membuat permintaan transfer dengan memilih: cabang tujuan, produk, dan jumlah
  - [ ] Transfer memiliki alur status: Pending → Approved → Shipped → Received → Cancelled
  - [ ] Stok cabang asal berkurang saat status berubah ke Shipped; stok cabang tujuan bertambah saat status berubah ke Received
  - [ ] Owner dapat melihat semua transfer antar cabang dalam satu dashboard; karyawan hanya melihat transfer yang melibatkan cabang mereka
  - [ ] Sistem mencatat quantity_sent dan quantity_received secara terpisah untuk mendeteksi selisih pengiriman
- **FR-08**: Penyesuaian stok (stock adjustment/opname): Fasilitas untuk menyesuaikan stok digital dengan stok fisik hasil opname gudang.
  - [ ] Karyawan dapat membuat stock adjustment dengan mencatat quantity_before (dari sistem) dan quantity_after (hasil hitung fisik) beserta alasan penyesuaian
  - [ ] Stock adjustment memerlukan persetujuan Owner sebelum stok resmi diubah di sistem
  - [ ] Sistem mencatat selisih (difference = quantity_after - quantity_before) dan alasan untuk audit trail
  - [ ] Owner dapat melihat riwayat semua stock adjustment per cabang beserta siapa yang membuat dan yang menyetujui
- **FR-09**: Dashboard dan laporan: Owner mendapatkan dashboard eksekutif lintas cabang; karyawan mendapatkan dashboard operasional cabang masing-masing.
  - [ ] Dashboard Owner menampilkan: total nilai stok seluruh cabang, cabang dengan stok terendah, produk dengan pergerakan tertinggi, grafik transaksi 30 hari terakhir, dan alert stok menipis per cabang
  - [ ] Dashboard Karyawan menampilkan: ringkasan stok cabang, transaksi hari ini, PO pending, dan transfer pending
  - [ ] Owner dapat mengekspor laporan (stok per cabang, mutasi stok, laporan penjualan) dalam format PDF dan Excel
  - [ ] Laporan dapat difilter berdasarkan rentang tanggal, cabang, kategori produk, dan jenis transaksi
- **FR-10**: Audit log: Semua aksi kritis dalam sistem dicatat secara otomatis untuk keperluan keamanan dan akuntabilitas.
  - [ ] Sistem mencatat semua aksi: login/logout, pembuatan/perubahan/penghapusan produk, perubahan stok, persetujuan PO/transfer/adjustment, dan perubahan data user
  - [ ] Audit log menyimpan: user_id, aksi, tabel yang terpengaruh, ID record, nilai lama (old_values), nilai baru (new_values), IP address, dan timestamp
  - [ ] Owner dapat mengakses audit log dan memfilternya berdasarkan user, aksi, cabang, dan rentang waktu
  - [ ] Audit log bersifat immutable — tidak dapat diubah atau dihapus oleh siapapun termasuk owner

### Non-Functional Requirements
- **NFR-01**: Performance: API response time harus cepat untuk pengalaman pengguna yang optimal bahkan saat diakses bersamaan dari 10 cabang. (Metric: 95% API response time < 300ms untuk operasi baca (GET); < 500ms untuk operasi tulis (POST/PUT/PATCH); < 2 detik untuk generate laporan kompleks. Diukur dengan p95 latency menggunakan tools seperti Laravel Telescope atau Datadog.)
- **NFR-02**: Skalabilitas: Sistem harus mampu menangani beban dari 10 cabang dengan puluhan pengguna konkuren tanpa degradasi performa. (Metric: Sistem mampu menangani minimal 50 concurrent users tanpa response time melebihi 1 detik. Database query harus menggunakan index yang tepat sehingga query time < 100ms untuk tabel dengan 1 juta+ records. Gunakan database cache driver (CACHE_DRIVER=database) untuk caching data yang sering diakses seperti stok summary dan laporan harian; jalankan php artisan cache:table saat setup awal.)
- **NFR-03**: Keamanan: Sistem harus memenuhi standar keamanan aplikasi web modern untuk melindungi data bisnis sensitif. (Metric: Implementasi: HTTPS enforced (HSTS), Laravel Sanctum token-based auth, rate limiting (60 req/menit per user), input sanitization via Form Request, SQL injection prevention via Eloquent ORM, XSS protection, CSRF protection, password minimum 8 karakter dengan kombinasi huruf dan angka. Audit log untuk semua aksi kritis.)
- **NFR-04**: Ketersediaan dan reliabilitas: Sistem harus tersedia saat jam operasional bisnis. (Metric: Uptime minimal 99.5% selama jam kerja (06:00–22:00 WIB). Implementasikan health check endpoint. Database backup otomatis setiap 24 jam via fitur cPanel Backup. Gunakan database transaction untuk semua operasi yang mengubah multiple tabel secara atomik.)
- **NFR-05**: Maintainability dan Developer Experience: Kode harus terstruktur sehingga developer baru dapat memahami flow aplikasi dalam waktu singkat. (Metric: Code coverage unit test minimal 70% untuk layer Service dan Repository. Semua public method di Service dan Repository memiliki docblock. Kepatuhan terhadap PSR-12 coding standard. Tidak ada business logic di Controller atau Model.)

## 3. Core Features
- **P0**: Multi-Branch Authentication & RBAC — Sistem login berbasis token (Laravel Sanctum) dengan dua role: Owner (akses global lintas cabang) dan Karyawan (akses terbatas ke cabang yang ditugaskan). Middleware memfilter akses data berdasarkan role dan cabang secara otomatis.
- **P0**: Master Data Produk Kaca Mobil — Katalog produk global dengan atribut spesifik otomotif: merek kendaraan, model, rentang tahun, tipe kaca, dan merek produk. Dilengkapi SKU auto-generation, soft delete, dan pencarian multi-filter.
- **P0**: Real-time Stock Visibility per Cabang — Tampilan stok setiap produk per cabang secara real-time. Owner melihat stok aggregat dan per-cabang; karyawan hanya melihat cabang mereka. Badge alert untuk stok di bawah minimum.
- **P0**: Pencatatan Stok Masuk (Purchase Order) — Alur kerja PO lengkap: Draft → Approved → Received. Mendukung partial receive, pencatatan harga beli, dan auto-update stok setelah PO diterima.
- **P0**: Pencatatan Stok Keluar (Penjualan) — Pencatatan transaksi penjualan atau pengeluaran barang. Validasi stok real-time sebelum konfirmasi, auto-decrement stok, dan nomor transaksi auto-generated.
- **P0**: Transfer Stok Antar Cabang — Mekanisme pengiriman stok dari gudang satu ke gudang lain dengan alur status dan persetujuan. Stok dikurangi di cabang asal dan ditambahkan di cabang tujuan secara atomik.
- **P0**: Stock Opname & Adjustment — Fasilitas penyesuaian stok fisik vs digital dengan mekanisme approval. Setiap adjustment dicatat lengkap untuk audit trail.
- **P0**: Dashboard Eksekutif Owner — Dashboard dengan KPI lintas cabang: total stok, nilai inventori, pergerakan barang teratas, grafik tren transaksi, dan alert stok menipis di semua cabang.
- **P1**: Laporan & Ekspor Data — Laporan mutasi stok, laporan penjualan, dan laporan stok per cabang dengan filter tanggal dan kategori. Ekspor ke PDF dan Excel.
- **P1**: Manajemen Supplier — Master data supplier untuk digunakan dalam Purchase Order. Berisi informasi kontak, alamat, dan riwayat transaksi.
- **P1**: Notifikasi In-App — Notifikasi untuk event penting: stok menipis, PO menunggu approval, transfer menunggu konfirmasi, stock adjustment menunggu persetujuan.
- **P2**: Audit Log Viewer — Halaman untuk Owner melihat riwayat semua aksi di sistem dengan filter berdasarkan user, cabang, aksi, dan rentang waktu.
- **P2**: Barcode / QR Code Produk — Generate dan print barcode/QR untuk setiap produk. Karyawan dapat scan untuk mempercepat input transaksi.

## 4. User Flow
### Login dan Navigasi Awal
**Steps**:
- Pengguna membuka aplikasi dan diarahkan ke halaman login
- Pengguna mengisi email dan password lalu submit
- Sistem memvalidasi kredensial melalui AuthController → AuthService → UserRepository
- Jika valid, sistem menerbitkan Sanctum token dan mengembalikan data user + role + branch_id
- Frontend menyimpan token di memory/localStorage dan menyimpan user context ke state management
- Sistem menentukan dashboard yang ditampilkan berdasarkan role: Owner diarahkan ke /dashboard/owner, Karyawan diarahkan ke /dashboard/branch/{branch_id}
- Navigasi sidebar ditampilkan sesuai role dan permission

**Edge Cases**:
- Password salah: response 401 dengan pesan 'Email atau password salah', tanpa menyebutkan field mana yang salah (security)
- Akun tidak aktif (is_active = false): response 403 dengan pesan 'Akun Anda dinonaktifkan, hubungi administrator'
- Rate limit tercapai (>5 percobaan login gagal dalam 1 menit): response 429 dengan pesan 'Terlalu banyak percobaan login, coba lagi dalam X detik'
- Token expired: API mengembalikan 401, frontend mengarahkan pengguna kembali ke halaman login dan menghapus token lokal
- Karyawan yang cabangnya dinonaktifkan: ditampilkan halaman informasi bahwa cabang sedang tidak aktif

### Pencatatan Stok Masuk via Purchase Order
**Steps**:
- Karyawan navigasi ke menu Purchase Order → klik 'Buat PO Baru'
- Karyawan memilih supplier dari dropdown (data dari tabel suppliers)
- Karyawan menambahkan item: search produk by SKU atau nama, masukkan quantity_ordered dan unit_price
- Karyawan dapat menambah multiple item sebelum submit
- Karyawan submit PO; sistem menyimpan dengan status 'draft' dan men-generate nomor PO: PO-{KODE_CABANG}-{YYYYMMDD}-{SEQUENCE}
- PO muncul di daftar menunggu approval (jika karyawan tidak memiliki izin approve)
- Owner atau karyawan senior membuka PO dan mengklik 'Approve'; status berubah ke 'approved'
- Saat barang tiba, karyawan membuka PO yang approved dan klik 'Terima Barang'
- Karyawan mengisi quantity_received untuk setiap item (boleh berbeda dari quantity_ordered)
- Karyawan mengkonfirmasi penerimaan; sistem: (1) mengubah status PO ke 'received', (2) menambah stok cabang di tabel stocks, (3) mencatat di tabel stock_transactions dengan type='in'
- Sistem membuat audit log untuk seluruh proses

**Edge Cases**:
- Supplier tidak ada di master data: karyawan diarahkan untuk menambah supplier baru terlebih dahulu, atau admin menambahkannya
- Produk tidak ada di katalog: karyawan tidak dapat memproses PO untuk produk yang tidak terdaftar; harus tambah produk dulu
- PO di-cancel setelah approved: stok tidak berubah karena barang belum diterima; status berubah ke 'cancelled' dengan catatan alasan
- Partial receive (quantity_received < quantity_ordered): sistem mencatat selisih dan PO tetap bisa di-close manual oleh owner
- Koneksi database terputus saat konfirmasi penerimaan: transaksi di-rollback, stok tidak berubah, pengguna diberikan pesan error untuk retry
- Karyawan mencoba approve PO milik cabang lain: sistem menolak dengan 403 Forbidden

### Transfer Stok Antar Cabang
**Steps**:
- Karyawan atau Owner navigasi ke menu Transfer Stok → klik 'Buat Transfer Baru'
- Pengguna memilih cabang tujuan (tidak bisa pilih cabang sendiri sebagai tujuan)
- Pengguna memilih produk dan mengisi jumlah yang akan ditransfer
- Sistem memvalidasi bahwa stok tersedia di cabang asal ≥ jumlah transfer
- Pengguna submit; sistem menyimpan transfer dengan status 'pending' dan men-generate nomor transfer: TRF-{CABANG_ASAL}-{CABANG_TUJUAN}-{YYYYMMDD}-{SEQ}
- Notifikasi dikirim ke Owner (dan karyawan cabang tujuan jika ada setting notifikasi)
- Owner membuka permintaan transfer dan mengklik 'Approve'; status berubah ke 'approved'
- Karyawan cabang asal mengkonfirmasi barang dikirim ('Mark as Shipped'); stok cabang asal berkurang di tabel stocks; status berubah ke 'shipped'
- Karyawan cabang tujuan mengkonfirmasi penerimaan dan mengisi quantity_received; stok cabang tujuan bertambah; status berubah ke 'received'
- Seluruh proses dicatat di stock_transactions untuk kedua cabang

**Edge Cases**:
- Stok tidak cukup saat submit transfer: validasi gagal dengan pesan 'Stok tidak mencukupi. Stok tersedia: X, diminta: Y'
- Transfer di-cancel setelah shipped: butuh proses reversal manual oleh owner; sistem tidak otomatis mengembalikan stok untuk mencegah abuse
- Selisih antara quantity_sent dan quantity_received: sistem mencatat selisih di kolom notes; owner dapat membuat stock adjustment di cabang tujuan
- Cabang tujuan dinonaktifkan saat transfer sedang berjalan: status transfer dibekukan; owner harus intervensi manual
- Karyawan cabang A mencoba melihat transfer cabang B: sistem mengembalikan 403 Forbidden

### Owner Melihat Dashboard Eksekutif
**Steps**:
- Owner login dan diarahkan ke /dashboard/owner
- Sistem memanggil DashboardService yang mengaggregasi data dari semua cabang menggunakan query yang dioptimasi
- Dashboard menampilkan KPI cards: total produk aktif, total nilai stok seluruh cabang, jumlah transaksi hari ini, jumlah PO pending approval
- Grafik batang perbandingan stok per cabang untuk top 10 produk
- Tabel alert: produk dengan stok di bawah minimum, dikelompokkan per cabang
- Grafik garis tren transaksi (masuk vs keluar) 30 hari terakhir
- Owner dapat klik cabang tertentu untuk drill-down ke data cabang tersebut
- Data di-cache menggunakan database cache driver dengan TTL 5 menit untuk performa optimal

**Edge Cases**:
- Tidak ada data transaksi (sistem baru): dashboard menampilkan state kosong yang informatif dengan panduan memulai
- Salah satu cabang offline/tidak ada data terbaru: dashboard tetap menampilkan data cabang lain dan menandai cabang yang bermasalah
- Cache tidak tersedia: sistem gracefully fallback ke query database langsung dengan indikator loading yang sedikit lebih lama
- Data sangat besar (jutaan transaksi): query menggunakan index dan agregasi di database level, bukan di aplikasi level

## 5. Database Schema
### branches
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `name` (VARCHAR(100) NOT NULL — Nama cabang, contoh: 'Gudang Jakarta Timur')
- `code` (VARCHAR(10) NOT NULL UNIQUE — Kode pendek unik, contoh: 'JKT-01', digunakan dalam format nomor transaksi)
- `address` (TEXT NOT NULL — Alamat lengkap gudang)
- `city` (VARCHAR(100) NOT NULL)
- `phone` (VARCHAR(20) NULLABLE)
- `pic_name` (VARCHAR(100) NULLABLE — Nama Person In Charge / penanggung jawab cabang)
- `is_active` (BOOLEAN NOT NULL DEFAULT TRUE — Cabang yang dinonaktifkan tidak menerima transaksi baru)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- HAS MANY users (satu cabang memiliki banyak karyawan yang terasosiasi)
- HAS MANY stocks (setiap cabang punya record stok sendiri per produk)
- HAS MANY purchase_orders (PO selalu terkait dengan cabang penerima)
- HAS MANY sales_transactions
- HAS MANY stock_transactions
- HAS MANY (as from_branch) stock_transfers
- HAS MANY (as to_branch) stock_transfers
- HAS MANY stock_adjustments
- INDEX: code (UNIQUE), is_active

### users
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `branch_id` (BIGINT UNSIGNED NULLABLE FK→branches.id — NULL berarti Owner (akses semua cabang); NOT NULL berarti Karyawan cabang tertentu)
- `name` (VARCHAR(100) NOT NULL)
- `email` (VARCHAR(191) NOT NULL UNIQUE)
- `password` (VARCHAR(255) NOT NULL — Bcrypt hash)
- `role` (ENUM('owner', 'karyawan') NOT NULL DEFAULT 'karyawan')
- `is_active` (BOOLEAN NOT NULL DEFAULT TRUE)
- `last_login_at` (TIMESTAMP NULL — Untuk monitoring aktivitas user)
- `email_verified_at` (TIMESTAMP NULL)
- `remember_token` (VARCHAR(100) NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO branches (nullable — owner tidak terikat cabang)
- HAS MANY personal_access_tokens (via Laravel Sanctum)
- HAS MANY purchase_orders (sebagai pembuat/approver)
- HAS MANY sales_transactions (sebagai kasir/petugas)
- HAS MANY stock_transfers (sebagai requester/approver)
- HAS MANY stock_adjustments (sebagai pembuat/approver)
- HAS MANY audit_logs
- INDEX: email (UNIQUE), branch_id, role, is_active

### product_categories
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `name` (VARCHAR(100) NOT NULL UNIQUE — Contoh: 'Kaca Depan', 'Kaca Belakang', 'Kaca Samping', 'Aksesoris Kaca')
- `description` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- HAS MANY products

### products
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `category_id` (BIGINT UNSIGNED NOT NULL FK→product_categories.id)
- `sku` (VARCHAR(50) NOT NULL UNIQUE — Format: KCA-{CATEGORY_CODE}-{CAR_BRAND_CODE}-{SEQUENCE}, contoh: KCA-DP-TYT-0001)
- `name` (VARCHAR(200) NOT NULL — Nama lengkap produk, contoh: 'Kaca Depan Toyota Avanza 2019-2023')
- `glass_type` (ENUM('depan','belakang','samping_kiri','samping_kanan','quarter_kiri','quarter_kanan','sunroof') NOT NULL — Posisi kaca pada kendaraan)
- `car_brand` (VARCHAR(100) NOT NULL — Contoh: Toyota, Honda, Daihatsu, Mitsubishi, Suzuki, Isuzu, Hyundai)
- `car_model` (VARCHAR(100) NOT NULL — Contoh: Avanza, Brio, Xenia, Pajero Sport)
- `car_year_start` (SMALLINT UNSIGNED NOT NULL — Tahun awal kompatibilitas kendaraan)
- `car_year_end` (SMALLINT UNSIGNED NULLABLE — NULL berarti berlaku hingga sekarang (model masih diproduksi))
- `product_brand` (VARCHAR(100) NULLABLE — Merek kaca, contoh: AGC, Pilkington, Saint-Gobain, OEM)
- `unit` (VARCHAR(20) NOT NULL DEFAULT 'pcs' — Satuan: pcs, set, lembar)
- `description` (TEXT NULLABLE — Deskripsi tambahan, catatan pemasangan, spesifikasi teknis)
- `image_path` (VARCHAR(255) NULLABLE — Path foto produk di storage/app/public (Laravel local disk))
- `is_active` (BOOLEAN NOT NULL DEFAULT TRUE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)
- `deleted_at` (TIMESTAMP NULL — Soft delete; produk dengan stok aktif tidak bisa dihapus)

**Relations**:
- BELONGS TO product_categories
- HAS MANY stocks (stok per cabang untuk produk ini)
- HAS MANY purchase_order_items
- HAS MANY sales_transaction_items
- HAS MANY stock_transfer_items
- HAS MANY stock_adjustment_items
- HAS MANY stock_transactions
- INDEX: sku (UNIQUE), category_id, car_brand, car_model, glass_type, is_active, deleted_at
- COMPOSITE INDEX: (car_brand, car_model, car_year_start, car_year_end) — untuk pencarian by spesifikasi kendaraan

### stocks
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id)
- `quantity` (INT UNSIGNED NOT NULL DEFAULT 0 — Stok saat ini; tidak boleh negatif (constraint di aplikasi layer))
- `min_stock_level` (INT UNSIGNED NOT NULL DEFAULT 0 — Batas minimum stok; trigger alert jika quantity <= nilai ini)
- `max_stock_level` (INT UNSIGNED NOT NULL DEFAULT 9999 — Batas maksimum yang direkomendasikan)
- `last_restock_at` (TIMESTAMP NULL — Timestamp terakhir stok bertambah (dari PO received))
- `last_sold_at` (TIMESTAMP NULL — Timestamp terakhir stok berkurang (dari penjualan))
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO products
- BELONGS TO branches
- UNIQUE CONSTRAINT: (product_id, branch_id) — satu record stok per produk per cabang
- INDEX: branch_id, product_id, quantity (untuk query stok menipis)

### stock_transactions
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id — Cabang yang stoknya berubah)
- `user_id` (BIGINT UNSIGNED NOT NULL FK→users.id — User yang memicu transaksi)
- `type` (ENUM('in','out','transfer_in','transfer_out','adjustment_in','adjustment_out') NOT NULL — Tipe mutasi stok)
- `quantity` (INT NOT NULL — Nilai positif; interpretasi IN/OUT ditentukan oleh kolom type)
- `quantity_before` (INT UNSIGNED NOT NULL — Stok sebelum transaksi ini; untuk audit trail)
- `quantity_after` (INT UNSIGNED NOT NULL — Stok setelah transaksi ini; untuk audit trail)
- `reference_type` (VARCHAR(50) NULLABLE — Polymorphic: 'PurchaseOrder', 'SalesTransaction', 'StockTransfer', 'StockAdjustment')
- `reference_id` (BIGINT UNSIGNED NULLABLE — ID dari tabel referensi (polymorphic))
- `notes` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL — Waktu transaksi terjadi)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO products
- BELONGS TO branches
- BELONGS TO users
- BELONGS TO (morphTo) reference (PurchaseOrder / SalesTransaction / StockTransfer / StockAdjustment)
- INDEX: branch_id, product_id, type, created_at — composite index untuk query laporan mutasi stok
- INDEX: reference_type, reference_id — untuk reverse lookup dari dokumen ke transaksi

### suppliers
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `name` (VARCHAR(150) NOT NULL)
- `contact_person` (VARCHAR(100) NULLABLE)
- `phone` (VARCHAR(25) NULLABLE)
- `email` (VARCHAR(191) NULLABLE)
- `address` (TEXT NULLABLE)
- `city` (VARCHAR(100) NULLABLE)
- `notes` (TEXT NULLABLE — Catatan tambahan, syarat pembayaran, dll.)
- `is_active` (BOOLEAN NOT NULL DEFAULT TRUE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- HAS MANY purchase_orders
- INDEX: name, is_active

### purchase_orders
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id — Cabang yang menerima barang)
- `supplier_id` (BIGINT UNSIGNED NOT NULL FK→suppliers.id)
- `created_by_user_id` (BIGINT UNSIGNED NOT NULL FK→users.id — User yang membuat PO)
- `approved_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id — User yang menyetujui PO)
- `received_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id — User yang mengkonfirmasi penerimaan barang)
- `po_number` (VARCHAR(50) NOT NULL UNIQUE — Format: PO-{BRANCH_CODE}-{YYYYMMDD}-{4_DIGIT_SEQ}, contoh: PO-JKT01-20260404-0001)
- `status` (ENUM('draft','approved','received','cancelled') NOT NULL DEFAULT 'draft')
- `total_items` (INT UNSIGNED NOT NULL DEFAULT 0 — Jumlah baris item dalam PO)
- `notes` (TEXT NULLABLE — Catatan tambahan untuk PO)
- `approved_at` (TIMESTAMP NULL)
- `received_at` (TIMESTAMP NULL)
- `cancelled_at` (TIMESTAMP NULL)
- `cancellation_reason` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO branches
- BELONGS TO suppliers
- BELONGS TO (creator) users
- BELONGS TO (approver) users
- BELONGS TO (receiver) users
- HAS MANY purchase_order_items
- HAS MANY stock_transactions (via polymorphic reference)
- INDEX: branch_id, status, supplier_id, po_number (UNIQUE), created_at

### purchase_order_items
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `purchase_order_id` (BIGINT UNSIGNED NOT NULL FK→purchase_orders.id ON DELETE CASCADE)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `quantity_ordered` (INT UNSIGNED NOT NULL — Jumlah yang dipesan)
- `quantity_received` (INT UNSIGNED NOT NULL DEFAULT 0 — Jumlah yang benar-benar diterima (partial receive support))
- `unit_price` (DECIMAL(15,2) NOT NULL DEFAULT 0.00 — Harga beli per unit saat transaksi)
- `subtotal` (DECIMAL(15,2) GENERATED ALWAYS AS (quantity_received * unit_price) STORED — Computed column)
- `notes` (VARCHAR(255) NULLABLE — Catatan per item (kondisi barang, dll.))
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO purchase_orders (CASCADE DELETE)
- BELONGS TO products
- UNIQUE CONSTRAINT: (purchase_order_id, product_id) — satu produk hanya muncul sekali per PO
- INDEX: purchase_order_id, product_id

### sales_transactions
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id)
- `user_id` (BIGINT UNSIGNED NOT NULL FK→users.id — Karyawan yang mencatat transaksi)
- `transaction_number` (VARCHAR(50) NOT NULL UNIQUE — Format: TRX-{BRANCH_CODE}-{YYYYMMDD}-{4_DIGIT_SEQ})
- `customer_name` (VARCHAR(150) NULLABLE)
- `customer_phone` (VARCHAR(25) NULLABLE)
- `customer_vehicle_info` (VARCHAR(200) NULLABLE — Opsional: info kendaraan pelanggan, contoh: 'Toyota Avanza 2020 B 1234 CD')
- `total_items` (INT UNSIGNED NOT NULL DEFAULT 0)
- `notes` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL — Tanggal dan waktu transaksi terjadi)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO branches
- BELONGS TO users
- HAS MANY sales_transaction_items
- HAS MANY stock_transactions (via polymorphic reference)
- INDEX: branch_id, transaction_number (UNIQUE), created_at, user_id

### sales_transaction_items
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `sales_transaction_id` (BIGINT UNSIGNED NOT NULL FK→sales_transactions.id ON DELETE CASCADE)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `quantity` (INT UNSIGNED NOT NULL)
- `unit_price` (DECIMAL(15,2) NOT NULL DEFAULT 0.00 — Harga jual per unit saat transaksi (snapshot harga))
- `subtotal` (DECIMAL(15,2) GENERATED ALWAYS AS (quantity * unit_price) STORED)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO sales_transactions (CASCADE DELETE)
- BELONGS TO products
- INDEX: sales_transaction_id, product_id

### stock_transfers
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `from_branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id — Cabang pengirim)
- `to_branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id — Cabang penerima; CONSTRAINT: from_branch_id != to_branch_id)
- `requested_by_user_id` (BIGINT UNSIGNED NOT NULL FK→users.id)
- `approved_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id)
- `shipped_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id)
- `received_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id)
- `transfer_number` (VARCHAR(60) NOT NULL UNIQUE — Format: TRF-{FROM_CODE}-{TO_CODE}-{YYYYMMDD}-{SEQ})
- `status` (ENUM('pending','approved','shipped','received','cancelled') NOT NULL DEFAULT 'pending')
- `notes` (TEXT NULLABLE)
- `approved_at` (TIMESTAMP NULL)
- `shipped_at` (TIMESTAMP NULL)
- `received_at` (TIMESTAMP NULL)
- `cancelled_at` (TIMESTAMP NULL)
- `cancellation_reason` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO (from) branches
- BELONGS TO (to) branches
- BELONGS TO (requester/approver/shipper/receiver) users
- HAS MANY stock_transfer_items
- HAS MANY stock_transactions (via polymorphic; dua record per transfer: transfer_out di from_branch, transfer_in di to_branch)
- INDEX: from_branch_id, to_branch_id, status, transfer_number (UNIQUE), created_at
- CHECK CONSTRAINT: from_branch_id != to_branch_id

### stock_transfer_items
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `stock_transfer_id` (BIGINT UNSIGNED NOT NULL FK→stock_transfers.id ON DELETE CASCADE)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `quantity_requested` (INT UNSIGNED NOT NULL — Jumlah yang diminta untuk ditransfer)
- `quantity_sent` (INT UNSIGNED NOT NULL DEFAULT 0 — Jumlah yang benar-benar dikirim (isi saat status→shipped))
- `quantity_received` (INT UNSIGNED NOT NULL DEFAULT 0 — Jumlah yang diterima cabang tujuan (isi saat status→received))
- `notes` (VARCHAR(255) NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO stock_transfers (CASCADE DELETE)
- BELONGS TO products
- UNIQUE CONSTRAINT: (stock_transfer_id, product_id)
- INDEX: stock_transfer_id, product_id

### stock_adjustments
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `branch_id` (BIGINT UNSIGNED NOT NULL FK→branches.id)
- `created_by_user_id` (BIGINT UNSIGNED NOT NULL FK→users.id)
- `approved_by_user_id` (BIGINT UNSIGNED NULLABLE FK→users.id)
- `adjustment_number` (VARCHAR(50) NOT NULL UNIQUE — Format: ADJ-{BRANCH_CODE}-{YYYYMMDD}-{SEQ})
- `reason` (VARCHAR(200) NOT NULL — Alasan opname: 'Opname Bulanan', 'Koreksi Data', 'Barang Rusak/Pecah', 'Selisih Transfer')
- `status` (ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending')
- `notes` (TEXT NULLABLE)
- `approved_at` (TIMESTAMP NULL)
- `rejection_reason` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO branches
- BELONGS TO (creator) users
- BELONGS TO (approver) users
- HAS MANY stock_adjustment_items
- HAS MANY stock_transactions (via polymorphic, dibuat saat adjustment approved)
- INDEX: branch_id, status, adjustment_number (UNIQUE), created_at

### stock_adjustment_items
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `stock_adjustment_id` (BIGINT UNSIGNED NOT NULL FK→stock_adjustments.id ON DELETE CASCADE)
- `product_id` (BIGINT UNSIGNED NOT NULL FK→products.id)
- `quantity_system` (INT UNSIGNED NOT NULL — Jumlah stok yang tercatat di sistem sebelum adjustment (snapshot))
- `quantity_physical` (INT UNSIGNED NOT NULL — Jumlah stok fisik hasil hitung manual)
- `difference` (INT GENERATED ALWAYS AS (quantity_physical - quantity_system) STORED — Negatif = selisih kurang, positif = selisih lebih)
- `notes` (VARCHAR(255) NULLABLE — Catatan per item, contoh: 'Kaca retak tidak terhitung')
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO stock_adjustments (CASCADE DELETE)
- BELONGS TO products
- UNIQUE CONSTRAINT: (stock_adjustment_id, product_id)
- INDEX: stock_adjustment_id, product_id

### audit_logs
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `user_id` (BIGINT UNSIGNED NULLABLE FK→users.id — NULL untuk aksi sistem (scheduled job))
- `branch_id` (BIGINT UNSIGNED NULLABLE FK→branches.id — Konteks cabang aksi)
- `action` (VARCHAR(100) NOT NULL — Contoh: 'user.login', 'stock.adjust.approve', 'product.create', 'transfer.ship')
- `table_name` (VARCHAR(100) NULLABLE — Nama tabel yang terpengaruh)
- `record_id` (BIGINT UNSIGNED NULLABLE — ID record yang terpengaruh)
- `old_values` (JSON NULLABLE — State sebelum perubahan (untuk aksi update/delete))
- `new_values` (JSON NULLABLE — State setelah perubahan (untuk aksi create/update))
- `ip_address` (VARCHAR(45) NULLABLE — IPv4 atau IPv6)
- `user_agent` (TEXT NULLABLE)
- `created_at` (TIMESTAMP NULL — Immutable; tidak ada updated_at karena log tidak boleh diubah)

**Relations**:
- BELONGS TO users (nullable)
- BELONGS TO branches (nullable)
- INDEX: user_id, action, table_name, record_id, created_at — diperlukan untuk query filter audit log
- INDEX: branch_id, created_at — untuk filter per cabang
- PARTITIONING (opsional untuk skala besar): RANGE partitioning by created_at per bulan

### personal_access_tokens
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `tokenable_type` (VARCHAR(255) NOT NULL — Polymorphic: 'App\Models\User')
- `tokenable_id` (BIGINT UNSIGNED NOT NULL)
- `name` (VARCHAR(255) NOT NULL — Nama token, contoh: 'auth-token')
- `token` (VARCHAR(64) NOT NULL UNIQUE — SHA-256 hash dari token asli)
- `abilities` (TEXT NULLABLE — JSON array of abilities (opsional untuk fine-grained permissions))
- `last_used_at` (TIMESTAMP NULL)
- `expires_at` (TIMESTAMP NULL — Token expiry untuk keamanan)
- `created_at` (TIMESTAMP NULL)
- `updated_at` (TIMESTAMP NULL)

**Relations**:
- BELONGS TO (morphTo) tokenable (User)
- INDEX: tokenable_type, tokenable_id — untuk lookup semua token milik user
- INDEX: token (UNIQUE)

### cache
**Columns**:
- `key` (VARCHAR(255) NOT NULL — Cache key unik)
- `value` (MEDIUMTEXT NOT NULL — Data yang di-cache (serialized))
- `expiration` (INT NOT NULL — Unix timestamp kapan cache kadaluarsa)

**Relations**:
- Tabel ini dibuat otomatis dengan perintah: php artisan cache:table
- PRIMARY KEY: key
- Digunakan sebagai pengganti Redis untuk CACHE_DRIVER=database

### jobs
**Columns**:
- `id` (BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY)
- `queue` (VARCHAR(255) NOT NULL — Nama antrian, default: 'default')
- `payload` (LONGTEXT NOT NULL — Serialized job data)
- `attempts` (TINYINT UNSIGNED NOT NULL DEFAULT 0)
- `reserved_at` (INT UNSIGNED NULLABLE)
- `available_at` (INT UNSIGNED NOT NULL)
- `created_at` (INT UNSIGNED NOT NULL)

**Relations**:
- Tabel ini dibuat otomatis dengan perintah: php artisan queue:table
- Digunakan sebagai pengganti Redis untuk QUEUE_CONNECTION=database
- Worker dijalankan via cPanel Cron Job: php artisan queue:work --stop-when-empty (setiap menit)

## 6. Architecture
**Components**: Frontend: React + TypeScript + Inertia.js (SSR-ready), dijalankan sebagai SPA yang berkomunikasi dengan Laravel backend, Backend: Laravel 12 dengan arsitektur Service-Repository pattern. Setiap domain memiliki Controller, Form Request, Service, dan Repository sendiri, Database: MySQL 8.0 (tersedia di cPanel) sebagai primary database, Session: Laravel file driver (SESSION_DRIVER=file) — disimpan di storage/framework/sessions, Cache: Laravel database driver (CACHE_DRIVER=database) — jalankan php artisan cache:table saat setup; digunakan untuk caching dashboard aggregates dengan TTL 5 menit, Queue Worker: Laravel Queue dengan database driver (QUEUE_CONNECTION=database) — jalankan php artisan queue:table saat setup; worker dijalankan via cPanel Cron Job setiap menit, File Storage: Laravel Storage local disk — foto produk dan file ekspor disimpan di storage/app/public, akses publik via php artisan storage:link, Authentication: Laravel Sanctum untuk API token-based authentication, Middleware Stack: EnsureAuthenticated → EnsureBranchAccess → EnsureRoleAccess → Controller

**Data Flow**: Semua request dari React frontend melewati Inertia.js atau Axios HTTP client ke Laravel routes → Route mendelegasikan ke Controller yang sesuai; Controller hanya bertanggung jawab menerima request, memanggil Service, dan mengembalikan response → Form Request memvalidasi semua input sebelum sampai ke Controller method → Service layer berisi seluruh business logic: kalkulasi, aturan bisnis, orkestrasi multi-repository, dan database transaction management → Repository layer berisi semua query database melalui Eloquent ORM; Service tidak boleh mengakses Model secara langsung → Model hanya berisi definisi relasi Eloquent, casts, fillable, dan accessor/mutator—tidak ada logic → Response dikembalikan sebagai Inertia response (untuk page render) atau JSON response (untuk AJAX request) → Event-driven side effects (audit log, notifikasi) menggunakan Laravel Events & Listeners yang di-dispatch ke queue (database driver)

**Deployment**: Development: Laravel Sail (Docker) untuk environment lokal yang konsisten antar developer, Staging/Production: cPanel shared hosting atau cPanel VPS — upload file via cPanel File Manager atau Git Deploy (fitur Git Version Control di cPanel), Web Server: Apache atau LiteSpeed yang sudah dikelola cPanel — tidak perlu konfigurasi Nginx manual. Pastikan mod_rewrite aktif dan file .htaccess di-allow untuk Laravel routing, Database: MySQL 8.0 yang tersedia di cPanel — buat database dan user via cPanel Database Wizard atau phpMyAdmin. Jalankan migrasi via SSH terminal atau cPanel Terminal, Queue Worker: Tidak menggunakan Supervisor. Gunakan cPanel Cron Job dengan perintah: 'php /home/{username}/public_html/artisan queue:work --stop-when-empty' dijadwalkan setiap menit (* * * * *), SSL: Aktifkan Let's Encrypt gratis via fitur SSL/TLS di cPanel (AutoSSL) — tidak perlu Certbot manual, File Storage: Laravel local disk di storage/app/public. Jalankan 'php artisan storage:link' sekali saat deploy untuk membuat symlink public/storage, Environment: Set file .env via cPanel File Manager. Pastikan APP_ENV=production, APP_DEBUG=false, CACHE_DRIVER=database, SESSION_DRIVER=file, QUEUE_CONNECTION=database, Backup: Gunakan fitur Backup atau Backup Wizard bawaan cPanel untuk backup database MySQL dan file secara berkala. Konfigurasikan jadwal backup otomatis dari cPanel

**Folder Structure**:
```text
glasswms/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Auth/
│   │   │   │   └── AuthController.php
│   │   │   ├── Branch/
│   │   │   │   └── BranchController.php
│   │   │   ├── Product/
│   │   │   │   ├── ProductController.php
│   │   │   │   └── ProductCategoryController.php
│   │   │   ├── Stock/
│   │   │   │   ├── StockController.php
│   │   │   │   └── StockTransferController.php
│   │   │   ├── PurchaseOrder/
│   │   │   │   └── PurchaseOrderController.php
│   │   │   ├── Sales/
│   │   │   │   └── SalesTransactionController.php
│   │   │   ├── StockAdjustment/
│   │   │   │   └── StockAdjustmentController.php
│   │   │   ├── Supplier/
│   │   │   │   └── SupplierController.php
│   │   │   ├── Report/
│   │   │   │   └── ReportController.php
│   │   │   ├── Dashboard/
│   │   │   │   └── DashboardController.php
│   │   │   └── User/
│   │   │       └── UserController.php
│   │   ├── Requests/
│   │   │   ├── Auth/
│   │   │   │   └── LoginRequest.php
│   │   │   ├── Branch/
│   │   │   │   ├── StoreBranchRequest.php
│   │   │   │   └── UpdateBranchRequest.php
│   │   │   ├── Product/
│   │   │   │   ├── StoreProductRequest.php
│   │   │   │   └── UpdateProductRequest.php
│   │   │   ├── PurchaseOrder/
│   │   │   │   ├── StorePurchaseOrderRequest.php
│   │   │   │   └── ReceivePurchaseOrderRequest.php
│   │   │   ├── Sales/
│   │   │   │   └── StoreSalesTransactionRequest.php
│   │   │   ├── Stock/
│   │   │   │   └── StoreStockTransferRequest.php
│   │   │   └── StockAdjustment/
│   │   │       ├── StoreStockAdjustmentRequest.php
│   │   │       └── ApproveAdjustmentRequest.php
│   │   └── Middleware/
│   │       ├── EnsureUserIsOwner.php
│   │       └── EnsureBranchAccess.php
│   ├── Models/
│   │   ├── User.php
│   │   ├── Branch.php
│   │   ├── Product.php
│   │   ├── ProductCategory.php
│   │   ├── Stock.php
│   │   ├── StockTransaction.php
│   │   ├── StockTransfer.php
│   │   ├── StockTransferItem.php
│   │   ├── PurchaseOrder.php
│   │   ├── PurchaseOrderItem.php
│   │   ├── Supplier.php
│   │   ├── SalesTransaction.php
│   │   ├── SalesTransactionItem.php
│   │   ├── StockAdjustment.php
│   │   ├── StockAdjustmentItem.php
│   │   └── AuditLog.php
│   ├── Services/
│   │   ├── Auth/
│   │   │   └── AuthService.php
│   │   ├── Branch/
│   │   │   └── BranchService.php
│   │   ├── Product/
│   │   │   └── ProductService.php
│   │   ├── Stock/
│   │   │   ├── StockService.php
│   │   │   └── StockTransferService.php
│   │   ├── PurchaseOrder/
│   │   │   └── PurchaseOrderService.php
│   │   ├── Sales/
│   │   │   └── SalesTransactionService.php
│   │   ├── StockAdjustment/
│   │   │   └── StockAdjustmentService.php
│   │   ├── Report/
│   │   │   └── ReportService.php
│   │   └── Dashboard/
│   │       └── DashboardService.php
│   ├── Repositories/
│   │   ├── Contracts/
│   │   │   ├── UserRepositoryInterface.php
│   │   │   ├── BranchRepositoryInterface.php
│   │   │   ├── ProductRepositoryInterface.php
│   │   │   ├── StockRepositoryInterface.php
│   │   │   ├── StockTransferRepositoryInterface.php
│   │   │   ├── PurchaseOrderRepositoryInterface.php
│   │   │   ├── SalesTransactionRepositoryInterface.php
│   │   │   └── ReportRepositoryInterface.php
│   │   ├── User/
│   │   │   └── UserRepository.php
│   │   ├── Branch/
│   │   │   └── BranchRepository.php
│   │   ├── Product/
│   │   │   └── ProductRepository.php
│   │   ├── Stock/
│   │   │   ├── StockRepository.php
│   │   │   └── StockTransactionRepository.php
│   │   ├── StockTransfer/
│   │   │   └── StockTransferRepository.php
│   │   ├── PurchaseOrder/
│   │   │   └── PurchaseOrderRepository.php
│   │   ├── Sales/
│   │   │   └── SalesTransactionRepository.php
│   │   ├── StockAdjustment/
│   │   │   └── StockAdjustmentRepository.php
│   │   └── Report/
│   │       └── ReportRepository.php
│   ├── Enums/
│   │   ├── UserRole.php
│   │   ├── GlassType.php
│   │   ├── StockTransactionType.php
│   │   ├── TransferStatus.php
│   │   ├── PurchaseOrderStatus.php
│   │   └── AdjustmentStatus.php
│   ├── Events/
│   │   ├── StockBelowMinimum.php
│   │   └── PurchaseOrderReceived.php
│   ├── Listeners/
│   │   ├── SendStockAlertNotification.php
│   │   └── LogAuditEvent.php
│   └── Providers/
│       ├── AppServiceProvider.php
│       └── RepositoryServiceProvider.php
├── resources/
│   ├── js/
│   │   ├── components/
│   │   │   ├── ui/           # Reusable UI components (Button, Input, Table, Badge, Modal)
│   │   │   ├── layout/       # AppLayout, Sidebar, Navbar, Breadcrumb
│   │   │   └── shared/       # DataTable, SearchFilter, ExportButton, StatusBadge
│   │   ├── pages/
│   │   │   ├── Auth/         # Login.tsx
│   │   │   ├── Dashboard/    # OwnerDashboard.tsx, BranchDashboard.tsx
│   │   │   ├── Branches/     # Index.tsx, Create.tsx, Edit.tsx
│   │   │   ├── Products/     # Index.tsx, Create.tsx, Edit.tsx, Show.tsx
│   │   │   ├── Stock/        # Index.tsx, StockOverview.tsx
│   │   │   ├── Transfers/    # Index.tsx, Create.tsx, Show.tsx
│   │   │   ├── PurchaseOrders/ # Index.tsx, Create.tsx, Show.tsx, Receive.tsx
│   │   │   ├── Sales/        # Index.tsx, Create.tsx, Show.tsx
│   │   │   ├── Adjustments/  # Index.tsx, Create.tsx, Show.tsx
│   │   │   ├── Suppliers/    # Index.tsx, Create.tsx, Edit.tsx
│   │   │   ├── Reports/      # StockReport.tsx, MutationReport.tsx, SalesReport.tsx
│   │   │   ├── Users/        # Index.tsx, Create.tsx, Edit.tsx
│   │   │   └── AuditLogs/    # Index.tsx
│   │   ├── hooks/
│   │   │   ├── useAuth.ts
│   │   │   ├── useBranch.ts
│   │   │   └── usePermission.ts
│   │   └── types/
│   │       ├── models.ts     # TypeScript interfaces mirroring DB schema
│   │       └── enums.ts
│   └── css/
│       └── app.css
├── routes/
│   ├── web.php
│   └── api.php
├── database/
│   ├── migrations/
│   └── seeders/
│       ├── DatabaseSeeder.php
│       ├── BranchSeeder.php
│       ├── UserSeeder.php
│       └── ProductCategorySeeder.php
└── tests/
    ├── Unit/
    │   ├── Services/
    │   │   ├── StockServiceTest.php
    │   │   ├── StockTransferServiceTest.php
    │   │   └── PurchaseOrderServiceTest.php
    │   └── Repositories/
    └── Feature/
        └── Api/
            ├── AuthTest.php
            ├── StockTest.php
            └── TransferTest.php
```

## 7. Tech Stack
| Layer | Choice | Reason |
|---|---|---|
| Backend Framework | Laravel 12 (PHP 8.3+) | Laravel 12 hadir dengan peningkatan performa signifikan, updated Eloquent, dan ekosistem yang matang. PHP 8.3 memberikan fitur typed class constants, readonly properties, dan performance improvements. Service-Repository pattern dapat diimplementasikan dengan sangat clean di Laravel. |
| Frontend Framework | React 19 + TypeScript + Inertia.js v2 | Inertia.js menjadi jembatan antara Laravel dan React tanpa perlu membangun REST API terpisah — controller Laravel langsung me-return Inertia response ke React component. TypeScript memberikan type safety yang krusial untuk menjaga konsistensi data antara backend schema dan frontend. React 19 hadir dengan React Compiler untuk performa lebih baik. |
| CSS & UI Component | Tailwind CSS v4 + shadcn/ui | Tailwind CSS v4 menggunakan Rust-based engine yang jauh lebih cepat. shadcn/ui menyediakan komponen accessible dan customizable (tidak locked ke satu library) sehingga bisa disesuaikan dengan design language sistem. Pilihan ini jauh lebih produktif dibanding membangun komponen dari nol. |
| Primary Database | MySQL 8.0 (via cPanel) | MySQL 8.0 mendukung JSON columns (untuk audit logs), Generated columns (untuk computed subtotals), Window functions, dan CTE yang sangat berguna untuk laporan kompleks. Tersedia langsung di cPanel tanpa biaya tambahan — buat database dan user via cPanel Database Wizard atau phpMyAdmin. |
| Cache & Session | Laravel file driver (session) + Laravel database driver (cache) | Tidak menggunakan Redis karena tidak tersedia di shared hosting cPanel standar. Session disimpan di storage/framework/sessions menggunakan file driver (SESSION_DRIVER=file). Cache menggunakan database driver (CACHE_DRIVER=database) dengan tabel cache di MySQL — jalankan php artisan cache:table saat setup awal. Cukup untuk kebutuhan caching dashboard aggregates dengan TTL 5 menit di skala 10 cabang. |
| Authentication | Laravel Sanctum | Sanctum cocok untuk SPA dengan Inertia.js karena menggunakan cookie-based authentication (lebih aman dari localStorage token). Untuk future mobile app, Sanctum juga support token-based auth dengan API tokens. |
| Queue & Background Jobs | Laravel Queue + database driver + cPanel Cron Job | Tidak menggunakan Redis driver maupun Supervisor daemon karena tidak tersedia di cPanel. Menggunakan database driver (QUEUE_CONNECTION=database) dengan tabel jobs di MySQL — jalankan php artisan queue:table saat setup. Worker dijalankan via cPanel Cron Job setiap menit: 'php artisan queue:work --stop-when-empty'. Digunakan untuk: generate laporan PDF/Excel secara async, mengirim notifikasi in-app, dan menulis audit logs secara async. |
| File Storage | Laravel Storage local disk | Tidak menggunakan layanan pihak ketiga (MinIO, AWS S3). Foto produk dan file ekspor laporan disimpan di storage/app/public menggunakan Laravel Storage local disk. Akses publik dikonfigurasi via php artisan storage:link yang membuat symlink dari public/storage ke storage/app/public. Cukup untuk kebutuhan bisnis skala 10 cabang dan tidak menambah biaya operasional. |
| PDF Generation | Laravel DomPDF (barryvdh/laravel-dompdf) | Solusi termudah untuk generate laporan PDF dari Blade template di Laravel tanpa dependency eksternal. Cukup untuk kebutuhan laporan inventori dan PO. |
| Excel Export | Laravel Excel (maatwebsite/excel) | Package paling mature untuk export Excel di Laravel. Support export besar dengan chunking untuk menghindari memory exhaustion saat laporan memiliki ribuan baris. |
| Development Environment | Laravel Sail (Docker Compose) | Memastikan seluruh tim developer bekerja di environment yang identik (PHP 8.3, MySQL 8.0). Mengurangi 'works on my machine' problems. Setup cukup dengan satu perintah: ./vendor/bin/sail up. Catatan: Sail hanya digunakan di development lokal; deployment ke cPanel tidak menggunakan Docker. |
| Testing | PestPHP + Laravel Testing Utilities | PestPHP memberikan syntax yang lebih expressive dan readable dibanding PHPUnit standar, namun tetap kompatibel penuh. Digunakan untuk unit test Service/Repository layer dan feature test untuk API endpoints. |

## 8. Design Guidelines
**Color Theme**: Primary: #1E40AF (Blue 700) — warna utama untuk action button, link, dan elemen aktif; mencerminkan kepercayaan dan profesionalisme, Primary Light: #DBEAFE (Blue 100) — background untuk elemen yang dipilih atau highlighted, Accent: #0EA5E9 (Sky 500) — untuk grafik dan highlight data, Success: #16A34A (Green 600) — status received, stok aman, transaksi berhasil, Warning: #D97706 (Amber 600) — stok mendekati minimum, status pending, Danger: #DC2626 (Red 600) — stok di bawah minimum, status cancelled, error, Background: #F8FAFC (Slate 50) — background halaman utama yang tidak melelahkan mata, Surface: #FFFFFF — background card dan panel, Border: #E2E8F0 (Slate 200) — border tabel dan card, Text Primary: #0F172A (Slate 900), Text Secondary: #64748B (Slate 500) — label, keterangan, dan metadata, Dark Mode: Dukung dark mode dengan menggunakan CSS variables dan class 'dark' pada root element

**Typography**: Font Family: Inter (Google Fonts) — pilihan tepat untuk dashboard data-heavy karena sangat legible di berbagai ukuran dan memiliki tabular number variant, Font Number (Tabular): Gunakan font-variant-numeric: tabular-nums pada semua cell yang menampilkan angka (stok, harga) agar angka sejajar rapi di tabel, Heading H1: 24px/700 — judul halaman, Heading H2: 20px/600 — judul section atau card, Heading H3: 16px/600 — sub-section, Body Regular: 14px/400 — konten tabel dan form, Body Small: 12px/400 — metadata, timestamp, label badge, Monospace: JetBrains Mono atau Fira Code — untuk menampilkan SKU, nomor PO, dan nomor transaksi agar mudah dibedakan karakternya

**Principles**:
- Data Density — Dashboard harus menampilkan maksimal informasi dalam tampilan yang tetap rapi; gunakan tabel kompak, badge, dan indikator visual untuk mengkomunikasikan status
- Role-Aware UI — Tampilan berubah signifikan berdasarkan role: Owner melihat agregasi multi-cabang, Karyawan melihat operasional harian cabang. Komponen yang sama me-render data berbeda sesuai konteks
- Action-Centric Design — Setiap halaman memiliki primary action yang jelas (misal: tombol 'Buat PO', 'Catat Penjualan'). Workflow tidak boleh membutuhkan lebih dari 3 klik untuk aksi paling umum
- Feedback Langsung — Setiap aksi yang mengubah data harus memberikan feedback visual segera: loading state, success toast notification, atau error message yang spesifik dan actionable
- Konsistensi Status Visual — Gunakan warna dan badge yang konsisten di seluruh aplikasi untuk menunjukkan status: abu-abu=draft, kuning=pending, biru=approved/shipped, hijau=received/done, merah=cancelled

**UI Patterns**:
- Data Table dengan filter, sorting, dan pagination server-side untuk semua daftar (produk, transaksi, PO, transfer)
- Multi-step form untuk proses kompleks seperti pembuatan PO (pilih supplier → tambah item → review → submit)
- Slide-over panel (sheet dari sisi kanan) untuk melihat detail tanpa meninggalkan halaman utama
- Confirmation dialog modal untuk aksi destruktif (cancel PO, reject adjustment, deactivate user)
- Combobox dengan search untuk memilih produk (karena katalog bisa ratusan SKU) — search by nama atau SKU
- Breadcrumb navigation yang selalu menampilkan konteks halaman saat ini
- Sticky header pada data table agar kolom judul selalu terlihat saat scroll panjang
- Empty state illustration yang informatif saat tidak ada data di suatu halaman
- Alert banner merah di atas halaman Dashboard ketika ada stok kritis di bawah minimum

**Accessibility**:
- Semua input form memiliki label yang benar (tidak hanya placeholder) untuk screen reader compatibility
- Color contrast minimum 4.5:1 untuk text normal (WCAG AA standard) — kritis karena dashboard sering dilihat dalam kondisi pencahayaan berbeda di gudang
- Keyboard navigable: semua aksi dapat dilakukan tanpa mouse (Tab, Enter, Escape)
- Focus indicator yang jelas pada semua interactive elements
- ARIA labels pada icon-only buttons (misal tombol edit/delete dengan ikon saja)
- Error messages selalu terhubung ke field yang bermasalah via aria-describedby

## 9. Development Process Flow
### Fase 0 — Setup & Foundation (3-5 hari)
- Setup project Laravel 12 + React + Inertia.js + Tailwind CSS v4 + shadcn/ui
- Konfigurasi Docker (Laravel Sail) untuk development environment lokal
- Setup database MySQL + konfigurasi .env: CACHE_DRIVER=database, SESSION_DRIVER=file, QUEUE_CONNECTION=database
- Jalankan php artisan cache:table dan php artisan queue:table untuk membuat tabel cache dan jobs
- Konfigurasi ESLint, Prettier, Laravel Pint (PHP code formatter)
- Setup PestPHP untuk testing
- Buat RepositoryServiceProvider dan binding interface ke implementation
- Implementasi Base Repository (abstract class dengan CRUD generik)
- Setup GitHub repository, branch strategy (main, develop, feature/*)
- Migrasi seluruh tabel database (semua 16 tabel termasuk cache dan jobs)
- Seeder untuk data awal: branches, product_categories, dan akun owner default

### Fase 1 — Authentication & User Management (5-7 hari)
- AuthController + AuthService + UserRepository
- LoginRequest (validasi form request)
- Middleware EnsureUserIsOwner dan EnsureBranchAccess
- Halaman Login (React/Inertia)
- Layout komponen: AppLayout, Sidebar (role-aware), Navbar
- CRUD User oleh Owner (halaman list, create, edit user)
- CRUD Branch oleh Owner
- Feature test: login, unauthorized access, role separation
- Unit test: AuthService, UserRepository

### Fase 2 — Master Data Produk & Supplier (5-7 hari)
- ProductController + ProductService + ProductRepository
- ProductCategoryController + Service + Repository
- SupplierController + Service + Repository
- Halaman: Product Index (dengan filter multi-column), Product Create/Edit, Product Show
- Halaman: Supplier Index, Create/Edit
- SKU auto-generation logic di ProductService
- Soft delete dengan validasi (tidak bisa hapus produk dengan stok aktif)
- Unit test ProductService (SKU generation, soft delete rules)

### Fase 3 — Manajemen Stok (Core) (10-14 hari)
- StockController + StockService + StockRepository
- Halaman Stock Overview per cabang (untuk karyawan) dan lintas cabang (untuk owner)
- StockTransactionRepository (mutasi log)
- Logika min/max stock level dan alert detection di StockService
- SalesTransactionController + SalesTransactionService + Repository
- Halaman: Sales Transaction Create (combobox produk + validasi stok real-time), Index, Show
- PurchaseOrderController + PurchaseOrderService + Repository
- Halaman: PO Create (multi-item), PO List, PO Show, PO Receive
- Alur status PO (draft→approved→received) dengan database transactions
- Auto-update stocks.quantity saat PO received (dalam DB Transaction)
- Unit test: StockService (decrement, insufficient stock error), PurchaseOrderService (status flow)

### Fase 4 — Transfer & Adjustment (7-10 hari)
- StockTransferController + StockTransferService + Repository
- Halaman: Transfer Create, Transfer List, Transfer Show (dengan timeline status)
- Alur status transfer (pending→approved→shipped→received) dengan atomic stock movement
- StockAdjustmentController + StockAdjustmentService + Repository
- Halaman: Adjustment Create, Adjustment List, Adjustment Show (approval view untuk owner)
- Alur approval adjustment dan pembaruan stok setelah approved
- Unit test: StockTransferService (concurrent stock check), StockAdjustmentService

### Fase 5 — Dashboard & Laporan (7-10 hari)
- DashboardController + DashboardService
- Owner Dashboard: KPI cards, grafik perbandingan cabang (Recharts), alert stok menipis
- Branch Dashboard: ringkasan stok, transaksi hari ini, pending items
- ReportController + ReportService + ReportRepository
- Laporan: Stok Per Cabang, Mutasi Stok, Laporan Penjualan
- Ekspor PDF (DomPDF) dan Excel (Laravel Excel) via background job (database queue)
- AuditLog viewer (halaman filter + table)
- Database cache di DashboardService dengan Cache::remember() menggunakan database driver, TTL 5 menit
- Unit test: ReportRepository (complex aggregation queries)

### Fase 6 — Polish, Testing & Deployment (7-10 hari)
- Notifikasi in-app untuk event: stok kritis, PO pending approval, transfer pending
- Complete feature test suite (semua critical user flows)
- Security review: rate limiting, input validation audit, SQL injection check
- Performance audit: EXPLAIN pada query lambat, tambah index jika perlu
- Setup cPanel: buat database MySQL, konfigurasi .env production, jalankan migrasi via SSH/cPanel Terminal
- Konfigurasi cPanel Cron Job untuk queue worker: '* * * * * php /home/{user}/public_html/artisan queue:work --stop-when-empty'
- Aktifkan AutoSSL (Let's Encrypt) via cPanel untuk HTTPS
- Jalankan php artisan storage:link untuk symlink file storage
- Konfigurasi cPanel Backup untuk backup otomatis database dan file
- Documentation: README pengembang dengan panduan deploy ke cPanel
- Seed data demo untuk demo/training
- User acceptance testing dengan owner dan karyawan pilot

## 10. Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Race condition pada stock decrement: dua transaksi penjualan terjadi bersamaan untuk produk dengan stok terbatas, keduanya lolos validasi stok, dan stok menjadi negatif | Gunakan database-level pessimistic locking: Stock::lockForUpdate()->find() dalam DB::transaction() saat proses decrement. Tambahkan CHECK CONSTRAINT di MySQL: CONSTRAINT chk_quantity_non_negative CHECK (quantity >= 0). Unit test dengan concurrent request simulation menggunakan database transactions. |
| Performa dashboard Owner melambat seiring bertambahnya data transaksi dari 10 cabang (jutaan records) | Implementasi caching menggunakan Laravel database cache driver dengan TTL 5 menit via Cache::remember(). Gunakan MySQL database-level aggregation (SUM, GROUP BY) bukan looping di PHP. Tambahkan scheduled job setiap jam (via cPanel Cron Job) untuk pre-compute summary data ke tabel terpisah jika dibutuhkan. Pantau dengan Laravel Telescope di staging. |
| Inkonsistensi data stok antara tabel stocks dan riwayat stock_transactions jika terjadi error di tengah proses multi-table update | Semua operasi yang mengubah stocks.quantity dan mencatat ke stock_transactions HARUS dibungkus dalam DB::transaction(). Jika ada exception, seluruh perubahan di-rollback otomatis. Service layer bertanggung jawab mengatur boundary transaction ini. Implementasi reconciliation job harian untuk mendeteksi inkonsistensi. |
| Keamanan data: karyawan satu cabang dapat mengakses atau memanipulasi data cabang lain melalui IDOR (Insecure Direct Object Reference) dengan mengganti ID di URL atau request body | Middleware EnsureBranchAccess memvalidasi bahwa branch_id dalam request sesuai dengan branch_id user yang login. Di Repository layer, semua query untuk karyawan selalu di-scope dengan ->where('branch_id', auth()->user()->branch_id). Feature test yang secara eksplisit mengetes akses lintas cabang untuk setiap endpoint. |
| Keterbatasan cPanel shared hosting: queue worker via Cron Job tidak berjalan secara real-time (ada jeda hingga 1 menit), dan storage lokal tidak tersedia jika berpindah server | Komunikasikan ke pengguna bahwa notifikasi dan generate laporan mungkin tertunda hingga 1 menit — ini wajar untuk skala bisnis ini. Untuk storage, lakukan backup file storage/app/public secara berkala via cPanel Backup. Jika bisnis berkembang dan butuh performa lebih, migrasi ke VPS dengan Supervisor dan Redis dapat dilakukan tanpa mengubah kode aplikasi (hanya ubah config .env). |
| Kehilangan data karena server failure atau kesalahan operator | Konfigurasi cPanel Backup otomatis untuk backup database MySQL dan file harian. Audit log untuk semua aksi destructive (tidak bisa dihapus). Soft delete untuk produk dan users. Staging environment untuk testing sebelum deploy ke production. |
| Adopsi sistem oleh karyawan yang tidak terbiasa dengan teknologi digital | UI dirancang mobile-responsive untuk diakses dari smartphone. Alur kerja dibuat sesederhana mungkin (minimal klik). Sediakan onboarding flow dengan tooltip pertama kali login. Buat panduan pengguna singkat dalam bentuk PDF. Lakukan sesi training untuk karyawan di setiap cabang sebelum go-live. |
