Refactor navigation structure by simplifying the menu items and removing unused pages for improved clarity and performance. Additionally, clean up the Prisma schema by removing obsolete notification models to streamline database management.

This commit is contained in:
Md Afiq Iskandar 2025-06-26 10:38:35 +08:00
parent 1408fbfd9d
commit 30a17a8c2e
4 changed files with 17 additions and 1332 deletions

View File

@ -1,94 +1,22 @@
export default [ export default [
{ {
"header": "Utama", header: "Utama",
"description": "", description: "",
"child": [ child: [
{ {
"title": "Dashboard", title: "Dashboard",
"path": "/dashboard", path: "/dashboard",
"icon": "ic:outline-dashboard", icon: "ic:outline-dashboard",
"child": [], child: [],
"meta": {} meta: {},
} },
{
title: "API Platform",
path: "/api-platform",
icon: "ic:outline-api",
child: [],
},
], ],
"meta": {} meta: {},
}, },
{ ];
"header": "Pentadbiran",
"description": "Urus aplikasi anda",
"child": [
{
"title": "API Platform",
"path": "/api-platform",
"icon": "",
"child": []
},
{
"title": "Konfigurasi",
"icon": "ic:outline-settings",
"child": [
{
"title": "Persekitaran",
"path": "/devtool/config/environment"
},
{
"title": "Site Settings",
"path": "/devtool/config/site-settings"
}
]
},
{
"title": "Penyunting Menu",
"icon": "ci:menu-alt-03",
"path": "/devtool/menu-editor",
"child": []
},
{
"title": "Urus Pengguna",
"path": "/devtool/user-management",
"icon": "ph:user-circle-gear",
"child": [
{
"title": "Senarai Pengguna",
"path": "/devtool/user-management/user",
"icon": "",
"child": []
},
{
"title": "Senarai Peranan",
"path": "/devtool/user-management/role",
"icon": "",
"child": []
}
]
},
{
"title": "Kandungan",
"icon": "mdi:pencil-ruler",
"child": [
{
"title": "Penyunting",
"path": "/devtool/content-editor"
},
{
"title": "Templat",
"path": "/devtool/content-editor/template"
}
]
},
{
"title": "Penyunting API",
"path": "/devtool/api-editor",
"icon": "material-symbols:api-rounded",
"child": []
}
],
"meta": {
"auth": {
"role": [
"Developer"
]
}
}
}
];

View File

@ -1,674 +0,0 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAsnafMockData } from '~/composables/useAsnafMockData';
import { useToast } from 'vue-toastification';
const route = useRoute();
const toast = useToast();
const { getProfileById } = useAsnafMockData();
const profile = ref(null);
const loading = ref(true);
const isLoadingAnalysis = ref(false);
const confirmDelete = ref(false);
const activeTab = ref('personal');
// Load profile data
onMounted(async () => {
const id = route.params.id;
loading.value = true;
const fetchedProfile = getProfileById(id);
if (fetchedProfile) {
profile.value = { ...fetchedProfile, analysis: null };
loading.value = false;
} else {
toast.error('Profil tidak dijumpai');
navigateTo('/BF-PRF/AS/LIST');
loading.value = false;
}
});
// New function to be called by a button
async function fetchAIAnalysis() {
if (!profile.value) {
toast.error('Profil data tidak tersedia untuk analisis.');
return;
}
isLoadingAnalysis.value = true;
try {
const requestBody = {
monthlyIncome: profile.value.monthlyIncome,
otherIncome: profile.value.otherIncome,
totalIncome: profile.value.totalIncome,
occupation: profile.value.occupation,
maritalStatus: profile.value.maritalStatus,
dependents: profile.value.dependents,
// Add other fields you want to send for analysis here
};
const analysisResponse = await $fetch('/api/analyze-asnaf', {
method: 'POST',
body: requestBody,
});
if (profile.value) {
profile.value.analysis = analysisResponse;
}
} catch (error) {
console.error("Error fetching AI Analysis from /api/analyze-asnaf:", error);
toast.error('Gagal memuat analisis AI dari server.');
if (profile.value) {
profile.value.analysis = {
hadKifayahPercentage: 'Ralat',
kategoriAsnaf: 'Ralat Server',
kategoriKeluarga: 'Ralat Server',
cadanganKategori: 'Ralat Server',
statusKelayakan: 'Ralat Server',
cadanganBantuan: [{ nama: 'Tidak dapat memuat cadangan bantuan', peratusan: 'Ralat' }],
ramalanJangkaMasaPulih: 'Ralat Server',
rumusan: 'Ralat Server'
};
}
} finally {
isLoadingAnalysis.value = false;
}
}
// Computed status color
const statusColor = computed(() => {
if (!profile.value) return '';
switch (profile.value.status) {
case 'Aktif': return 'success';
case 'Tidak Aktif': return 'danger';
case 'Dalam Semakan': return 'warning';
default: return 'secondary';
}
});
// Computed category color
const categoryColor = computed(() => {
if (!profile.value) return '';
switch (profile.value.kategori) {
case 'Fakir': return 'danger';
case 'Miskin': return 'warning';
case 'Mualaf': return 'info';
case 'Fi-sabilillah': return 'primary';
case 'Gharimin': return 'secondary';
case 'Ibnu Sabil': return 'success';
default: return 'primary';
}
});
// Page metadata
definePageMeta({
title: "Maklumat Asnaf",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/",
},
{
name: "BF-PRF",
path: "/BF-PRF",
},
{
name: "Asnaf",
path: "/BF-PRF/AS",
},
{
name: "Senarai",
path: "/BF-PRF/AS/LIST",
},
{
name: "Maklumat",
path: "/BF-PRF/AS/DETAIL",
},
],
});
// Navigation functions
function navigateToList() {
navigateTo('/BF-PRF/AS/LIST');
}
function navigateToEdit() {
navigateTo(`/BF-PRF/AS/UP/01?id=${profile.value.id}`);
}
function handleDelete() {
confirmDelete.value = true;
}
function confirmDeleteProfile() {
toast.success('Profil telah dipadamkan');
navigateTo('/BF-PRF/AS/LIST');
confirmDelete.value = false;
}
function cancelDelete() {
confirmDelete.value = false;
}
</script>
<template>
<div class="space-y-6">
<LayoutsBreadcrumb />
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center py-20">
<div class="text-center">
<Loading />
<p class="mt-4 text-gray-600">Memuat maklumat asnaf...</p>
</div>
</div>
<div v-else>
<!-- Header with actions -->
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-primary">{{ profile.nama }}</h1>
<rs-badge :variant="statusColor">{{ profile.status }}</rs-badge>
<rs-badge :variant="categoryColor">{{ profile.kategori }}</rs-badge>
</div>
<div class="flex items-center gap-2">
<rs-button variant="secondary-outline" @click="navigateToList">
<Icon name="mdi:arrow-left" size="18" class="mr-1" />
Kembali
</rs-button>
<rs-button variant="primary" @click="navigateToEdit">
<Icon name="mdi:pencil" size="18" class="mr-1" />
Kemaskini
</rs-button>
<rs-button variant="danger" @click="handleDelete">
<Icon name="mdi:delete" size="18" class="mr-1" />
Padam
</rs-button>
</div>
</div>
<!-- Profile Overview -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<!-- Profile Photo and Basic Info -->
<rs-card class="lg:col-span-1">
<div class="p-6 flex flex-col items-center">
<div class="w-32 h-32 rounded-full bg-gray-200 flex items-center justify-center mb-4 overflow-hidden">
<Icon name="mdi:account" size="64" class="text-gray-400" />
</div>
<h2 class="text-xl font-semibold text-center">{{ profile.nama }}</h2>
<p class="text-gray-500 text-center mb-4">{{ profile.id }}</p>
<div class="w-full text-center">
<rs-badge :variant="categoryColor" class="mb-2">{{ profile.kategori }}</rs-badge>
<p class="text-sm text-gray-600">Didaftarkan pada {{ new Date(profile.tarikhDaftar).toLocaleDateString('ms-MY') }}</p>
</div>
</div>
</rs-card>
<!-- Personal Information -->
<rs-card class="lg:col-span-2">
<template #header>
<div class="px-4 py-3">
<h3 class="text-lg font-semibold text-primary flex items-center">
<Icon name="mdi:account-details" size="20" class="mr-2" />
Maklumat Peribadi
</h3>
</div>
</template>
<template #body>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<div>
<h4 class="text-sm font-medium text-gray-500">No. Kad Pengenalan</h4>
<p>{{ profile.idNumber }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Jantina</h4>
<p>{{ profile.gender }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Tarikh Lahir</h4>
<p>{{ new Date(profile.birthDate).toLocaleDateString('ms-MY') }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Status Perkahwinan</h4>
<p>{{ profile.maritalStatus }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Pekerjaan</h4>
<p>{{ profile.occupation }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Pendapatan Bulanan</h4>
<p>RM {{ profile.monthlyIncome }}</p>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Contact Information -->
<rs-card class="lg:col-span-3">
<template #header>
<div class="px-4 py-3">
<h3 class="text-lg font-semibold text-primary flex items-center">
<Icon name="mdi:contacts" size="20" class="mr-2" />
Maklumat Perhubungan
</h3>
</div>
</template>
<template #body>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-6 gap-y-4">
<div>
<h4 class="text-sm font-medium text-gray-500">Alamat</h4>
<p>{{ profile.alamat || 'Tiada' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">No. Telefon</h4>
<p>{{ profile.telefon }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Emel</h4>
<p>{{ profile.email }}</p>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Tabbed Details -->
<rs-card>
<template #header>
<div class="px-4 py-3 border-b">
<div class="flex overflow-x-auto space-x-4">
<button
@click="activeTab = 'personal'"
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
:class="activeTab === 'personal' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
>
<Icon name="mdi:account-group" size="18" class="mr-1" />
Maklumat Keluarga
</button>
<button
@click="activeTab = 'income'"
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
:class="activeTab === 'income' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
>
<Icon name="mdi:cash" size="18" class="mr-1" />
Maklumat Pendapatan
</button>
<button
@click="activeTab = 'aid'"
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
:class="activeTab === 'aid' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
>
<Icon name="mdi:gift" size="18" class="mr-1" />
Maklumat Bantuan
</button>
<button
@click="activeTab = 'documents'"
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
:class="activeTab === 'documents' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
>
<Icon name="mdi:file-document" size="18" class="mr-1" />
Dokumen
</button>
<button
@click="activeTab = 'analysis'"
class="py-2 px-1 border-b-2 font-medium text-sm whitespace-nowrap transition-colors"
:class="activeTab === 'analysis' ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'"
>
<Icon name="mdi:chart-bar" size="18" class="mr-1" />
Analisis Data
</button>
</div>
</div>
</template>
<template #body>
<!-- Family Information Tab -->
<div v-if="activeTab === 'personal'" class="p-6">
<div v-if="profile.spouse" class="mb-8">
<h3 class="text-lg font-semibold text-primary mb-4">Maklumat Pasangan</h3>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-500">Nama</h4>
<p>{{ profile.spouse.name }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">No. Kad Pengenalan</h4>
<p>{{ profile.spouse.idNumber }}</p>
</div>
</div>
</div>
</div>
<div>
<h3 class="text-lg font-semibold text-primary mb-4">Tanggungan</h3>
<div v-if="profile.dependents && profile.dependents.length > 0">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Bil.</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nama</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Umur</th>
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Hubungan</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="(dependent, index) in profile.dependents" :key="index" class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">{{ index + 1 }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.name }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.age }}</td>
<td class="px-6 py-4 whitespace-nowrap">{{ dependent.relationship }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else class="bg-gray-50 p-6 rounded-lg text-center">
<Icon name="mdi:account-off" size="48" class="text-gray-400 mb-2" />
<p class="text-gray-500">Tiada tanggungan</p>
</div>
</div>
</div>
<!-- Income Information Tab -->
<div v-if="activeTab === 'income'" class="p-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<rs-card>
<div class="p-4 text-center">
<div class="mb-2">
<Icon name="mdi:cash-multiple" size="36" class="text-primary" />
</div>
<p class="text-sm text-gray-500">Pendapatan Bulanan</p>
<p class="text-xl font-bold text-primary">RM {{ profile.monthlyIncome }}</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<div class="mb-2">
<Icon name="mdi:cash-plus" size="36" class="text-primary" />
</div>
<p class="text-sm text-gray-500">Pendapatan Lain</p>
<p class="text-xl font-bold text-primary">RM {{ profile.otherIncome }}</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<div class="mb-2">
<Icon name="mdi:cash-register" size="36" class="text-primary" />
</div>
<p class="text-sm text-gray-500">Jumlah Pendapatan</p>
<p class="text-xl font-bold text-primary">RM {{ profile.totalIncome }}</p>
</div>
</rs-card>
</div>
<div class="mt-6">
<h3 class="text-lg font-semibold text-primary mb-4">Butiran Pendapatan</h3>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 class="text-sm font-medium text-gray-500">Pekerjaan</h4>
<p>{{ profile.occupation }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Pendapatan Bulanan</h4>
<p>RM {{ profile.monthlyIncome }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Pendapatan Lain</h4>
<p>RM {{ profile.otherIncome }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Jumlah Pendapatan</h4>
<p>RM {{ profile.totalIncome }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Aid Information Tab -->
<div v-if="activeTab === 'aid'" class="p-6">
<div class="bg-gray-50 p-6 rounded-lg text-center">
<Icon name="mdi:gift-off" size="48" class="text-gray-400 mb-2" />
<p class="text-gray-500">Tiada maklumat bantuan</p>
</div>
</div>
<!-- Documents Tab -->
<div v-if="activeTab === 'documents'" class="p-6">
<h3 class="text-lg font-semibold text-primary mb-4">Dokumen Sokongan</h3>
<div v-if="profile.documents && profile.documents.length > 0">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="(doc, index) in profile.documents" :key="index" class="border rounded-lg overflow-hidden">
<div class="bg-gray-50 p-4 flex items-center">
<Icon name="mdi:file-document" size="24" class="text-primary mr-3" />
<div>
<h4 class="font-medium">{{ doc.name }}</h4>
<p class="text-sm text-gray-500">{{ doc.size }}</p>
</div>
</div>
<div class="p-3 flex justify-end">
<rs-button variant="primary-text" size="sm">
<Icon name="mdi:download" size="16" class="mr-1" />
Muat Turun
</rs-button>
<rs-button variant="secondary-text" size="sm">
<Icon name="mdi:eye" size="16" class="mr-1" />
Papar
</rs-button>
</div>
</div>
</div>
</div>
<div v-else class="bg-gray-50 p-6 rounded-lg text-center">
<Icon name="mdi:file-document-off" size="48" class="text-gray-400 mb-2" />
<p class="text-gray-500">Tiada dokumen</p>
</div>
</div>
<!-- Analysis Tab -->
<div v-if="activeTab === 'analysis'" class="p-6">
<!-- Button to trigger AI Analysis -->
<div v-if="!profile.analysis && !isLoadingAnalysis" class="text-center mb-6">
<rs-button variant="primary" @click="fetchAIAnalysis" size="lg">
<Icon name="mdi:brain" size="20" class="mr-2" />
Jalankan Analisis AI
</rs-button>
<p class="text-sm text-gray-500 mt-2">Klik untuk mendapatkan penilaian berdasarkan data profil.</p>
</div>
<!-- Loading State for AI Analysis -->
<div v-if="isLoadingAnalysis" class="text-center py-10">
<Loading />
<p class="mt-4 text-gray-600">Analisis AI sedang dijalankan...</p>
<p class="text-sm text-gray-500">Sila tunggu sebentar.</p>
</div>
<!-- Display Analysis Results -->
<div v-if="profile.analysis && !isLoadingAnalysis" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- AI Analysis Main Column (takes 2/3 on lg screens) -->
<div class="lg:col-span-2 space-y-6">
<!-- Card 1: Analisis Had Kifayah & Kelayakan -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary p-4 border-b">Analisis Had Kifayah & Kelayakan (AI)</h3>
</template>
<template #body>
<div class="p-4 space-y-4">
<div>
<h4 class="text-sm font-medium text-gray-500 mb-1">Peratusan Had Kifayah</h4>
<div v-if="profile.analysis.hadKifayahPercentage === 'N/A' || profile.analysis.hadKifayahPercentage === 'Ralat'" class="text-gray-500">
{{ profile.analysis.hadKifayahPercentage }}
</div>
<div v-else class="relative pt-1">
<div class="overflow-hidden h-4 text-xs flex rounded bg-gray-200">
<div
:style="{ width: profile.analysis.hadKifayahPercentage }"
:class="{
'bg-red-500': parseInt(profile.analysis.hadKifayahPercentage) < 60,
'bg-yellow-500': parseInt(profile.analysis.hadKifayahPercentage) >= 60 && parseInt(profile.analysis.hadKifayahPercentage) < 80,
'bg-green-500': parseInt(profile.analysis.hadKifayahPercentage) >= 80 && parseInt(profile.analysis.hadKifayahPercentage) <= 100,
'bg-blue-500': parseInt(profile.analysis.hadKifayahPercentage) > 100
}"
class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center"
>
{{ profile.analysis.hadKifayahPercentage }}
</div>
</div>
</div>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Kategori Asnaf (AI)</h4>
<p>{{ profile.analysis.kategoriAsnaf }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Kategori Keluarga (AI)</h4>
<p>{{ profile.analysis.kategoriKeluarga }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Cadangan Kategori (AI)</h4>
<p>{{ profile.analysis.cadanganKategori }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Status Kelayakan (AI)</h4>
<p>{{ profile.analysis.statusKelayakan }}</p>
</div>
</div>
</template>
</rs-card>
<!-- Card 2: Cadangan & Rumusan AI -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary p-4 border-b">Cadangan & Rumusan (AI)</h3>
</template>
<template #body>
<div class="p-4 space-y-4">
<div>
<h4 class="text-sm font-medium text-gray-500">Cadangan Bantuan (AI)</h4>
<ul v-if="profile.analysis.cadanganBantuan && profile.analysis.cadanganBantuan.length > 0" class="list-disc list-inside space-y-1 mt-1">
<li v-for="(bantuan, index) in profile.analysis.cadanganBantuan" :key="index" class="text-gray-700">
{{ bantuan.nama }}
<span v-if="bantuan.peratusan && bantuan.peratusan !== 'Ralat'" class="font-semibold text-blue-600">({{ bantuan.peratusan }})</span>
<span v-else-if="bantuan.peratusan === 'Ralat'" class="text-red-500 text-xs">({{ bantuan.peratusan }})</span>
</li>
</ul>
<p v-else class="text-gray-500 mt-1">Tiada cadangan bantuan spesifik.</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Ramalan Jangka Masa Taraf Hidup Pulih (AI)</h4>
<p>{{ profile.analysis.ramalanJangkaMasaPulih }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Rumusan Keseluruhan (AI)</h4>
<div class="mt-1 p-3 bg-blue-50 border-l-4 border-blue-500 rounded-r-md">
<p class="whitespace-pre-line text-gray-700 text-sm">{{ profile.analysis.rumusan }}</p>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Original Data Column (takes 1/3 on lg screens) -->
<div class="lg:col-span-1">
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-gray-700 p-4 border-b">Ringkasan Profil (Data Asal)</h3>
</template>
<template #body>
<div class="p-4 space-y-3">
<div>
<h4 class="text-sm font-medium text-gray-500">Jenis Kategori (Asal)</h4>
<rs-badge :variant="categoryColor" class="mt-1">{{ profile.kategori }}</rs-badge>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Status Semasa (Asal)</h4>
<rs-badge :variant="statusColor" class="mt-1">{{ profile.status }}</rs-badge>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Jumlah Pendapatan (Asal)</h4>
<p>RM {{ profile.totalIncome }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">Jumlah Tanggungan (Asal)</h4>
<p>{{ profile.dependents.length }} orang</p>
</div>
</div>
</template>
</rs-card>
</div>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Delete Confirmation Modal -->
<rs-modal v-model="confirmDelete">
<template #header>
<div class="flex items-center">
<Icon name="mdi:alert-circle" size="24" class="text-red-500 mr-2" />
<h3 class="text-lg font-medium">Padam Profil</h3>
</div>
</template>
<template #default>
<div class="p-4">
<p class="mb-4">Adakah anda pasti ingin memadam profil ini?</p>
<p class="text-sm text-gray-500 mb-2">Nama: <span class="font-medium">{{ profile?.nama }}</span></p>
<p class="text-sm text-gray-500">ID: <span class="font-medium">{{ profile?.id }}</span></p>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="secondary-outline" @click="cancelDelete">
Batal
</rs-button>
<rs-button variant="danger" @click="confirmDeleteProfile">
Padam
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>

View File

@ -1,337 +0,0 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useAsnafMockData } from '~/composables/useAsnafMockData';
definePageMeta({
title: "Senarai Asnaf",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/",
},
{
name: "BF-PRF",
path: "/BF-PRF",
},
{
name: "Asnaf",
path: "/BF-PRF/AS",
},
{
name: "Senarai",
path: "/BF-PRF/AS/LIST",
},
],
});
// Get asnaf data from the composable
const { asnafProfiles, statistics, filterProfiles, categories, statuses } = useAsnafMockData();
// Table reactivity control
const tableKey = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const totalProfiles = ref(0);
const isLoading = ref(false);
// Search and filter variables
const searchQuery = ref('');
const selectedStatus = ref('All');
const selectedCategory = ref('All');
// Table data and fields
const tableData = computed(() => {
return filterProfiles(searchQuery.value, selectedStatus.value, selectedCategory.value);
});
const tableFields = [
{ field: 'no', label: 'No.' },
{ field: 'id', label: 'ID' },
{ field: 'nama', label: 'Nama' },
{ field: 'idNumber', label: 'No. ID' },
{ field: 'kategori', label: 'Kategori' },
{ field: 'status', label: 'Status' },
{ field: 'tindakan', label: 'Tindakan' }
];
// Generate table field and data mapping
const formattedTableData = computed(() => {
return tableData.value.map((profile, index) => ({
no: index + 1,
id: profile.id,
nama: profile.nama,
idNumber: profile.idNumber,
kategori: profile.kategori,
status: profile.status,
tindakan: profile.id
}));
});
// Helper functions
function getBadgeVariantForCategory(category) {
switch (category) {
case 'Fakir': return 'danger';
case 'Miskin': return 'warning';
case 'Mualaf': return 'info';
case 'Fi-sabilillah': return 'primary';
case 'Gharimin': return 'secondary';
case 'Ibnu Sabil': return 'success';
default: return 'primary';
}
}
function getBadgeVariantForStatus(status) {
switch (status) {
case 'Aktif': return 'success';
case 'Tidak Aktif': return 'danger';
case 'Dalam Semakan': return 'warning';
default: return 'secondary';
}
}
function navigateToDetail(id) {
console.log("Attempting to navigate to detail for ID:", id);
if (id) {
navigateTo(`/BF-PRF/AS/DETAIL/${id}`);
} else {
console.error("Navigation failed: ID is undefined or null");
}
}
function navigateToRegistration() {
navigateTo('/BF-PRF/AS/FR/01');
}
// Pagination
const handlePageChange = (newPage) => {
currentPage.value = newPage;
fetchProfiles();
};
// Fetch data
async function fetchProfiles() {
isLoading.value = true;
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 500));
totalProfiles.value = tableData.value.length;
isLoading.value = false;
tableKey.value++; // Force table re-render
}
// Lifecycle hooks
onMounted(() => {
fetchProfiles();
});
</script>
<template>
<div class="space-y-6">
<LayoutsBreadcrumb />
<!-- Header -->
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold text-primary">Senarai Asnaf</h1>
<rs-button
variant="primary"
class="flex items-center gap-2"
@click="navigateToRegistration"
>
<Icon name="mdi:plus" size="18" />
<span>Tambah Asnaf</span>
</rs-button>
</div>
<!-- Statistics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<rs-card class="transition-all duration-300 hover:shadow-lg">
<div class="p-4 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-blue-100 rounded-xl">
<Icon name="mdi:account-group" size="24" class="text-primary" />
</div>
<div>
<span class="block text-2xl font-bold text-primary">{{ statistics.total }}</span>
<span class="text-sm text-gray-600">Jumlah Asnaf</span>
</div>
</div>
</rs-card>
<rs-card class="transition-all duration-300 hover:shadow-lg">
<div class="p-4 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-green-100 rounded-xl">
<Icon name="mdi:check-circle" size="24" class="text-green-600" />
</div>
<div>
<span class="block text-2xl font-bold text-green-600">{{ statistics.active }}</span>
<span class="text-sm text-gray-600">Aktif</span>
</div>
</div>
</rs-card>
<rs-card class="transition-all duration-300 hover:shadow-lg">
<div class="p-4 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-yellow-100 rounded-xl">
<Icon name="mdi:clock-time-four" size="24" class="text-yellow-600" />
</div>
<div>
<span class="block text-2xl font-bold text-yellow-600">{{ statistics.review }}</span>
<span class="text-sm text-gray-600">Dalam Semakan</span>
</div>
</div>
</rs-card>
<rs-card class="transition-all duration-300 hover:shadow-lg">
<div class="p-4 flex items-center gap-4">
<div class="p-4 flex justify-center items-center bg-red-100 rounded-xl">
<Icon name="mdi:close-circle" size="24" class="text-red-600" />
</div>
<div>
<span class="block text-2xl font-bold text-red-600">{{ statistics.inactive }}</span>
<span class="text-sm text-gray-600">Tidak Aktif</span>
</div>
</div>
</rs-card>
</div>
<!-- Search and Filters -->
<rs-card>
<div class="p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Carian</label>
<div class="relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon name="mdi:magnify" size="18" class="text-gray-400" />
</div>
<input
v-model="searchQuery"
type="text"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
placeholder="Cari dengan nama atau ID..."
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select
v-model="selectedStatus"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
>
<option value="All">Semua Status</option>
<option v-for="status in statuses" :key="status" :value="status">
{{ status }}
</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Kategori</label>
<select
v-model="selectedCategory"
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
>
<option value="All">Semua Kategori</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
</div>
</div>
</rs-card>
<!-- Data Table -->
<rs-card>
<template #header>
<div class="px-4 py-3 flex items-center justify-between">
<h2 class="text-lg font-semibold text-primary">Senarai Asnaf</h2>
<span class="text-sm text-gray-500">
{{ tableData.length }} asnaf dijumpai
</span>
</div>
</template>
<template #body>
<div v-if="isLoading && tableData.length === 0" class="py-8 text-center">
<div class="flex justify-center">
<Icon name="mdi:loading" size="2rem" class="text-blue-500 animate-spin" />
</div>
<p class="mt-2 text-gray-600">Memuat data...</p>
</div>
<rs-table
v-else
class="mt-4"
:key="tableKey"
:data="formattedTableData"
:columns="tableFields"
:pageSize="pageSize"
:showNoColumn="true"
:options="{
variant: 'default',
hover: true,
striped: true,
bordered: true
}"
:current-page="currentPage"
:total-items="totalProfiles"
@page-change="handlePageChange"
>
<template v-slot:no="data">
{{ data.text }}
</template>
<template v-slot:id="data">
{{ data.text }}
</template>
<template v-slot:nama="data">
<div class="font-medium">{{ data.text }}</div>
</template>
<template v-slot:idNumber="data">
{{ data.text }}
</template>
<template v-slot:kategori="data">
<rs-badge :variant="getBadgeVariantForCategory(data.text)">{{ data.text }}</rs-badge>
</template>
<template v-slot:status="data">
<rs-badge :variant="getBadgeVariantForStatus(data.text)">
{{ data.text }}
</rs-badge>
</template>
<template v-slot:tindakan="data">
<div class="flex gap-2">
<rs-button
variant="primary"
size="sm"
class="!px-2 !py-1"
@click="() => {
navigateToDetail(data.value.tindakan);
}"
>
<Icon name="mdi:eye" size="1rem" class="mr-1" />
Lihat
</rs-button>
</div>
</template>
</rs-table>
<!-- Empty State -->
<div v-if="!isLoading && tableData.length === 0" class="text-center py-8">
<div class="flex justify-center mb-4">
<Icon name="mdi:magnify" size="4rem" class="text-gray-400" />
</div>
<h3 class="text-lg font-medium text-gray-500">Tiada Profil Ditemui</h3>
<p class="text-gray-500 mt-2">Sila cuba carian lain atau reset penapis.</p>
</div>
</template>
</rs-card>
</div>
</template>

View File

@ -99,235 +99,3 @@ model site_settings {
settingModifiedDate DateTime? @db.DateTime(0) settingModifiedDate DateTime? @db.DateTime(0)
siteLoginLogo String? @db.VarChar(500) siteLoginLogo String? @db.VarChar(500)
} }
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model notification_analytics {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
notification_id String @db.VarChar(36)
channel_type String @db.VarChar(20)
metric_type String @db.VarChar(30)
metric_value Int? @default(0)
recorded_at DateTime? @default(now()) @db.Timestamp(0)
metadata Json? @default(dbgenerated("(_utf8mb4\\'{}\\')"))
notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_analytics_ibfk_1")
@@index([notification_id], map: "idx_notification_analytics_notification_id")
}
model notification_categories {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
name String @db.VarChar(100)
value String @unique(map: "value") @db.VarChar(50)
description String? @db.Text
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
notifications notifications[]
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model notification_channels {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
notification_id String @db.VarChar(36)
channel_type String @db.VarChar(20)
is_enabled Boolean? @default(true)
created_at DateTime? @default(now()) @db.Timestamp(0)
notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_channels_ibfk_1")
@@index([notification_id], map: "notification_id")
}
model notification_delivery {
id Int @id @default(autoincrement())
notification_id Int
channel_type String
recipient String
is_success Boolean @default(false)
error_message String? @db.Text
attempts Int @default(0)
sent_at DateTime?
delivered_at DateTime?
created_at DateTime @default(now())
updated_at DateTime
}
model notification_delivery_config {
id Int @id @default(autoincrement())
channel_type String @unique
is_enabled Boolean @default(false)
provider String
provider_config Json @default(dbgenerated("(_utf8mb4\\'{}\\')"))
status String @default("Not Configured")
success_rate Float @default(0) @db.Float
created_at DateTime @default(now())
updated_at DateTime
created_by Int
updated_by Int
}
model notification_delivery_settings {
id Int @id @default(1)
auto_retry Boolean @default(true)
enable_fallback Boolean @default(true)
max_retries Int @default(3)
retry_delay Int @default(30)
priority String @default("normal")
enable_reports Boolean @default(true)
created_at DateTime @default(now())
updated_at DateTime
created_by Int
updated_by Int
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model notification_queue {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
notification_id String @db.VarChar(36)
recipient_id String @db.VarChar(36)
scheduled_for DateTime @db.Timestamp(0)
priority Int? @default(5)
attempts Int? @default(0)
max_attempts Int? @default(3)
status String? @default("queued") @db.VarChar(20)
last_attempt_at DateTime? @db.Timestamp(0)
error_message String? @db.Text
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_queue_ibfk_1")
notification_recipients notification_recipients @relation(fields: [recipient_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_queue_ibfk_2")
@@index([scheduled_for], map: "idx_notification_queue_scheduled_for")
@@index([status], map: "idx_notification_queue_status")
@@index([notification_id], map: "notification_id")
@@index([recipient_id], map: "recipient_id")
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model notification_recipients {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
notification_id String @db.VarChar(36)
user_id String @db.VarChar(36)
email String? @db.VarChar(255)
channel_type String @db.VarChar(20)
status String? @default("pending") @db.VarChar(20)
sent_at DateTime? @db.Timestamp(0)
delivered_at DateTime? @db.Timestamp(0)
opened_at DateTime? @db.Timestamp(0)
clicked_at DateTime? @db.Timestamp(0)
error_message String? @db.Text
ab_test_variant String? @db.VarChar(1)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
notification_queue notification_queue[]
notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_recipients_ibfk_1")
@@index([status], map: "idx_notification_recipients_status")
@@index([user_id], map: "idx_notification_recipients_user_id")
@@index([notification_id], map: "notification_id")
}
model notification_templates {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
name String @db.VarChar(100)
value String @unique(map: "value") @db.VarChar(50)
subject String? @db.VarChar(255)
email_content String? @db.Text
push_title String? @db.VarChar(100)
push_body String? @db.VarChar(300)
variables Json? @default(dbgenerated("(_utf8mb4\\'[]\\')"))
is_active Boolean? @default(true)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
notifications notifications[]
}
model notification_user_segments {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
notification_id String @db.VarChar(36)
segment_id String @db.VarChar(36)
created_at DateTime? @default(now()) @db.Timestamp(0)
notifications notifications @relation(fields: [notification_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "notification_user_segments_ibfk_1")
user_segments user_segments @relation(fields: [segment_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notification_user_segments_ibfk_2")
@@index([notification_id], map: "notification_id")
@@index([segment_id], map: "segment_id")
}
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
model notifications {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
title String @db.VarChar(255)
type String @db.VarChar(20)
priority String @db.VarChar(20)
category_id String? @db.VarChar(36)
status String? @default("draft") @db.VarChar(20)
delivery_type String @db.VarChar(20)
scheduled_at DateTime? @db.Timestamp(0)
timezone String? @default("UTC") @db.VarChar(50)
expires_at DateTime? @db.Timestamp(0)
enable_ab_testing Boolean? @default(false)
ab_test_split Int? @default(50)
ab_test_name String? @db.VarChar(100)
enable_tracking Boolean? @default(true)
audience_type String @db.VarChar(20)
specific_users String? @db.Text
user_status String? @db.VarChar(20)
registration_period String? @db.VarChar(50)
exclude_unsubscribed Boolean? @default(true)
respect_do_not_disturb Boolean? @default(true)
content_type String @db.VarChar(20)
template_id String? @db.VarChar(36)
email_subject String? @db.VarChar(255)
email_content String? @db.Text
call_to_action_text String? @db.VarChar(100)
call_to_action_url String? @db.Text
push_title String? @db.VarChar(100)
push_body String? @db.VarChar(300)
push_image_url String? @db.Text
estimated_reach Int? @default(0)
actual_sent Int? @default(0)
created_by String @db.VarChar(36)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
sent_at DateTime? @db.Timestamp(0)
notification_analytics notification_analytics[]
notification_channels notification_channels[]
notification_queue notification_queue[]
notification_recipients notification_recipients[]
notification_user_segments notification_user_segments[]
notification_categories notification_categories? @relation(fields: [category_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notifications_ibfk_1")
notification_templates notification_templates? @relation(fields: [template_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "notifications_ibfk_2")
@@index([category_id], map: "category_id")
@@index([created_by], map: "idx_notifications_created_by")
@@index([scheduled_at], map: "idx_notifications_scheduled_at")
@@index([status], map: "idx_notifications_status")
@@index([template_id], map: "template_id")
}
model user_notification_preferences {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
user_id String @db.VarChar(36)
channel_type String @db.VarChar(20)
category_value String? @db.VarChar(50)
is_enabled Boolean? @default(true)
do_not_disturb_start DateTime? @db.Time(0)
do_not_disturb_end DateTime? @db.Time(0)
timezone String? @default("UTC") @db.VarChar(50)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
@@unique([user_id, channel_type, category_value], map: "user_id")
@@index([user_id], map: "idx_user_notification_preferences_user_id")
}
model user_segments {
id String @id @default(dbgenerated("(uuid())")) @db.VarChar(36)
name String @db.VarChar(100)
value String @unique(map: "value") @db.VarChar(50)
description String? @db.Text
criteria Json
is_active Boolean? @default(true)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
notification_user_segments notification_user_segments[]
}