12 KiB
@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 +
pguntuk database aplikasi (Postgres). - Better Auth 1.7 untuk login: Drizzle adapter ke Postgres, plugin
username, sign-up publik ditutup (disableSignUp). CLI-nya packageauth(bukan@better-auth/cliyang berhenti di 1.4). - shadcn/ui 4 preset
base-nova(di atas Base UI, bukan Radix).cn()dari packagecnmilik shadcn, pengganticlsx+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
cleodi Docker lokal (localhost:3307, usercleo_next), hasilmysqldumpdari server LAN192.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:generatelaludb:migrate). -
MariaDB
cleomasih dipakai aplikasi PHP lama. Jangan pernahgenerate,migrate, ataupushke database ini. Schema Drizzle-nya hanya boleh berasal daripnpm db:amigo:pull(configdrizzle-amigo.config.ts), jangan diedit manual. Script itu menjalankanscripts/fix-amigo-schema.tsyang memperbaiki bug introspeksi drizzle-kit di MariaDB (default ber-kutip, primary key hilang, import kurang). Koneksi:amigoDbdari@/db/amigo. -
Kolom tanggal berisi
0000-00-00(tanggal nol, diizinkansql_modeserver). Schema memakaidate({ mode: 'string' }), jadi nilai itu sampai sebagai string; anggap sebagai "kosong", jangan diubah keDate(hasilnyaInvalid 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
cleober-engine MyISAM: tidak ada transaksi (rollback tidak membatalkan apa pun) danSELECT ... FOR UPDATEtidak mengunci baris. Untuk bentrok pakai update atomik satu statement (stok = stok - ?) atauLOCK 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 adalahALTER, dilarang selama PHP masih jalan. -
Database
cleojuga 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 daridrizzle-kit pull. User DB produksi diberi hak per tabel bisnis, bukancleo.*, 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
(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)
<nama>.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.tsdikolokasikan dengan fitur yang memakainya. Action yang dipakai lintas route group (mis.logoutdipanggil 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.tsdiawali'use server'. Setiap action:- Cek session dan hak akses di dalam action itu sendiri. Menyembunyikan tombol di UI bukan pengaman, karena action bisa dipanggil langsung lewat POST.
- Validasi input (
FormData/argumen) dengan zod sebelum menyentuh DB. - Panggil fungsi di
data/, bukan query langsung dari action. - Kembalikan objek kecil yang dibutuhkan UI (mis.
{ ok: true }atau{ ok: false, error: string }), bukan baris DB mentah. - Segarkan cache dengan
revalidatePath/updateTagsebelumredirect, karenaredirectmelempar exception dan kode sesudahnya tidak jalan. - 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
useActionStateuntuk menampilkan error/pending dari action. - Form master yang isinya input teks saja memakai
MasterSheetForm, dan dialog hapus memakaiHapusMasterButton; tipe state action dan log error darilib/server-action.ts. Form yang butuh select, textarea, atau lookup ditulis sendiri di_components/fiturnya; jangan menambah opsi ke configMasterSheetForm. - File di
data/diawaliimport "server-only"supaya tidak bisa ter-import ke client component.db/danlib/auth.tssengaja tanpa itu karena dibaca CLIauthdi luar Next.js (packageserver-onlytidak di-install; Next.js menanganinya sendiri). Hanyadata/,db/, danlib/auth.tsyang membacaprocess.envuntuk secret. - Session dicek lewat
getCurrentUser()didata/auth.ts(redirect ke/loginkalau 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. - Fungsi baca di
data/yang dipanggil beberapa komponen dalam satu render (mis.getCurrentUser,listGroupProductOptions) dibungkuscachedarireact: query jalan sekali per request, request berikutnya mengambil ulang. Jangan pakai"use cache"(cache lintas request) untuk datacleoatau session: PHP lama menulis ke DB yang sama sehingga data jadi basi, dan session yang di-cache membuat user yang sudah logout tetap dianggap login. cacheComponentsaktif: komponen yang membaca session (headers()/cookies()) wajib berada di dalam<Suspense>, kalau tidak build gagal. Janganawaitsession di level atas layout.proxy.tshanya untuk redirect cepat berdasarkan ada/tidaknya cookie session, bukan penggantigetCurrentUser().- Query selalu lewat Drizzle query builder atau
sqltemplate 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.usernamedankaryawan.password(hash MD5), lalu mengisi sessionuseracc,nik,job(id_pekerjaan),namatoko. - Jabatan dibaca dari
karyawan.id_pekerjaanberupa teks:Pemimpin Toko,Asisten Pemimpin Toko,Petugas Administrasi. - Harga pokok ditampilkan dalam kode huruf:
convHrgP/DeconvHrgPmemetakan angka 1-0 keB R A W N F O X E S; profit dipetakanconvProfitkeD J U N G K A R E T. - Format tanggal tampilan
dd-mm-yyyy(encTgl), DByyyy-mm-dd(decTgl). bukaKoneksiPusat()merujuk konstanta*_PUSATyang tidak didefinisikan didb_login.inc.php; fitur koneksi ke DB pusat kemungkinan sudah mati.
- Login lama mencocokkan
- 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'.