You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

169 lines
11 KiB
Markdown

@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)/<fitur>/
page.tsx Server Component: ambil data lewat data/, render
actions.ts Server Actions fitur ini ('use server')
_components/ komponen khusus fitur ini
api/auth/[...all]/route.ts handler Better Auth (satu-satunya route handler yang wajar)
components/
ui/ komponen shadcn (generated, jangan diedit manual kecuali perlu)
<nama>.tsx komponen yang dipakai lintas fitur
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
utils.ts helper umum (dibuat shadcn)
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.
- 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 `<Suspense>`, 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'`.