@AGENTS.md # Cleo Minimarket Aplikasi Next.js 16 (App Router) untuk menggantikan aplikasi back-office minimarket lama berbasis PHP di `old-source/`. Kode baru ditulis ulang dari nol; `old-source/` hanya referensi. ## Stack - Next.js 16.4 + React 19, TypeScript strict, Tailwind CSS 4, pnpm 11. - Drizzle ORM + `pg` untuk database aplikasi (Postgres). - Better Auth 1.7 untuk login: Drizzle adapter ke Postgres, plugin `username`, sign-up publik ditutup (`disableSignUp`). CLI-nya package `auth` (bukan `@better-auth/cli` yang berhenti di 1.4). - shadcn/ui 4 preset `base-nova` (di atas Base UI, bukan Radix). `cn()` dari package `cn` milik shadcn, pengganti `clsx` + `tailwind-merge`. ## Dua database | Env var | Database | Isi | | -------------------- | -------------------------------- | ----------------------------------------- | | `DATABASE_URL` | Postgres (Docker lokal) | Milik aplikasi ini: user, session, auth. | | `AMIGO_DATABASE_URL` | MariaDB 10.4 `cleo` | Data bisnis lama: barang, transaksi, dll. | - Development memakai **salinan** `cleo` di Docker lokal (`localhost:3307`, user `cleo_next`), hasil `mysqldump` dari server LAN `192.168.3.3`. Server LAN adalah produksi yang dipakai toko; development tidak pernah menulis ke sana. Restore salinan lokal = dump + import ulang. - Postgres dikelola penuh oleh Drizzle migration (`db:generate` lalu `db:migrate`). - MariaDB `cleo` masih dipakai aplikasi PHP lama. **Jangan pernah** `generate`, `migrate`, atau `push` ke database ini. Schema Drizzle-nya hanya boleh berasal dari `pnpm db:amigo:pull` (config `drizzle-amigo.config.ts`), jangan diedit manual. Script itu menjalankan `scripts/fix-amigo-schema.ts` yang memperbaiki bug introspeksi drizzle-kit di MariaDB (default ber-kutip, primary key hilang, import kurang). Koneksi: `amigoDb` dari `@/db/amigo`. - Kolom tanggal berisi `0000-00-00` (tanggal nol, diizinkan `sql_mode` server). Schema memakai `date({ mode: 'string' })`, jadi nilai itu sampai sebagai string; anggap sebagai "kosong", jangan diubah ke `Date` (hasilnya `Invalid Date`). - Aplikasi ini melakukan CRUD ke MariaDB (data master, barang, transaksi), jadi user DB-nya butuh `SELECT`, `INSERT`, `UPDATE`, `DELETE`, tapi tanpa hak DDL (`CREATE`, `ALTER`, `DROP`). - UI tidak meniru aplikasi lama; fitur dan aturan bisnisnya yang ditiru, tampilannya dirombak. - Selama transisi, aplikasi PHP dan aplikasi ini menulis ke MariaDB bersamaan. Penomoran otomatis (mis. kode pelanggan) dan update stok harus aman dari bentrok. - Hampir semua tabel bisnis `cleo` ber-engine **MyISAM**: tidak ada transaksi (rollback tidak membatalkan apa pun) dan `SELECT ... FOR UPDATE` tidak mengunci baris. Untuk bentrok pakai update atomik satu statement (`stok = stok - ?`) atau `LOCK TABLES`; jangan baca-hitung-tulis. Penulisan master + detail (mis. `m_nota_tunai` + `d_nota_tunai`) bisa tersimpan setengah kalau gagal di tengah, jadi validasi semua input dulu sebelum statement pertama. Mengubah engine ke InnoDB adalah `ALTER`, dilarang selama PHP masih jalan. - Database `cleo` juga berisi tabel dari proyek lain (Laravel: `users`, `sessions`, `jobs`, `cache`, `migrations`, dll.; Better Auth: `user`, `account`, `session`, `verification`; Prisma: `_prisma_migrations`). Bukan milik aplikasi ini dan dianggap masih dipakai aplikasi lain; jangan disentuh dan kecualikan dari `drizzle-kit pull`. User DB produksi diberi hak per tabel bisnis, bukan `cleo.*`, supaya tabel-tabel itu tidak terjangkau. - Server MariaDB 10.4, bukan MySQL 8: hindari fitur yang tidak didukung (mis. sebagian fungsi JSON dan CTE tertentu). Driver: `mysql2`. ## Struktur folder Tanpa folder `src/`. Kode aplikasi di root, sejajar config. ``` app/ (auth)/actions.ts Server Actions auth (login, logout), dipakai lintas route group (auth)/login/page.tsx halaman publik (belum login) (dashboard)/layout.tsx kerangka UI halaman yang wajib login (bukan tempat cek session) (dashboard)// page.tsx Server Component: ambil data lewat data/, render actions.ts Server Actions fitur ini ('use server') _components/ komponen khusus fitur ini (dashboard)/master/_components/ komponen bersama semua halaman master cari-master.tsx kotak pencarian master (prop basePath + placeholder) master-sheet-form.tsx form sheet master, input teks saja (action, fields, defaultValues) hapus-master-button.tsx dialog konfirmasi hapus master (id, idName, entityLabel) api/auth/[...all]/route.ts handler Better Auth (satu-satunya route handler yang wajar) components/ ui/ komponen shadcn (generated, jangan diedit manual kecuali perlu) .tsx komponen yang dipakai lintas fitur pagination.tsx tombol halaman daftar (prop page, hasNext, hrefPage) data/ Data Access Layer: fungsi baca/tulis DB, selalu 'server-only' db/ index.ts, schema.ts koneksi + schema Postgres amigo/index.ts, amigo/schema.ts koneksi + schema MariaDB (hasil `drizzle-kit pull`) lib/ auth.ts config Better Auth (server) auth-client.ts client Better Auth pagination.ts PER_PAGE, parse `?page=`, dan href halaman server-action.ts tipe state action (FormState/DeleteState) dan logActionError utils.ts helper umum: cn (dibuat shadcn) dan escapeLike untuk LIKE drizzle/ file migrasi Postgres (generated, ikut di-commit) proxy.ts pengganti middleware di Next 16; hanya cek ringan (redirect) ``` - Nama folder route dan file: kebab-case. Komponen React: PascalCase untuk nama fungsinya. - Folder berawalan `_` tidak menjadi route; pakai untuk komponen yang dikolokasikan. - Route group `(nama)` hanya untuk mengelompokkan layout, tidak muncul di URL. - Import pakai alias `@/` (mis. `@/db`, `@/data/barang`). - `actions.ts` dikolokasikan dengan fitur yang memakainya. Action yang dipakai lintas route group (mis. `logout` dipanggil dari `(dashboard)`) ditaruh di level route group pemiliknya, lalu di-import lewat alias (`@/app/(auth)/actions`), bukan path relatif `../(auth)/...`. ## Aturan data dan Server Actions - **Mutasi (create/update/delete, submit form) wajib lewat Server Action.** Tidak membuat route handler `app/api/*` untuk hal yang bisa dikerjakan Server Action. - **Membaca data tidak lewat Server Action.** Server Action adalah POST dan dijalankan berurutan, jadi tidak cocok untuk fetch. Data dibaca di Server Component dengan memanggil fungsi di `data/`. - Setiap file `actions.ts` diawali `'use server'`. Setiap action: 1. Cek session dan hak akses di dalam action itu sendiri. Menyembunyikan tombol di UI bukan pengaman, karena action bisa dipanggil langsung lewat POST. 2. Validasi input (`FormData`/argumen) dengan zod sebelum menyentuh DB. 3. Panggil fungsi di `data/`, bukan query langsung dari action. 4. Kembalikan objek kecil yang dibutuhkan UI (mis. `{ ok: true }` atau `{ ok: false, error: string }`), bukan baris DB mentah. 5. Segarkan cache dengan `revalidatePath`/`updateTag` sebelum `redirect`, karena `redirect` melempar exception dan kode sesudahnya tidak jalan. 6. Error yang tidak terduga di-log di server dengan konteks yang cukup untuk dilacak: nama action, user id, input yang relevan (tanpa password/secret), dan error aslinya (stack ikut). Ke UI hanya dikirim pesan bahasa Indonesia yang jelas dan bisa ditindaklanjuti user (mis. "Kode pelanggan sudah dipakai, coba simpan ulang"), tidak pernah pesan error DB mentah. - Form di client memakai `useActionState` untuk menampilkan error/pending dari action. - Form master yang isinya input teks saja memakai `MasterSheetForm`, dan dialog hapus memakai `HapusMasterButton`; tipe state action dan log error dari `lib/server-action.ts`. Form yang butuh select, textarea, atau lookup ditulis sendiri di `_components/` fiturnya; jangan menambah opsi ke config `MasterSheetForm`. - File di `data/` diawali `import "server-only"` supaya tidak bisa ter-import ke client component. `db/` dan `lib/auth.ts` sengaja tanpa itu karena dibaca CLI `auth` di luar Next.js (package `server-only` tidak di-install; Next.js menanganinya sendiri). Hanya `data/`, `db/`, dan `lib/auth.ts` yang membaca `process.env` untuk secret. - Session dicek lewat `getCurrentUser()` di `data/auth.ts` (redirect ke `/login` kalau tidak ada), dipanggil oleh page dan action yang butuh user. Layout tidak dipakai untuk cek session: layout tidak di-render ulang saat navigasi antar halaman di dalamnya. - `cacheComponents` aktif: komponen yang membaca session (`headers()`/`cookies()`) wajib berada di dalam ``, kalau tidak build gagal. Jangan `await` session di level atas layout. - `proxy.ts` hanya untuk redirect cepat berdasarkan ada/tidaknya cookie session, bukan pengganti `getCurrentUser()`. - Query selalu lewat Drizzle query builder atau `sql` template tag (parameterized). Jangan menyusun SQL dengan string concatenation. ## Kode lama (`old-source/`) Read-only, jangan diedit dan jangan di-import. Dipakai untuk memahami fitur dan aturan bisnis. - PHP prosedural + jQuery, akses DB lewat `mysqli`/`mysql_*` langsung di tiap halaman. Banyak query disusun dengan string concatenation (rentan SQL injection); jangan tiru polanya. - Peta fitur ada di `old-source/menu.php`: Data Master (pelanggan, karyawan, group/sub group product, rak, supplier, satuan), Barang, Pengadaan, Barcode, Retur Pembelian, Transfer Barang, Revisi, Laporan Persediaan/Beli/Jual, BON, Pembayaran. - Helper bisnis di `old-source/func_lib.php`. Hal yang tidak terlihat dari namanya: - Login lama mencocokkan `karyawan.username` dan `karyawan.password` (hash MD5), lalu mengisi session `useracc`, `nik`, `job` (`id_pekerjaan`), `namatoko`. - Jabatan dibaca dari `karyawan.id_pekerjaan` berupa teks: `Pemimpin Toko`, `Asisten Pemimpin Toko`, `Petugas Administrasi`. - Harga pokok ditampilkan dalam kode huruf: `convHrgP`/`DeconvHrgP` memetakan angka 1-0 ke `B R A W N F O X E S`; profit dipetakan `convProfit` ke `D J U N G K A R E T`. - Format tanggal tampilan `dd-mm-yyyy` (`encTgl`), DB `yyyy-mm-dd` (`decTgl`). - `bukaKoneksiPusat()` merujuk konstanta `*_PUSAT` yang tidak didefinisikan di `db_login.inc.php`; fitur koneksi ke DB pusat kemungkinan sudah mati. - File `*~` dan `(copy)` adalah sisa backup editor, abaikan. ## Perintah ``` pnpm dev jalankan dev server pnpm build build production pnpm lint eslint pnpm exec tsc --noEmit type-check pnpm db:generate buat migrasi dari db/schema.ts (Postgres) pnpm db:migrate jalankan migrasi ke Postgres pnpm db:studio lihat isi Postgres di https://local.drizzle.studio pnpm db:seed buat akun admin dari SEED_ADMIN_* di .env (idempotent) pnpm db:amigo:pull introspeksi MariaDB ke db/amigo/schema.ts (satu-satunya perintah drizzle-kit untuk MariaDB) docker compose up -d nyalakan Postgres + MariaDB lokal (docker-compose.yml) ``` Postgres lokal: service di `docker-compose.yml`, container `cleo-postgres` (image `postgres:17`, port 5432, volume `cleo-pgdata`). MariaDB lokal: container `cleo-mariadb` (image `mariadb:10.4`, port 3307, volume `cleo-mariadb-data`). Setup laptop baru ada di `README.md`. Query ke MariaDB dari PowerShell 5.1: jangan pakai `mysql -e "..."` (kutip ganda dibuang PowerShell); kirim lewat pipe: `'SELECT 1' | docker exec -i cleo-mariadb sh -c 'mysql -uroot -p$MARIADB_ROOT_PASSWORD -N cleo'`.