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.

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 + 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
  (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.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 <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'.