generated from corrad-software/corrad-af-2024
Removed Unecessary Pages
This commit is contained in:
parent
e37bbaeb46
commit
b8373092f3
1
docs/DMS_IMPLEMENTATION_PLAN.md
Normal file
1
docs/DMS_IMPLEMENTATION_PLAN.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -463,55 +463,55 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="font-medium text-gray-900 dark:text-gray-100">Department Performance</h2>
|
<h2 class="font-medium text-gray-900 dark:text-gray-100">Department Performance</h2>
|
||||||
<RsButton variant="secondary-outline" size="sm">
|
<RsButton variant="secondary-outline" size="sm">
|
||||||
<Icon name="mdi:download" class="w-4 h-4 mr-2" />
|
<Icon name="mdi:download" class="w-4 h-4 mr-2" />
|
||||||
Export Report
|
Export Report
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="overflow-auto h-full">
|
<div class="overflow-auto h-full">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0">
|
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
DEPARTMENT
|
DEPARTMENT
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
TOTAL REQUESTS
|
TOTAL REQUESTS
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
AVG RESPONSE TIME
|
AVG RESPONSE TIME
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
APPROVAL RATE
|
APPROVAL RATE
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
PENDING
|
PENDING
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||||
OVERDUE %
|
OVERDUE %
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<tr v-for="dept in deptPerformance" :key="dept.name" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
<tr v-for="dept in deptPerformance" :key="dept.name" class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ dept.name }}</div>
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ dept.name }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.total }}</div>
|
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.total }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatDuration(dept.avgResponseHours) }}</div>
|
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatDuration(dept.avgResponseHours) }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatPercentage(dept.approvalRate) }}</div>
|
<div class="text-sm text-gray-900 dark:text-gray-100">{{ formatPercentage(dept.approvalRate) }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.pending }}</div>
|
<div class="text-sm text-gray-900 dark:text-gray-100">{{ dept.pending }}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
@ -520,21 +520,21 @@ onMounted(() => {
|
|||||||
dept.overduePercentage > 0 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400' :
|
dept.overduePercentage > 0 ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400' :
|
||||||
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
{{ formatPercentage(dept.overduePercentage) }}
|
{{ formatPercentage(dept.overduePercentage) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<tr v-if="deptPerformance.length === 0">
|
<tr v-if="deptPerformance.length === 0">
|
||||||
<td colspan="6" class="px-6 py-8 text-center">
|
<td colspan="6" class="px-6 py-8 text-center">
|
||||||
<p class="text-gray-500 dark:text-gray-400">No department data available for the selected time period.</p>
|
<p class="text-gray-500 dark:text-gray-400">No department data available for the selected time period.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</RsCard>
|
</RsCard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,800 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useDesignSystem } from '~/composables/useDesignSystem';
|
|
||||||
import { useNotifications } from '~/composables/useNotifications';
|
|
||||||
import { useTouchInteractions } from '~/composables/useTouchInteractions';
|
|
||||||
|
|
||||||
// Page meta
|
|
||||||
definePageMeta({
|
|
||||||
title: "Design System Demo",
|
|
||||||
layout: "default"
|
|
||||||
});
|
|
||||||
|
|
||||||
// Components
|
|
||||||
import ResponsiveContainer from '~/components/base/ResponsiveContainer.vue';
|
|
||||||
import BaseModal from '~/components/base/BaseModal.vue';
|
|
||||||
import LoadingStates from '~/components/base/LoadingStates.vue';
|
|
||||||
import AdvancedDataTable from '~/components/base/AdvancedDataTable.vue';
|
|
||||||
import WindowsExplorerTree from '~/components/dms/explorer/WindowsExplorerTree.vue';
|
|
||||||
import NotificationDisplay from '~/components/base/NotificationDisplay.vue';
|
|
||||||
import RsButton from '~/components/RsButton.vue';
|
|
||||||
|
|
||||||
// Design system and utilities - wrapped in try-catch to handle SSR
|
|
||||||
let designSystem, notifications;
|
|
||||||
try {
|
|
||||||
designSystem = useDesignSystem();
|
|
||||||
notifications = useNotifications();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Design system initialization error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tokens, utils, dmsPatterns, statusColors } = designSystem || {};
|
|
||||||
const { success, error, warning, info, confirm, loading, dms } = notifications || {};
|
|
||||||
|
|
||||||
// Demo state
|
|
||||||
const showModal = ref(false);
|
|
||||||
const currentDemo = ref('overview');
|
|
||||||
const loadingDemo = ref(false);
|
|
||||||
const tableLoading = ref(false);
|
|
||||||
const treeLoading = ref(false);
|
|
||||||
const isClient = ref(false);
|
|
||||||
|
|
||||||
// Sample data for demos
|
|
||||||
const sampleTreeData = ref([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Documents',
|
|
||||||
type: 'folder',
|
|
||||||
parentId: null,
|
|
||||||
size: null,
|
|
||||||
modifiedAt: '2024-01-15T10:30:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'Projects',
|
|
||||||
type: 'folder',
|
|
||||||
parentId: '1',
|
|
||||||
size: null,
|
|
||||||
modifiedAt: '2024-01-14T15:45:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Project_Plan.pdf',
|
|
||||||
type: 'file',
|
|
||||||
parentId: '2',
|
|
||||||
size: 2048576,
|
|
||||||
modifiedAt: '2024-01-13T09:15:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
name: 'Budget.xlsx',
|
|
||||||
type: 'file',
|
|
||||||
parentId: '2',
|
|
||||||
size: 1024000,
|
|
||||||
modifiedAt: '2024-01-12T14:20:00Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
name: 'Archive',
|
|
||||||
type: 'folder',
|
|
||||||
parentId: null,
|
|
||||||
size: null,
|
|
||||||
modifiedAt: '2024-01-10T11:00:00Z'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const sampleTableData = ref([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Annual_Report_2024.pdf',
|
|
||||||
type: 'PDF Document',
|
|
||||||
size: 2048576,
|
|
||||||
owner: 'John Doe',
|
|
||||||
modified: '2024-01-15T10:30:00Z',
|
|
||||||
status: 'active',
|
|
||||||
version: '2.1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Q4_Budget_Analysis.xlsx',
|
|
||||||
type: 'Spreadsheet',
|
|
||||||
size: 1536000,
|
|
||||||
owner: 'Jane Smith',
|
|
||||||
modified: '2024-01-14T15:45:00Z',
|
|
||||||
status: 'active',
|
|
||||||
version: '1.3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: 'Meeting_Notes_Draft.docx',
|
|
||||||
type: 'Word Document',
|
|
||||||
size: 512000,
|
|
||||||
owner: 'Mike Johnson',
|
|
||||||
modified: '2024-01-13T09:15:00Z',
|
|
||||||
status: 'inactive',
|
|
||||||
version: '1.0'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Project_Presentation.pptx',
|
|
||||||
type: 'Presentation',
|
|
||||||
size: 3072000,
|
|
||||||
owner: 'Sarah Wilson',
|
|
||||||
modified: '2024-01-12T14:20:00Z',
|
|
||||||
status: 'active',
|
|
||||||
version: '1.7'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tableColumns = ref([
|
|
||||||
{
|
|
||||||
key: 'name',
|
|
||||||
title: 'Document Name',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'type',
|
|
||||||
title: 'Type',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'size',
|
|
||||||
title: 'Size',
|
|
||||||
type: 'filesize',
|
|
||||||
sortable: true,
|
|
||||||
align: 'right'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'owner',
|
|
||||||
title: 'Owner',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'modified',
|
|
||||||
title: 'Modified',
|
|
||||||
type: 'date',
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
title: 'Status',
|
|
||||||
type: 'status',
|
|
||||||
sortable: true,
|
|
||||||
filterable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'version',
|
|
||||||
title: 'Version',
|
|
||||||
sortable: true,
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const tableActions = ref([
|
|
||||||
{
|
|
||||||
key: 'view',
|
|
||||||
icon: 'mdi:eye',
|
|
||||||
title: 'View Document',
|
|
||||||
variant: 'secondary-outline'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'edit',
|
|
||||||
icon: 'mdi:pencil',
|
|
||||||
title: 'Edit Document',
|
|
||||||
variant: 'primary'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
icon: 'mdi:delete',
|
|
||||||
title: 'Delete Document',
|
|
||||||
variant: 'danger'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Demo sections
|
|
||||||
const demoSections = ref([
|
|
||||||
{ id: 'overview', title: 'Design System Overview', icon: 'mdi:palette' },
|
|
||||||
{ id: 'components', title: 'Base Components', icon: 'mdi:view-grid' },
|
|
||||||
{ id: 'loading', title: 'Loading States', icon: 'mdi:loading' },
|
|
||||||
{ id: 'notifications', title: 'Notifications', icon: 'mdi:bell' },
|
|
||||||
{ id: 'table', title: 'Data Tables', icon: 'mdi:table' },
|
|
||||||
{ id: 'tree', title: 'File Explorer', icon: 'mdi:file-tree' },
|
|
||||||
{ id: 'responsive', title: 'Responsive Design', icon: 'mdi:responsive' },
|
|
||||||
{ id: 'touch', title: 'Touch Interactions', icon: 'mdi:gesture-tap' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Demo functions
|
|
||||||
const showNotificationDemo = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'success':
|
|
||||||
success('Document uploaded successfully!');
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
error('Failed to save document. Please try again.');
|
|
||||||
break;
|
|
||||||
case 'warning':
|
|
||||||
warning('Document will be auto-saved in 30 seconds.');
|
|
||||||
break;
|
|
||||||
case 'info':
|
|
||||||
info('New version of the document is available.');
|
|
||||||
break;
|
|
||||||
case 'loading':
|
|
||||||
loading(
|
|
||||||
new Promise(resolve => setTimeout(resolve, 3000)),
|
|
||||||
{
|
|
||||||
title: 'Processing Document',
|
|
||||||
message: 'Please wait while we process your document...'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'confirm':
|
|
||||||
confirm({
|
|
||||||
title: 'Delete Document',
|
|
||||||
message: 'Are you sure you want to permanently delete this document?',
|
|
||||||
dangerous: true
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showDMSNotificationDemo = (type) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'upload':
|
|
||||||
dms.documentUploaded('Annual_Report_2024.pdf');
|
|
||||||
break;
|
|
||||||
case 'share':
|
|
||||||
dms.documentShared('Budget_Analysis.xlsx', ['john.doe', 'jane.smith']);
|
|
||||||
break;
|
|
||||||
case 'sync':
|
|
||||||
dms.syncStarted();
|
|
||||||
setTimeout(() => dms.syncCompleted(42), 2000);
|
|
||||||
break;
|
|
||||||
case 'version':
|
|
||||||
dms.newVersionCreated('Project_Plan.pdf', '2.1');
|
|
||||||
break;
|
|
||||||
case 'access':
|
|
||||||
dms.accessGranted('Financial Reports folder');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTableLoading = () => {
|
|
||||||
tableLoading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
tableLoading.value = false;
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleTreeLoading = () => {
|
|
||||||
treeLoading.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
treeLoading.value = false;
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Touch interaction demo
|
|
||||||
const setupTouchDemo = () => {
|
|
||||||
const { setupTouchInteractions, onSwipeLeft, onSwipeRight, onTap, onLongPress } = useTouchInteractions();
|
|
||||||
|
|
||||||
const touchArea = document.getElementById('touch-demo-area');
|
|
||||||
if (touchArea) {
|
|
||||||
setupTouchInteractions(touchArea);
|
|
||||||
|
|
||||||
onSwipeLeft(() => {
|
|
||||||
info('Swiped left! In DMS, this could delete a file.');
|
|
||||||
});
|
|
||||||
|
|
||||||
onSwipeRight(() => {
|
|
||||||
info('Swiped right! In DMS, this could open file details.');
|
|
||||||
});
|
|
||||||
|
|
||||||
onTap(() => {
|
|
||||||
info('Tapped! This would select a file.');
|
|
||||||
});
|
|
||||||
|
|
||||||
onLongPress(() => {
|
|
||||||
info('Long pressed! This would show the context menu.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// File size formatter
|
|
||||||
const formatFileSize = (bytes) => {
|
|
||||||
if (!bytes) return '0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Date formatter
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
return new Date(dateString).toLocaleDateString();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
onMounted(() => {
|
|
||||||
setupTouchDemo();
|
|
||||||
isClient.value = true;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<!-- Loading state for SSR -->
|
|
||||||
<div v-if="!isClient" class="flex items-center justify-center min-h-screen">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<p class="text-gray-600">Loading Design System Demo...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Client-only content -->
|
|
||||||
<ClientOnly>
|
|
||||||
<ResponsiveContainer v-if="isClient" layout="sidebar" class="design-system-demo">
|
|
||||||
<!-- Sidebar Navigation -->
|
|
||||||
<template #sidebar>
|
|
||||||
<div class="h-full flex flex-col">
|
|
||||||
<div class="p-6 border-b border-gray-200 dark:border-gray-600">
|
|
||||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
DMS Design System
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Interactive component demonstrations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="flex-1 p-4 space-y-2">
|
|
||||||
<button
|
|
||||||
v-for="section in demoSections"
|
|
||||||
:key="section.id"
|
|
||||||
@click="currentDemo = section.id"
|
|
||||||
:class="[
|
|
||||||
'w-full flex items-center space-x-3 px-3 py-2 rounded-md text-left transition-colors',
|
|
||||||
currentDemo === section.id
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<Icon :name="section.icon" class="w-5 h-5" />
|
|
||||||
<span>{{ section.title }}</span>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="space-y-8">
|
|
||||||
<!-- Design System Overview -->
|
|
||||||
<section v-if="currentDemo === 'overview'" class="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Design System Overview
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400">
|
|
||||||
A comprehensive design system for the Document Management System featuring consistent components, responsive design, and enhanced user experience.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color Palette -->
|
|
||||||
<div v-if="statusColors" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Status Colors
|
|
||||||
</h3>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
||||||
<div v-for="(color, status) in statusColors" :key="status" class="text-center">
|
|
||||||
<div
|
|
||||||
:class="[color.bg, color.border, 'w-16 h-16 rounded-lg border-2 mx-auto mb-2']"
|
|
||||||
></div>
|
|
||||||
<p class="text-sm font-medium capitalize">{{ status }}</p>
|
|
||||||
<p :class="[color.text, 'text-xs']">{{ status }} state</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Typography Scale -->
|
|
||||||
<div v-if="tokens?.typography" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Typography Scale
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div v-for="(size, key) in tokens.typography.sizes" :key="key" class="flex items-center space-x-4">
|
|
||||||
<span class="w-12 text-xs text-gray-500 dark:text-gray-400">{{ key }}</span>
|
|
||||||
<span :style="{ fontSize: size }" class="text-gray-900 dark:text-gray-100">
|
|
||||||
The quick brown fox jumps over the lazy dog
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spacing System -->
|
|
||||||
<div v-if="tokens?.spacing" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Spacing System
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div v-for="(space, key) in tokens.spacing" :key="key" class="flex items-center space-x-4">
|
|
||||||
<span class="w-12 text-xs text-gray-500 dark:text-gray-400">{{ key }}</span>
|
|
||||||
<div
|
|
||||||
class="bg-blue-200 dark:bg-blue-300 rounded"
|
|
||||||
:style="{ width: space, height: '20px' }"
|
|
||||||
></div>
|
|
||||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ space }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Base Components -->
|
|
||||||
<section v-if="currentDemo === 'components'" class="space-y-6">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Base Components
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Button Variants
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<RsButton variant="primary">Primary</RsButton>
|
|
||||||
<RsButton variant="secondary">Secondary</RsButton>
|
|
||||||
<RsButton variant="secondary-outline">Secondary Outline</RsButton>
|
|
||||||
<RsButton variant="danger">Danger</RsButton>
|
|
||||||
<RsButton variant="success">Success</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Demo -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Enhanced Modal
|
|
||||||
</h3>
|
|
||||||
<RsButton @click="showModal = true">Show Modal</RsButton>
|
|
||||||
|
|
||||||
<BaseModal
|
|
||||||
v-model:visible="showModal"
|
|
||||||
title="Enhanced Modal Dialog"
|
|
||||||
size="lg"
|
|
||||||
@confirm="success && success('Modal confirmed!')"
|
|
||||||
@cancel="info && info('Modal cancelled')"
|
|
||||||
>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<p class="text-gray-600 dark:text-gray-400">
|
|
||||||
This is an enhanced modal with consistent styling, animations, and responsive behavior.
|
|
||||||
</p>
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-4">
|
|
||||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Features:</h4>
|
|
||||||
<ul class="list-disc list-inside space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<li>Responsive sizing and positioning</li>
|
|
||||||
<li>Keyboard navigation and focus management</li>
|
|
||||||
<li>Smooth entrance and exit animations</li>
|
|
||||||
<li>Dark mode support</li>
|
|
||||||
<li>Accessibility features</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseModal>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Loading States -->
|
|
||||||
<section v-if="currentDemo === 'loading'" class="space-y-6">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Loading States
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Spinners -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Spinner Variants
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<LoadingStates type="spinner" size="sm" message="Small spinner" />
|
|
||||||
<LoadingStates type="spinner" size="md" message="Medium spinner" />
|
|
||||||
<LoadingStates type="spinner" size="lg" message="Large spinner" />
|
|
||||||
<LoadingStates type="pulse" message="Pulse animation" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Skeleton Loaders -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Skeleton Loaders
|
|
||||||
</h3>
|
|
||||||
<LoadingStates type="skeleton-list" :count="3" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table Skeleton -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 lg:col-span-2">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Table Skeleton
|
|
||||||
</h3>
|
|
||||||
<LoadingStates type="skeleton-table" :count="4" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card Skeleton -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6 lg:col-span-2">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Card Skeletons
|
|
||||||
</h3>
|
|
||||||
<LoadingStates type="skeleton-card" :count="3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Notifications -->
|
|
||||||
<section v-if="currentDemo === 'notifications'" class="space-y-6">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Notification System
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Basic Notifications -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Basic Notifications
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<RsButton @click="showNotificationDemo('success')" variant="success">
|
|
||||||
Success Toast
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showNotificationDemo('error')" variant="danger">
|
|
||||||
Error Toast
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showNotificationDemo('warning')" variant="secondary">
|
|
||||||
Warning Toast
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showNotificationDemo('info')" variant="primary">
|
|
||||||
Info Toast
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showNotificationDemo('loading')" variant="secondary-outline">
|
|
||||||
Loading Toast
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showNotificationDemo('confirm')" variant="secondary-outline">
|
|
||||||
Confirmation Dialog
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- DMS-Specific Notifications -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
DMS-Specific Notifications
|
|
||||||
</h3>
|
|
||||||
<div class="flex flex-wrap gap-4">
|
|
||||||
<RsButton @click="showDMSNotificationDemo('upload')" variant="success">
|
|
||||||
Document Uploaded
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showDMSNotificationDemo('share')" variant="primary">
|
|
||||||
Document Shared
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showDMSNotificationDemo('sync')" variant="secondary">
|
|
||||||
Sync Operation
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showDMSNotificationDemo('version')" variant="secondary-outline">
|
|
||||||
New Version
|
|
||||||
</RsButton>
|
|
||||||
<RsButton @click="showDMSNotificationDemo('access')" variant="success">
|
|
||||||
Access Granted
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Data Table -->
|
|
||||||
<section v-if="currentDemo === 'table'" class="space-y-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Advanced Data Table
|
|
||||||
</h1>
|
|
||||||
<RsButton @click="toggleTableLoading" :disabled="tableLoading">
|
|
||||||
{{ tableLoading ? 'Loading...' : 'Demo Loading' }}
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Advanced Data Table
|
|
||||||
</h3>
|
|
||||||
<div class="h-96 overflow-hidden">
|
|
||||||
<AdvancedDataTable
|
|
||||||
:data="sampleTableData"
|
|
||||||
:columns="tableColumns"
|
|
||||||
:actions="tableActions"
|
|
||||||
:loading="tableLoading"
|
|
||||||
:pagination="{ page: 1, pageSize: 10, total: 4, showSizeChanger: true }"
|
|
||||||
selectable
|
|
||||||
@action-click="(action, row) => info && info(`Action '${action}' clicked for ${row.name}`)"
|
|
||||||
@row-click="(row) => info && info(`Clicked on ${row.name}`)"
|
|
||||||
@selection-change="(selected) => info && info(`Selected ${selected.length} items`)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- File Explorer Tree -->
|
|
||||||
<section v-if="currentDemo === 'tree'" class="space-y-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Windows Explorer Tree
|
|
||||||
</h1>
|
|
||||||
<RsButton @click="toggleTreeLoading" :disabled="treeLoading">
|
|
||||||
{{ treeLoading ? 'Loading...' : 'Demo Loading' }}
|
|
||||||
</RsButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden">
|
|
||||||
<div class="h-96">
|
|
||||||
<WindowsExplorerTree
|
|
||||||
:data="sampleTreeData"
|
|
||||||
:loading="treeLoading"
|
|
||||||
@node-click="(node) => info && info(`Clicked on ${node.name}`)"
|
|
||||||
@node-rename="(data) => success && success(`Renamed to ${data.newName}`)"
|
|
||||||
@folder-create="(data) => success && success(`Created folder: ${data.name}`)"
|
|
||||||
@file-upload="() => dms && dms.documentUploaded('NewDocument.pdf')"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Responsive Design -->
|
|
||||||
<section v-if="currentDemo === 'responsive'" class="space-y-6">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Responsive Design
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div v-if="utils?.responsive" class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Breakpoint System
|
|
||||||
</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
<div v-for="(width, breakpoint) in utils.responsive" :key="breakpoint" class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<h4 class="font-medium text-gray-900 dark:text-gray-100">{{ breakpoint }}</h4>
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">{{ width }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Layout Patterns
|
|
||||||
</h3>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Stack Layout</h4>
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Item 1</div>
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Item 2</div>
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Item 3</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">Grid Layout</h4>
|
|
||||||
<div class="grid grid-cols-3 gap-2">
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Grid 1</div>
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Grid 2</div>
|
|
||||||
<div class="bg-blue-200 dark:bg-blue-300 p-2 rounded">Grid 3</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Touch Interactions -->
|
|
||||||
<section v-if="currentDemo === 'touch'" class="space-y-6">
|
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
||||||
Touch Interactions
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 p-6">
|
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
|
|
||||||
Interactive Touch Area
|
|
||||||
</h3>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
Try different touch gestures on the area below (works on mobile devices):
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="touch-demo-area"
|
|
||||||
class="bg-gradient-to-br from-blue-100 to-blue-200 dark:from-blue-200 dark:to-blue-300
|
|
||||||
border-2 border-dashed border-blue-500 rounded-lg p-8 text-center"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:gesture-tap" class="w-16 h-16 mx-auto mb-4 text-blue-600" />
|
|
||||||
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
||||||
Touch Gesture Area
|
|
||||||
</h4>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 text-sm">
|
|
||||||
Tap, long press, swipe left/right to test interactions
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
||||||
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<Icon name="mdi:gesture-tap" class="w-6 h-6 mx-auto mb-1 text-blue-500" />
|
|
||||||
<p class="font-medium">Tap</p>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">Select file</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<Icon name="mdi:gesture-tap-hold" class="w-6 h-6 mx-auto mb-1 text-green-500" />
|
|
||||||
<p class="font-medium">Long Press</p>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">Context menu</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<Icon name="mdi:gesture-swipe-left" class="w-6 h-6 mx-auto mb-1 text-red-500" />
|
|
||||||
<p class="font-medium">Swipe Left</p>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">Delete action</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
||||||
<Icon name="mdi:gesture-swipe-right" class="w-6 h-6 mx-auto mb-1 text-purple-500" />
|
|
||||||
<p class="font-medium">Swipe Right</p>
|
|
||||||
<p class="text-gray-500 dark:text-gray-400">File details</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notification Display Component -->
|
|
||||||
<NotificationDisplay />
|
|
||||||
</ResponsiveContainer>
|
|
||||||
|
|
||||||
<template #fallback>
|
|
||||||
<div class="flex items-center justify-center min-h-screen">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<p class="text-gray-600">Loading components...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.design-system-demo {
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Touch demo area enhancements */
|
|
||||||
#touch-demo-area {
|
|
||||||
user-select: none;
|
|
||||||
touch-action: manipulation;
|
|
||||||
min-height: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Demo section transitions */
|
|
||||||
.demo-section {
|
|
||||||
animation: fadeInUp 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.grid.grid-cols-2.md\\:grid-cols-4 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid.grid-cols-1.lg\\:grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,585 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
import { useDmsStore } from '~/stores/dms';
|
|
||||||
|
|
||||||
// Define page metadata
|
|
||||||
definePageMeta({
|
|
||||||
title: "Role Management",
|
|
||||||
middleware: ["auth"],
|
|
||||||
requiresAuth: true,
|
|
||||||
breadcrumb: [
|
|
||||||
{
|
|
||||||
name: "DMS",
|
|
||||||
path: "/dms",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Role Management",
|
|
||||||
path: "/dms/role-management",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store
|
|
||||||
const dmsStore = useDmsStore();
|
|
||||||
|
|
||||||
// State variables
|
|
||||||
const isLoading = ref(true);
|
|
||||||
const users = ref([]);
|
|
||||||
const roles = ref([]);
|
|
||||||
const activeTab = ref('users');
|
|
||||||
const searchQuery = ref('');
|
|
||||||
const hasError = ref(false);
|
|
||||||
const errorMessage = ref('');
|
|
||||||
|
|
||||||
// Filtered users
|
|
||||||
const filteredUsers = computed(() => {
|
|
||||||
if (!searchQuery.value) return users.value;
|
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
|
||||||
return users.value.filter(user =>
|
|
||||||
user.name.toLowerCase().includes(query) ||
|
|
||||||
user.email.toLowerCase().includes(query) ||
|
|
||||||
user.department.toLowerCase().includes(query) ||
|
|
||||||
user.roles.some(role => role.toLowerCase().includes(query))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const loadUsers = async () => {
|
|
||||||
isLoading.value = true;
|
|
||||||
hasError.value = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// This would be an actual API call in production
|
|
||||||
// For demo, we'll simulate it with mock data
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
|
||||||
|
|
||||||
users.value = [
|
|
||||||
{
|
|
||||||
id: 'user1',
|
|
||||||
name: 'Aiman Fakhrullah',
|
|
||||||
email: 'aiman@example.com',
|
|
||||||
department: 'JKR Bahagian Kejuruteraan Awam Cawangan Kota Bharu',
|
|
||||||
roles: ['engineer'],
|
|
||||||
lastLogin: '2023-12-15T08:30:00Z',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user2',
|
|
||||||
name: 'Ahmad Zaki',
|
|
||||||
email: 'ahmad@example.com',
|
|
||||||
department: 'JKR Bahagian Kewangan',
|
|
||||||
roles: ['finance', 'approver'],
|
|
||||||
lastLogin: '2023-12-14T14:45:00Z',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user3',
|
|
||||||
name: 'Siti Aminah',
|
|
||||||
email: 'siti@example.com',
|
|
||||||
department: 'JKR Cawangan Kuala Terengganu',
|
|
||||||
roles: ['manager', 'approver'],
|
|
||||||
lastLogin: '2023-12-13T09:15:00Z',
|
|
||||||
status: 'active'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'admin1',
|
|
||||||
name: 'Admin User',
|
|
||||||
email: 'admin@example.com',
|
|
||||||
department: 'IT Department',
|
|
||||||
roles: ['admin'],
|
|
||||||
lastLogin: '2023-12-15T10:00:00Z',
|
|
||||||
status: 'active'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load users:', error);
|
|
||||||
hasError.value = true;
|
|
||||||
errorMessage.value = 'Failed to load users. Please try again.';
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadRoles = async () => {
|
|
||||||
isLoading.value = true;
|
|
||||||
hasError.value = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// This would be an actual API call to Authentik in production
|
|
||||||
// For demo, we'll simulate it with mock data
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
|
||||||
|
|
||||||
roles.value = [
|
|
||||||
{
|
|
||||||
id: 'admin',
|
|
||||||
name: 'Administrator',
|
|
||||||
description: 'Full system access',
|
|
||||||
userCount: 1,
|
|
||||||
permissions: {
|
|
||||||
documents: {
|
|
||||||
view: true,
|
|
||||||
edit: true,
|
|
||||||
delete: true,
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
download: true
|
|
||||||
},
|
|
||||||
cabinets: {
|
|
||||||
view: true,
|
|
||||||
create: true,
|
|
||||||
edit: true,
|
|
||||||
delete: true
|
|
||||||
},
|
|
||||||
accessRequests: {
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
viewAll: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'manager',
|
|
||||||
name: 'Manager',
|
|
||||||
description: 'Department management',
|
|
||||||
userCount: 1,
|
|
||||||
permissions: {
|
|
||||||
documents: {
|
|
||||||
view: true,
|
|
||||||
edit: true,
|
|
||||||
delete: false,
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
download: true
|
|
||||||
},
|
|
||||||
cabinets: {
|
|
||||||
view: true,
|
|
||||||
create: true,
|
|
||||||
edit: true,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
accessRequests: {
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
viewAll: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'engineer',
|
|
||||||
name: 'Engineer',
|
|
||||||
description: 'Engineering staff',
|
|
||||||
userCount: 1,
|
|
||||||
permissions: {
|
|
||||||
documents: {
|
|
||||||
view: true,
|
|
||||||
edit: true,
|
|
||||||
delete: false,
|
|
||||||
approve: false,
|
|
||||||
reject: false,
|
|
||||||
download: true
|
|
||||||
},
|
|
||||||
cabinets: {
|
|
||||||
view: true,
|
|
||||||
create: false,
|
|
||||||
edit: false,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
accessRequests: {
|
|
||||||
approve: false,
|
|
||||||
reject: false,
|
|
||||||
viewAll: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'finance',
|
|
||||||
name: 'Finance',
|
|
||||||
description: 'Finance department staff',
|
|
||||||
userCount: 1,
|
|
||||||
permissions: {
|
|
||||||
documents: {
|
|
||||||
view: true,
|
|
||||||
edit: true,
|
|
||||||
delete: false,
|
|
||||||
approve: false,
|
|
||||||
reject: false,
|
|
||||||
download: true
|
|
||||||
},
|
|
||||||
cabinets: {
|
|
||||||
view: true,
|
|
||||||
create: false,
|
|
||||||
edit: false,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
accessRequests: {
|
|
||||||
approve: false,
|
|
||||||
reject: false,
|
|
||||||
viewAll: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'approver',
|
|
||||||
name: 'Approver',
|
|
||||||
description: 'Can approve access requests',
|
|
||||||
userCount: 2,
|
|
||||||
permissions: {
|
|
||||||
documents: {
|
|
||||||
view: true,
|
|
||||||
edit: false,
|
|
||||||
delete: false,
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
download: false
|
|
||||||
},
|
|
||||||
cabinets: {
|
|
||||||
view: true,
|
|
||||||
create: false,
|
|
||||||
edit: false,
|
|
||||||
delete: false
|
|
||||||
},
|
|
||||||
accessRequests: {
|
|
||||||
approve: true,
|
|
||||||
reject: true,
|
|
||||||
viewAll: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load roles:', error);
|
|
||||||
hasError.value = true;
|
|
||||||
errorMessage.value = 'Failed to load roles. Please try again.';
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const switchTab = (tab) => {
|
|
||||||
activeTab.value = tab;
|
|
||||||
|
|
||||||
if (tab === 'users' && users.value.length === 0) {
|
|
||||||
loadUsers();
|
|
||||||
} else if (tab === 'roles' && roles.value.length === 0) {
|
|
||||||
loadRoles();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDateTime = (dateString) => {
|
|
||||||
if (!dateString) return 'Never';
|
|
||||||
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
const getSvgIcon = (iconName) => {
|
|
||||||
const icons = {
|
|
||||||
'users': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>`,
|
|
||||||
'roles': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path></svg>`,
|
|
||||||
'search': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>`,
|
|
||||||
'add': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
|
|
||||||
'edit': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
|
||||||
'trash': `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`
|
|
||||||
};
|
|
||||||
|
|
||||||
return icons[iconName] || '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle hooks
|
|
||||||
onMounted(() => {
|
|
||||||
loadUsers();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="dms-role-management">
|
|
||||||
<LayoutsBreadcrumb />
|
|
||||||
|
|
||||||
<rs-card class="h-full">
|
|
||||||
<template #body>
|
|
||||||
<div class="h-full flex flex-col">
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div v-if="isLoading" class="flex items-center justify-center h-full">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400">Loading role management...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error State -->
|
|
||||||
<div v-else-if="hasError" class="flex items-center justify-center h-full">
|
|
||||||
<div class="text-center p-6">
|
|
||||||
<div class="text-red-500 text-5xl mb-4">⚠️</div>
|
|
||||||
<h2 class="text-xl font-semibold text-red-600 mb-2">Error Loading Data</h2>
|
|
||||||
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ errorMessage }}</p>
|
|
||||||
<rs-button @click="activeTab === 'users' ? loadUsers() : loadRoles()" variant="primary">
|
|
||||||
Retry
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div v-else class="h-full flex flex-col">
|
|
||||||
<!-- Header with authentik info -->
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-900/20 border-b border-indigo-200 dark:border-indigo-800">
|
|
||||||
<div class="px-6 py-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="w-10 h-10 rounded bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mr-3">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600 dark:text-indigo-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-lg font-medium text-indigo-900 dark:text-indigo-100">Authentik Integration</h2>
|
|
||||||
<p class="text-sm text-indigo-600 dark:text-indigo-300">Role-Based Access Control Management</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs Navigation -->
|
|
||||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div class="px-6">
|
|
||||||
<nav class="-mb-px flex space-x-6" aria-label="Tabs">
|
|
||||||
<button
|
|
||||||
v-for="tab in [{ id: 'users', label: 'Users', icon: 'users' }, { id: 'roles', label: 'Roles', icon: 'roles' }]"
|
|
||||||
:key="tab.id"
|
|
||||||
@click="switchTab(tab.id)"
|
|
||||||
class="py-4 px-1 border-b-2 font-medium text-sm transition-colors"
|
|
||||||
:class="[
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:border-gray-600'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span v-html="getSvgIcon(tab.icon)" class="mr-2"></span>
|
|
||||||
<span>{{ tab.label }}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Bar -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4">
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<!-- Search -->
|
|
||||||
<div class="relative">
|
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
||||||
<span v-html="getSvgIcon('search')" class="text-gray-400 dark:text-gray-500"></span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
v-model="searchQuery"
|
|
||||||
class="pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500 w-64"
|
|
||||||
placeholder="Search..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Actions -->
|
|
||||||
<div>
|
|
||||||
<rs-button variant="primary" class="flex items-center">
|
|
||||||
<span v-html="getSvgIcon('add')" class="mr-1.5"></span>
|
|
||||||
<span>{{ activeTab === 'users' ? 'Add User' : 'Add Role' }}</span>
|
|
||||||
</rs-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab Content -->
|
|
||||||
<div class="flex-1 p-4 overflow-auto">
|
|
||||||
<!-- Users Tab -->
|
|
||||||
<div v-if="activeTab === 'users'" class="h-full">
|
|
||||||
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<thead class="bg-gray-50 dark:bg-gray-900/10">
|
|
||||||
<tr>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
User
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Department
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Roles
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Last Login
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<tr v-for="user in filteredUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-900/10">
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-gray-500 dark:text-gray-400">
|
|
||||||
{{ user.name.charAt(0) }}
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{{ user.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ user.email }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<div class="max-w-xs truncate">{{ user.department }}</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<span
|
|
||||||
v-for="role in user.roles"
|
|
||||||
:key="role"
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
:class="{
|
|
||||||
'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/20 dark:text-indigo-300': role === 'admin',
|
|
||||||
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': role === 'manager',
|
|
||||||
'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-300': role === 'engineer',
|
|
||||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': role === 'finance',
|
|
||||||
'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-300': role === 'approver'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ role }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ formatDateTime(user.lastLogin) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
:class="{
|
|
||||||
'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300': user.status === 'active',
|
|
||||||
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-300': user.status === 'pending',
|
|
||||||
'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-300': user.status === 'inactive'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{{ user.status }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3">
|
|
||||||
<span v-html="getSvgIcon('edit')"></span>
|
|
||||||
</button>
|
|
||||||
<button class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
|
|
||||||
<span v-html="getSvgIcon('trash')"></span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Roles Tab -->
|
|
||||||
<div v-else-if="activeTab === 'roles'" class="h-full">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div
|
|
||||||
v-for="role in roles"
|
|
||||||
:key="role.id"
|
|
||||||
class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"
|
|
||||||
>
|
|
||||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div class="flex justify-between items-start">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ role.name }}</h3>
|
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ role.description }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex space-x-2">
|
|
||||||
<button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
|
|
||||||
<span v-html="getSvgIcon('edit')"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4">
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Document Permissions</h4>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="(value, key) in role.permissions.documents"
|
|
||||||
:key="`doc-${key}`"
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
|
|
||||||
>
|
|
||||||
{{ key }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Cabinet Permissions</h4>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="(value, key) in role.permissions.cabinets"
|
|
||||||
:key="`cab-${key}`"
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
|
|
||||||
>
|
|
||||||
{{ key }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">Access Request Permissions</h4>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<span
|
|
||||||
v-for="(value, key) in role.permissions.accessRequests"
|
|
||||||
:key="`acc-${key}`"
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
||||||
:class="value ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-900/20 dark:text-gray-400 line-through'"
|
|
||||||
>
|
|
||||||
{{ key }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900/10 text-right">
|
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{{ role.userCount }} {{ role.userCount === 1 ? 'user' : 'users' }} with this role
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</rs-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dms-role-management {
|
|
||||||
height: calc(100vh - 64px);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -350,9 +350,9 @@ onMounted(async () => {
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">DMS Settings</h1>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<RsButton @click="exportSettingsFile" variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
|
<RsButton @click="exportSettingsFile" variant="secondary-outline" size="sm" :disabled="isLoading || isSaving">
|
||||||
Export Settings
|
Export Settings
|
||||||
@ -369,205 +369,205 @@ onMounted(async () => {
|
|||||||
<span v-if="isSaving" class="flex items-center">
|
<span v-if="isSaving" class="flex items-center">
|
||||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
Saving...
|
Saving...
|
||||||
</span>
|
</span>
|
||||||
<span v-else>Save Settings</span>
|
<span v-else>Save Settings</span>
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Success/Error Messages -->
|
|
||||||
<div v-if="saveSuccess" class="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
|
|
||||||
{{ saveSuccess }}
|
|
||||||
</div>
|
|
||||||
<div v-if="saveError" class="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
|
||||||
{{ saveError }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #body>
|
<!-- Success/Error Messages -->
|
||||||
<!-- Loading State -->
|
<div v-if="saveSuccess" class="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
|
||||||
<div v-if="isLoading" class="flex items-center justify-center h-64">
|
{{ saveSuccess }}
|
||||||
<div class="text-center">
|
</div>
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
<div v-if="saveError" class="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
|
||||||
<p class="text-gray-600 dark:text-gray-400">Loading DMS settings...</p>
|
{{ saveError }}
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="isLoading" class="flex items-center justify-center h-64">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">Loading DMS settings...</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Settings Content -->
|
|
||||||
|
<!-- Settings Content -->
|
||||||
<div v-else class="settings-layout flex h-full min-h-0">
|
<div v-else class="settings-layout flex h-full min-h-0">
|
||||||
<!-- Settings Navigation -->
|
<!-- Settings Navigation -->
|
||||||
<div class="settings-nav w-80 border-r border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
<div class="settings-nav w-80 border-r border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<RsButton
|
<RsButton
|
||||||
v-for="category in settingsCategories"
|
v-for="category in settingsCategories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
@click="activeCategory = category.id"
|
@click="activeCategory = category.id"
|
||||||
:variant="activeCategory === category.id ? 'primary' : 'secondary-outline'"
|
:variant="activeCategory === category.id ? 'primary' : 'secondary-outline'"
|
||||||
class="flex items-center space-x-2 px-4 py-2 text-left w-full justify-start"
|
class="flex items-center space-x-2 px-4 py-2 text-left w-full justify-start"
|
||||||
>
|
>
|
||||||
<span class="text-lg mr-3">{{ category.icon }}</span>
|
<span class="text-lg mr-3">{{ category.icon }}</span>
|
||||||
{{ category.name }}
|
{{ category.name }}
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings Content - Scrollable -->
|
<!-- Settings Content - Scrollable -->
|
||||||
<div class="settings-content flex-1 p-6 overflow-y-auto min-h-0">
|
<div class="settings-content flex-1 p-6 overflow-y-auto min-h-0">
|
||||||
|
|
||||||
<!-- User & Access Management -->
|
<!-- User & Access Management -->
|
||||||
<div v-if="activeCategory === 'access'" class="space-y-8">
|
<div v-if="activeCategory === 'access'" class="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xl font-semibold mb-4">🔐 User & Access Management</h2>
|
<h2 class="text-xl font-semibold mb-4">🔐 User & Access Management</h2>
|
||||||
|
|
||||||
<!-- User Roles -->
|
<!-- User Roles -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||||
<h3 class="text-lg font-medium mb-4">User Roles & Permissions</h3>
|
<h3 class="text-lg font-medium mb-4">User Roles & Permissions</h3>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-2">User Roles</label>
|
<label class="block text-sm font-medium mb-2">User Roles</label>
|
||||||
<div class="space-y-2 max-h-48 overflow-y-auto">
|
<div class="space-y-2 max-h-48 overflow-y-auto">
|
||||||
<div v-for="role in settings.access.userRoles" :key="role" class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded">
|
<div v-for="role in settings.access.userRoles" :key="role" class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm">{{ role }}</span>
|
<span class="text-sm">{{ role }}</span>
|
||||||
<RsButton @click="removeUserRole(role)" variant="danger-text" size="sm">
|
<RsButton @click="removeUserRole(role)" variant="danger-text" size="sm">
|
||||||
Remove
|
Remove
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RsButton @click="addUserRole" variant="primary-text" size="sm">
|
<RsButton @click="addUserRole" variant="primary-text" size="sm">
|
||||||
+ Add Role
|
+ Add Role
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Access Permissions</label>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.access.permissions.view" class="mr-2" />
|
|
||||||
View Documents
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.access.permissions.edit" class="mr-2" />
|
|
||||||
Edit Documents
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.access.permissions.delete" class="mr-2" />
|
|
||||||
Delete Documents
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.access.permissions.download" class="mr-2" />
|
|
||||||
Download Documents
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.access.permissions.share" class="mr-2" />
|
|
||||||
Share Documents
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Access Permissions</label>
|
||||||
<!-- Authentication Settings -->
|
<div class="space-y-3">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">Authentication Settings</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.access.authentication.ssoEnabled" class="mr-2" />
|
<input type="checkbox" v-model="settings.access.permissions.view" class="mr-2" />
|
||||||
Enable Single Sign-On (SSO)
|
View Documents
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.access.authentication.mfaRequired" class="mr-2" />
|
<input type="checkbox" v-model="settings.access.permissions.edit" class="mr-2" />
|
||||||
Require Multi-Factor Authentication
|
Edit Documents
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.access.authentication.ldapIntegration" class="mr-2" />
|
<input type="checkbox" v-model="settings.access.permissions.delete" class="mr-2" />
|
||||||
LDAP/Active Directory Integration
|
Delete Documents
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.access.permissions.download" class="mr-2" />
|
||||||
|
Download Documents
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.access.permissions.share" class="mr-2" />
|
||||||
|
Share Documents
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Session Timeout (hours)</label>
|
|
||||||
<input type="number" v-model="settings.access.authentication.sessionTimeout"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="24" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Document & Folder Settings -->
|
|
||||||
<div v-if="activeCategory === 'documents'" class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">📁 Document & Folder Settings</h2>
|
|
||||||
|
|
||||||
<!-- Naming Conventions -->
|
<!-- Authentication Settings -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
<h3 class="text-lg font-medium mb-4">Document Naming Conventions</h3>
|
<h3 class="text-lg font-medium mb-4">Authentication Settings</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.documents.namingConventions.autoGenerate" class="mr-2" />
|
<input type="checkbox" v-model="settings.access.authentication.ssoEnabled" class="mr-2" />
|
||||||
Auto-generate document names
|
Enable Single Sign-On (SSO)
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.access.authentication.mfaRequired" class="mr-2" />
|
||||||
|
Require Multi-Factor Authentication
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.access.authentication.ldapIntegration" class="mr-2" />
|
||||||
|
LDAP/Active Directory Integration
|
||||||
</label>
|
</label>
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Naming Pattern</label>
|
|
||||||
<input type="text" v-model="settings.documents.namingConventions.pattern"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
||||||
placeholder="{department}_{title}_{date}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Session Timeout (hours)</label>
|
||||||
<!-- Version Control -->
|
<input type="number" v-model="settings.access.authentication.sessionTimeout"
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="24" />
|
||||||
<h3 class="text-lg font-medium mb-4">Version Control</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.documents.versionControl.enabled" class="mr-2" />
|
|
||||||
Enable Version Control
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.documents.versionControl.autoVersioning" class="mr-2" />
|
|
||||||
Automatic Versioning
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Maximum Versions to Retain</label>
|
|
||||||
<input type="number" v-model="settings.documents.versionControl.maxVersions"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="50" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Document & Folder Settings -->
|
||||||
|
<div v-if="activeCategory === 'documents'" class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">📁 Document & Folder Settings</h2>
|
||||||
|
|
||||||
<!-- Metadata & Tagging -->
|
<!-- Naming Conventions -->
|
||||||
<div v-if="activeCategory === 'metadata'" class="space-y-8">
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||||
<div>
|
<h3 class="text-lg font-medium mb-4">Document Naming Conventions</h3>
|
||||||
<h2 class="text-xl font-semibold mb-4">📝 Metadata & Tagging</h2>
|
<div class="space-y-4">
|
||||||
|
<label class="flex items-center">
|
||||||
<!-- Custom Metadata Fields -->
|
<input type="checkbox" v-model="settings.documents.namingConventions.autoGenerate" class="mr-2" />
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
Auto-generate document names
|
||||||
<h3 class="text-lg font-medium mb-4">Custom Metadata Fields</h3>
|
</label>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Naming Pattern</label>
|
||||||
|
<input type="text" v-model="settings.documents.namingConventions.pattern"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="{department}_{title}_{date}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version Control -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Version Control</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div v-for="(field, index) in settings.metadata.customFields" :key="index"
|
<label class="flex items-center">
|
||||||
class="grid grid-cols-4 gap-4 items-center bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
<input type="checkbox" v-model="settings.documents.versionControl.enabled" class="mr-2" />
|
||||||
<input type="text" v-model="field.name" placeholder="Field Name"
|
Enable Version Control
|
||||||
class="px-3 py-2 border border-gray-300 rounded-md" />
|
</label>
|
||||||
<select v-model="field.type" class="px-3 py-2 border border-gray-300 rounded-md">
|
<label class="flex items-center">
|
||||||
<option value="text">Text</option>
|
<input type="checkbox" v-model="settings.documents.versionControl.autoVersioning" class="mr-2" />
|
||||||
<option value="dropdown">Dropdown</option>
|
Automatic Versioning
|
||||||
<option value="date">Date</option>
|
</label>
|
||||||
<option value="number">Number</option>
|
</div>
|
||||||
<option value="select">Multi-select</option>
|
<div>
|
||||||
</select>
|
<label class="block text-sm font-medium mb-2">Maximum Versions to Retain</label>
|
||||||
<label class="flex items-center">
|
<input type="number" v-model="settings.documents.versionControl.maxVersions"
|
||||||
<input type="checkbox" v-model="field.required" class="mr-2" />
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" max="50" />
|
||||||
Required
|
</div>
|
||||||
</label>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata & Tagging -->
|
||||||
|
<div v-if="activeCategory === 'metadata'" class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">📝 Metadata & Tagging</h2>
|
||||||
|
|
||||||
|
<!-- Custom Metadata Fields -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Custom Metadata Fields</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div v-for="(field, index) in settings.metadata.customFields" :key="index"
|
||||||
|
class="grid grid-cols-4 gap-4 items-center bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
||||||
|
<input type="text" v-model="field.name" placeholder="Field Name"
|
||||||
|
class="px-3 py-2 border border-gray-300 rounded-md" />
|
||||||
|
<select v-model="field.type" class="px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="text">Text</option>
|
||||||
|
<option value="dropdown">Dropdown</option>
|
||||||
|
<option value="date">Date</option>
|
||||||
|
<option value="number">Number</option>
|
||||||
|
<option value="select">Multi-select</option>
|
||||||
|
</select>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="field.required" class="mr-2" />
|
||||||
|
Required
|
||||||
|
</label>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<RsButton @click="removeCustomField(index)" variant="danger-text" size="sm">
|
<RsButton @click="removeCustomField(index)" variant="danger-text" size="sm">
|
||||||
Remove
|
Remove
|
||||||
@ -578,195 +578,195 @@ onMounted(async () => {
|
|||||||
<RsButton @click="addCustomField" variant="primary-text" size="sm">
|
<RsButton @click="addCustomField" variant="primary-text" size="sm">
|
||||||
+ Add Custom Field
|
+ Add Custom Field
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tagging System -->
|
<!-- Tagging System -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
<h3 class="text-lg font-medium mb-4">Tagging System</h3>
|
<h3 class="text-lg font-medium mb-4">Tagging System</h3>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.metadata.tagging.userGeneratedTags" class="mr-2" />
|
<input type="checkbox" v-model="settings.metadata.tagging.userGeneratedTags" class="mr-2" />
|
||||||
Allow User-Generated Tags
|
Allow User-Generated Tags
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<label class="flex items-center">
|
||||||
<input type="checkbox" v-model="settings.metadata.tagging.tagSuggestions" class="mr-2" />
|
<input type="checkbox" v-model="settings.metadata.tagging.tagSuggestions" class="mr-2" />
|
||||||
Enable Tag Suggestions
|
Enable Tag Suggestions
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-2">Predefined Tags</label>
|
<label class="block text-sm font-medium mb-2">Predefined Tags</label>
|
||||||
<textarea v-model="predefinedTagsString"
|
<textarea v-model="predefinedTagsString"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md h-20"
|
class="w-full px-3 py-2 border border-gray-300 rounded-md h-20"
|
||||||
placeholder="urgent, confidential, public, draft, final"></textarea>
|
placeholder="urgent, confidential, public, draft, final"></textarea>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload & Storage Settings -->
|
|
||||||
<div v-if="activeCategory === 'upload'" class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">📤 Upload & Storage Settings</h2>
|
|
||||||
|
|
||||||
<!-- File Type Restrictions -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">File Type Restrictions</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Allowed File Types</label>
|
|
||||||
<textarea v-model="allowedFileTypesString"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
|
||||||
placeholder="pdf, doc, docx, xls, xlsx"></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Blocked File Types</label>
|
|
||||||
<textarea v-model="blockedFileTypesString"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
|
||||||
placeholder="exe, bat, cmd"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- File Size and Quotas -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">File Size Limits & Storage Quotas</h3>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Max File Size (MB)</label>
|
|
||||||
<input type="number" v-model="settings.upload.fileSizeLimit"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Per User Quota (MB)</label>
|
|
||||||
<input type="number" v-model="settings.upload.quotas.perUser"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Per Group Quota (MB)</label>
|
|
||||||
<input type="number" v-model="settings.upload.quotas.perGroup"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Per Project Quota (MB)</label>
|
|
||||||
<input type="number" v-model="settings.upload.quotas.perProject"
|
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- System Settings -->
|
|
||||||
<div v-if="activeCategory === 'system'" class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">📅 System Settings</h2>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">General System Configuration</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">System Timezone</label>
|
|
||||||
<select v-model="settings.system.timezone" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
|
||||||
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
|
||||||
<option value="UTC">UTC</option>
|
|
||||||
<option value="America/New_York">America/New_York</option>
|
|
||||||
<option value="Europe/London">Europe/London</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Backup Schedule</label>
|
|
||||||
<select v-model="settings.system.backupSchedule" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
|
||||||
<option value="hourly">Hourly</option>
|
|
||||||
<option value="daily">Daily</option>
|
|
||||||
<option value="weekly">Weekly</option>
|
|
||||||
<option value="monthly">Monthly</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Log Level</label>
|
|
||||||
<select v-model="settings.system.logLevel" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
|
||||||
<option value="debug">Debug</option>
|
|
||||||
<option value="info">Info</option>
|
|
||||||
<option value="warning">Warning</option>
|
|
||||||
<option value="error">Error</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.system.maintenanceMode" class="mr-2" />
|
|
||||||
Maintenance Mode
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.system.autoUpdates" class="mr-2" />
|
|
||||||
Automatic Updates
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.system.systemMonitoring" class="mr-2" />
|
|
||||||
System Monitoring
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Workflow Settings -->
|
|
||||||
<div v-if="activeCategory === 'workflow'" class="space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 class="text-xl font-semibold mb-4">🔄 Workflow & Automation</h2>
|
|
||||||
|
|
||||||
<!-- Approval Flows -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">Approval Workflows</h3>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.workflow.approvalFlows.enabled" class="mr-2" />
|
|
||||||
Enable Approval Workflows
|
|
||||||
</label>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-2">Default Approval Flow</label>
|
|
||||||
<select v-model="settings.workflow.approvalFlows.defaultFlow" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
|
||||||
<option value="department-head-approval">Department Head Approval</option>
|
|
||||||
<option value="legal-review">Legal Review</option>
|
|
||||||
<option value="finance-approval">Finance Approval</option>
|
|
||||||
<option value="director-sign-off">Director Sign-off</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notifications -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
|
||||||
<h3 class="text-lg font-medium mb-4">Notification Settings</h3>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.workflow.notifications.emailNotifications" class="mr-2" />
|
|
||||||
Email Notifications
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.workflow.notifications.inAppNotifications" class="mr-2" />
|
|
||||||
In-App Notifications
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.workflow.notifications.uploadAlerts" class="mr-2" />
|
|
||||||
Upload Alerts
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center">
|
|
||||||
<input type="checkbox" v-model="settings.workflow.notifications.deadlineReminders" class="mr-2" />
|
|
||||||
Deadline Reminders
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload & Storage Settings -->
|
||||||
|
<div v-if="activeCategory === 'upload'" class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">📤 Upload & Storage Settings</h2>
|
||||||
|
|
||||||
|
<!-- File Type Restrictions -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">File Type Restrictions</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Allowed File Types</label>
|
||||||
|
<textarea v-model="allowedFileTypesString"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
||||||
|
placeholder="pdf, doc, docx, xls, xlsx"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Blocked File Types</label>
|
||||||
|
<textarea v-model="blockedFileTypesString"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md h-24"
|
||||||
|
placeholder="exe, bat, cmd"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Size and Quotas -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">File Size Limits & Storage Quotas</h3>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Max File Size (MB)</label>
|
||||||
|
<input type="number" v-model="settings.upload.fileSizeLimit"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" min="1" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Per User Quota (MB)</label>
|
||||||
|
<input type="number" v-model="settings.upload.quotas.perUser"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Per Group Quota (MB)</label>
|
||||||
|
<input type="number" v-model="settings.upload.quotas.perGroup"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Per Project Quota (MB)</label>
|
||||||
|
<input type="number" v-model="settings.upload.quotas.perProject"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Settings -->
|
||||||
|
<div v-if="activeCategory === 'system'" class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">📅 System Settings</h2>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">General System Configuration</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">System Timezone</label>
|
||||||
|
<select v-model="settings.system.timezone" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option value="America/New_York">America/New_York</option>
|
||||||
|
<option value="Europe/London">Europe/London</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Backup Schedule</label>
|
||||||
|
<select v-model="settings.system.backupSchedule" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="hourly">Hourly</option>
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Log Level</label>
|
||||||
|
<select v-model="settings.system.logLevel" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="debug">Debug</option>
|
||||||
|
<option value="info">Info</option>
|
||||||
|
<option value="warning">Warning</option>
|
||||||
|
<option value="error">Error</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.system.maintenanceMode" class="mr-2" />
|
||||||
|
Maintenance Mode
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.system.autoUpdates" class="mr-2" />
|
||||||
|
Automatic Updates
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.system.systemMonitoring" class="mr-2" />
|
||||||
|
System Monitoring
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Workflow Settings -->
|
||||||
|
<div v-if="activeCategory === 'workflow'" class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold mb-4">🔄 Workflow & Automation</h2>
|
||||||
|
|
||||||
|
<!-- Approval Flows -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6 mb-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Approval Workflows</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.workflow.approvalFlows.enabled" class="mr-2" />
|
||||||
|
Enable Approval Workflows
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-2">Default Approval Flow</label>
|
||||||
|
<select v-model="settings.workflow.approvalFlows.defaultFlow" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="department-head-approval">Department Head Approval</option>
|
||||||
|
<option value="legal-review">Legal Review</option>
|
||||||
|
<option value="finance-approval">Finance Approval</option>
|
||||||
|
<option value="director-sign-off">Director Sign-off</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg border p-6">
|
||||||
|
<h3 class="text-lg font-medium mb-4">Notification Settings</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.workflow.notifications.emailNotifications" class="mr-2" />
|
||||||
|
Email Notifications
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.workflow.notifications.inAppNotifications" class="mr-2" />
|
||||||
|
In-App Notifications
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.workflow.notifications.uploadAlerts" class="mr-2" />
|
||||||
|
Upload Alerts
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input type="checkbox" v-model="settings.workflow.notifications.deadlineReminders" class="mr-2" />
|
||||||
|
Deadline Reminders
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
</template>
|
||||||
</RsCard>
|
</RsCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1706,7 +1706,7 @@ export const useDmsStore = defineStore('dms', {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
console.error('DMS Store Error:', error);
|
console.error('DMS Store Error:', error);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Authentik integration placeholder - this would be replaced with actual Authentik API calls
|
// Authentik integration placeholder - this would be replaced with actual Authentik API calls
|
||||||
async authenticateWithAuthentik(username, password) {
|
async authenticateWithAuthentik(username, password) {
|
||||||
this.setLoading(true);
|
this.setLoading(true);
|
||||||
@ -1719,57 +1719,57 @@ export const useDmsStore = defineStore('dms', {
|
|||||||
|
|
||||||
this.requestTimeouts.set('auth', timeoutId);
|
this.requestTimeouts.set('auth', timeoutId);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
this.requestTimeouts.delete('auth');
|
this.requestTimeouts.delete('auth');
|
||||||
|
|
||||||
// This is a placeholder for the actual Authentik integration
|
// This is a placeholder for the actual Authentik integration
|
||||||
// In a real implementation, this would make API calls to Authentik
|
// In a real implementation, this would make API calls to Authentik
|
||||||
|
|
||||||
if (username === 'superadmin' && password === 'password') {
|
if (username === 'superadmin' && password === 'password') {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: 'superadmin1',
|
id: 'superadmin1',
|
||||||
name: 'Super Admin User',
|
name: 'Super Admin User',
|
||||||
email: 'superadmin@example.com',
|
email: 'superadmin@example.com',
|
||||||
role: 'superadmin',
|
role: 'superadmin',
|
||||||
department: 'IT Department'
|
department: 'IT Department'
|
||||||
},
|
},
|
||||||
token: 'sample-authentik-token'
|
token: 'sample-authentik-token'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username === 'admin' && password === 'password') {
|
if (username === 'admin' && password === 'password') {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: 'admin1',
|
id: 'admin1',
|
||||||
name: 'Admin User',
|
name: 'Admin User',
|
||||||
email: 'admin@example.com',
|
email: 'admin@example.com',
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
department: 'IT Department'
|
department: 'IT Department'
|
||||||
},
|
},
|
||||||
token: 'sample-authentik-token'
|
token: 'sample-authentik-token'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username === 'user' && password === 'password') {
|
if (username === 'user' && password === 'password') {
|
||||||
this.setLoading(false);
|
this.setLoading(false);
|
||||||
return {
|
return {
|
||||||
user: {
|
user: {
|
||||||
id: 'user1',
|
id: 'user1',
|
||||||
name: 'Aiman Fakhrullah',
|
name: 'Aiman Fakhrullah',
|
||||||
email: 'aiman@example.com',
|
email: 'aiman@example.com',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
department: 'General Department'
|
department: 'General Department'
|
||||||
},
|
},
|
||||||
token: 'sample-authentik-token'
|
token: 'sample-authentik-token'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Authentication failed');
|
throw new Error('Authentication failed');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setError(error.message);
|
this.setError(error.message);
|
||||||
throw error;
|
throw error;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user