Laravel 12 Nuxt UI #9 Halaman Index

Belajar cara membangun aplikasi fullstack modern menggunakan Laravel 12, Inertia.js, Nuxt UI, dan Tailwind CSS. Tutorial ini membahas langkah demi langkah mulai dari instalasi, konfigurasi, hingga integrasi Laravel dengan Nuxt tanpa perlu membuat REST API. Cocok untuk pemula yang ingin memahami konsep fullstack Laravel dengan tampilan modern dan reaktif.

✅ Telah dilihat 1087 kali

Rating: 5.00 ⭐

... 07 November 2025, 08:14

Sekarang, kita beralih ke bagian Index. Silakan teman-teman buka folder pages yang terletak di dalam direktori resources/js.

Selanjutnya, buatlah sebuah folder baru dengan nama finance-records. Folder ini akan digunakan untuk menampung tampilan halaman utama dari data keuangan yang sudah kita kelola sebelumnya.

Di dalam folder finance-records tersebut, buat satu buah file baru dengan nama Index.vue. Perhatikan bahwa nama file diawali dengan huruf kapital, sesuai dengan konvensi penamaan komponen Vue.

Setelah langkah ini, struktur folder teman-teman seharusnya terlihat seperti berikut:

resources/
└── js/
    └── pages/
        └── finance-records/
            └── Index.vue

Dengan demikian, kita sudah memiliki dasar untuk halaman utama modul Finance Records.


Konfigurasi Index

Silakan teman-teman buka file Index.vue yang baru saja dibuat, kemudian masukkan kode berikut ini:

<script setup lang="ts">
import Layout from '@/layouts/Default.vue'
import { Head, router } from '@inertiajs/vue3'
import { h, ref, resolveComponent, reactive } from 'vue'
import { getPaginationRowModel } from '@tanstack/table-core'
import type { TableColumn } from '@nuxt/ui'

defineOptions({ layout: Layout })

const props = defineProps<{
  records: Array<{
    id: number
    date: string
    income: string
    expense: string
    net: string
  }>
  filters: { date?: string }
}>()

// Komponen UI yang digunakan
const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const UAlert = resolveComponent('UAlert')
const UInput = resolveComponent('UInput')
const UPagination = resolveComponent('UPagination')
const UTable = resolveComponent('UTable')
const UModal = resolveComponent('UModal')

// State modal edit
const selectedRecord = ref<any>(null)
const showEditModal = ref(false)

// Filter tanggal
const filters = reactive({
  date: props.filters.date || '',
})

// State alert
const showAlert = ref(false)
const alertMessage = ref('')
const alertColor = ref<'success' | 'error'>('success')

function showAlertMessage(message: string, color: 'success' | 'error' = 'success') {
  alertMessage.value = message
  alertColor.value = color
  showAlert.value = true
  setTimeout(() => (showAlert.value = false), 3000)
}

// Modal hapus
const deleteModal = ref<{ open: boolean; recordId?: number }>({ open: false })

function openDeleteModal(id: number) {
  deleteModal.value = { open: true, recordId: id }
}

// Konfirmasi hapus data
async function confirmDelete() {
  if (!deleteModal.value.recordId) return
  await router.delete(`/finance-records/${deleteModal.value.recordId}`, {
    onSuccess: () => {
      deleteModal.value.open = false
      showAlertMessage('Catatan keuangan berhasil dihapus.', 'success')
    },
    onError: () => {
      deleteModal.value.open = false
      showAlertMessage('Gagal menghapus catatan keuangan.', 'error')
    },
  })
}

// Filter berdasarkan tanggal
function searchFinance() {
  router.get('/finance-records', { date: filters.date }, { preserveState: true, replace: true })
}

// Kolom tabel
const columns: TableColumn[] = [
  {
    id: 'no',
    header: 'No',
    cell: ({ row, table }) => {
      const pageIndex = table.getState().pagination.pageIndex
      const pageSize = table.getState().pagination.pageSize
      return h('span', {}, pageIndex * pageSize + row.index + 1)
    },
  },
  {
    accessorKey: 'date',
    header: 'Tanggal',
    cell: ({ row }) => h('span', {}, row.original.date),
  },
  {
    accessorKey: 'income',
    header: 'Uang Masuk (Rp)',
    cell: ({ row }) => h('span', {}, row.original.income),
  },
  {
    accessorKey: 'expense',
    header: 'Uang Keluar (Rp)',
    cell: ({ row }) => h('span', {}, row.original.expense),
  },
  {
    accessorKey: 'net',
    header: 'Selisih (Rp)',
    cell: ({ row }) =>
      h(
        'span',
        { class: row.original.net.startsWith('-') ? 'text-red-600' : 'text-green-600' },
        row.original.net
      ),
  },
  {
    id: 'actions',
    header: 'Aksi',
    cell: ({ row }) =>
      h(
        'div',
        { class: 'text-right' },
        h(
          UDropdownMenu,
          {
            content: { align: 'end' },
            items: [
              {
                label: 'Edit',
                onSelect: () => {
                  selectedRecord.value = row.original
                  showEditModal.value = true
                },
              },
              { type: 'separator' },
              {
                label: 'Hapus',
                color: 'error',
                onSelect: () => openDeleteModal(row.original.id),
              },
            ],
          },
          () =>
            h(UButton, {
              icon: 'i-lucide-ellipsis-vertical',
              color: 'neutral',
              variant: 'ghost',
            })
        )
      ),
  },
]

const pagination = ref({ pageIndex: 0, pageSize: 10 })
</script>

<template>
  <Head title="Catatan Keuangan" />

  <UDashboardPanel id="finance-records">
    <template #header>
      <UDashboardNavbar title="Catatan Keuangan">
        <template #right>
          <FinanceAddModal />
        </template>
      </UDashboardNavbar>
    </template>

    <template #body>
      <!-- Filter tanggal -->
      <div class="flex gap-2 mb-4 items-center">
        <UInput
          v-model="filters.date"
          type="date"
          class="max-w-xs"
          icon="i-lucide-calendar"
          placeholder="Pilih tanggal..."
          @change="searchFinance"
        />
        <UButton label="Cari" icon="i-lucide-search" @click="searchFinance" />
      </div>

      <!-- Tabel catatan keuangan -->
      <UTable
        :data="records"
        :columns="columns"
        v-model:pagination="pagination"
        :pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
        :ui="{
          base: 'table-fixed border-separate border-spacing-0 w-full',
          thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
          th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
          td: 'border-b border-default py-2 px-3',
        }"
      />

      <!-- Footer tabel -->
      <div
        class="mt-4 flex items-center justify-between text-sm text-muted border-t border-default pt-4"
      >
        <div>Total Catatan: <strong>{{ records.length }}</strong></div>
        <UPagination
          :default-page="(pagination.pageIndex || 0) + 1"
          :items-per-page="pagination.pageSize"
          :total="records.length"
          @update:page="(p: number) => (pagination.pageIndex = p - 1)"
        />
      </div>

      <!-- Modal Edit -->
      <FinanceEditModal
        v-if="selectedRecord"
        :record="selectedRecord"
        v-model:open="showEditModal"
      />

      <!-- Modal Hapus -->
      <UModal
        v-model:open="deleteModal.open"
        title="Hapus Catatan Keuangan?"
        description="Tindakan ini tidak dapat dibatalkan."
      >
        <template #body>
          <div class="flex justify-end gap-2">
            <UButton
              label="Batal"
              color="neutral"
              variant="subtle"
              @click="deleteModal.open = false"
            />
            <UButton
              label="Hapus"
              color="error"
              variant="solid"
              @click="confirmDelete"
            />
          </div>
        </template>
      </UModal>

      <!-- Alert notifikasi -->
      <div class="fixed bottom-4 right-4 z-50">
        <UAlert
          v-if="showAlert"
          :title="alertMessage"
          :color="alertColor"
          variant="subtle"
          :icon="
            alertColor === 'success'
              ? 'i-lucide-check-circle'
              : 'i-lucide-alert-circle'
          "
          close
          @update:open="showAlert = false"
        />
      </div>
    </template>
  </UDashboardPanel>
</template>

Komponen ini juga ditulis menggunakan pendekatan <script setup> dari Vue 3 dengan dukungan TypeScript (lang="ts"), serta memanfaatkan integrasi Inertia.js untuk komunikasi dengan backend Laravel.

1. Bagian Import dan Setup Dasar

<script setup lang="ts">
import Layout from '@/layouts/Default.vue'
import { Head, router } from '@inertiajs/vue3'
import { h, ref, resolveComponent, reactive } from 'vue'
import { getPaginationRowModel } from '@tanstack/table-core'
import type { TableColumn } from '@nuxt/ui'

Pada bagian awal ini:

  • Layout diimpor untuk menentukan tata letak halaman menggunakan layout Default.vue.
  • Head digunakan untuk mengatur judul halaman di browser.
  • router dari Inertia berfungsi menggantikan axios atau fetch, karena semua navigasi dan request dilakukan melalui sistem SPA Inertia.
  • ref dan reactive adalah reactive primitives dari Vue untuk mendefinisikan variabel yang dapat berubah secara reaktif.
  • h digunakan untuk membuat elemen virtual (hyperscript) dalam definisi kolom tabel.
  • TableColumn dan getPaginationRowModel berasal dari Nuxt UI dan TanStack Table, digunakan untuk mengatur tabel data dengan pagination.

2. Menentukan Layout dan Props

defineOptions({ layout: Layout })

const props = defineProps<{
  records: Array<{
    id: number
    date: string
    income: string
    expense: string
    net: string
  }>
  filters: { date?: string }
}>()
  • defineOptions({ layout: Layout }) memastikan bahwa komponen ini akan dibungkus dalam layout utama aplikasi.
  • defineProps mendefinisikan properti (props) yang diterima dari Laravel melalui Inertia. Dalam hal ini, komponen menerima:
    • records: daftar data keuangan (berisi tanggal, pemasukan, pengeluaran, dan selisih).
    • filters: parameter pencarian (khususnya berdasarkan tanggal).

3. Resolusi Komponen UI

const UButton = resolveComponent('UButton')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const UAlert = resolveComponent('UAlert')
const UInput = resolveComponent('UInput')
const UPagination = resolveComponent('UPagination')
const UTable = resolveComponent('UTable')
const UModal = resolveComponent('UModal')

Bagian ini digunakan untuk memanggil komponen UI secara dinamis dari library Nuxt UI. Tujuannya adalah agar komponen dapat digunakan langsung di dalam h() function pada kolom tabel, tanpa perlu import satu per satu di bagian atas.


4. State dan Reaktivitas

const selectedRecord = ref<any>(null)
const showEditModal = ref(false)

Variabel di atas digunakan untuk:

  • Menyimpan data catatan keuangan yang sedang dipilih untuk diedit.
  • Mengatur visibilitas modal edit.

5. Filter Tanggal

const filters = reactive({
  date: props.filters.date || '',
})

Objek filters diset sebagai reaktif agar setiap perubahan input tanggal langsung terdeteksi dan bisa dikirim ke server menggunakan Inertia.


6. Notifikasi (Alert)

const showAlert = ref(false)
const alertMessage = ref('')
const alertColor = ref<'success' | 'error'>('success')

function showAlertMessage(message: string, color: 'success' | 'error' = 'success') {
  alertMessage.value = message
  alertColor.value = color
  showAlert.value = true
  setTimeout(() => (showAlert.value = false), 3000)
}

Bagian ini menangani notifikasi sukses atau error yang muncul di pojok kanan bawah layar. Fungsinya menampilkan pesan selama 3 detik, kemudian otomatis hilang.


7. Modal Hapus dan Fungsi Hapus Data

const deleteModal = ref<{ open: boolean; recordId?: number }>({ open: false })

function openDeleteModal(id: number) {
  deleteModal.value = { open: true, recordId: id }
}
  • deleteModal menyimpan status apakah modal konfirmasi hapus sedang dibuka atau tidak.
  • openDeleteModal() memanggil modal ketika pengguna menekan tombol “Hapus”.

Fungsi konfirmasi hapus:

async function confirmDelete() {
  if (!deleteModal.value.recordId) return
  await router.delete(`/finance-records/${deleteModal.value.recordId}`, {
    onSuccess: () => {
      deleteModal.value.open = false
      showAlertMessage('Catatan keuangan berhasil dihapus.', 'success')
    },
    onError: () => {
      deleteModal.value.open = false
      showAlertMessage('Gagal menghapus catatan keuangan.', 'error')
    },
  })
}
  • Mengirim permintaan penghapusan data ke Laravel melalui Inertia router.
  • Menampilkan notifikasi sesuai hasilnya (sukses atau gagal).

8. Fungsi Pencarian Data

function searchFinance() {
  router.get('/finance-records', { date: filters.date }, { preserveState: true, replace: true })
}

Fungsi ini akan memuat ulang halaman finance-records sambil mengirimkan parameter pencarian tanggal ke server. preserveState: true menjaga posisi tabel dan data agar tidak ter-reset sepenuhnya.


9. Struktur Kolom Tabel

const columns: TableColumn[] = [
  { id: 'no', header: 'No', cell: ... },
  { accessorKey: 'date', header: 'Tanggal', cell: ... },
  { accessorKey: 'income', header: 'Uang Masuk (Rp)', cell: ... },
  { accessorKey: 'expense', header: 'Uang Keluar (Rp)', cell: ... },
  { accessorKey: 'net', header: 'Selisih (Rp)', cell: ... },
  { id: 'actions', header: 'Aksi', cell: ... },
]

Kolom actions berisi menu dropdown dengan dua opsi: Edit dan Hapus. Opsi Edit akan membuka modal edit dan memuat data yang dipilih. Opsi Hapus akan membuka modal konfirmasi sebelum menghapus data.


10. Template (Bagian Tampilan)

Bagian <template> berisi struktur tampilan utama halaman:

  • Head mengatur judul halaman.
  • UDashboardPanel dan UDashboardNavbar digunakan sebagai kerangka dashboard.
  • UInput untuk filter tanggal.
  • UTable menampilkan daftar catatan keuangan.
  • UPagination untuk navigasi halaman tabel.
  • FinanceAddModal dan FinanceEditModal sebagai komponen tambahan.
  • UModal untuk konfirmasi penghapusan.
  • UAlert untuk notifikasi dinamis.

Contoh potongan penting:

<UInput
  v-model="filters.date"
  type="date"
  @change="searchFinance"
/>

<UTable
  :data="records"
  :columns="columns"
  v-model:pagination="pagination"
/>

Pada materi berikutnya, kita akan menampilkan menu didalam sidebar dan juga menambahkan icon untuk halaman Finance Record /catatan keuangan.

Daftar eBook