add latest devtools
This commit is contained in:
parent
924e424251
commit
10a3209021
@ -122,4 +122,70 @@ export default [
|
||||
],
|
||||
meta: {},
|
||||
},
|
||||
// {
|
||||
// header: "Pentadbiran",
|
||||
// description: "Urus aplikasi anda",
|
||||
// child: [
|
||||
// {
|
||||
// title: "Konfigurasi",
|
||||
// icon: "ic:outline-settings",
|
||||
// child: [
|
||||
// {
|
||||
// title: "Persekitaran",
|
||||
// path: "/devtool/config/environment",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Penyunting Menu",
|
||||
// icon: "ci:menu-alt-03",
|
||||
// path: "/devtool/menu-editor",
|
||||
// child: [],
|
||||
// },
|
||||
// {
|
||||
// title: "Urus Pengguna",
|
||||
// path: "/devtool/user-management",
|
||||
// icon: "ph:user-circle-gear",
|
||||
// child: [
|
||||
// {
|
||||
// title: "Senarai Pengguna",
|
||||
// path: "/devtool/user-management/user",
|
||||
// icon: "",
|
||||
// child: [],
|
||||
// },
|
||||
// {
|
||||
// title: "Senarai Peranan",
|
||||
// path: "/devtool/user-management/role",
|
||||
// icon: "",
|
||||
// child: [],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Kandungan",
|
||||
// icon: "mdi:pencil-ruler",
|
||||
// child: [
|
||||
// {
|
||||
// title: "Penyunting",
|
||||
// path: "/devtool/content-editor",
|
||||
// },
|
||||
// {
|
||||
// title: "Templat",
|
||||
// path: "/devtool/content-editor/template",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// title: "Penyunting API",
|
||||
// path: "/devtool/api-editor",
|
||||
// icon: "material-symbols:api-rounded",
|
||||
// child: [],
|
||||
// },
|
||||
// ],
|
||||
// meta: {
|
||||
// auth: {
|
||||
// role: ["Developer"],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
];
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { useThemeStore } from "~/stores/theme";
|
||||
|
||||
definePageMeta({
|
||||
title: "Penyunting Kod API",
|
||||
title: "API Code Editor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -31,6 +31,9 @@ const linterErrorText = ref("");
|
||||
const linterErrorColumn = ref(0);
|
||||
const linterErrorLine = ref(0);
|
||||
|
||||
// Add new ref for loading state
|
||||
const isLinterChecking = ref(false);
|
||||
|
||||
// Get all themes
|
||||
const themes = codemirrorThemes();
|
||||
|
||||
@ -63,8 +66,8 @@ if (data.value.statusCode === 200) {
|
||||
} else {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Ralat",
|
||||
text: "API yang anda cuba sunting tidak dijumpai. Sila pilih API untuk disunting.",
|
||||
title: "Error",
|
||||
text: "The API you are trying to edit is not found. Please choose a API to edit.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
})
|
||||
@ -92,25 +95,30 @@ async function formatCode() {
|
||||
}
|
||||
|
||||
async function checkLinterVue() {
|
||||
// Call API to get the code
|
||||
const { data } = await useFetch("/api/devtool/api/linter", {
|
||||
initialCache: false,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code: fileCode.value,
|
||||
}),
|
||||
});
|
||||
isLinterChecking.value = true;
|
||||
try {
|
||||
// Call API to get the code
|
||||
const { data } = await useFetch("/api/devtool/api/linter", {
|
||||
initialCache: false,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code: fileCode.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
linterError.value = false;
|
||||
linterErrorText.value = "";
|
||||
linterErrorColumn.value = 0;
|
||||
linterErrorLine.value = 0;
|
||||
} else if (data.value.statusCode === 400) {
|
||||
linterError.value = true;
|
||||
linterErrorText.value = data.value.data.message;
|
||||
linterErrorColumn.value = data.value.data.column;
|
||||
linterErrorLine.value = data.value.data.line;
|
||||
if (data.value.statusCode === 200) {
|
||||
linterError.value = false;
|
||||
linterErrorText.value = "";
|
||||
linterErrorColumn.value = 0;
|
||||
linterErrorLine.value = 0;
|
||||
} else if (data.value.statusCode === 400) {
|
||||
linterError.value = true;
|
||||
linterErrorText.value = data.value.data.message;
|
||||
linterErrorColumn.value = data.value.data.column;
|
||||
linterErrorLine.value = data.value.data.line;
|
||||
}
|
||||
} finally {
|
||||
isLinterChecking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,8 +142,8 @@ const saveCode = async () => {
|
||||
|
||||
if (linterError.value) {
|
||||
$swal.fire({
|
||||
title: "Ralat",
|
||||
text: "Terdapat ralat dalam kod anda. Sila betulkannya sebelum menyimpan.",
|
||||
title: "Error",
|
||||
text: "There is an error in your code. Please fix it before saving.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
});
|
||||
@ -153,8 +161,8 @@ const saveCode = async () => {
|
||||
});
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire({
|
||||
title: "Berjaya",
|
||||
text: "Kod telah berjaya disimpan.",
|
||||
title: "Success",
|
||||
text: "The code has been saved successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 1000,
|
||||
@ -170,40 +178,49 @@ const saveCode = async () => {
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-alert v-if="hasError" class="mb-4" variant="primary">{{
|
||||
<rs-alert v-if="hasError" class="mb-4" variant="danger">{{
|
||||
error
|
||||
}}</rs-alert>
|
||||
<rs-card>
|
||||
<rs-tab fill>
|
||||
<rs-tab-item title="Penyunting">
|
||||
<rs-tab-item title="Editor">
|
||||
<div class="flex justify-end gap-2 mb-4">
|
||||
<rs-button class="!p-2" @click="formatCode">
|
||||
<Icon name="simple-icons:prettier" size="20px" class="mr-1" />
|
||||
Format Kod</rs-button
|
||||
<rs-button
|
||||
class="!p-2"
|
||||
@click="saveCode"
|
||||
:disabled="isLinterChecking"
|
||||
>
|
||||
<rs-button class="!p-2" @click="saveCode">
|
||||
<Icon
|
||||
name="material-symbols:save-outline-rounded"
|
||||
size="20px"
|
||||
class="mr-1"
|
||||
/>
|
||||
Simpan API
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
v-if="!isLinterChecking"
|
||||
name="material-symbols:save-outline-rounded"
|
||||
size="20px"
|
||||
class="mr-1"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="eos-icons:loading"
|
||||
size="20px"
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
{{ isLinterChecking ? "Checking..." : "Save API" }}
|
||||
</div>
|
||||
</rs-button>
|
||||
</div>
|
||||
<Transition>
|
||||
<rs-alert v-if="linterError">
|
||||
<rs-alert v-if="linterError" variant="danger" class="mb-4">
|
||||
<div class="flex gap-2">
|
||||
<Icon
|
||||
name="material-symbols:error-outline-rounded"
|
||||
size="20px"
|
||||
/>
|
||||
<div>
|
||||
<div class="font-bold">Ralat ESLint</div>
|
||||
<div class="font-bold">ESLint Error</div>
|
||||
<div class="text-sm">
|
||||
{{ linterErrorText }}
|
||||
</div>
|
||||
<div class="text-xs mt-2">
|
||||
Baris: {{ linterErrorLine }} Lajur: {{ linterErrorColumn }}
|
||||
Line: {{ linterErrorLine }} Column: {{ linterErrorColumn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,7 +233,7 @@ const saveCode = async () => {
|
||||
mode="javascript"
|
||||
/>
|
||||
</rs-tab-item>
|
||||
<rs-tab-item title="Penguji API">
|
||||
<rs-tab-item title="API Tester">
|
||||
<rs-api-tester :url="route.query?.path" />
|
||||
</rs-tab-item>
|
||||
</rs-tab>
|
||||
|
@ -23,35 +23,34 @@ const showModalEditForm = ref({
|
||||
const openModalAdd = () => {
|
||||
showModalAddForm.value = {
|
||||
apiURL: "",
|
||||
method: "all",
|
||||
};
|
||||
|
||||
showModalAdd.value = true;
|
||||
};
|
||||
|
||||
const openModalEdit = (url) => {
|
||||
const openModalEdit = (url, method = "all") => {
|
||||
const apiURL = url.replace("/api/", "");
|
||||
|
||||
showModalEditForm.value = {
|
||||
apiURL: apiURL,
|
||||
oldApiURL: apiURL,
|
||||
method: method,
|
||||
};
|
||||
|
||||
showModalEdit.value = true;
|
||||
};
|
||||
|
||||
// Get api list from api folder
|
||||
const getApiList = async () => {
|
||||
const { data } = await useFetch("/api/devtool/api/list", {
|
||||
initialCache: false,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const apiList = await getApiList();
|
||||
const { data: apiList, refresh } = await useFetch("/api/devtool/api/list");
|
||||
|
||||
const searchApi = () => {
|
||||
if (!apiList.value || !apiList.value.data) return [];
|
||||
|
||||
return apiList.value.data.filter((api) => {
|
||||
return api.name.toLowerCase().includes(searchText.value.toLowerCase());
|
||||
return (
|
||||
api.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
|
||||
api.url.toLowerCase().includes(searchText.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@ -78,17 +77,15 @@ const saveAddAPI = async () => {
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Berjaya",
|
||||
text: "Kod telah berjaya disimpan.",
|
||||
title: "Success",
|
||||
text: "The code has been saved successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 1000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
nuxtApp.$router.go(
|
||||
`/devtool/api-editor/code?path=/api${data.value.data.path}`
|
||||
);
|
||||
}, 1000);
|
||||
// Close modal and refresh list
|
||||
showModalAdd.value = false;
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@ -105,31 +102,28 @@ const saveEditAPI = async () => {
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Berjaya",
|
||||
text: "Kod telah berjaya disimpan.",
|
||||
title: "Success",
|
||||
text: "The code has been saved successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 1000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
nuxtApp.$router.go(
|
||||
`/devtool/api-editor/code?path=/api/${showModalEditForm.value.apiURL}`
|
||||
);
|
||||
}, 1000);
|
||||
// Close modal and refresh list
|
||||
showModalEdit.value = false;
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAPI = async (apiURL) => {
|
||||
nuxtApp.$swal
|
||||
.fire({
|
||||
title: "Adakah anda pasti untuk memadam API ini?",
|
||||
text: "Anda tidak akan dapat memulihkan ini!",
|
||||
title: "Are you sure to delete this API?",
|
||||
text: "You won't be able to revert this!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya, padamkan!",
|
||||
cancelButtonText: "Batal",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
@ -144,15 +138,14 @@ const deleteAPI = async (apiURL) => {
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Berjaya",
|
||||
text: "Kod telah berjaya disimpan.",
|
||||
title: "Success",
|
||||
text: "The code has been saved successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 1000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
nuxtApp.$router.go();
|
||||
}, 1000);
|
||||
// Refresh list after deletion
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -165,13 +158,13 @@ const deleteAPI = async (apiURL) => {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Halaman ini digunakan untuk mengedit API untuk bahagian pelayan. Anda boleh mengedit
|
||||
API dengan memilih API untuk diedit dari senarai kad di bawah.
|
||||
This page is used to edit the api for the server side. You can edit
|
||||
the api by choosing the api to edit from the card list below.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -181,14 +174,14 @@ const deleteAPI = async (apiURL) => {
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<rs-button @click="openModalAdd">
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Tambah API
|
||||
Add API
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Search Button -->
|
||||
<FormKit
|
||||
v-model="searchText"
|
||||
placeholder="Cari Tajuk..."
|
||||
placeholder="Search Title..."
|
||||
type="search"
|
||||
class="mb-4"
|
||||
/>
|
||||
@ -215,7 +208,7 @@ const deleteAPI = async (apiURL) => {
|
||||
name="material-symbols:code-blocks-outline-rounded"
|
||||
class="mr-2"
|
||||
/>
|
||||
Editor Kod
|
||||
Code Editor
|
||||
</rs-button>
|
||||
<div class="flex gap-2">
|
||||
<rs-button @click="openModalEdit(api.url)">
|
||||
@ -233,34 +226,102 @@ const deleteAPI = async (apiURL) => {
|
||||
</div>
|
||||
</rs-card>
|
||||
|
||||
<rs-modal
|
||||
title="Tambah API"
|
||||
v-model="showModalAdd"
|
||||
ok-title="Simpan"
|
||||
:ok-callback="saveAddAPI"
|
||||
>
|
||||
<FormKit type="text" label="Url" v-model="showModalAddForm.apiURL">
|
||||
<template #prefix>
|
||||
<div class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3">
|
||||
/api/
|
||||
<rs-modal title="Add API" v-model="showModalAdd" :overlay-close="false">
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="saveAddAPI">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="URL"
|
||||
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'URL is required',
|
||||
matches:
|
||||
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
|
||||
}"
|
||||
v-model="showModalAddForm.apiURL"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
|
||||
>
|
||||
/api/
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
|
||||
<!-- <FormKit
|
||||
type="select"
|
||||
label="Request Method"
|
||||
:options="requestMethods"
|
||||
validation="required"
|
||||
placeholder="Select a method"
|
||||
v-model="showModalAddForm.method"
|
||||
/> -->
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="outline" @click="showModalAdd = false">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button btnType="submit">
|
||||
<Icon
|
||||
name="material-symbols:save-outline"
|
||||
class="mr-2 !w-4 !h-4"
|
||||
/>
|
||||
Save
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer></template>
|
||||
</rs-modal>
|
||||
|
||||
<rs-modal
|
||||
title="Edit API"
|
||||
v-model="showModalEdit"
|
||||
ok-title="Simpan"
|
||||
:ok-callback="saveEditAPI"
|
||||
>
|
||||
<FormKit type="text" label="Url" v-model="showModalEditForm.apiURL">
|
||||
<template #prefix>
|
||||
<div class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3">
|
||||
/api/
|
||||
<rs-modal title="Edit API" v-model="showModalEdit" :overlay-close="false">
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="saveEditAPI">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="URL"
|
||||
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'URL is required',
|
||||
matches:
|
||||
'URL contains invalid characters. Only letters, numbers, dashes, and forward slashes are allowed.',
|
||||
}"
|
||||
v-model="showModalEditForm.apiURL"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
|
||||
>
|
||||
/api/
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
|
||||
<!-- <FormKit
|
||||
type="select"
|
||||
label="Request Method"
|
||||
:options="requestMethods"
|
||||
validation="required"
|
||||
placeholder="Select a method"
|
||||
v-model="showModalEditForm.method"
|
||||
/> -->
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="outline" @click="showModalEdit = false">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button btnType="submit">
|
||||
<Icon
|
||||
name="material-symbols:save-outline"
|
||||
class="mr-2 !w-4 !h-4"
|
||||
/>
|
||||
Save
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer></template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
35
pages/devtool/code-playground/index.js
Normal file
35
pages/devtool/code-playground/index.js
Normal file
@ -0,0 +1,35 @@
|
||||
import RsAlert from "../../../components/RsAlert.vue";
|
||||
import RsBadge from "../../../components/RsBadge.vue";
|
||||
import RsButton from "../../../components/RsButton.vue";
|
||||
import RsCard from "../../../components/RsCard.vue";
|
||||
import RsCodeMirror from "../../../components/RsCodeMirror.vue";
|
||||
import RsCollapse from "../../../components/RsCollapse.vue";
|
||||
import RsCollapseItem from "../../../components/RsCollapseItem.vue";
|
||||
import RsDropdown from "../../../components/RsDropdown.vue";
|
||||
import RsDropdownItem from "../../../components/RsDropdownItem.vue";
|
||||
import RsFieldset from "../../../components/RsFieldset.vue";
|
||||
import RsModal from "../../../components/RsModal.vue";
|
||||
import RsProgressBar from "../../../components/RsProgressBar.vue";
|
||||
import RsTab from "../../../components/RsTab.vue";
|
||||
import RsTabItem from "../../../components/RsTabItem.vue";
|
||||
import RsTable from "../../../components/RsTable.vue";
|
||||
import RsWizard from "../../../components/RsWizard.vue";
|
||||
|
||||
export {
|
||||
RsAlert,
|
||||
RsBadge,
|
||||
RsButton,
|
||||
RsCard,
|
||||
RsCodeMirror,
|
||||
RsCollapse,
|
||||
RsCollapseItem,
|
||||
RsDropdown,
|
||||
RsDropdownItem,
|
||||
RsFieldset,
|
||||
RsModal,
|
||||
RsProgressBar,
|
||||
RsTab,
|
||||
RsTabItem,
|
||||
RsTable,
|
||||
RsWizard,
|
||||
};
|
422
pages/devtool/code-playground/index.vue
Normal file
422
pages/devtool/code-playground/index.vue
Normal file
@ -0,0 +1,422 @@
|
||||
<script setup>
|
||||
import { parse } from "@vue/compiler-sfc";
|
||||
import { watchDebounced } from "@vueuse/core";
|
||||
import {
|
||||
RsAlert,
|
||||
RsBadge,
|
||||
RsButton,
|
||||
RsCard,
|
||||
RsCodeMirror,
|
||||
RsCollapse,
|
||||
RsCollapseItem,
|
||||
RsDropdown,
|
||||
RsDropdownItem,
|
||||
RsFieldset,
|
||||
RsModal,
|
||||
RsProgressBar,
|
||||
RsTab,
|
||||
RsTabItem,
|
||||
RsTable,
|
||||
RsWizard,
|
||||
} from "./index.js";
|
||||
|
||||
// Import pinia store
|
||||
import { useThemeStore } from "~/stores/theme";
|
||||
|
||||
definePageMeta({
|
||||
title: "AI SFC Playground",
|
||||
description: "AI SFC Playground page",
|
||||
layout: "empty",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const CODE_STORAGE_KEY = "playground-code";
|
||||
|
||||
const code = ref(
|
||||
localStorage.getItem(CODE_STORAGE_KEY) ||
|
||||
`<template>
|
||||
<rs-card>
|
||||
<template #header>SFC Playground Demo</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<rs-alert variant="info">{{ msg }}</rs-alert>
|
||||
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
|
||||
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const msg = 'Hello from SFC Playground';
|
||||
const count = ref(0);
|
||||
<\/script>`
|
||||
);
|
||||
|
||||
const compiledCode = ref(null);
|
||||
const componentKey = ref(0);
|
||||
const compilationError = ref(null);
|
||||
|
||||
const previewSizes = [
|
||||
{ name: "Mobile", width: "320px", icon: "ph:device-mobile-camera" },
|
||||
{ name: "Tablet", width: "768px", icon: "ph:device-tablet-camera" },
|
||||
{ name: "Desktop", width: "1024px", icon: "ph:desktop" },
|
||||
{ name: "Full", width: "100%", icon: "material-symbols:fullscreen" },
|
||||
];
|
||||
|
||||
const currentPreviewSize = ref(previewSizes[3]); // Default to Full
|
||||
|
||||
// Theme-related code
|
||||
const themeStore = useThemeStore();
|
||||
const editorTheme = ref({
|
||||
label: themeStore.codeTheme,
|
||||
value: themeStore.codeTheme,
|
||||
});
|
||||
const dropdownThemes = ref([]);
|
||||
|
||||
// Get all themes
|
||||
const themes = codemirrorThemes();
|
||||
|
||||
// map the themes to the dropdown
|
||||
dropdownThemes.value = themes.map((theme) => {
|
||||
return {
|
||||
label: theme.name,
|
||||
value: theme.name,
|
||||
};
|
||||
});
|
||||
|
||||
// watch for changes in the theme
|
||||
watch(editorTheme, (theme) => {
|
||||
themeStore.setCodeTheme(theme.value);
|
||||
});
|
||||
|
||||
const compileCode = async (newCode) => {
|
||||
try {
|
||||
const { descriptor, errors } = parse(newCode);
|
||||
if (errors && errors.length > 0) {
|
||||
compilationError.value = {
|
||||
message: errors[0].message,
|
||||
location: errors[0].loc,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (descriptor.template && descriptor.scriptSetup) {
|
||||
const template = descriptor.template.content;
|
||||
const scriptSetup = descriptor.scriptSetup.content;
|
||||
|
||||
// Dynamically import FormKit components
|
||||
const {
|
||||
FormKit,
|
||||
FormKitSchema,
|
||||
FormKitSchemaNode,
|
||||
FormKitSchemaCondition,
|
||||
FormKitSchemaValidation,
|
||||
} = await import("@formkit/vue");
|
||||
|
||||
const component = defineComponent({
|
||||
components: {
|
||||
RsAlert,
|
||||
RsBadge,
|
||||
RsButton,
|
||||
RsCard,
|
||||
RsCodeMirror,
|
||||
RsCollapse,
|
||||
RsCollapseItem,
|
||||
RsDropdown,
|
||||
RsDropdownItem,
|
||||
RsFieldset,
|
||||
RsModal,
|
||||
RsProgressBar,
|
||||
RsTab,
|
||||
RsTabItem,
|
||||
RsTable,
|
||||
RsWizard,
|
||||
FormKit,
|
||||
FormKitSchema,
|
||||
FormKitSchemaNode,
|
||||
FormKitSchemaCondition,
|
||||
FormKitSchemaValidation,
|
||||
},
|
||||
template,
|
||||
setup() {
|
||||
const setupContext = reactive({});
|
||||
|
||||
try {
|
||||
// Extract top-level declarations
|
||||
const declarations =
|
||||
scriptSetup.match(/const\s+(\w+)\s*=\s*([^;]+)/g) || [];
|
||||
declarations.forEach((decl) => {
|
||||
const [, varName, varValue] = decl.match(
|
||||
/const\s+(\w+)\s*=\s*(.+)/
|
||||
);
|
||||
if (
|
||||
varValue.trim().startsWith("'") ||
|
||||
varValue.trim().startsWith('"')
|
||||
) {
|
||||
// It's a string literal, use it directly
|
||||
setupContext[varName] = varValue.trim().slice(1, -1);
|
||||
} else if (varValue.trim().startsWith("ref(")) {
|
||||
// It's already a ref, use ref
|
||||
setupContext[varName] = ref(null);
|
||||
} else {
|
||||
// For other cases, wrap in ref
|
||||
setupContext[varName] = ref(null);
|
||||
}
|
||||
});
|
||||
|
||||
const setupFunction = new Function(
|
||||
"ctx",
|
||||
"ref",
|
||||
"reactive",
|
||||
"computed",
|
||||
"watch",
|
||||
"onMounted",
|
||||
"onUnmounted",
|
||||
"useFetch",
|
||||
"fetch",
|
||||
"useAsyncData",
|
||||
"useNuxtApp",
|
||||
"useRuntimeConfig",
|
||||
"useRoute",
|
||||
"useRouter",
|
||||
"useState",
|
||||
"FormKit",
|
||||
"FormKitSchema",
|
||||
"FormKitSchemaNode",
|
||||
"FormKitSchemaCondition",
|
||||
"FormKitSchemaValidation",
|
||||
`
|
||||
with (ctx) {
|
||||
${scriptSetup}
|
||||
}
|
||||
return ctx;
|
||||
`
|
||||
);
|
||||
|
||||
const result = setupFunction(
|
||||
setupContext,
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
useFetch,
|
||||
fetch,
|
||||
useAsyncData,
|
||||
useNuxtApp,
|
||||
useRuntimeConfig,
|
||||
useRoute,
|
||||
useRouter,
|
||||
useState,
|
||||
FormKit,
|
||||
FormKitSchema,
|
||||
FormKitSchemaNode,
|
||||
FormKitSchemaCondition,
|
||||
FormKitSchemaValidation
|
||||
);
|
||||
|
||||
// Merge the result back into setupContext
|
||||
Object.assign(setupContext, result);
|
||||
|
||||
return setupContext;
|
||||
} catch (error) {
|
||||
console.error("Error in setup function:", error);
|
||||
compilationError.value = {
|
||||
message: `Error in setup function: ${error.message}`,
|
||||
location: { start: 0, end: 0 },
|
||||
};
|
||||
// Return an empty object to prevent breaking the component
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
compiledCode.value = markRaw(component);
|
||||
componentKey.value++;
|
||||
compilationError.value = null;
|
||||
} else {
|
||||
compiledCode.value = null;
|
||||
compilationError.value = {
|
||||
message: "Invalid SFC format.",
|
||||
location: { start: 0, end: 0 },
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Compilation error:", error);
|
||||
compiledCode.value = null;
|
||||
compilationError.value = {
|
||||
message: `Compilation error: ${error.message}`,
|
||||
location: { start: 0, end: 0 },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
watchDebounced(
|
||||
code,
|
||||
async (newCode) => {
|
||||
await compileCode(newCode);
|
||||
},
|
||||
{ debounce: 300, immediate: true }
|
||||
);
|
||||
|
||||
const handleFormatCode = () => {
|
||||
// Recompile the code after formatting
|
||||
setTimeout(() => compileCode(code.value), 100);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await compileCode(code.value);
|
||||
});
|
||||
|
||||
const defaultCode = `<template>
|
||||
<rs-card>
|
||||
<template #header>SFC Playground Demo</template>
|
||||
<template #body>
|
||||
<div class="space-y-4">
|
||||
<rs-alert variant="info">{{ msg }}</rs-alert>
|
||||
<rs-button @click="count++">Clicked {{ count }} times</rs-button>
|
||||
<rs-badge>{{ count > 5 ? 'High' : 'Low' }}</rs-badge>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const msg = 'Hello from SFC Playground';
|
||||
const count = ref(0);
|
||||
<\/script>`;
|
||||
|
||||
const resetCode = () => {
|
||||
code.value = defaultCode;
|
||||
localStorage.setItem(CODE_STORAGE_KEY, defaultCode);
|
||||
compileCode(code.value);
|
||||
};
|
||||
|
||||
// Add a watch effect to save code changes to localStorage
|
||||
watch(
|
||||
code,
|
||||
(newCode) => {
|
||||
localStorage.setItem(CODE_STORAGE_KEY, newCode);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col h-screen bg-gray-900">
|
||||
<!-- Header -->
|
||||
<header
|
||||
class="bg-gray-800 p-2 flex flex-wrap items-center justify-between text-white"
|
||||
>
|
||||
<div class="flex items-center mb-2 sm:mb-0 gap-4">
|
||||
<Icon
|
||||
@click="navigateTo('/')"
|
||||
name="ph:arrow-circle-left-duotone"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
<img
|
||||
src="@/assets/img/logo/logo-word-white.svg"
|
||||
alt="Vue Logo"
|
||||
class="h-8 block mr-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center space-x-2">
|
||||
<rs-button @click="resetCode" class="mr-2">
|
||||
<Icon name="material-symbols:refresh" class="mr-2" />
|
||||
Reset Code
|
||||
</rs-button>
|
||||
<h1 class="text-lg font-semibold">Code Playground</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex flex-col sm:flex-row flex-1 overflow-hidden">
|
||||
<!-- Editor section -->
|
||||
<div
|
||||
class="w-full sm:w-1/2 flex flex-col border-b sm:border-b-0 sm:border-r border-gray-900"
|
||||
>
|
||||
<div class="flex-grow overflow-hidden">
|
||||
<rs-code-mirror
|
||||
v-model="code"
|
||||
mode="javascript"
|
||||
class="h-full"
|
||||
@format-code="handleFormatCode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview section -->
|
||||
<div class="w-full sm:w-1/2 bg-white overflow-auto flex flex-col">
|
||||
<div
|
||||
class="bg-gray-800 p-2 flex justify-between items-center text-white"
|
||||
>
|
||||
<h2 class="text-sm font-semibold">Preview</h2>
|
||||
<div class="flex space-x-2">
|
||||
<rs-button
|
||||
v-for="size in previewSizes"
|
||||
:key="size.name"
|
||||
@click="currentPreviewSize = size"
|
||||
:class="{
|
||||
'bg-blue-600': currentPreviewSize === size,
|
||||
'bg-gray-600': currentPreviewSize !== size,
|
||||
}"
|
||||
class="px-2 py-1 text-xs rounded"
|
||||
>
|
||||
<Icon v-if="size.icon" :name="size.icon" class="!w-5 !h-5 mr-2" />
|
||||
{{ size.name }}
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow overflow-auto p-4 flex justify-center">
|
||||
<div
|
||||
:style="{
|
||||
width: currentPreviewSize.width,
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
}"
|
||||
class="border border-gray-300 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<component
|
||||
:key="componentKey"
|
||||
v-if="compiledCode && !compilationError"
|
||||
:is="compiledCode"
|
||||
/>
|
||||
<div v-else-if="compilationError?.message">
|
||||
<div class="flex justify-center items-center p-5">
|
||||
<div class="text-center">
|
||||
<Icon name="ph:warning" class="text-6xl" />
|
||||
<p class="text-lg font-semibold mt-4">
|
||||
Something went wrong. Please refer the error in the editor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500">Waiting for code changes...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.device-frame {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.device-frame {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.cm-editor) {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,11 +1,7 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Application logs",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Penyunting Kod",
|
||||
title: "Code Editor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -21,6 +21,8 @@ const linterErrorText = ref("");
|
||||
const linterErrorColumn = ref(0);
|
||||
const linterErrorLine = ref(0);
|
||||
|
||||
const isLinterChecking = ref(false);
|
||||
|
||||
const page = router.getRoutes().find((page) => {
|
||||
return page.name === route.query?.page;
|
||||
});
|
||||
@ -28,8 +30,8 @@ const page = router.getRoutes().find((page) => {
|
||||
if (!route.query.page || !page) {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Ralat",
|
||||
text: "Halaman yang anda cuba sunting tidak dijumpai. Sila pilih halaman untuk disunting.",
|
||||
title: "Error",
|
||||
text: "The page you are trying to edit is not found. Please choose a page to edit.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
})
|
||||
@ -62,8 +64,8 @@ if (data.value.statusCode === 200) {
|
||||
if (data.value?.mode == "index") page.path = page.path + "/index";
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat",
|
||||
text: "Halaman yang anda cuba sunting tidak dijumpai. Sila pilih halaman untuk disunting. Anda akan dialihkan ke halaman penyunting kandungan.",
|
||||
title: "Error",
|
||||
text: "The page you are trying to edit is not found. Please choose a page to edit. You will be redirected to the content editor page.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 3000,
|
||||
@ -90,25 +92,30 @@ async function formatCode() {
|
||||
}
|
||||
|
||||
async function checkLinterVue() {
|
||||
// Call API to get the code
|
||||
const { data } = await useFetch("/api/devtool/content/code/linter", {
|
||||
initialCache: false,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code: fileCode.value,
|
||||
}),
|
||||
});
|
||||
isLinterChecking.value = true;
|
||||
try {
|
||||
// Call API to get the code
|
||||
const { data } = await useFetch("/api/devtool/content/code/linter", {
|
||||
initialCache: false,
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code: fileCode.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
linterError.value = false;
|
||||
linterErrorText.value = "";
|
||||
linterErrorColumn.value = 0;
|
||||
linterErrorLine.value = 0;
|
||||
} else if (data.value.statusCode === 400) {
|
||||
linterError.value = true;
|
||||
linterErrorText.value = data.value.data.message;
|
||||
linterErrorColumn.value = data.value.data.column;
|
||||
linterErrorLine.value = data.value.data.line;
|
||||
if (data.value.statusCode === 200) {
|
||||
linterError.value = false;
|
||||
linterErrorText.value = "";
|
||||
linterErrorColumn.value = 0;
|
||||
linterErrorLine.value = 0;
|
||||
} else if (data.value.statusCode === 400) {
|
||||
linterError.value = true;
|
||||
linterErrorText.value = data.value.data.message;
|
||||
linterErrorColumn.value = data.value.data.column;
|
||||
linterErrorLine.value = data.value.data.line;
|
||||
}
|
||||
} finally {
|
||||
isLinterChecking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,8 +139,8 @@ const saveCode = async () => {
|
||||
|
||||
if (linterError.value) {
|
||||
$swal.fire({
|
||||
title: "Ralat",
|
||||
text: "Terdapat ralat dalam kod anda. Sila betulkannya sebelum menyimpan.",
|
||||
title: "Error",
|
||||
text: "There is an error in your code. Please fix it before saving.",
|
||||
icon: "error",
|
||||
confirmButtonText: "Ok",
|
||||
});
|
||||
@ -150,8 +157,8 @@ const saveCode = async () => {
|
||||
});
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire({
|
||||
title: "Berjaya",
|
||||
text: "Kod telah berjaya disimpan.",
|
||||
title: "Success",
|
||||
text: "The code has been saved successfully.",
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
timer: 1000,
|
||||
@ -167,36 +174,45 @@ const saveCode = async () => {
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-alert v-if="hasError" class="mb-4" variant="primary">{{
|
||||
<rs-alert v-if="hasError" variant="danger" class="mb-4">{{
|
||||
error
|
||||
}}</rs-alert>
|
||||
<rs-card class="mb-0">
|
||||
<div class="p-4">
|
||||
<div class="flex justify-end gap-2 mb-4">
|
||||
<rs-button class="!p-2" @click="formatCode">
|
||||
<Icon name="simple-icons:prettier" size="20px" class="mr-1" />
|
||||
Format Kod</rs-button
|
||||
<rs-button
|
||||
class="!p-2"
|
||||
@click="saveCode"
|
||||
:disabled="isLinterChecking"
|
||||
>
|
||||
<rs-button class="!p-2" @click="saveCode">
|
||||
<Icon
|
||||
name="material-symbols:save-outline-rounded"
|
||||
size="20px"
|
||||
class="mr-1"
|
||||
/>
|
||||
Simpan Kod
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
v-if="!isLinterChecking"
|
||||
name="material-symbols:save-outline-rounded"
|
||||
size="20px"
|
||||
class="mr-1"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="eos-icons:loading"
|
||||
size="20px"
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
{{ isLinterChecking ? "Checking..." : "Save Code" }}
|
||||
</div>
|
||||
</rs-button>
|
||||
</div>
|
||||
<Transition>
|
||||
<rs-alert v-if="linterError">
|
||||
<rs-alert v-if="linterError" variant="danger" class="mb-4">
|
||||
<div class="flex gap-2">
|
||||
<Icon name="material-symbols:error-outline-rounded" size="20px" />
|
||||
<div>
|
||||
<div class="font-bold">Ralat ESLint</div>
|
||||
<div class="font-bold">ESLint Error</div>
|
||||
<div class="text-sm">
|
||||
{{ linterErrorText }}
|
||||
</div>
|
||||
<div class="text-xs mt-2">
|
||||
Baris: {{ linterErrorLine }} Lajur: {{ linterErrorColumn }}
|
||||
Line: {{ linterErrorLine }} Column: {{ linterErrorColumn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Penyunting Kandungan",
|
||||
title: "Content Editor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -11,13 +11,15 @@ const router = useRouter();
|
||||
const getPages = router.getRoutes();
|
||||
|
||||
const pages = getPages.filter((page) => {
|
||||
// filter out the pages that are not in the admin folder
|
||||
return (
|
||||
page.path.includes("/devtool") === false &&
|
||||
page.meta?.title &&
|
||||
page.meta?.title !== "Laman Utama" &&
|
||||
page.name
|
||||
);
|
||||
// Filter out pages in the devtool path
|
||||
if (page.path.includes("/devtool")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use page.name if page.meta.title doesn't exist
|
||||
const pageTitle = page.meta?.title || page.name;
|
||||
|
||||
return pageTitle && pageTitle !== "Home" && page.name;
|
||||
});
|
||||
|
||||
const searchText = ref("");
|
||||
@ -29,9 +31,8 @@ const modalData = ref({
|
||||
|
||||
const searchPages = () => {
|
||||
return pages.filter((page) => {
|
||||
return page.meta.title
|
||||
.toLowerCase()
|
||||
.includes(searchText.value.toLowerCase());
|
||||
const pageTitle = page.meta?.title || page.name;
|
||||
return pageTitle.toLowerCase().includes(searchText.value.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
@ -44,7 +45,7 @@ const capitalizeSentence = (sentence) => {
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const templateOptions = ref([{ label: "Pilih Templat", value: "" }]);
|
||||
const templateOptions = ref([{ label: "Select Template", value: "" }]);
|
||||
const selectTemplate = ref("");
|
||||
|
||||
const { data: templates } = await useFetch(
|
||||
@ -74,13 +75,13 @@ const importTemplate = (pageName) => {
|
||||
const confirmModal = async () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Adakah anda pasti mahu mengimport templat ini?",
|
||||
text: "Tindakan ini tidak boleh dibatalkan.",
|
||||
title: "Are you sure you want to import this template?",
|
||||
text: "This action cannot be undone.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya",
|
||||
confirmButtonText: "Yes",
|
||||
})
|
||||
.then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
@ -98,7 +99,7 @@ const confirmModal = async () => {
|
||||
|
||||
if (res.value.statusCode == 200) {
|
||||
$swal.fire({
|
||||
title: "Berjaya",
|
||||
title: "Success",
|
||||
text: res.value.message,
|
||||
icon: "success",
|
||||
confirmButtonText: "Ok",
|
||||
@ -123,14 +124,14 @@ const confirmModal = async () => {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Halaman ini digunakan untuk menyunting kandungan halaman. Anda boleh menyunting
|
||||
kandungan halaman dengan memilih halaman untuk disunting dari senarai kad
|
||||
di bawah.
|
||||
This page is used to edit the content of a page. You can edit the
|
||||
content of the page by choosing the page to edit from the card list
|
||||
below.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -140,7 +141,7 @@ const confirmModal = async () => {
|
||||
<!-- Search Button -->
|
||||
<FormKit
|
||||
v-model="searchText"
|
||||
placeholder="Cari Tajuk..."
|
||||
placeholder="Search Title..."
|
||||
type="search"
|
||||
/>
|
||||
|
||||
@ -152,7 +153,7 @@ const confirmModal = async () => {
|
||||
class="page border-2 border-gray-400 border-dashed rounded-lg"
|
||||
style="min-height: 250px"
|
||||
>
|
||||
Tambah Halaman Baru
|
||||
Add New Page
|
||||
</div> -->
|
||||
<div
|
||||
v-for="page in searchPages()"
|
||||
@ -161,7 +162,7 @@ const confirmModal = async () => {
|
||||
>
|
||||
<div class="pb-4">
|
||||
<h4 class="font-semibold">
|
||||
{{ capitalizeSentence(page.meta.title) }}
|
||||
{{ capitalizeSentence(page.meta?.title || page.name) }}
|
||||
</h4>
|
||||
<nuxt-link :to="page.path">
|
||||
<div
|
||||
@ -180,6 +181,13 @@ const confirmModal = async () => {
|
||||
class="button-list flex justify-between border-t pt-4 border-gray-300"
|
||||
>
|
||||
<div class="flex gap-x-2">
|
||||
<!-- <nuxt-link
|
||||
:to="`/devtool/content-editor/canvas?page=${page.name}`"
|
||||
>
|
||||
<rs-button variant="primary" class="!py-2 !px-3">
|
||||
<Icon name="ph:paint-brush-broad"></Icon>
|
||||
</rs-button>
|
||||
</nuxt-link> -->
|
||||
<nuxt-link
|
||||
:to="`/devtool/content-editor/code?page=${page.name}`"
|
||||
>
|
||||
@ -207,18 +215,18 @@ const confirmModal = async () => {
|
||||
<FormKit
|
||||
v-model="selectTemplate"
|
||||
type="select"
|
||||
label="Templat Kandungan"
|
||||
label="Content Template"
|
||||
:options="templateOptions"
|
||||
validation="required"
|
||||
validation-visibility="dirty"
|
||||
help="Sila pilih dengan teliti templat yang anda ingin import. Tindakan ini tidak boleh dibatalkan."
|
||||
help="Please choose carefully the template that you want to import. This action cannot be undone."
|
||||
/>
|
||||
<template #footer>
|
||||
<rs-button @click="showModal = false" variant="primary-text">
|
||||
Batal
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button @click="confirmModal" :disabled="!selectTemplate"
|
||||
>Sahkan</rs-button
|
||||
>Confirm</rs-button
|
||||
>
|
||||
</template>
|
||||
</rs-modal>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Penyunting Templat",
|
||||
title: "Template Editor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -31,14 +31,14 @@ const searchTemplate = () => {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Laman web ini berfungsi sebagai platform untuk pengurusan templat, membolehkan
|
||||
pengguna memilih dan menggunakan templat untuk memaparkan halaman mengikut
|
||||
reka bentuk pilihan mereka.
|
||||
This webpage serves as a platform for template management, enabling
|
||||
users to select and utilize templates for rendering pages according to
|
||||
their chosen design.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -48,7 +48,7 @@ const searchTemplate = () => {
|
||||
<!-- Search Button -->
|
||||
<FormKit
|
||||
v-model="searchText"
|
||||
placeholder="Cari Tajuk..."
|
||||
placeholder="Search Title..."
|
||||
type="search"
|
||||
/>
|
||||
|
||||
@ -60,7 +60,7 @@ const searchTemplate = () => {
|
||||
class="page border-2 border-gray-400 border-dashed rounded-lg"
|
||||
style="min-height: 250px"
|
||||
>
|
||||
Tambah Halaman Baru
|
||||
Add New Page
|
||||
</div> -->
|
||||
<div
|
||||
v-for="val in searchTemplate()"
|
||||
@ -98,7 +98,9 @@ const searchTemplate = () => {
|
||||
<div class="flex items-center mb-4">
|
||||
<p class="text-sm">{{ val.description }}</p>
|
||||
</div>
|
||||
<div class="tag h-10 flex justify-start items-center overflow-x-auto gap-x-2">
|
||||
<div
|
||||
class="tag h-10 flex justify-start items-center overflow-x-auto gap-x-2"
|
||||
>
|
||||
<rs-badge v-for="val2 in val.tag">
|
||||
{{ val2 }}
|
||||
</rs-badge>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Pelihat Templat",
|
||||
title: "Template Viewer",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -19,8 +19,8 @@ const { data: template } = await useFetch(
|
||||
|
||||
console.log(template.value.data);
|
||||
|
||||
const templateComponent = defineAsyncComponent(() =>
|
||||
import(`../../../../../templates/${template.value.data.filename}.vue`)
|
||||
const templateComponent = defineAsyncComponent(
|
||||
() => import(`../../../../../templates/${template.value.data.filename}.vue`)
|
||||
);
|
||||
</script>
|
||||
|
@ -14,7 +14,7 @@ const router = useRouter();
|
||||
const getRoutes = router.getRoutes();
|
||||
const getNavigation = Menu ? ref(Menu) : ref([]);
|
||||
|
||||
const allMenus = [];
|
||||
const allMenus = reactive([]);
|
||||
|
||||
const showCode = ref(false);
|
||||
let i = 1;
|
||||
@ -43,6 +43,16 @@ const showModalAddForm = ref({
|
||||
path: "",
|
||||
});
|
||||
|
||||
const systemPages = [
|
||||
"/devtool",
|
||||
"/dashboard",
|
||||
"/login",
|
||||
"/logout",
|
||||
"/register",
|
||||
"/reset-password",
|
||||
"/forgot-password",
|
||||
];
|
||||
|
||||
const kebabtoTitle = (str) => {
|
||||
if (!str) return str;
|
||||
return str
|
||||
@ -116,6 +126,10 @@ const openModalEdit = (menu) => {
|
||||
};
|
||||
|
||||
const saveEditMenu = async () => {
|
||||
// Clean the name and title ensure not spacing at the beginning or end
|
||||
showModalEditForm.value.title = showModalEditForm.value.title.trim();
|
||||
showModalEditForm.value.name = showModalEditForm.value.name.trim();
|
||||
|
||||
const res = await useFetch("/api/devtool/menu/edit", {
|
||||
method: "POST",
|
||||
initialCache: false,
|
||||
@ -133,6 +147,8 @@ const saveEditMenu = async () => {
|
||||
const data = res.data.value;
|
||||
|
||||
if (data.statusCode === 200) {
|
||||
showModalEdit.value = false;
|
||||
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Success",
|
||||
text: data.message,
|
||||
@ -140,8 +156,8 @@ const saveEditMenu = async () => {
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
// refresh the page
|
||||
nuxtApp.$router.go();
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
@ -154,6 +170,10 @@ const openModalAdd = () => {
|
||||
};
|
||||
|
||||
const saveAddMenu = async () => {
|
||||
// Clean the name and title ensure not spacing at the beginning or end
|
||||
showModalAddForm.value.title = showModalAddForm.value.title.trim();
|
||||
showModalAddForm.value.name = showModalAddForm.value.name.trim();
|
||||
|
||||
const res = await useFetch("/api/devtool/menu/add", {
|
||||
method: "POST",
|
||||
initialCache: false,
|
||||
@ -170,6 +190,8 @@ const saveAddMenu = async () => {
|
||||
const data = res.data.value;
|
||||
|
||||
if (data.statusCode === 200) {
|
||||
showModalAdd.value = false;
|
||||
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Success",
|
||||
text: data.message,
|
||||
@ -177,8 +199,8 @@ const saveAddMenu = async () => {
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
// refresh the page
|
||||
nuxtApp.$router.go();
|
||||
|
||||
window.location.reload();
|
||||
} else {
|
||||
nuxtApp.$swal.fire({
|
||||
title: "Error",
|
||||
@ -222,8 +244,7 @@ const deleteMenu = async (menu) => {
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// refresh the page
|
||||
nuxtApp.$router.go();
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -319,9 +340,6 @@ const overwriteJsonFileLocal = async (menus) => {
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// refresh the page
|
||||
nuxtApp.$router.go();
|
||||
}
|
||||
};
|
||||
|
||||
@ -398,6 +416,21 @@ const addMenuFromList = () => {
|
||||
//-----------------------------------------------------------------------------
|
||||
//-------------------------SECOND CHILD TAB ITEM (END)-------------------------
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// Add this watcher after the showModalEditForm ref declaration
|
||||
watch(
|
||||
() => showModalEditForm.value,
|
||||
(newTitle) => {
|
||||
showModalEditForm.value.name = newTitle.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => showModalAddForm.value.title,
|
||||
(newTitle) => {
|
||||
showModalAddForm.value.name = newTitle.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -408,14 +441,14 @@ const addMenuFromList = () => {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Halaman ini digunakan untuk mengedit menu laman web. Anda boleh
|
||||
menambah, mengedit, dan memadam item menu. Anda juga boleh mengubah
|
||||
susunan item menu dengan menyeret dan melepaskannya.
|
||||
This page is used to edit the menu of the website. You can add, edit,
|
||||
and delete menu items. You can also change the order of the menu items
|
||||
by dragging and dropping them.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -423,11 +456,11 @@ const addMenuFromList = () => {
|
||||
<rs-card>
|
||||
<div class="pt-2">
|
||||
<rs-tab fill>
|
||||
<rs-tab-item title="Semua Menu">
|
||||
<rs-tab-item title="All Menu">
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<rs-button @click="openModalAdd">
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Tambah Menu
|
||||
Add Menu
|
||||
</rs-button>
|
||||
</div>
|
||||
<!-- Table All Menu -->
|
||||
@ -447,7 +480,7 @@ const addMenuFromList = () => {
|
||||
>
|
||||
<template v-slot:name="data">
|
||||
<NuxtLink
|
||||
class="text-primary hover:underline"
|
||||
class="text-blue-700 hover:underline"
|
||||
:to="data.value.path"
|
||||
target="_blank"
|
||||
>{{ data.text }}</NuxtLink
|
||||
@ -470,39 +503,44 @@ const addMenuFromList = () => {
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:action="data">
|
||||
<div
|
||||
class="flex items-center"
|
||||
v-if="data.value.parentMenu != 'admin'"
|
||||
>
|
||||
<Icon
|
||||
name="material-symbols:edit-outline-rounded"
|
||||
class="text-primary hover:text-primary/90 cursor-pointer mr-1"
|
||||
size="22"
|
||||
@click="openModalEdit(data.value)"
|
||||
></Icon>
|
||||
<Icon
|
||||
name="material-symbols:close-rounded"
|
||||
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||
size="22"
|
||||
@click="deleteMenu(data.value)"
|
||||
></Icon>
|
||||
<div class="flex items-center">
|
||||
<template
|
||||
v-if="
|
||||
!systemPages.some((path) =>
|
||||
data.value.path.startsWith(path)
|
||||
) && data.value.parentMenu != 'admin'
|
||||
"
|
||||
>
|
||||
<Icon
|
||||
name="material-symbols:edit-outline-rounded"
|
||||
class="text-primary hover:text-primary/90 cursor-pointer mr-1"
|
||||
size="22"
|
||||
@click="openModalEdit(data.value)"
|
||||
></Icon>
|
||||
<Icon
|
||||
name="material-symbols:close-rounded"
|
||||
class="text-primary hover:text-primary/90 cursor-pointer"
|
||||
size="22"
|
||||
@click="deleteMenu(data.value)"
|
||||
></Icon>
|
||||
</template>
|
||||
<div v-else class="text-gray-400">-</div>
|
||||
</div>
|
||||
<div class="flex items-center" v-else>-</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-tab-item>
|
||||
<rs-tab-item title="Urus Menu Sisi">
|
||||
<rs-tab-item title="Manage Side Menu">
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<rs-button
|
||||
class="mr-2"
|
||||
@click="showCode ? (showCode = false) : (showCode = true)"
|
||||
>
|
||||
<Icon name="ic:baseline-code" class="mr-2"></Icon>
|
||||
{{ showCode ? "Sembunyikan" : "Tunjukkan" }} Kod JSON
|
||||
{{ showCode ? "Hide" : "Show" }} JSON Code
|
||||
</rs-button>
|
||||
<rs-button @click="overwriteJsonFileLocal(sideMenuList)">
|
||||
<Icon name="mdi:content-save-outline" class="mr-2"></Icon>
|
||||
Simpan Menu
|
||||
Save Menu
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
@ -510,7 +548,7 @@ const addMenuFromList = () => {
|
||||
<div>
|
||||
<FormKit
|
||||
type="search"
|
||||
placeholder="Cari Menu..."
|
||||
placeholder="Search Menu..."
|
||||
outer-class="mb-5"
|
||||
v-model="searchInput"
|
||||
/>
|
||||
@ -598,62 +636,110 @@ const addMenuFromList = () => {
|
||||
></FormKit>
|
||||
</rs-modal>
|
||||
|
||||
<rs-modal
|
||||
title="Edit Menu"
|
||||
v-model="showModalEdit"
|
||||
ok-title="Save"
|
||||
:ok-callback="saveEditMenu"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Title"
|
||||
v-model="showModalEditForm.title"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Name"
|
||||
v-model="showModalEditForm.name"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Path"
|
||||
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
|
||||
v-model="showModalEditForm.path"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="bg-slate-100 h-full rounded-l-md p-3">/</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
<rs-modal title="Edit Menu" v-model="showModalEdit" :overlay-close="false">
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="saveEditMenu">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Title"
|
||||
:validation="[['required'], ['matches', '/^[a-zA-Z0-9]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'Title is required',
|
||||
matches:
|
||||
'Title contains invalid characters. Only letters and numbers are allowed.',
|
||||
}"
|
||||
v-model="showModalEditForm.title"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Path"
|
||||
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
|
||||
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'Path is required',
|
||||
matches:
|
||||
'Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.',
|
||||
}"
|
||||
v-model="showModalEditForm.path"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
|
||||
>
|
||||
/
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="outline" @click="showModalEdit = false">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button btnType="submit">
|
||||
<Icon
|
||||
name="material-symbols:save-outline"
|
||||
class="mr-2 !w-4 !h-4"
|
||||
/>
|
||||
Save Changes
|
||||
</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
||||
|
||||
<template #footer> </template>
|
||||
</rs-modal>
|
||||
|
||||
<rs-modal
|
||||
title="Add Menu"
|
||||
v-model="showModalAdd"
|
||||
ok-title="Save"
|
||||
:ok-callback="saveAddMenu"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Title"
|
||||
v-model="showModalAddForm.title"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Name"
|
||||
v-model="showModalAddForm.name"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Path"
|
||||
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
|
||||
v-model="showModalAddForm.path"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3">
|
||||
/
|
||||
<rs-modal title="Add Menu" v-model="showModalAdd" :overlay-close="false">
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="saveAddMenu">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Title"
|
||||
:validation="[['required'], ['matches', '/^[a-zA-Z0-9]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'Title is required',
|
||||
matches:
|
||||
'Title contains invalid characters. Only letters and numbers are allowed.',
|
||||
}"
|
||||
v-model="showModalAddForm.title"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Path"
|
||||
help="If the last path name is '/', the name of the file will be from its name property. While if the last path name is not '/', the name of the file will be from its path property."
|
||||
:validation="[['required'], ['matches', '/^[a-z0-9/-]+$/']]"
|
||||
:validation-messages="{
|
||||
required: 'Path is required',
|
||||
matches:
|
||||
'Path contains invalid characters or spacing before or after. Only letters, numbers, dashes, and underscores are allowed.',
|
||||
}"
|
||||
v-model="showModalAddForm.path"
|
||||
>
|
||||
<template #prefix>
|
||||
<div
|
||||
class="bg-slate-100 dark:bg-slate-700 h-full rounded-l-md p-3"
|
||||
>
|
||||
/
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="outline" @click="showModalAdd = false">
|
||||
Cancel
|
||||
</rs-button>
|
||||
<rs-button btnType="submit">
|
||||
<Icon
|
||||
name="material-symbols:add-circle-outline-rounded"
|
||||
class="mr-2 !w-4 !h-4"
|
||||
/>
|
||||
Add Menu
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
</template>
|
||||
|
||||
<template #footer> </template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
169
pages/devtool/orm/index.vue
Normal file
169
pages/devtool/orm/index.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Database (ORM)",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const searchText = ref("");
|
||||
const tableList = ref([]);
|
||||
|
||||
const { data } = await useFetch("/api/devtool/orm/schema", {
|
||||
method: "GET",
|
||||
query: {
|
||||
type: "table",
|
||||
},
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
tableList.value = data.value.data;
|
||||
}
|
||||
|
||||
const deleteTable = async (tableName) => {
|
||||
try {
|
||||
const result = await $swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: `You are about to delete the table '${tableName}'. This action cannot be undone!`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Yes, delete it!",
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
const { data } = await useFetch(
|
||||
`/api/devtool/orm/table/delete/${tableName}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
await $swal.fire("Deleted!", data.value.message, "success");
|
||||
// Remove the deleted table from the list
|
||||
tableList.value = tableList.value.filter(
|
||||
(table) => table.name !== tableName
|
||||
);
|
||||
} else {
|
||||
throw new Error(data.value.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting table:", error);
|
||||
await $swal.fire(
|
||||
"Error!",
|
||||
`Failed to delete table: ${error.message}`,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
This page is used to edit the ORM schema. You can add, edit, and
|
||||
delete the model and its fields. The changes will be saved to the
|
||||
database.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<rs-card>
|
||||
<div class="p-4">
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<nuxt-link to="/devtool/orm/table/create">
|
||||
<rs-button>
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Add Table
|
||||
</rs-button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<!-- Search Button -->
|
||||
<FormKit
|
||||
v-model="searchText"
|
||||
placeholder="Search Title..."
|
||||
type="search"
|
||||
/>
|
||||
|
||||
<div v-if="tableList" class="grid grid-cols-1 gap-5">
|
||||
<div v-for="tbl in tableList" class="p-5 border rounded-md">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<div class="flex items-center text-primary gap-2">
|
||||
<Icon name="ph:table-fill" />
|
||||
<h4>
|
||||
{{ tbl.name }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<span
|
||||
v-for="field in tbl.fields"
|
||||
class="text-xs py-1 px-3 inline-block bg-slate-100 rounded-lg ring-1 ring-slate-200"
|
||||
>
|
||||
{{ field }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between mt-5">
|
||||
<NuxtLink
|
||||
:to="`/devtool/orm/view/${tbl.name}`"
|
||||
class="flex justify-center items-center"
|
||||
>
|
||||
<rs-button
|
||||
variant="primary"
|
||||
class="flex justify-center items-center"
|
||||
>
|
||||
View Data
|
||||
</rs-button>
|
||||
</NuxtLink>
|
||||
|
||||
<div v-if="!tbl.disabled" class="flex justify-between gap-3">
|
||||
<NuxtLink
|
||||
:to="`/devtool/orm/table/modify/${tbl.name}`"
|
||||
class="flex justify-center items-center"
|
||||
>
|
||||
<rs-button
|
||||
variant="secondary"
|
||||
class="flex justify-center items-center"
|
||||
>
|
||||
<Icon name="ph:note-pencil-bold" class="w-4 h-4 mr-1" />
|
||||
Modify
|
||||
</rs-button>
|
||||
</NuxtLink>
|
||||
<rs-button
|
||||
variant="danger-outline"
|
||||
class="flex justify-center items-center"
|
||||
@click="deleteTable(tbl.name)"
|
||||
>
|
||||
<Icon name="ph:trash-simple-bold" class="w-4 h-4 mr-1" />
|
||||
Delete
|
||||
</rs-button>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-end text-xs py-1 px-3 text-slate-400 cursor-not-allowed"
|
||||
v-else
|
||||
>
|
||||
Cannot Modify System Table
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
315
pages/devtool/orm/table/create/index.vue
Normal file
315
pages/devtool/orm/table/create/index.vue
Normal file
@ -0,0 +1,315 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Database Create Table",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const tableName = ref("");
|
||||
const tableKey = ref(0);
|
||||
const tableData = ref([]);
|
||||
const columnTypes = ref([]);
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const { data: dbConfiguration } = await useFetch(
|
||||
"/api/devtool/orm/table/config"
|
||||
);
|
||||
|
||||
if (dbConfiguration.value && dbConfiguration.value.statusCode === 200) {
|
||||
let tempObject = {};
|
||||
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.push(tempObject);
|
||||
|
||||
// Update columnTypes to use the new structure
|
||||
columnTypes.value = dbConfiguration.value.data.columnTypes.flatMap((group) =>
|
||||
group.options.map((option) =>
|
||||
typeof option === "string" ? { label: option, value: option } : option
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const addNewField = () => {
|
||||
let tempObject = {};
|
||||
|
||||
// Add new field after the last field
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.push(tempObject);
|
||||
|
||||
tableKey.value++;
|
||||
};
|
||||
|
||||
const removeField = (index) => {
|
||||
tableData.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const sortField = (index, direction) => {
|
||||
if (direction === "up") {
|
||||
if (index === 0) return;
|
||||
|
||||
let temp = tableData.value[index];
|
||||
tableData.value[index] = tableData.value[index - 1];
|
||||
tableData.value[index - 1] = temp;
|
||||
|
||||
tableKey.value++;
|
||||
} else {
|
||||
if (index === tableData.value.length - 1) return;
|
||||
|
||||
let temp = tableData.value[index];
|
||||
tableData.value[index] = tableData.value[index + 1];
|
||||
tableData.value[index + 1] = temp;
|
||||
|
||||
tableKey.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const autoIcrementColumn = ref("");
|
||||
|
||||
const computedAutoIncrementColumn = computed(() => {
|
||||
return tableData.value.map((data) => {
|
||||
return {
|
||||
label: data.name,
|
||||
value: data.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const checkRadioButton = async (index, event) => {
|
||||
try {
|
||||
// change tableData[index].primaryKey value to true
|
||||
tableData.value[index].primaryKey = event.target.checked;
|
||||
|
||||
// change all other tableData[index].primaryKey value to false
|
||||
tableData.value.forEach((data, i) => {
|
||||
if (i !== index) {
|
||||
tableData.value[i].primaryKey = "";
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Are you sure?",
|
||||
text: "You will lose all the data you have entered.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, reset it!",
|
||||
cancelButtonText: "No, cancel!",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
tableName.value = "";
|
||||
tableData.value = [];
|
||||
tableKey.value = 0;
|
||||
|
||||
let tempObject = {};
|
||||
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.push(tempObject);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitCreateTable = async () => {
|
||||
try {
|
||||
const { data } = await useFetch("/api/devtool/orm/table/create", {
|
||||
method: "POST",
|
||||
body: {
|
||||
tableName: tableName.value,
|
||||
tableSchema: tableData.value,
|
||||
autoIncrementColumn: autoIcrementColumn.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.value.statusCode == 200) {
|
||||
$swal.fire({
|
||||
title: "Success",
|
||||
text: data.value.message,
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
navigateTo("/devtool/orm");
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<rs-card class="py-5">
|
||||
<FormKit
|
||||
type="form"
|
||||
:classes="{
|
||||
messages: 'px-5',
|
||||
}"
|
||||
:actions="false"
|
||||
@submit="submitCreateTable"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 mb-5 px-5"
|
||||
>
|
||||
<div>
|
||||
<h5 class="font-semibold">Create Table</h5>
|
||||
<span class="text-sm text-gray-500">
|
||||
Create a new table in the database.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="resetData" variant="primary-outline">
|
||||
Reset Table
|
||||
</rs-button>
|
||||
<rs-button btnType="submit" class="mb-4 w-[100px]">
|
||||
Save
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="px-5">
|
||||
<FormKit
|
||||
v-model="tableName"
|
||||
type="text"
|
||||
label="Table name"
|
||||
placeholder="Enter table name"
|
||||
validation="required|length:3,64"
|
||||
:classes="{ outer: 'mb-8' }"
|
||||
:validation-messages="{
|
||||
required: 'Table name is required',
|
||||
length:
|
||||
'Table name must be at least 3 characters and at most 64 characters',
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<rs-table
|
||||
v-if="tableData && tableData.length > 0"
|
||||
:data="tableData"
|
||||
:key="tableKey"
|
||||
:disableSort="true"
|
||||
class="mb-8"
|
||||
>
|
||||
<template v-slot:name="data">
|
||||
<FormKit
|
||||
v-model="data.value.name"
|
||||
:classes="{
|
||||
outer: 'mb-0 w-[200px]',
|
||||
}"
|
||||
placeholder="Enter column name"
|
||||
type="text"
|
||||
validation="required|length:3,64"
|
||||
:validation-messages="{
|
||||
required: 'Column name is required',
|
||||
length:
|
||||
'Column name must be at least 3 characters and at most 64 characters',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:type="data">
|
||||
<FormKit
|
||||
v-if="columnTypes && columnTypes.length > 0"
|
||||
v-model="data.value.type"
|
||||
:classes="{ outer: 'mb-0 w-[100px]' }"
|
||||
:options="columnTypes"
|
||||
type="select"
|
||||
placeholder="Select type"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Column type is required',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:length="data">
|
||||
<FormKit
|
||||
v-model="data.value.length"
|
||||
:classes="{ outer: 'mb-0 w-[150px]' }"
|
||||
placeholder="Enter length"
|
||||
type="number"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:defaultValue="data">
|
||||
<FormKit
|
||||
v-model="data.value.defaultValue"
|
||||
:classes="{ outer: 'mb-0 w-[150px]' }"
|
||||
placeholder="Optional"
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:nullable="data">
|
||||
<FormKit
|
||||
v-model="data.value.nullable"
|
||||
:classes="{ wrapper: 'mb-0', outer: 'mb-0' }"
|
||||
type="checkbox"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:primaryKey="data">
|
||||
<FormKit
|
||||
v-model="data.value.primaryKey"
|
||||
:classes="{
|
||||
wrapper: 'mb-0',
|
||||
outer: 'mb-0',
|
||||
input: 'icon-check rounded-full',
|
||||
}"
|
||||
type="checkbox"
|
||||
@change="checkRadioButton(data.index, $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:actions="data">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<rs-button @click="addNewField" type="button" class="p-1 w-6 h-6">
|
||||
<Icon name="ph:plus" />
|
||||
</rs-button>
|
||||
|
||||
<rs-button
|
||||
@click="sortField(data.index, 'up')"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
:disabled="data.index === 0"
|
||||
>
|
||||
<Icon name="ph:arrow-up" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="sortField(data.index, 'down')"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
:disabled="data.index === tableData.length - 1"
|
||||
>
|
||||
<Icon name="ph:arrow-down" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="removeField(data.index)"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
variant="danger"
|
||||
:disabled="data.index === 0 && tableData.length === 1"
|
||||
>
|
||||
<Icon name="ph:x" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
360
pages/devtool/orm/table/modify/[table].vue
Normal file
360
pages/devtool/orm/table/modify/[table].vue
Normal file
@ -0,0 +1,360 @@
|
||||
<script setup>
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
definePageMeta({
|
||||
title: "Database Modify Table",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const tableName = ref("");
|
||||
const tableKey = ref(0);
|
||||
const tableData = ref([]);
|
||||
const columnTypes = ref([]);
|
||||
|
||||
const { table } = useRoute().params;
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const { data: dbConfiguration } = await useFetch(
|
||||
"/api/devtool/orm/table/config"
|
||||
);
|
||||
|
||||
if (dbConfiguration.value && dbConfiguration.value.statusCode === 200) {
|
||||
let tempObject = {};
|
||||
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.push(tempObject);
|
||||
|
||||
// Update columnTypes to use the new structure
|
||||
columnTypes.value = dbConfiguration.value.data.columnTypes.flatMap((group) =>
|
||||
group.options.map((option) =>
|
||||
typeof option === "string" ? { label: option, value: option } : option
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { data: tableDetail } = await useFetch(
|
||||
`/api/devtool/orm/table/modify/get`,
|
||||
{
|
||||
method: "GET",
|
||||
params: {
|
||||
tableName: table,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// console.log(tableDetail.value);
|
||||
|
||||
if (tableDetail.value.statusCode === 200) {
|
||||
tableData.value = tableDetail.value.data.map((item) => ({
|
||||
...item,
|
||||
actions: {
|
||||
...item.actions,
|
||||
id: uuidv4(), // Add a unique id to each item's actions
|
||||
},
|
||||
}));
|
||||
tableName.value = table;
|
||||
}
|
||||
|
||||
const addNewField = (index) => {
|
||||
let tempObject = {
|
||||
actions: {
|
||||
id: uuidv4(), // Add a unique id to the new field's actions
|
||||
},
|
||||
};
|
||||
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.splice(index + 1, 0, tempObject);
|
||||
tableKey.value++;
|
||||
};
|
||||
|
||||
const sortField = (id, direction) => {
|
||||
const index = tableData.value.findIndex((item) => item.actions.id === id);
|
||||
if (direction === "up" && index > 0) {
|
||||
// Move the current field up
|
||||
const temp = tableData.value[index];
|
||||
tableData.value[index] = tableData.value[index - 1];
|
||||
tableData.value[index - 1] = temp;
|
||||
} else if (direction === "down" && index < tableData.value.length - 1) {
|
||||
// Move the current field down
|
||||
const temp = tableData.value[index];
|
||||
tableData.value[index] = tableData.value[index + 1];
|
||||
tableData.value[index + 1] = temp;
|
||||
}
|
||||
tableKey.value++;
|
||||
};
|
||||
|
||||
const removeField = (id) => {
|
||||
if (tableData.value.length > 1) {
|
||||
const index = tableData.value.findIndex((item) => item.actions.id === id);
|
||||
tableData.value.splice(index, 1);
|
||||
tableKey.value++;
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Error",
|
||||
text: "Cannot remove the last field.",
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const autoIcrementColumn = ref("");
|
||||
|
||||
const computedAutoIncrementColumn = computed(() => {
|
||||
return tableData.value.map((data) => {
|
||||
return {
|
||||
label: data.name,
|
||||
value: data.name,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const checkRadioButton = async (index, event) => {
|
||||
try {
|
||||
// change tableData[index].primaryKey value to true
|
||||
tableData.value[index].primaryKey = event.target.checked;
|
||||
|
||||
// change all other tableData[index].primaryKey value to false
|
||||
tableData.value.forEach((data, i) => {
|
||||
if (i !== index) {
|
||||
tableData.value[i].primaryKey = "";
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Are you sure?",
|
||||
text: "You will lose all the data you have entered.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, reset it!",
|
||||
cancelButtonText: "No, cancel!",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
tableName.value = "";
|
||||
tableData.value = [];
|
||||
tableKey.value = 0;
|
||||
|
||||
let tempObject = {};
|
||||
|
||||
dbConfiguration.value.data.tableField.forEach((field) => {
|
||||
tempObject[field] = "";
|
||||
});
|
||||
|
||||
tableData.value.push(tempObject);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const submitModifyTable = async () => {
|
||||
try {
|
||||
// console.log({
|
||||
// tableName: tableName.value,
|
||||
// tableSchema: tableData.value,
|
||||
// autoIncrementColumn: autoIcrementColumn.value,
|
||||
// });
|
||||
|
||||
// return;
|
||||
const { data } = await useFetch("/api/devtool/orm/table/modify", {
|
||||
method: "POST",
|
||||
body: {
|
||||
tableName: tableName.value,
|
||||
tableSchema: tableData.value,
|
||||
autoIncrementColumn: autoIcrementColumn.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.value.statusCode == 200) {
|
||||
$swal.fire({
|
||||
title: "Success",
|
||||
text: data.value.message,
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<rs-card class="py-5">
|
||||
<FormKit
|
||||
type="form"
|
||||
:classes="{
|
||||
messages: 'px-5',
|
||||
}"
|
||||
:actions="false"
|
||||
@submit="submitModifyTable"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 mb-5 px-5"
|
||||
>
|
||||
<div>
|
||||
<h5 class="font-semibold">Modify Table</h5>
|
||||
<span class="text-sm text-gray-500">
|
||||
Modify a new table in the database.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<rs-button @click="resetData" variant="primary-outline">
|
||||
Reset Table
|
||||
</rs-button>
|
||||
<rs-button btnType="submit" class="mb-4 w-[100px]">
|
||||
Save
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="px-5">
|
||||
<FormKit
|
||||
v-model="tableName"
|
||||
type="text"
|
||||
label="Table name"
|
||||
placeholder="Enter table name"
|
||||
validation="required|length:3,64"
|
||||
:classes="{ outer: 'mb-8' }"
|
||||
:validation-messages="{
|
||||
required: 'Table name is required',
|
||||
length:
|
||||
'Table name must be at least 3 characters and at most 64 characters',
|
||||
}"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<rs-table
|
||||
v-if="tableData && tableData.length > 0"
|
||||
:data="tableData"
|
||||
:key="tableKey"
|
||||
:disableSort="true"
|
||||
:pageSize="100"
|
||||
class="mb-8"
|
||||
>
|
||||
<template v-slot:name="data">
|
||||
<FormKit
|
||||
v-model="data.value.name"
|
||||
:classes="{
|
||||
outer: 'mb-0 w-[200px]',
|
||||
}"
|
||||
placeholder="Enter column name"
|
||||
type="text"
|
||||
validation="required|length:3,64"
|
||||
:validation-messages="{
|
||||
required: 'Column name is required',
|
||||
length:
|
||||
'Column name must be at least 3 characters and at most 64 characters',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:type="data">
|
||||
<FormKit
|
||||
v-if="columnTypes && columnTypes.length > 0"
|
||||
v-model="data.value.type"
|
||||
:classes="{ outer: 'mb-0 w-[100px]' }"
|
||||
:options="columnTypes"
|
||||
type="select"
|
||||
placeholder="Select type"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Column type is required',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:length="data">
|
||||
<FormKit
|
||||
v-model="data.value.length"
|
||||
:classes="{ outer: 'mb-0 w-[150px]' }"
|
||||
placeholder="Enter length"
|
||||
type="number"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:defaultValue="data">
|
||||
<FormKit
|
||||
v-model="data.value.defaultValue"
|
||||
:classes="{ outer: 'mb-0 w-[150px]' }"
|
||||
placeholder="Optional"
|
||||
type="text"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:nullable="data">
|
||||
<FormKit
|
||||
v-model="data.value.nullable"
|
||||
:classes="{ wrapper: 'mb-0', outer: 'mb-0' }"
|
||||
type="checkbox"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:primaryKey="data">
|
||||
<FormKit
|
||||
v-model="data.value.primaryKey"
|
||||
:classes="{
|
||||
wrapper: 'mb-0',
|
||||
outer: 'mb-0',
|
||||
input: 'icon-check rounded-full',
|
||||
}"
|
||||
type="checkbox"
|
||||
@change="checkRadioButton(data.index, $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:actions="data">
|
||||
<div class="flex justify-center items-center gap-2">
|
||||
<rs-button
|
||||
@click="addNewField(tableData.indexOf(data.value))"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
>
|
||||
<Icon name="ph:plus" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="sortField(data.value.actions.id, 'up')"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
:disabled="tableData.indexOf(data.value) === 0"
|
||||
>
|
||||
<Icon name="ph:arrow-up" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="sortField(data.value.actions.id, 'down')"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
:disabled="
|
||||
tableData.indexOf(data.value) === tableData.length - 1
|
||||
"
|
||||
>
|
||||
<Icon name="ph:arrow-down" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="removeField(data.value.actions.id)"
|
||||
type="button"
|
||||
class="p-1 w-6 h-6"
|
||||
variant="danger"
|
||||
:disabled="tableData.length === 1"
|
||||
>
|
||||
<Icon name="ph:x" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
79
pages/devtool/orm/view/[table]/index.vue
Normal file
79
pages/devtool/orm/view/[table]/index.vue
Normal file
@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "ORM Table Editor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const { table } = useRoute().params;
|
||||
|
||||
const { data: tableData } = await useFetch("/api/devtool/orm/data/get", {
|
||||
method: "GET",
|
||||
query: {
|
||||
tableName: table,
|
||||
},
|
||||
});
|
||||
|
||||
const openPrismaStudio = async () => {
|
||||
const { data } = await useFetch("/api/devtool/orm/studio", {
|
||||
method: "GET",
|
||||
query: {
|
||||
tableName: table,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire({
|
||||
title: "Prisma Studio",
|
||||
text: data.value.message,
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Prisma Studio",
|
||||
text: data.value.message,
|
||||
icon: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<rs-card class="py-5">
|
||||
<div
|
||||
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-y-2 px-5"
|
||||
>
|
||||
<div>
|
||||
<h5 class="font-semibold">Table - {{ table }}</h5>
|
||||
<span class="text-sm text-gray-500">
|
||||
Below is the data of the table.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<rs-button @click="openPrismaStudio" class="mb-4">
|
||||
Open Prisma Studio
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<rs-table
|
||||
v-if="tableData.data && tableData.data.length > 0"
|
||||
:key="tableData.data"
|
||||
:data="tableData.data"
|
||||
advanced
|
||||
/>
|
||||
<div v-else class="flex justify-center my-3">
|
||||
<div class="text-center">
|
||||
<h6 class="font-semibold">Data Not Found</h6>
|
||||
<span class="text-sm text-gray-500">
|
||||
There is no data available for this table.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Senarai Peranan",
|
||||
title: "Role List",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
});
|
||||
@ -38,8 +38,8 @@ const showModalDeleteForm = ref({
|
||||
});
|
||||
|
||||
const statusDropdown = ref([
|
||||
{ label: "Aktif", value: "ACTIVE" },
|
||||
{ label: "Tidak Aktif", value: "INACTIVE" },
|
||||
{ label: "Active", value: "ACTIVE" },
|
||||
{ label: "Inactive", value: "INACTIVE" },
|
||||
]);
|
||||
|
||||
const roleListbyUser = ref([]);
|
||||
@ -199,8 +199,8 @@ const saveUser = async () => {
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Pengguna telah berjaya ditambah",
|
||||
title: "Success",
|
||||
text: "User has been added successfully",
|
||||
});
|
||||
|
||||
await getUserList();
|
||||
@ -209,7 +209,7 @@ const saveUser = async () => {
|
||||
} else {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -227,22 +227,18 @@ const saveRole = async () => {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Peranan telah berjaya dikemas kini",
|
||||
title: "Success",
|
||||
text: "Role has been updated successfully",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// showModal.value = false;
|
||||
|
||||
// await getRoleList();
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getRoleList();
|
||||
showModal.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -257,22 +253,18 @@ const saveRole = async () => {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Peranan telah ditambah",
|
||||
title: "Success",
|
||||
text: "Role has been added",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// showModal.value = false;
|
||||
|
||||
// await getRoleList();
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getRoleList();
|
||||
showModal.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -290,21 +282,19 @@ const deleteRole = async () => {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Pengguna telah dipadam",
|
||||
title: "Success",
|
||||
text: "Role has been deleted",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// Timer to wait timer in swal
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getRoleList();
|
||||
showModalDelete.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -331,13 +321,13 @@ function groupRoleByUser() {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Halaman ini hanya boleh diakses oleh pengguna admin. Anda boleh menguruskan pengguna
|
||||
di sini. Anda juga boleh menambah pengguna baru. Anda juga boleh menukar peranan pengguna.
|
||||
This page is only accessible by admin users. You can manage users
|
||||
here. You can also add new users. You can also change user roles.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -345,11 +335,11 @@ function groupRoleByUser() {
|
||||
<rs-card>
|
||||
<div class="pt-2">
|
||||
<rs-tab fill>
|
||||
<rs-tab-item title="Semua Peranan">
|
||||
<rs-tab-item title="All Role">
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<rs-button @click="openModal(null, 'add')">
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Tambah Peranan
|
||||
Add Role
|
||||
</rs-button>
|
||||
</div>
|
||||
<rs-table
|
||||
@ -397,31 +387,32 @@ function groupRoleByUser() {
|
||||
</rs-card>
|
||||
|
||||
<rs-modal
|
||||
:title="modalType == 'edit' ? 'Sunting Peranan' : 'Tambah Peranan'"
|
||||
ok-title="Simpan"
|
||||
:title="modalType == 'edit' ? 'Edit Role' : 'Add Role'"
|
||||
ok-title="Save"
|
||||
:ok-callback="saveRole"
|
||||
v-model="showModal"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalForm.name"
|
||||
label="Nama"
|
||||
label="Name"
|
||||
validation="required"
|
||||
validation-visibility="live"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
v-model="showModalForm.description"
|
||||
label="Penerangan"
|
||||
label="Description"
|
||||
/>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label
|
||||
class="formkit-label font-semibold text-gray-700 dark:text-gray-200 blockfont-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
|
||||
for="input_4"
|
||||
>
|
||||
Pengguna
|
||||
Users
|
||||
</label>
|
||||
<rs-button size="sm" @click="openModalUser"> Tambah Pengguna </rs-button>
|
||||
<rs-button size="sm" @click="openModalUser"> Add User </rs-button>
|
||||
</div>
|
||||
<v-select
|
||||
class="formkit-vselect"
|
||||
@ -432,7 +423,7 @@ function groupRoleByUser() {
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
v-model="checkAllUser"
|
||||
label="Semua Pengguna"
|
||||
label="All Users"
|
||||
input-class="icon-check"
|
||||
/>
|
||||
<FormKit
|
||||
@ -446,30 +437,31 @@ function groupRoleByUser() {
|
||||
|
||||
<!-- Modal Role -->
|
||||
<rs-modal
|
||||
title="Tambah Pengguna"
|
||||
ok-title="Simpan"
|
||||
cancel-title="Kembali"
|
||||
title="Add User"
|
||||
ok-title="Save"
|
||||
cancel-title="Back"
|
||||
:cancel-callback="closeModalUser"
|
||||
:ok-callback="saveUser"
|
||||
v-model="showModalUser"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalUserForm.username"
|
||||
name="username"
|
||||
label="Nama Pengguna"
|
||||
label="Username"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalUserForm.fullname"
|
||||
name="fullname"
|
||||
label="Nama Penuh"
|
||||
label="Fullname"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalUserForm.email"
|
||||
name="email"
|
||||
label="E-mel"
|
||||
label="Email"
|
||||
validation="email"
|
||||
validation-visibility="dirty"
|
||||
/>
|
||||
@ -477,7 +469,7 @@ function groupRoleByUser() {
|
||||
type="mask"
|
||||
v-model="showModalUserForm.phone"
|
||||
name="phone"
|
||||
label="Telefon"
|
||||
label="Phone"
|
||||
mask="###########"
|
||||
/>
|
||||
|
||||
@ -492,14 +484,15 @@ function groupRoleByUser() {
|
||||
|
||||
<!-- Modal Delete Confirmation -->
|
||||
<rs-modal
|
||||
title="Pengesahan Padam"
|
||||
ok-title="Ya"
|
||||
cancel-title="Tidak"
|
||||
title="Delete Confirmation"
|
||||
ok-title="Yes"
|
||||
cancel-title="No"
|
||||
:ok-callback="deleteRole"
|
||||
v-model="showModalDelete"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<p>
|
||||
Adakah anda pasti mahu memadam peranan ini ({{ showModalDeleteForm.name }})?
|
||||
Are you sure want to delete this role ({{ showModalDeleteForm.name }})?
|
||||
</p>
|
||||
</rs-modal>
|
||||
</div>
|
@ -3,12 +3,9 @@ definePageMeta({
|
||||
title: "User List",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
keepalive: {
|
||||
exclude: ["rs-table"],
|
||||
},
|
||||
});
|
||||
|
||||
const { $swal, $router } = useNuxtApp();
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const userList = ref([]);
|
||||
const userRoleList = ref([]);
|
||||
@ -37,8 +34,8 @@ const showModalDeleteForm = ref({
|
||||
});
|
||||
|
||||
const statusDropdown = ref([
|
||||
{ label: "Aktif", value: "ACTIVE" },
|
||||
{ label: "Tidak Aktif", value: "INACTIVE" },
|
||||
{ label: "Active", value: "ACTIVE" },
|
||||
{ label: "Inactive", value: "INACTIVE" },
|
||||
]);
|
||||
|
||||
const checkAllRole = ref(false);
|
||||
@ -191,26 +188,21 @@ const saveUser = async () => {
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
// console.log("data.value", data.value);
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Pengguna telah ditambah",
|
||||
title: "Success",
|
||||
text: "User has been added",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
// await getUserList();
|
||||
// showModal.value = false;
|
||||
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getUserList();
|
||||
showModal.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -225,22 +217,18 @@ const saveUser = async () => {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Pengguna telah dikemaskini",
|
||||
title: "Success",
|
||||
text: "User has been updated",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
// await getUserList();
|
||||
// showModal.value = false;
|
||||
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getUserList();
|
||||
showModal.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -258,21 +246,18 @@ const deleteUser = async () => {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "success",
|
||||
title: "Berjaya",
|
||||
text: "Pengguna telah dipadam",
|
||||
title: "Success",
|
||||
text: "User has been deleted",
|
||||
timer: 1000,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
|
||||
// Timer to wait timer in swal
|
||||
setTimeout(() => {
|
||||
$router.go();
|
||||
}, 1000);
|
||||
await getUserList();
|
||||
showModalDelete.value = false;
|
||||
} else {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
icon: "error",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
});
|
||||
}
|
||||
@ -298,7 +283,7 @@ const saveRole = async () => {
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
title: "Berjaya",
|
||||
title: "Success",
|
||||
text: data.value.message,
|
||||
icon: "success",
|
||||
timer: 1000,
|
||||
@ -310,7 +295,7 @@ const saveRole = async () => {
|
||||
} else {
|
||||
$swal.fire({
|
||||
position: "center",
|
||||
title: "Ralat",
|
||||
title: "Error",
|
||||
text: data.value.message,
|
||||
icon: "error",
|
||||
});
|
||||
@ -338,13 +323,13 @@ function groupUserByRole() {
|
||||
<template #header>
|
||||
<div class="flex">
|
||||
<Icon class="mr-2 flex justify-center" name="ic:outline-info"></Icon
|
||||
>Maklumat
|
||||
>Info
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="mb-4">
|
||||
Halaman ini hanya boleh diakses oleh pengguna admin. Anda boleh menguruskan pengguna
|
||||
di sini. Anda juga boleh menambah pengguna baru. Anda juga boleh menukar peranan pengguna.
|
||||
This page is only accessible by admin users. You can manage users
|
||||
here. You can also add new users. You can also change user roles.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
@ -352,11 +337,11 @@ function groupUserByRole() {
|
||||
<rs-card>
|
||||
<div class="pt-2">
|
||||
<rs-tab fill>
|
||||
<rs-tab-item title="Semua Pengguna">
|
||||
<rs-tab-item title="All User">
|
||||
<div class="flex justify-end items-center mb-4">
|
||||
<rs-button @click="openModal(null, 'add')">
|
||||
<Icon name="material-symbols:add" class="mr-1"></Icon>
|
||||
Tambah Pengguna
|
||||
Add User
|
||||
</rs-button>
|
||||
</div>
|
||||
<rs-table
|
||||
@ -405,29 +390,30 @@ function groupUserByRole() {
|
||||
</rs-card>
|
||||
|
||||
<rs-modal
|
||||
:title="modalType == 'edit' ? 'Sunting Pengguna' : 'Tambah Pengguna'"
|
||||
ok-title="Simpan"
|
||||
:title="modalType == 'edit' ? 'Edit User' : 'Add User'"
|
||||
ok-title="Save"
|
||||
:ok-callback="saveUser"
|
||||
v-model="showModal"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalForm.username"
|
||||
name="username"
|
||||
label="Nama Pengguna"
|
||||
label="Username"
|
||||
:disabled="modalType == 'edit' ? true : false"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalForm.fullname"
|
||||
name="fullname"
|
||||
label="Nama Penuh"
|
||||
label="Fullname"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalForm.email"
|
||||
name="email"
|
||||
label="E-mel"
|
||||
label="Email"
|
||||
validation="email"
|
||||
validation-visibility="dirty"
|
||||
/>
|
||||
@ -435,7 +421,7 @@ function groupUserByRole() {
|
||||
type="mask"
|
||||
v-model="showModalForm.phone"
|
||||
name="phone"
|
||||
label="Telefon"
|
||||
label="Phone"
|
||||
mask="###########"
|
||||
/>
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
@ -443,9 +429,9 @@ function groupUserByRole() {
|
||||
class="formkit-label flex items-center gap-x-4 font-semibold text-gray-700 dark:text-gray-200 blockfont-semibold text-sm formkit-invalid:text-red-500 dark:formkit-invalid:text-danger"
|
||||
for="input_4"
|
||||
>
|
||||
Peranan
|
||||
Role
|
||||
</label>
|
||||
<rs-button size="sm" @click="openModalRole"> Tambah Peranan </rs-button>
|
||||
<rs-button size="sm" @click="openModalRole"> Add Role </rs-button>
|
||||
</div>
|
||||
<v-select
|
||||
class="formkit-vselect"
|
||||
@ -456,7 +442,7 @@ function groupUserByRole() {
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
v-model="checkAllRole"
|
||||
label="Semua Peranan"
|
||||
label="All Role"
|
||||
input-class="icon-check"
|
||||
/>
|
||||
<FormKit
|
||||
@ -470,37 +456,39 @@ function groupUserByRole() {
|
||||
|
||||
<!-- Modal Role -->
|
||||
<rs-modal
|
||||
title="Tambah Peranan"
|
||||
ok-title="Simpan"
|
||||
cancel-title="Kembali"
|
||||
title="Add Role"
|
||||
ok-title="Save"
|
||||
cancel-title="Back"
|
||||
:cancel-callback="closeModalRole"
|
||||
:ok-callback="saveRole"
|
||||
v-model="showModalRole"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
v-model="showModalRoleForm.role"
|
||||
label="Nama"
|
||||
label="Name"
|
||||
validation="required"
|
||||
validation-visibility="live"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
v-model="showModalRoleForm.description"
|
||||
label="Penerangan"
|
||||
label="Description"
|
||||
/>
|
||||
</rs-modal>
|
||||
|
||||
<!-- Modal Delete Confirmation -->
|
||||
<rs-modal
|
||||
title="Pengesahan Padam"
|
||||
ok-title="Ya"
|
||||
cancel-title="Tidak"
|
||||
title="Delete Confirmation"
|
||||
ok-title="Yes"
|
||||
cancel-title="No"
|
||||
:ok-callback="deleteUser"
|
||||
v-model="showModalDelete"
|
||||
:overlay-close="false"
|
||||
>
|
||||
<p>
|
||||
Adakah anda pasti mahu memadam pengguna ini ({{
|
||||
Are you sure want to delete this user ({{
|
||||
showModalDeleteForm.username
|
||||
}})?
|
||||
</p>
|
@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>E-Library</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<rs-card class="py-1">
|
||||
<rs-table
|
||||
:data="tableData"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: true,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template v-slot:aksi="data">
|
||||
<div class="flex gap-2">
|
||||
<rs-button
|
||||
@click="viewItem(data.value.noSiri)"
|
||||
variant="info"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Lihat"
|
||||
>
|
||||
<Icon name="ic:outline-visibility" size="1.2rem" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
const generateData = () => {
|
||||
const data = [];
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
data.push({
|
||||
noSiri: `NS${String(i).padStart(3, "0")}`,
|
||||
pengguna: `Pengguna ${i}`,
|
||||
subjek: `Subjek ${i}`,
|
||||
tarikh: new Date(2023, 0, i).toLocaleDateString("ms-MY"),
|
||||
aksi: { noSiri: `NS${String(i).padStart(3, "0")}` },
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const viewItem = (noSiri) => {
|
||||
console.log(`View item with noSiri: ${noSiri}`);
|
||||
// Implement view functionality
|
||||
|
||||
navigateTo(`/e-library/maklumat/${noSiri}`);
|
||||
};
|
||||
|
||||
const editItem = (noSiri) => {
|
||||
console.log(`Edit item with noSiri: ${noSiri}`);
|
||||
// Implement edit functionality
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
tableData.value = generateData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold mb-4">Maklumat E-Library</h1>
|
||||
<rs-card class="mt-4 p-4">
|
||||
<div v-if="loading">Loading...</div>
|
||||
<div v-else-if="error">{{ error }}</div>
|
||||
<div v-else>
|
||||
<h2 class="text-xl font-semibold mb-2">No. Siri: {{ noSiri }}</h2>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p><strong>Nama Pemohon:</strong> {{ permohonanData.namaPemohon }}</p>
|
||||
<p><strong>Pangkat Pemohon:</strong> {{ permohonanData.pangkatPemohon }}</p>
|
||||
<p>
|
||||
<strong>No. Pegawai Pemohon:</strong> {{ permohonanData.noPegawaiPemohon }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>Nama Penghantar:</strong> {{ permohonanData.namaPenghantar }}</p>
|
||||
<p>
|
||||
<strong>Pangkat Penghantar:</strong> {{ permohonanData.pangkatPenghantar }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>No. Pegawai Penghantar:</strong>
|
||||
{{ permohonanData.noPegawaiPenghantar }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p>
|
||||
<strong>Ringkasan Kenyataan Kes:</strong>
|
||||
{{ permohonanData.ringkasanKenyataanKes }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>No. Kertas Siasatan:</strong> {{ permohonanData.noKertasSiasatan }}
|
||||
</p>
|
||||
<p><strong>No. Laporan Polis:</strong> {{ permohonanData.noLaporanPolis }}</p>
|
||||
<p><strong>Tarikh Temujanji:</strong> {{ permohonanData.tarikhTemujanji }}</p>
|
||||
<p><strong>Slot Masa:</strong> {{ permohonanData.slotMasa }}</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Senarai Barang</h3>
|
||||
<ul>
|
||||
<li v-for="(barang, index) in permohonanData.barangList" :key="index">
|
||||
{{ barang.jenisBarangDetailLabel }} - {{ barang.jenisBarangSiber }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute();
|
||||
const noSiri = ref(route.params.noSiri);
|
||||
const permohonanData = ref({});
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
|
||||
const fetchPermohonanData = async () => {
|
||||
try {
|
||||
const response = await $fetch(`/api/permohonan/${noSiri.value}`);
|
||||
if (response.statusCode === 200) {
|
||||
permohonanData.value = response.data;
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = "Failed to fetch permohonan data. Please try again.";
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPermohonanData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any scoped styles here if needed */
|
||||
</style>
|
@ -1,305 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Kemaskini Permohonan</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Nama Pemohon"
|
||||
v-model="namaPemohon"
|
||||
validation="required"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Pangkat Pemohon"
|
||||
v-model="pangkatPemohon"
|
||||
validation="required"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Pegawai Pemohon"
|
||||
v-model="noPegawaiPemohon"
|
||||
validation="required"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Nama Penghantar"
|
||||
v-model="namaPenghantar"
|
||||
validation="required"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Pangkat Penghantar"
|
||||
v-model="pangkatPenghantar"
|
||||
validation="required"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Ringkasan Kenyataan Kes"
|
||||
v-model="ringkasanKenyataanKes"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Bilangan"
|
||||
v-model="bilangan"
|
||||
validation="required|number"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<!-- Barang Section -->
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2">Senarai Barang</h3>
|
||||
<table
|
||||
v-if="barangList.length > 0"
|
||||
class="w-full border-collapse border border-gray-300 mb-2"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-gray-300 p-2">Jenis Barang</th>
|
||||
<th class="border border-gray-300 p-2">Kuantiti</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(barang, index) in barangList" :key="index">
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{
|
||||
barang.jenisBarangDetailLabel
|
||||
? barang.jenisBarangDetailLabel
|
||||
: barang.jenisBarangDetail
|
||||
}}
|
||||
</td>
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{ barang.kuantitiBarang }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="text-gray-500 mb-2">Tiada barang ditambah</div>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Kertas Siasatan"
|
||||
v-model="noKertasSiasatan"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Laporan Polis"
|
||||
v-model="noLaporanPolis"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button type="button" @click="navigateBack" variant="danger"
|
||||
>Kembali</rs-button
|
||||
>
|
||||
<!-- <rs-button type="button" @click="confirmBatal" variant="primary"
|
||||
>Tolak</rs-button
|
||||
> -->
|
||||
<rs-button btn-type="submit" variant="success">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const noSiri = ref(route.params.noSiri);
|
||||
|
||||
const namaPemohon = ref("");
|
||||
const pangkatPemohon = ref("");
|
||||
const noPegawaiPemohon = ref("");
|
||||
const namaPenghantar = ref("");
|
||||
const pangkatPenghantar = ref("");
|
||||
const ringkasanKenyataanKes = ref("");
|
||||
const bilangan = ref(0);
|
||||
const barangList = ref([]);
|
||||
const noKertasSiasatan = ref("");
|
||||
const noLaporanPolis = ref("");
|
||||
|
||||
const jenisBarangDetailOptions = ref([]);
|
||||
|
||||
const navigateBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
const requiredFields = [
|
||||
namaPemohon,
|
||||
pangkatPemohon,
|
||||
noPegawaiPemohon,
|
||||
namaPenghantar,
|
||||
pangkatPenghantar,
|
||||
ringkasanKenyataanKes,
|
||||
bilangan,
|
||||
noKertasSiasatan,
|
||||
noLaporanPolis,
|
||||
];
|
||||
|
||||
return requiredFields.every(
|
||||
(field) => field.value !== "" && field.value !== 0
|
||||
);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (isFormValid()) {
|
||||
try {
|
||||
const response = await $fetch(`/api/kaunter-permohonan/${noSiri.value}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
ringkasanKenyataanKes: ringkasanKenyataanKes.value,
|
||||
noKertasSiasatan: noKertasSiasatan.value,
|
||||
noLaporanPolis: noLaporanPolis.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
await $swal.fire({
|
||||
title: "Berjaya!",
|
||||
text: "Permohonan telah berjaya dikemaskini.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
||||
router.push("/kemaskini-daftar/senarai");
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: error.message || "Gagal mengemaskini permohonan. Sila cuba lagi.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Sila isi semua medan yang diperlukan.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBatal = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin menolak?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, batalkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
navigateTo(`/kemaskini-daftar/kemaskini/batal/${noSiri.value}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmSah = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin mengesahkan borang ini?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, Sahkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fetchLookupData = async (type) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/lookup?type=${type}`);
|
||||
if (response.statusCode === 200) {
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${type} lookup data:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchExistingData = async (noSiri) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/permohonan/${noSiri}`);
|
||||
if (response.statusCode === 200) {
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing data:", error);
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Gagal mendapatkan data permohonan.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const existingData = await fetchExistingData(noSiri.value);
|
||||
|
||||
if (existingData) {
|
||||
namaPemohon.value = existingData.namaPemohon;
|
||||
pangkatPemohon.value = existingData.pangkatPemohon;
|
||||
noPegawaiPemohon.value = existingData.noPegawaiPemohon;
|
||||
namaPenghantar.value = existingData.namaPenghantar;
|
||||
pangkatPenghantar.value = existingData.pangkatPenghantar;
|
||||
ringkasanKenyataanKes.value = existingData.ringkasanKenyataanKes;
|
||||
bilangan.value = existingData.bilangan;
|
||||
barangList.value = existingData.barangList;
|
||||
noKertasSiasatan.value = existingData.noKertasSiasatan;
|
||||
noLaporanPolis.value = existingData.noLaporanPolis;
|
||||
}
|
||||
|
||||
jenisBarangDetailOptions.value = await fetchLookupData("jenis_barang");
|
||||
});
|
||||
|
||||
const getJenisBarangLabel = (value) => {
|
||||
const option = jenisBarangDetailOptions.value.find(
|
||||
(opt) => opt.__original === value
|
||||
);
|
||||
return option ? option.label : value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,158 +0,0 @@
|
||||
@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Borang Akuan Penolakan Barang Kes</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- New Form Fields -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Sebab penolakan permohonan"
|
||||
v-model="sebabPenolakan"
|
||||
:options="[
|
||||
'Tiada permohonan dibuat',
|
||||
'Bungkusan barang kes tidak ditanda',
|
||||
'Barang kes yang dihantar tidak ditanda',
|
||||
'Barang kes yang dihantar tidak sesuai',
|
||||
'Lain-lain (Nyatakan)',
|
||||
]"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Lain-lain sebab"
|
||||
v-model="lainLainSebab"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button type="button" @click="navigateBack()" variant="danger"
|
||||
>Kembali</rs-button
|
||||
>
|
||||
<rs-button
|
||||
type="submit"
|
||||
@click="confirmSah"
|
||||
btn-type="submit"
|
||||
variant="success"
|
||||
>Sah</rs-button
|
||||
>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const noSiri = ref(route.params.noSiri);
|
||||
|
||||
// New reactive properties
|
||||
const sebabPenolakan = ref("");
|
||||
const lainLainSebab = ref("");
|
||||
|
||||
const navigateBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
const requiredFields = [sebabPenolakan];
|
||||
|
||||
return requiredFields.every(
|
||||
(field) => field.value !== "" && field.value !== 0 && field.value !== false
|
||||
);
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
if (isFormValid()) {
|
||||
console.log({
|
||||
sebabPenolakan: sebabPenolakan.value,
|
||||
lainLainSebab: lainLainSebab.value,
|
||||
});
|
||||
|
||||
navigateTo(`/kemaskini-daftar/senarai`);
|
||||
|
||||
$swal.fire({
|
||||
title: "Berjaya!",
|
||||
text: "Borang telah berjaya dihantar.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Sila isi semua medan yang diperlukan.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBatal = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin membatalkan?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, batalkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
navigateBack();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmSah = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin mengesahkan borang ini?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, Sahkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
submitForm();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
sebabPenolakan.value = "Tiada permohonan dibuat";
|
||||
lainLainSebab.value = "";
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,194 +0,0 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Borang Akuan Penerimaan Barang Kes</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- New Form Fields -->
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Peralatan dalam keadaan baik"
|
||||
v-model="peralatanBaik"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Pegawai berkelayakan"
|
||||
v-model="pegawaiBerkelayakan"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Kaedah dapat dilakukan"
|
||||
v-model="kaedahDilakukan"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Subkontrak diperlukan"
|
||||
v-model="subkontrakDiperlukan"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Tugasan diterima (Ya/Tidak)"
|
||||
v-model="tugasanDiterima"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Ulasan pegawai kaunter"
|
||||
v-model="ulasanPegawaiKaunter"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button type="button" @click="navigateBack()" variant="danger"
|
||||
>Kembali</rs-button
|
||||
>
|
||||
<rs-button
|
||||
type="submit"
|
||||
@click="confirmSah"
|
||||
btn-type="submit"
|
||||
variant="success"
|
||||
>Sah</rs-button
|
||||
>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const noSiri = ref(route.params.noSiri);
|
||||
|
||||
// New reactive properties
|
||||
const peralatanBaik = ref(false);
|
||||
const pegawaiBerkelayakan = ref(false);
|
||||
const kaedahDilakukan = ref(false);
|
||||
const subkontrakDiperlukan = ref(false);
|
||||
const tugasanDiterima = ref("");
|
||||
const ulasanPegawaiKaunter = ref("");
|
||||
|
||||
const navigateBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
const requiredFields = [
|
||||
peralatanBaik,
|
||||
pegawaiBerkelayakan,
|
||||
kaedahDilakukan,
|
||||
subkontrakDiperlukan,
|
||||
tugasanDiterima,
|
||||
];
|
||||
|
||||
return requiredFields.every(
|
||||
(field) => field.value !== "" && field.value !== 0 && field.value !== false
|
||||
);
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
if (isFormValid()) {
|
||||
console.log({
|
||||
peralatanBaik: peralatanBaik.value,
|
||||
pegawaiBerkelayakan: pegawaiBerkelayakan.value,
|
||||
kaedahDilakukan: kaedahDilakukan.value,
|
||||
subkontrakDiperlukan: subkontrakDiperlukan.value,
|
||||
tugasanDiterima: tugasanDiterima.value,
|
||||
ulasanPegawaiKaunter: ulasanPegawaiKaunter.value,
|
||||
});
|
||||
|
||||
navigateTo(`/kemaskini-daftar/senarai`);
|
||||
|
||||
$swal.fire({
|
||||
title: "Berjaya!",
|
||||
text: "Borang telah berjaya dihantar.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Sila isi semua medan yang diperlukan.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBatal = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin membatalkan?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, batalkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
navigateBack();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmSah = () => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Adakah anda pasti ingin mengesahkan borang ini?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Ya, Sahkan!",
|
||||
cancelButtonText: "Tidak",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
submitForm();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
peralatanBaik.value = true;
|
||||
pegawaiBerkelayakan.value = true;
|
||||
kaedahDilakukan.value = true;
|
||||
subkontrakDiperlukan.value = true;
|
||||
tugasanDiterima.value = "Ya";
|
||||
ulasanPegawaiKaunter.value = "Ulasan contoh";
|
||||
});
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,345 +0,0 @@
|
||||
<template>
|
||||
<div class="container mx-auto p-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-2xl font-bold">Laporan Bahan Bukti</h1>
|
||||
<button
|
||||
@click="generatePDF"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Jana PDF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<rs-card class="p-4">
|
||||
<FormKit
|
||||
type="form"
|
||||
@submit="submitForm"
|
||||
#default="{ state }"
|
||||
:actions="false"
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- KES ID and BARANG KES DETAIL -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="kesId"
|
||||
label="KES ID"
|
||||
v-model="generatedData.kesId"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'KES ID diperlukan' }"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="tagNo"
|
||||
label="TAG NO"
|
||||
v-model="generatedData.tagNo"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'TAG NO diperlukan' }"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="jenisBrg"
|
||||
label="Jenis Barang"
|
||||
v-model="generatedData.jenisBrg"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Jenis Barang diperlukan' }"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="jenisPemeriksaan"
|
||||
label="Jenis Pemeriksaan"
|
||||
v-model="generatedData.jenisPemeriksaan"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Jenis Pemeriksaan diperlukan' }"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- BUTIRAN PEGAWAI -->
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Butiran Pegawai</h2>
|
||||
<div
|
||||
v-for="role in ['PENYIASAT', 'PENGHANTAR', 'PEMERIKSA', 'PENERIMA']"
|
||||
:key="role"
|
||||
class="grid grid-cols-1 md:grid-cols-3 gap-4"
|
||||
>
|
||||
<FormKit
|
||||
type="text"
|
||||
:name="`pegawai.${role}.nama`"
|
||||
:label="`${role} - Nama`"
|
||||
v-model="generatedData.pegawai[role].nama"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
:name="`pegawai.${role}.pangkat`"
|
||||
:label="`${role} - Pangkat`"
|
||||
v-model="generatedData.pegawai[role].pangkat"
|
||||
disabled
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
:name="`pegawai.${role}.noPegawai`"
|
||||
:label="`${role} - No Pegawai`"
|
||||
v-model="generatedData.pegawai[role].noPegawai"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Peralatan and Langkah2 -->
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
v-model="generatedData.peralatan"
|
||||
type="textarea"
|
||||
name="peralatan"
|
||||
label="Peralatan"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Peralatan diperlukan' }"
|
||||
:rows="3"
|
||||
/>
|
||||
<FormKit
|
||||
v-model="generatedData.langkah2"
|
||||
type="textarea"
|
||||
name="langkah2"
|
||||
label="Langkah-langkah"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Langkah-langkah diperlukan' }"
|
||||
:rows="5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dapatan -->
|
||||
<FormKit
|
||||
v-model="generatedData.dapatan.value"
|
||||
type="radio"
|
||||
name="dapatan"
|
||||
label="Dapatan"
|
||||
:options="dapatanOptions"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Dapatan diperlukan' }"
|
||||
/>
|
||||
|
||||
<!-- Document Tambahan -->
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-2">Document Tambahan</h2>
|
||||
<FormKit type="list" name="documentTambahan" :value="[]">
|
||||
<FormKit type="group" :repeatable="true">
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="nama"
|
||||
placeholder="Nama dokumen"
|
||||
validation="required"
|
||||
:validation-messages="{ required: 'Nama dokumen diperlukan' }"
|
||||
/>
|
||||
<FormKit
|
||||
type="file"
|
||||
name="file"
|
||||
validation="required"
|
||||
multiple
|
||||
:validation-messages="{ required: 'Fail diperlukan' }"
|
||||
/>
|
||||
</div>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="danger" btn-type="reset" @click="previousPage()"
|
||||
>Kembali</rs-button
|
||||
>
|
||||
<rs-button type="submit" btn-type="submit">Hantar Laporan</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from "vue-router";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { jsPDF } from "jspdf";
|
||||
|
||||
const { $swal } = useNuxtApp();
|
||||
const route = useRoute();
|
||||
const reportID = route.params.reportID;
|
||||
|
||||
const generatedData = ref({
|
||||
kesId: "",
|
||||
tagNo: "",
|
||||
jenisBrg: "",
|
||||
jenisPemeriksaan: "",
|
||||
pegawai: {
|
||||
PENYIASAT: { nama: "", pangkat: "", noPegawai: "" },
|
||||
PENGHANTAR: { nama: "", pangkat: "", noPegawai: "" },
|
||||
PEMERIKSA: { nama: "", pangkat: "", noPegawai: "" },
|
||||
PENERIMA: { nama: "", pangkat: "", noPegawai: "" },
|
||||
},
|
||||
peralatan: "",
|
||||
langkah2: "",
|
||||
dapatan: "",
|
||||
documentTambahan: [],
|
||||
});
|
||||
// State to store dapatan options
|
||||
const dapatanOptions = ref([]);
|
||||
|
||||
// Fetch dapatan options from the lookup API
|
||||
const fetchDapatanOptions = async () => {
|
||||
try {
|
||||
const { data } = await useFetch("/api/lookup?type=dapatan");
|
||||
if (data.value.statusCode === 200) {
|
||||
dapatanOptions.value = data.value.data.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
} else {
|
||||
$swal.fire("Error", "Failed to fetch dapatan options.", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Failed to load dapatan options.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await useFetch(`/api/laporan/${reportID}`);
|
||||
if (data.value.statusCode === 200) {
|
||||
generatedData.value = {
|
||||
...generatedData.value, // Keep the default structure
|
||||
...data.value.data, // Merge API data
|
||||
};
|
||||
} else {
|
||||
$swal.fire("Error", "Failed to fetch report data.", "error");
|
||||
}
|
||||
|
||||
// Fetch dapatan options on mount
|
||||
await fetchDapatanOptions();
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Failed to load data.", "error");
|
||||
}
|
||||
});
|
||||
|
||||
// function generatePegawai(role = "") {
|
||||
// const names = ["Ahmad", "Siti", "Mohd", "Nurul", "Lim", "Raj"];
|
||||
// const surnames = ["Abdullah", "Tan", "Kumar", "Lee", "Muthu", "Hassan"];
|
||||
// const pangkat = ["Inspektor", "Sarjan", "Koperal", "Konstabel"];
|
||||
|
||||
// return {
|
||||
// nama: `${names[Math.floor(Math.random() * names.length)]} ${
|
||||
// surnames[Math.floor(Math.random() * surnames.length)]
|
||||
// }`,
|
||||
// pangkat:
|
||||
// role === "KB"
|
||||
// ? "Ketua Bahagian"
|
||||
// : pangkat[Math.floor(Math.random() * pangkat.length)],
|
||||
// noPegawai: `P${Math.floor(Math.random() * 100000)
|
||||
// .toString()
|
||||
// .padStart(5, "0")}`,
|
||||
// };
|
||||
// }
|
||||
|
||||
const submitForm = async (formData) => {
|
||||
try {
|
||||
const { data } = await useFetch(`/api/laporan/${reportID}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
$swal.fire("Success", "Report updated successfully", "success");
|
||||
} else {
|
||||
$swal.fire("Error", data.value.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Failed to submit report", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const generatePDF = () => {
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Set font sizes
|
||||
const titleSize = 16;
|
||||
const subtitleSize = 14;
|
||||
const normalSize = 10;
|
||||
|
||||
// Add title
|
||||
doc.setFontSize(titleSize);
|
||||
doc.text("Laporan Bahan Bukti", 105, 20, { align: "center" });
|
||||
|
||||
// Add case details
|
||||
doc.setFontSize(subtitleSize);
|
||||
doc.text("Butiran Kes", 20, 40);
|
||||
|
||||
doc.setFontSize(normalSize);
|
||||
doc.text(`KES ID: ${generatedData.value.kesId}`, 30, 50);
|
||||
doc.text(`TAG NO: ${generatedData.value.tagNo}`, 30, 60);
|
||||
doc.text(`Jenis Barang: ${generatedData.value.jenisBrg}`, 30, 70);
|
||||
doc.text(
|
||||
`Jenis Pemeriksaan: ${generatedData.value.jenisPemeriksaan}`,
|
||||
30,
|
||||
80
|
||||
);
|
||||
|
||||
// Add officer details
|
||||
doc.setFontSize(subtitleSize);
|
||||
doc.text("Butiran Pegawai", 20, 100);
|
||||
|
||||
doc.setFontSize(normalSize);
|
||||
let yPos = 110;
|
||||
for (const [role, officer] of Object.entries(generatedData.value.pegawai)) {
|
||||
doc.text(`${role}:`, 30, yPos);
|
||||
doc.text(`Nama: ${officer.nama}`, 40, yPos + 10);
|
||||
doc.text(`Pangkat: ${officer.pangkat}`, 40, yPos + 20);
|
||||
doc.text(`No Pegawai: ${officer.noPegawai}`, 40, yPos + 30);
|
||||
yPos += 45;
|
||||
}
|
||||
|
||||
// Add examination details
|
||||
doc.setFontSize(subtitleSize);
|
||||
doc.text("Butiran Pemeriksaan", 20, yPos);
|
||||
|
||||
doc.setFontSize(normalSize);
|
||||
doc.text(
|
||||
`Peralatan: ${generatedData.value.peralatan || "N/A"}`,
|
||||
30,
|
||||
yPos + 10
|
||||
);
|
||||
doc.text(
|
||||
`Langkah-langkah: ${generatedData.value.langkah2 || "N/A"}`,
|
||||
30,
|
||||
yPos + 20
|
||||
);
|
||||
doc.text(`Dapatan: ${generatedData.value.dapatan || "N/A"}`, 30, yPos + 30);
|
||||
|
||||
// Add additional documents
|
||||
// if (
|
||||
// generatedData.value.documentTambahan &&
|
||||
// generatedData.value.documentTambahan.length > 0
|
||||
// ) {
|
||||
// yPos += 50;
|
||||
// doc.setFontSize(subtitleSize);
|
||||
// doc.text("Dokumen Tambahan", 20, yPos);
|
||||
|
||||
// doc.setFontSize(normalSize);
|
||||
// generatedData.value.documentTambahan.forEach((doc, index) => {
|
||||
// doc.text(`${index + 1}. ${doc.nama}`, 30, yPos + 10 + index * 10);
|
||||
// });
|
||||
// }
|
||||
|
||||
// Generate and download the PDF
|
||||
doc.save(`Laporan_${generatedData.value.kesId}.pdf`);
|
||||
};
|
||||
|
||||
function previousPage() {
|
||||
window.history.back();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Add any scoped styles here if needed
|
||||
</style>
|
@ -1,734 +0,0 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- CARD: Status Semakan & Status Penerimaan -->
|
||||
<rs-card class="p-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold">Status Semakan</h3>
|
||||
<rs-badge
|
||||
:variant="statusSemakan === 'Selesai' ? 'success' : 'warning'"
|
||||
>
|
||||
{{ statusSemakan }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<h3 class="text-lg font-semibold">Status Penerimaan</h3>
|
||||
<rs-badge
|
||||
:variant="statusPenerimaan === 'Diterima' ? 'success' : 'danger'"
|
||||
>
|
||||
{{ statusPenerimaan }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-5">
|
||||
<rs-button @click="openSemakModal" variant="primary">Semak</rs-button>
|
||||
<rs-button @click="openTerimaModal" variant="success">Terima</rs-button>
|
||||
<rs-button @click="openTolakModal" variant="danger">Tolak</rs-button>
|
||||
</div>
|
||||
</rs-card>
|
||||
|
||||
<!-- LIST: Pegawai Forensic Yang Terlibat -->
|
||||
<rs-card class="p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">Pegawai Forensik Yang Terlibat</h3>
|
||||
<rs-button
|
||||
v-if="isKetuaBahagian"
|
||||
@click="openAddModal"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
Tambah Pegawai
|
||||
</rs-button>
|
||||
</div>
|
||||
<rs-table
|
||||
v-if="forensicOfficers.length > 0"
|
||||
:data="forensicOfficers"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: false,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template v-slot:header>
|
||||
<tr>
|
||||
<th>Pangkat</th>
|
||||
<th>Nama</th>
|
||||
<th>No Pegawai</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:pangkat="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:nama="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:noPegawai="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:tindakan="data">
|
||||
<div class="flex gap-2">
|
||||
<rs-button
|
||||
@click="openEditModal(data.text.userID, data.text.assignID)"
|
||||
variant="info"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="ic:baseline-edit" size="1.2rem" />
|
||||
</rs-button>
|
||||
<rs-button
|
||||
@click="confirmDelete(data.text.userID, data.text.assignID)"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="ic:baseline-delete" size="1.2rem" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
<div v-else class="text-center p-10">
|
||||
<p>Tidak ada pegawai forensik yang terlibat.</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
|
||||
<!-- LIST: Bahan Bukti -->
|
||||
<rs-card class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Bahan Bukti</h3>
|
||||
<rs-table
|
||||
v-if="evidences.length > 0"
|
||||
:data="evidences"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: false,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template v-slot:header>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Jenis Barang</th>
|
||||
<th>Tag No.</th>
|
||||
<th>Keadaan</th>
|
||||
<th>Kuantiti</th>
|
||||
<th>Tindakan</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:jenisBarang="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:tagNo="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:keadaan="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:kuantiti="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:tindakan="data">
|
||||
<rs-button
|
||||
@click="generateReport(data.text)"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="text-primary hover:text-primary-dark"
|
||||
>
|
||||
<Icon name="mdi:file-report-outline" size="1.5rem" />
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-table>
|
||||
<div v-else class="text-center p-10">
|
||||
<p>Tidak ada bahan bukti yang terlibat.</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<rs-modal v-model="showModal" @close="closeModal">
|
||||
<template #header>
|
||||
<h3>
|
||||
{{ editMode ? "Edit Pegawai Forensik" : "Tambah Pegawai Forensik" }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="handleSubmit">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="id"
|
||||
label="Pilih Pegawai"
|
||||
:options="pegawaiOption"
|
||||
v-model="selectedPegawai"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih pegawai',
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<rs-button variant="secondary" @click="closeModal">Tutup</rs-button>
|
||||
<rs-button variant="primary" btn-type="submit"> Simpan </rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer> </template>
|
||||
</rs-modal>
|
||||
|
||||
<rs-modal v-model="showSemakModal" @close="closeSemakModal">
|
||||
<template #header>
|
||||
<h3>Semak Maklumat</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<FormKit
|
||||
v-if="userRole === 'Pegawai Kaunter'"
|
||||
type="form"
|
||||
:actions="false"
|
||||
@submit="handleSemakSubmit"
|
||||
>
|
||||
<!-- Existing form for kaunter role -->
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="peralatanBaik"
|
||||
label="Peralatan dalam keadaan baik"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="pegawaiBerkelayakan"
|
||||
label="Pegawai berkelayakan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="kaedahDapatDilakukan"
|
||||
label="Kaedah dapat dilakukan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="subkontrakDiperlukan"
|
||||
label="Subkontrak diperlukan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="tugasanDiterima"
|
||||
label="Tugasan diterima"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="ulasanPegawaiKaunter"
|
||||
label="Ulasan pegawai kaunter"
|
||||
validation="required"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button variant="danger" @click="closeSemakModal"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button variant="primary" btn-type="submit">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
|
||||
<FormKit
|
||||
v-else-if="userRole === 'Ketua Bahagian'"
|
||||
type="form"
|
||||
:actions="false"
|
||||
@submit="handleSemakSubmit"
|
||||
>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="kelulusanKetuaBahagian"
|
||||
label="Kelulusan ketua bahagian"
|
||||
:options="['Diterima', 'Ditolak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="ulasanKetuaBahagian"
|
||||
label="Ulasan"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan ulasan',
|
||||
}"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button variant="danger" @click="closeSemakModal"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button variant="primary" btn-type="submit">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div></div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
|
||||
<!-- Terima Modal -->
|
||||
<rs-modal v-model="showTerimaModal" @close="closeTerimaModal">
|
||||
<template #header>
|
||||
<h3>Terima Permohonan</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="handleTerimaSubmit">
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="peralatanBaik"
|
||||
label="Peralatan dalam keadaan baik"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="pegawaiBerkelayakan"
|
||||
label="Pegawai berkelayakan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="kaedahDapatDilakukan"
|
||||
label="Kaedah dapat dilakukan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="subkontrakDiperlukan"
|
||||
label="Subkontrak diperlukan"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="radio"
|
||||
name="tugasanDiterima"
|
||||
label="Tugasan diterima"
|
||||
:options="['Ya', 'Tidak']"
|
||||
validation="required"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="ulasanPegawaiKaunter"
|
||||
label="Ulasan pegawai kaunter"
|
||||
validation="required"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button variant="danger" @click="closeTerimaModal"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button variant="primary" btn-type="submit">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div></div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
|
||||
<!-- Tolak Modal -->
|
||||
<rs-modal v-model="showTolakModal" @close="closeTolakModal">
|
||||
<template #header>
|
||||
<h3>Tolak Permohonan</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<FormKit type="form" :actions="false" @submit="handleTolakSubmit">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="sebabPenolakan"
|
||||
label="Sebab penolakan permohonan"
|
||||
:options="sebabPenolakanOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih sebab penolakan',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="lainLainSebab"
|
||||
label="Lain-lain sebab"
|
||||
validation="required_if:sebabPenolakan,Lain-lain"
|
||||
:validation-messages="{
|
||||
required_if: 'Sila nyatakan sebab lain',
|
||||
}"
|
||||
/>
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button variant="secondary" @click="closeTolakModal"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button variant="danger" btn-type="submit">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div></div>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: "default",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
// Status data
|
||||
const statusSemakan = ref("Selesai");
|
||||
const statusPenerimaan = ref("Diterima");
|
||||
|
||||
// State variables
|
||||
const showModal = ref(false);
|
||||
const forensicOfficers = ref([]);
|
||||
const pegawaiOption = ref([]);
|
||||
const selectedPegawai = ref(null);
|
||||
const editMode = ref(false);
|
||||
const currentOfficerID = ref(null);
|
||||
const currentAssignID = ref(null);
|
||||
const isKetuaBahagian = ref(true);
|
||||
const isKetuaJabatan = ref(false);
|
||||
|
||||
const sebabPenolakanOptions = ref([]);
|
||||
|
||||
// Evidence Data
|
||||
const evidences = ref([]);
|
||||
|
||||
// Fetch the status data
|
||||
const fetchStatusData = async () => {
|
||||
try {
|
||||
const { data } = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/status`
|
||||
);
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
statusSemakan.value = data.value.data.statusSemakan || "Belum Disemak";
|
||||
statusPenerimaan.value =
|
||||
data.value.data.statusPenerimaan || "Belum Diterima";
|
||||
} else {
|
||||
$swal.fire(
|
||||
"Error",
|
||||
"Gagal mendapatkan status semakan dan penerimaan.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal memuatkan data status.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch reports (Bahan Bukti)
|
||||
const fetchReports = async () => {
|
||||
try {
|
||||
const { data } = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/reports`
|
||||
);
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
evidences.value = data.value.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching reports:", error);
|
||||
$swal.fire("Error", "Gagal mendapatkan senarai bahan bukti.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch existing forensic officers and available officers
|
||||
const fetchAssignedOfficers = async () => {
|
||||
try {
|
||||
const { data } = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/forensik/list`
|
||||
);
|
||||
if (data.value.statusCode === 200) {
|
||||
forensicOfficers.value = data.value.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching forensic officers:", error);
|
||||
$swal.fire("Error", "Gagal mendapatkan senarai pegawai forensik.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableOfficers = async () => {
|
||||
try {
|
||||
const { data } = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/forensik/available`
|
||||
);
|
||||
|
||||
if (data.value.statusCode === 200) {
|
||||
pegawaiOption.value = data.value.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching available officers:", error);
|
||||
$swal.fire("Error", "Gagal mendapatkan senarai pegawai tersedia.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSebabPenolakanOptions = async () => {
|
||||
try {
|
||||
const { data } = await useFetch("/api/lookup?type=sebab_penolakan");
|
||||
if (data.value.statusCode === 200) {
|
||||
sebabPenolakanOptions.value = data.value.data;
|
||||
} else {
|
||||
$swal.fire("Error", "Failed to fetch sebab penolakan options", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Failed to load lookup data", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Open modals
|
||||
const openAddModal = () => {
|
||||
editMode.value = false;
|
||||
selectedPegawai.value = null;
|
||||
fetchAvailableOfficers();
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const openEditModal = (userID, assignID) => {
|
||||
editMode.value = true;
|
||||
currentOfficerID.value = userID;
|
||||
currentAssignID.value = assignID;
|
||||
selectedPegawai.value = null;
|
||||
fetchAvailableOfficers(); // Get updated available officers for edit mode
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const confirmDelete = (userID, assignID) => {
|
||||
$swal
|
||||
.fire({
|
||||
title: "Apakah anda pasti?",
|
||||
text: "Anda tidak akan dapat mengembalikannya!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya, hapus!",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
deletePegawai(userID, assignID);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Close modal
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
selectedPegawai.value = null;
|
||||
};
|
||||
|
||||
// Add new officer
|
||||
const addNewPegawai = async () => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/forensik/add`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { pegawaiID: selectedPegawai.value },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.value.statusCode === 200) {
|
||||
await fetchAssignedOfficers();
|
||||
$swal.fire("Berjaya", "Pegawai baru telah ditambah", "success");
|
||||
closeModal();
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal menambah pegawai.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Edit existing officer
|
||||
const updatePegawai = async () => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/forensik/edit`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: {
|
||||
assignID: currentAssignID.value,
|
||||
newPegawaiID: selectedPegawai.value,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.value.statusCode === 200) {
|
||||
await fetchAssignedOfficers(); // Refresh the list of officers
|
||||
$swal.fire("Berjaya", "Maklumat pegawai telah dikemaskini", "success");
|
||||
closeModal();
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal mengemaskini maklumat pegawai.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Delete officer
|
||||
const deletePegawai = async (officer, assignID) => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/forensik/delete`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: { assignID: assignID },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.value.statusCode === 200) {
|
||||
await fetchAssignedOfficers();
|
||||
$swal.fire("Dihapuskan!", "Pegawai telah dipadam.", "success");
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal memadam pegawai.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Submit Semak form
|
||||
const handleSemakSubmit = async (formData) => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/semak`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
if (response.data.value.statusCode === 200) {
|
||||
$swal.fire("Berjaya", "Maklumat semakan telah disimpan", "success");
|
||||
await fetchStatusData();
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal menyimpan semakan.", "error");
|
||||
}
|
||||
closeSemakModal();
|
||||
};
|
||||
|
||||
// Submit Terima form
|
||||
const handleTerimaSubmit = async (formData) => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/terima`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
if (response.data.value.statusCode === 200) {
|
||||
$swal.fire("Berjaya", "Permohonan telah diterima", "success");
|
||||
await fetchStatusData();
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal menerima permohonan.", "error");
|
||||
}
|
||||
closeTerimaModal();
|
||||
};
|
||||
|
||||
// Submit Tolak form
|
||||
const handleTolakSubmit = async (formData) => {
|
||||
try {
|
||||
const response = await useFetch(
|
||||
`/api/permohonan/${route.params.noSiri}/tolak`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
if (response.data.value.statusCode === 200) {
|
||||
$swal.fire("Berjaya", "Permohonan telah ditolak", "success");
|
||||
navigateTo("/kemaskini-daftar/senarai");
|
||||
} else {
|
||||
$swal.fire("Error", response.data.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error", "Gagal menolak permohonan.", "error");
|
||||
}
|
||||
closeTolakModal();
|
||||
};
|
||||
|
||||
// Handle form submission (add/edit)
|
||||
const handleSubmit = () => {
|
||||
if (editMode.value) {
|
||||
updatePegawai();
|
||||
} else {
|
||||
addNewPegawai();
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch officers when the component mounts
|
||||
onMounted(() => {
|
||||
fetchStatusData();
|
||||
fetchAssignedOfficers();
|
||||
fetchReports(); // Fetch reports related to the permohonan
|
||||
});
|
||||
|
||||
const generateReport = (bahanBukti) => {
|
||||
console.log("Generate Report for:", bahanBukti);
|
||||
navigateTo(`/kemaskini-daftar/laporan/${bahanBukti}`);
|
||||
};
|
||||
|
||||
// Semak Modal Controls
|
||||
const showSemakModal = ref(false);
|
||||
|
||||
const openSemakModal = () => {
|
||||
showSemakModal.value = true;
|
||||
};
|
||||
|
||||
const closeSemakModal = () => {
|
||||
showSemakModal.value = false;
|
||||
};
|
||||
|
||||
// User role (you might want to fetch this from your auth system)
|
||||
const userRole = ref("Pegawai Kaunter"); // Change this to 'Ketua Bahagian' to test the other form
|
||||
|
||||
// For ketua bahagian form
|
||||
const kelulusanKetuaBahagian = ref(null);
|
||||
|
||||
// Terima Modal Controls
|
||||
const showTerimaModal = ref(false);
|
||||
|
||||
const openTerimaModal = () => {
|
||||
showTerimaModal.value = true;
|
||||
};
|
||||
|
||||
const closeTerimaModal = () => {
|
||||
showTerimaModal.value = false;
|
||||
};
|
||||
|
||||
// Tolak Modal Controls
|
||||
const showTolakModal = ref(false);
|
||||
|
||||
const openTolakModal = async () => {
|
||||
await fetchSebabPenolakanOptions();
|
||||
showTolakModal.value = true;
|
||||
};
|
||||
|
||||
const closeTolakModal = () => {
|
||||
showTolakModal.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Your existing styles */
|
||||
</style>
|
@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Senarai Permohonan</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 py-2">
|
||||
<rs-table
|
||||
:data="tableData"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: true,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template v-slot:header>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>No Siri</th>
|
||||
<th>Tarikh & Masa</th>
|
||||
<th>Status</th>
|
||||
<th>Butiran</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-slot:no="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:noSiri="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:tarikhMasa="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:status="data">
|
||||
<rs-badge :variant="data.text === 'Aktif' ? 'success' : 'danger'">
|
||||
{{ data.text || "N/A" }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
<template v-slot:butiran="data">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- View Button -->
|
||||
<rs-button
|
||||
@click="lihat(data.value.noSiri)"
|
||||
variant="info"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Lihat"
|
||||
>
|
||||
<Icon name="ic:outline-visibility" size="1.2rem" />
|
||||
</rs-button>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<rs-button
|
||||
@click="kemaskini(data.value.noSiri)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Kemaskini"
|
||||
>
|
||||
<Icon name="ic:baseline-edit" size="1.2rem" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
definePageMeta({
|
||||
title: "Senarai Permohonan",
|
||||
});
|
||||
|
||||
// Reactive variable to store table data
|
||||
const tableData = ref([]);
|
||||
|
||||
// Fetch permohonan list from API
|
||||
const fetchPermohonan = async () => {
|
||||
try {
|
||||
const response = await useFetch("/api/permohonan");
|
||||
if (response.data.value.statusCode === 200) {
|
||||
// Populate tableData with the fetched permohonan list
|
||||
tableData.value = response.data.value.data;
|
||||
} else {
|
||||
console.error(response.data.value.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const permohonanBaru = () => {
|
||||
navigateTo("/permohonan-temujanji/baru");
|
||||
};
|
||||
|
||||
const kemaskini = (item) => {
|
||||
navigateTo(`/kemaskini-daftar/kemaskini/${item}`);
|
||||
};
|
||||
|
||||
const lihat = (item) => {
|
||||
// Navigate to a detailed view page for the selected item
|
||||
navigateTo(`/kemaskini-daftar/maklumat/${item}`);
|
||||
};
|
||||
|
||||
const hapus = async (noSiri) => {
|
||||
const confirmation = await $swal.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Anda tidak akan dapat memulihkan semula data ini!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya, hapuskan!",
|
||||
cancelButtonText: "Batal",
|
||||
});
|
||||
|
||||
if (confirmation.isConfirmed) {
|
||||
try {
|
||||
const response = await useFetch(`/api/permohonan/${noSiri}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.data.value.statusCode === 200) {
|
||||
// Remove the deleted permohonan from tableData
|
||||
tableData.value = tableData.value.filter(
|
||||
(row) => row.noSiri !== noSiri
|
||||
);
|
||||
$swal.fire("Dihapuskan!", response.data.value.message, "success");
|
||||
} else {
|
||||
$swal.fire("Error!", response.data.value.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error!", "Failed to delete permohonan.", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the permohonan list when the component is mounted
|
||||
onMounted(() => {
|
||||
fetchPermohonan();
|
||||
});
|
||||
</script>
|
@ -1,116 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Tambah Temujanji</h1>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- Maklumat Pemohon (Auto-filled) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormKit type="text" label="Nama Pemohon" v-model="pemohon.nama" />
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Jawatan Pemohon"
|
||||
v-model="pemohon.jawatan"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Pegawai Pemohon"
|
||||
v-model="pemohon.noPegawai"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Jenis Semakan Dropdown -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Jenis Semakan"
|
||||
v-model="jenisSemakan"
|
||||
:options="jenisSemakanOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Conditional fields based on jenisSemakan -->
|
||||
<template v-if="jenisSemakan === 'Subjek Hadir'">
|
||||
<FormKit type="file" label="Gambar Subjek" />
|
||||
<FormKit type="file" label="Gambar Cap Jari" />
|
||||
</template>
|
||||
|
||||
<template v-else-if="jenisSemakan === 'Hantar Gambar'">
|
||||
<FormKit type="file" label="Gambar Subjek" validation="required" />
|
||||
<FormKit type="file" label="Gambar Cap Jari" validation="required" />
|
||||
</template>
|
||||
|
||||
<!-- Date field -->
|
||||
<FormKit
|
||||
type="date"
|
||||
label="Tarikh"
|
||||
v-model="tarikh"
|
||||
validation="required|date"
|
||||
/>
|
||||
|
||||
<!-- Time field -->
|
||||
<FormKit
|
||||
type="time"
|
||||
label="Masa"
|
||||
v-model="masa"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button @click="goBack" variant="danger">Kembali</rs-button>
|
||||
<rs-button btn-type="submit" variant="success">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
|
||||
const pemohon = ref({
|
||||
nama: "",
|
||||
jawatan: "",
|
||||
noPegawai: "",
|
||||
});
|
||||
|
||||
const jenisSemakan = ref("Subjek Hadir");
|
||||
const jenisSemakanOptions = ref([
|
||||
{ label: "Subjek Hadir", value: "Subjek Hadir" },
|
||||
{ label: "Hantar Gambar", value: "Hantar Gambar" },
|
||||
]);
|
||||
|
||||
const tarikh = ref("");
|
||||
const masa = ref("");
|
||||
|
||||
// Navigate back to the listing page
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Submit form function
|
||||
const submitForm = async (formData) => {
|
||||
try {
|
||||
const response = await $fetch("/api/temujanji/tambah", {
|
||||
method: "POST",
|
||||
body: {
|
||||
pemohon: pemohon.value,
|
||||
jenisSemakan: jenisSemakan.value,
|
||||
tarikh: tarikh.value,
|
||||
masa: masa.value,
|
||||
...formData,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
$swal.fire("Berjaya!", response.message, "success");
|
||||
router.push("/pengesanan-penyamaran/senarai");
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Ralat!", error.message, "error");
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,300 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Kemaskini Maklumat Pengesanan Penyamaran</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- JENIS DOKUMEN -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Jenis Dokumen"
|
||||
v-model="formData.jenisDokumen"
|
||||
:options="jenisDokumenOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- NEGARA -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Negara"
|
||||
v-model="formData.negara"
|
||||
:options="negaraOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- NAMA PEMILIK -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Nama Pemilik"
|
||||
v-model="formData.namaPemilik"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- NO DOKUMEN -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Dokumen"
|
||||
v-model="formData.noDokumen"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- KEWARGANEGARAAN -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Kewarganegaraan"
|
||||
v-model="formData.kewarganegaraan"
|
||||
:options="kewarganegaraanOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- TARIKH LAHIR -->
|
||||
<FormKit
|
||||
type="date"
|
||||
label="Tarikh Lahir"
|
||||
v-model="formData.tarikhLahir"
|
||||
validation="required|date"
|
||||
/>
|
||||
|
||||
<!-- JANTINA -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Jantina"
|
||||
v-model="formData.jantina"
|
||||
:options="jantinaOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- TARIKH LUPUT DOKUMEN -->
|
||||
<FormKit
|
||||
type="date"
|
||||
label="Tarikh Luput Dokumen"
|
||||
v-model="formData.tarikhLuputDokumen"
|
||||
validation="required|date"
|
||||
/>
|
||||
|
||||
<!-- SKOR PERSAMAAN MUKA -->
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Skor Persamaan Muka"
|
||||
v-model="formData.skorPersamaanMuka"
|
||||
validation="required|number|min_value:0|max_value:100"
|
||||
step="0.01"
|
||||
/>
|
||||
|
||||
<!-- SKOR PERSAMAAN CAP JARI -->
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Skor Persamaan Cap Jari"
|
||||
v-model="formData.skorPersamaanCapJari"
|
||||
validation="required|number|min_value:0|max_value:100"
|
||||
step="0.01"
|
||||
/>
|
||||
|
||||
<!-- OPTIONAL FIELDS -->
|
||||
<FormKit type="number" label="Umur" v-model="formData.umur" />
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Tinggi (cm)"
|
||||
v-model="formData.tinggi"
|
||||
step="0.01"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Warna Rambut"
|
||||
v-model="formData.warnaRambut"
|
||||
/>
|
||||
<FormKit type="text" label="Bangsa" v-model="formData.bangsa" />
|
||||
<FormKit type="text" label="Etnik" v-model="formData.etnik" />
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Bentuk Kepala"
|
||||
v-model="formData.bentukKepala"
|
||||
/>
|
||||
<FormKit type="text" label="Mata" v-model="formData.mata" />
|
||||
<FormKit type="text" label="Telinga" v-model="formData.telinga" />
|
||||
<FormKit type="text" label="Hidung" v-model="formData.hidung" />
|
||||
<FormKit type="text" label="Mulut" v-model="formData.mulut" />
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Parut, Tahi Lalat, Tatu, dsb."
|
||||
v-model="formData.parut"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Sejarah Perjalanan"
|
||||
v-model="formData.sejarahPerjalanan"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Persamaan Tanda Tangan"
|
||||
v-model="formData.persamaanTandaTangan"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Pemeriksaan Lain"
|
||||
v-model="formData.pemeriksaanLain"
|
||||
/>
|
||||
|
||||
<!-- DAPATAN -->
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Dapatan"
|
||||
v-model="formData.dapatan"
|
||||
:options="dapatanOptions"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- LAPORAN SYSTEM TD&B -->
|
||||
<FormKit
|
||||
type="file"
|
||||
label="Laporan Sistem TD&B (JPG/PDF)"
|
||||
validation="file|mimes:pdf,jpg"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button @click="goBack" variant="danger">Kembali</rs-button>
|
||||
<rs-button btn-type="submit" variant="success">Kemaskini</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const kesID = ref(route.params.kesID);
|
||||
|
||||
// Reactive variables for form data
|
||||
const formData = ref({
|
||||
jenisDokumen: "Passport",
|
||||
negara: "Malaysia",
|
||||
namaPemilik: "",
|
||||
noDokumen: "",
|
||||
kewarganegaraan: "Malaysia",
|
||||
tarikhLahir: "",
|
||||
jantina: "Lelaki",
|
||||
tarikhLuputDokumen: "",
|
||||
skorPersamaanMuka: 0,
|
||||
skorPersamaanCapJari: 0,
|
||||
umur: null,
|
||||
tinggi: null,
|
||||
warnaRambut: "",
|
||||
bangsa: "",
|
||||
etnik: "",
|
||||
bentukKepala: "",
|
||||
mata: "",
|
||||
telinga: "",
|
||||
hidung: "",
|
||||
mulut: "",
|
||||
parut: "",
|
||||
sejarahPerjalanan: "",
|
||||
persamaanTandaTangan: "",
|
||||
pemeriksaanLain: "",
|
||||
dapatan: "Sama",
|
||||
});
|
||||
|
||||
// Dropdown options
|
||||
const jenisDokumenOptions = ref([
|
||||
{ label: "Sila Pilih", value: "" },
|
||||
{ label: "Passport", value: "Passport" },
|
||||
{ label: "Kad Pengenalan", value: "Kad Pengenalan" },
|
||||
]);
|
||||
|
||||
const negaraOptions = ref([
|
||||
{ label: "Sila Pilih", value: "" },
|
||||
|
||||
{ label: "Malaysia", value: "Malaysia" },
|
||||
{ label: "Singapura", value: "Singapura" },
|
||||
]);
|
||||
|
||||
const kewarganegaraanOptions = ref([
|
||||
{ label: "Sila Pilih", value: "" },
|
||||
|
||||
{ label: "Malaysia", value: "Malaysia" },
|
||||
{ label: "Singapura", value: "Singapura" },
|
||||
{ label: "Tiada", value: "Tiada" },
|
||||
]);
|
||||
|
||||
const jantinaOptions = ref([
|
||||
{ label: "Sila Pilih", value: "" },
|
||||
{ label: "Lelaki", value: "Lelaki" },
|
||||
{ label: "Perempuan", value: "Perempuan" },
|
||||
{ label: "Lain-lain", value: "Lain-lain" },
|
||||
]);
|
||||
|
||||
const dapatanOptions = ref([
|
||||
{ label: "Sila Pilih", value: "" },
|
||||
{ label: "Sama", value: "Sama" },
|
||||
{ label: "Tidak Sama", value: "Tidak Sama" },
|
||||
{ label: "Tidak Dapat Dikenalpasti", value: "Tidak Dapat Dikenalpasti" },
|
||||
]);
|
||||
|
||||
const fetchAppointment = async (kesID) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/temujanji/${kesID}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
Object.assign(formData.value, response.data);
|
||||
} else {
|
||||
throw new Error("Failed to fetch appointment data.");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Ralat!", error.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchAppointment(kesID.value);
|
||||
});
|
||||
|
||||
// Submit the updated form data
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
const response = await $fetch(`/api/temujanji/${kesID.value}`, {
|
||||
method: "PUT",
|
||||
body: formData.value,
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
$swal.fire("Berjaya!", response.message, "success");
|
||||
router.push("/pengesanan-penyamaran/senarai");
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Ralat!", error.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// Go back to the previous page
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header with title and "Add Appointment" button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Senarai Temujanji</h1>
|
||||
<rs-button @click="addAppointment" variant="primary" class="mt-2">
|
||||
Tambah Temujanji
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Table displaying the list of appointments -->
|
||||
<rs-card class="mt-4 py-2">
|
||||
<rs-table
|
||||
:data="tableData"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: true,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<!-- Table Data Rows -->
|
||||
<template v-slot:no="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:kesId="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:namaPemohon="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:caraSemakan="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:status="data">
|
||||
<rs-badge
|
||||
:variant="
|
||||
data.text === 'Aktif' || data.text === 'Selesai'
|
||||
? 'success'
|
||||
: 'danger'
|
||||
"
|
||||
>
|
||||
{{ data.text || "N/A" }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<!-- Actions for each appointment -->
|
||||
<template v-slot:tindakan="data">
|
||||
<div class="flex gap-2">
|
||||
<!-- Button to navigate to the "Update" page for the selected appointment -->
|
||||
<rs-button
|
||||
@click="updateAppointment(data.value.kesId)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Kemaskini"
|
||||
>
|
||||
<Icon name="ic:baseline-edit" size="1.2rem" />
|
||||
</rs-button>
|
||||
|
||||
<!-- Button to delete the selected appointment -->
|
||||
<rs-button
|
||||
@click="deleteAppointment(data.value.kesId)"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Hapus"
|
||||
>
|
||||
<Icon name="ic:baseline-delete" size="1.2rem" />
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
// Reactive variable to store table data
|
||||
const tableData = ref([]);
|
||||
|
||||
// Fetch appointment list from API
|
||||
const fetchAppointments = async () => {
|
||||
try {
|
||||
const response = await useFetch("/api/temujanji");
|
||||
if (response.data.value && response.data.value.statusCode === 200) {
|
||||
tableData.value = response.data.value.data;
|
||||
} else {
|
||||
console.error(response.data.value.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching appointments:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to navigate to the "Add Appointment" page
|
||||
const addAppointment = () => {
|
||||
navigateTo("/pengesanan-penyamaran/baru");
|
||||
};
|
||||
|
||||
// Function to navigate to the "Update Appointment" page
|
||||
const updateAppointment = (kesId) => {
|
||||
navigateTo(`/pengesanan-penyamaran/kemaskini/${kesId}`);
|
||||
};
|
||||
|
||||
// Function to delete an appointment by its kesId
|
||||
const deleteAppointment = async (kesId) => {
|
||||
const confirmation = await $swal.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Anda tidak akan dapat memulihkan semula data ini!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya, hapuskan!",
|
||||
cancelButtonText: "Batal",
|
||||
});
|
||||
|
||||
if (confirmation.isConfirmed) {
|
||||
try {
|
||||
const { data, error } = await useFetch(`/api/temujanji/${kesId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (data.value && data.value.statusCode === 200) {
|
||||
// Remove the deleted appointment from tableData
|
||||
await fetchAppointments();
|
||||
$swal.fire("Dihapuskan!", data.value.message, "success");
|
||||
} else {
|
||||
$swal.fire(
|
||||
"Error!",
|
||||
error.value?.message || "Failed to delete appointment.",
|
||||
"error"
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error!", "Failed to delete appointment.", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the appointment list when the component is mounted
|
||||
onMounted(() => {
|
||||
fetchAppointments();
|
||||
});
|
||||
</script>
|
@ -1,520 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Permohonan Baru</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- Nama Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Nama Pemohon"
|
||||
v-model="namaPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Pangkat Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Pangkat Pemohon"
|
||||
v-model="pangkatPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- No Pegawai Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Pegawai Pemohon"
|
||||
v-model="noPegawaiPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Checkbox: Apply Pemohon info to Penghantar -->
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Penghantar Sama seperti Pemohon"
|
||||
v-model="isPenghantarSameAsPemohon"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render Nama Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="Nama Penghantar"
|
||||
v-model="namaPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render Pangkat Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="Pangkat Penghantar"
|
||||
v-model="pangkatPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render No Pegawai Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="No Pegawai Penghantar"
|
||||
v-model="noPegawaiPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Ringkasan Kenyataan Kes Input -->
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Ringkasan Kenyataan Kes"
|
||||
v-model="ringkasanKenyataanKes"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Bilangan Input -->
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Bilangan"
|
||||
v-model="bilangan"
|
||||
validation="required|number"
|
||||
/>
|
||||
|
||||
<!-- Barang Section -->
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2">Senarai Barang</h3>
|
||||
<table
|
||||
v-if="barangList.length > 0"
|
||||
class="w-full border-collapse border border-gray-300 mb-2"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-gray-300 p-2">Jenis Barang</th>
|
||||
<th class="border border-gray-300 p-2">Kuantiti</th>
|
||||
<th class="border border-gray-300 p-2">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(barang, index) in barangList" :key="index">
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{
|
||||
barang.jenisBarangDetailLabel
|
||||
? barang.jenisBarangDetailLabel
|
||||
: barang.jenisBarangDetail
|
||||
}}
|
||||
</td>
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{ barang.kuantitiBarang }}
|
||||
</td>
|
||||
<td class="border border-gray-300 p-2">
|
||||
<rs-button
|
||||
type="button"
|
||||
@click="editBarang(index)"
|
||||
variant="secondary"
|
||||
class="mr-2"
|
||||
>
|
||||
Edit
|
||||
</rs-button>
|
||||
<rs-button
|
||||
type="button"
|
||||
@click="removeBarang(index)"
|
||||
variant="danger"
|
||||
>
|
||||
Buang
|
||||
</rs-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="text-gray-500 mb-2">Tiada barang ditambah</div>
|
||||
<rs-button type="button" @click="openBarangModal" variant="primary">
|
||||
Tambah Barang
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- No Kertas Siasatan Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Kertas Siasatan"
|
||||
v-model="noKertasSiasatan"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- No Laporan Polis Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Laporan Polis"
|
||||
v-model="noLaporanPolis"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Tarikh Temujanji Input -->
|
||||
<FormKit
|
||||
type="date"
|
||||
label="Tarikh temujanji"
|
||||
v-model="tarikhTemujanji"
|
||||
validation="required|date|after:today"
|
||||
:validation-messages="{
|
||||
after: 'Tarikh temujanji harus selepas hari ini',
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Slot Masa Input -->
|
||||
<FormKit
|
||||
type="time"
|
||||
label="Slot masa"
|
||||
v-model="slotMasa"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button @click="navigateBack" variant="danger">Kembali</rs-button>
|
||||
<rs-button @click.prevent="simpan" variant="primary"
|
||||
>Simpan</rs-button
|
||||
>
|
||||
<rs-button btn-type="submit" variant="success">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
|
||||
<!-- Barang Modal -->
|
||||
<div
|
||||
v-if="isBarangModalOpen"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
|
||||
>
|
||||
<div class="bg-white p-6 rounded-lg w-full max-w-2xl">
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{{ editingBarangIndex === null ? "Tambah" : "Edit" }} Barang
|
||||
</h2>
|
||||
|
||||
<FormKit
|
||||
type="form"
|
||||
:actions="false"
|
||||
@submit="saveBarangModal"
|
||||
#default="{ state: formState }"
|
||||
>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="jenisBarangDetail"
|
||||
label="Jenis Barang"
|
||||
v-model="currentBarang.jenisBarangDetail"
|
||||
:options="jenisBarangDetailOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Jenis Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="tandaBarang"
|
||||
label="Tanda Barang"
|
||||
v-model="currentBarang.tandaBarang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Tanda Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="keadaanBarang"
|
||||
label="Keadaan Barang"
|
||||
v-model="currentBarang.keadaanBarang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Keadaan Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="number"
|
||||
name="kuantitiBarang"
|
||||
label="Kuantiti Barang"
|
||||
v-model="currentBarang.kuantitiBarang"
|
||||
validation="required|number|min:1"
|
||||
:validation-messages="{
|
||||
required: 'Kuantiti Barang diperlukan',
|
||||
number: 'Kuantiti Barang mesti nombor',
|
||||
min: 'Kuantiti Barang mesti sekurang-kurangnya 1',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="jenisBarangSiber"
|
||||
label="Jenis Barang Siber"
|
||||
v-model="currentBarang.jenisBarangSiber"
|
||||
:options="jenisBarangSiberOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Jenis Barang Siber diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button
|
||||
type="button"
|
||||
btn-type="reset"
|
||||
@click="cancelBarangModal"
|
||||
variant="danger"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button
|
||||
type="submit"
|
||||
btn-type="submit"
|
||||
variant="success"
|
||||
:disabled="!formState.valid"
|
||||
>Simpan</rs-button
|
||||
>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
|
||||
const namaPemohon = ref("");
|
||||
const pangkatPemohon = ref("");
|
||||
const noPegawaiPemohon = ref("");
|
||||
const namaPenghantar = ref("");
|
||||
const pangkatPenghantar = ref("");
|
||||
const noPegawaiPenghantar = ref("");
|
||||
const ringkasanKenyataanKes = ref("");
|
||||
const bilangan = ref(0);
|
||||
const barangList = ref([]);
|
||||
const noKertasSiasatan = ref("");
|
||||
const noLaporanPolis = ref("");
|
||||
const tarikhTemujanji = ref("");
|
||||
const slotMasa = ref("");
|
||||
|
||||
// State for single checkbox
|
||||
const isPenghantarSameAsPemohon = ref(false);
|
||||
|
||||
watch(isPenghantarSameAsPemohon, (newValue) => {
|
||||
if (newValue) {
|
||||
namaPenghantar.value = namaPemohon.value;
|
||||
pangkatPenghantar.value = pangkatPemohon.value;
|
||||
noPegawaiPenghantar.value = noPegawaiPemohon.value;
|
||||
} else {
|
||||
namaPenghantar.value = "";
|
||||
pangkatPenghantar.value = "";
|
||||
noPegawaiPenghantar.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
const isBarangModalOpen = ref(false);
|
||||
const editingBarangIndex = ref(null);
|
||||
const currentBarang = ref({
|
||||
jenisBarangDetail: "",
|
||||
tandaBarang: "",
|
||||
keadaanBarang: "",
|
||||
kuantitiBarang: 1,
|
||||
jenisBarangSiber: "",
|
||||
});
|
||||
|
||||
const jenisBarangDetailOptions = ref([]);
|
||||
const jenisBarangSiberOptions = ref([]);
|
||||
|
||||
// Fetch lookup data from API
|
||||
const fetchLookupData = async (type) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/lookup?type=${type}`);
|
||||
if (response.statusCode === 200) {
|
||||
// Data return with value and label
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${type} lookup data:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
jenisBarangDetailOptions.value = await fetchLookupData("jenis_barang");
|
||||
jenisBarangSiberOptions.value = await fetchLookupData("jenis_barang_siber");
|
||||
});
|
||||
|
||||
const navigateBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const openBarangModal = () => {
|
||||
editingBarangIndex.value = null;
|
||||
currentBarang.value = {
|
||||
jenisBarangDetail: "",
|
||||
tandaBarang: "",
|
||||
keadaanBarang: "",
|
||||
kuantitiBarang: 1,
|
||||
jenisBarangSiber: "",
|
||||
};
|
||||
isBarangModalOpen.value = true;
|
||||
};
|
||||
|
||||
const editBarang = (index) => {
|
||||
editingBarangIndex.value = index;
|
||||
currentBarang.value = { ...barangList.value[index] };
|
||||
isBarangModalOpen.value = true;
|
||||
};
|
||||
|
||||
const removeBarang = (index) => {
|
||||
barangList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const cancelBarangModal = () => {
|
||||
isBarangModalOpen.value = false;
|
||||
};
|
||||
|
||||
const saveBarangModal = () => {
|
||||
if (editingBarangIndex.value === null) {
|
||||
barangList.value.push({
|
||||
...currentBarang.value,
|
||||
jenisBarangDetailLabel: getJenisBarangLabel(
|
||||
currentBarang.value.jenisBarangDetail
|
||||
),
|
||||
});
|
||||
} else {
|
||||
barangList.value[editingBarangIndex.value] = {
|
||||
...currentBarang.value,
|
||||
jenisBarangDetailLabel: getJenisBarangLabel(
|
||||
currentBarang.value.jenisBarangDetail
|
||||
),
|
||||
};
|
||||
}
|
||||
isBarangModalOpen.value = false;
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
const requiredFields = [
|
||||
namaPemohon,
|
||||
pangkatPemohon,
|
||||
noPegawaiPemohon,
|
||||
namaPenghantar,
|
||||
pangkatPenghantar,
|
||||
noPegawaiPenghantar,
|
||||
ringkasanKenyataanKes,
|
||||
bilangan,
|
||||
noKertasSiasatan,
|
||||
noLaporanPolis,
|
||||
tarikhTemujanji,
|
||||
slotMasa,
|
||||
];
|
||||
|
||||
const areRequiredFieldsFilled = requiredFields.every(
|
||||
(field) => field.value !== "" && field.value !== 0
|
||||
);
|
||||
|
||||
// const areBarangFieldsValid = barangList.value.every((barang) =>
|
||||
// Object.values(barang).every((value) => value !== "" && value !== 0)
|
||||
// );
|
||||
|
||||
return areRequiredFieldsFilled && barangList.value.length > 0;
|
||||
};
|
||||
|
||||
const simpan = async () => {
|
||||
const isDraft = true; // Simpan means saving as draft
|
||||
await submitData(isDraft);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
const isDraft = false; // Submit means final submission
|
||||
await submitData(isDraft);
|
||||
};
|
||||
|
||||
// Helper function to send the data to the API
|
||||
const submitData = async (isDraft) => {
|
||||
if (isFormValid()) {
|
||||
try {
|
||||
const response = await $fetch("/api/permohonan/create", {
|
||||
method: "POST",
|
||||
body: {
|
||||
namaPemohon: namaPemohon.value,
|
||||
pangkatPemohon: pangkatPemohon.value,
|
||||
noPegawaiPemohon: noPegawaiPemohon.value,
|
||||
namaPenghantar: namaPenghantar.value,
|
||||
pangkatPenghantar: pangkatPenghantar.value,
|
||||
noPegawaiPenghantar: noPegawaiPenghantar.value,
|
||||
isPenghantarSameAsPemohon: isPenghantarSameAsPemohon.value,
|
||||
ringkasanKenyataanKes: ringkasanKenyataanKes.value,
|
||||
bilangan: bilangan.value.toString(),
|
||||
barangList: barangList.value,
|
||||
noKertasSiasatan: noKertasSiasatan.value,
|
||||
noLaporanPolis: noLaporanPolis.value,
|
||||
tarikhTemujanji: tarikhTemujanji.value,
|
||||
slotMasa: slotMasa.value,
|
||||
isDraft: isDraft,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
await $swal.fire({
|
||||
title: "Berjaya!",
|
||||
text: response.message,
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
||||
// Redirect to senarai page after successful submission
|
||||
if (!isDraft) {
|
||||
router.push("/permohonan-temujanji/senarai");
|
||||
}
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: response.message,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: error.message || "Something went wrong, please try again.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Sila isi semua medan yang diperlukan dan tambah sekurang-kurangnya satu barang.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getJenisBarangLabel = (value) => {
|
||||
const option = jenisBarangDetailOptions.value.find(
|
||||
(opt) => opt.__original === value
|
||||
);
|
||||
return option ? option.label : value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,557 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Kemaskini Permohonan</h1>
|
||||
</div>
|
||||
|
||||
<rs-card class="mt-4 p-4">
|
||||
<FormKit type="form" :actions="false" @submit="submitForm">
|
||||
<!-- Nama Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Nama Pemohon"
|
||||
v-model="namaPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Pangkat Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Pangkat Pemohon"
|
||||
v-model="pangkatPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- No Pegawai Pemohon Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Pegawai Pemohon"
|
||||
v-model="noPegawaiPemohon"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Checkbox: Apply Pemohon info to Penghantar -->
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
label="Sama seperti Pemohon"
|
||||
v-model="isPenghantarSameAsPemohon"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render Nama Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="Nama Penghantar"
|
||||
v-model="namaPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render Pangkat Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="Pangkat Penghantar"
|
||||
v-model="pangkatPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Conditionally render No Pegawai Penghantar field if checkbox is not checked -->
|
||||
<FormKit
|
||||
v-if="!isPenghantarSameAsPemohon"
|
||||
type="text"
|
||||
label="No Pegawai Penghantar"
|
||||
v-model="noPegawaiPenghantar"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Ringkasan Kenyataan Kes Input -->
|
||||
<FormKit
|
||||
type="textarea"
|
||||
label="Ringkasan Kenyataan Kes"
|
||||
v-model="ringkasanKenyataanKes"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Bilangan Input -->
|
||||
<FormKit
|
||||
type="number"
|
||||
label="Bilangan"
|
||||
v-model="bilangan"
|
||||
validation="required|number"
|
||||
/>
|
||||
|
||||
<!-- Barang Section -->
|
||||
<div class="mb-4">
|
||||
<h3 class="mb-2">Senarai Barang</h3>
|
||||
<table
|
||||
v-if="barangList.length > 0"
|
||||
class="w-full border-collapse border border-gray-300 mb-2"
|
||||
>
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-gray-300 p-2">Jenis Barang</th>
|
||||
<th class="border border-gray-300 p-2">Kuantiti</th>
|
||||
<th class="border border-gray-300 p-2">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(barang, index) in barangList" :key="index">
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{
|
||||
barang.jenisBarangDetailLabel
|
||||
? barang.jenisBarangDetailLabel
|
||||
: barang.jenisBarangDetail
|
||||
}}
|
||||
</td>
|
||||
<td class="border border-gray-300 p-2">
|
||||
{{ barang.kuantitiBarang }}
|
||||
</td>
|
||||
<td class="border border-gray-300 p-2">
|
||||
<rs-button
|
||||
type="button"
|
||||
@click="editBarang(index)"
|
||||
variant="secondary"
|
||||
class="mr-2"
|
||||
>
|
||||
Edit
|
||||
</rs-button>
|
||||
<rs-button
|
||||
type="button"
|
||||
@click="removeBarang(index)"
|
||||
variant="danger"
|
||||
>
|
||||
Buang
|
||||
</rs-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="text-gray-500 mb-2">Tiada barang ditambah</div>
|
||||
<rs-button type="button" @click="openBarangModal" variant="primary">
|
||||
Tambah Barang
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- No Kertas Siasatan Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Kertas Siasatan"
|
||||
v-model="noKertasSiasatan"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- No Laporan Polis Input -->
|
||||
<FormKit
|
||||
type="text"
|
||||
label="No Laporan Polis"
|
||||
v-model="noLaporanPolis"
|
||||
validation="required"
|
||||
/>
|
||||
|
||||
<!-- Tarikh Temujanji Input -->
|
||||
<FormKit
|
||||
type="date"
|
||||
label="Tarikh temujanji"
|
||||
v-model="tarikhTemujanji"
|
||||
validation="date|after:today"
|
||||
:validation-messages="{
|
||||
after: 'Tarikh temujanji mestilah selepas hari ini',
|
||||
}"
|
||||
/>
|
||||
|
||||
<!-- Slot Masa Input -->
|
||||
<FormKit type="time" label="Slot masa" v-model="slotMasa" />
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button @click="navigateBack" variant="danger">Kembali</rs-button>
|
||||
<rs-button @click.prevent="simpan" variant="primary"
|
||||
>Kemaskini</rs-button
|
||||
>
|
||||
<rs-button btn-type="submit" variant="success">Hantar</rs-button>
|
||||
</div>
|
||||
</FormKit>
|
||||
</rs-card>
|
||||
|
||||
<!-- Barang Modal -->
|
||||
<div
|
||||
v-if="isBarangModalOpen"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
|
||||
>
|
||||
<div class="bg-white p-6 rounded-lg w-full max-w-2xl">
|
||||
<h2 class="text-2xl font-bold mb-4">
|
||||
{{ editingBarangIndex === null ? "Tambah" : "Edit" }} Barang
|
||||
</h2>
|
||||
|
||||
<FormKit
|
||||
type="form"
|
||||
:actions="false"
|
||||
@submit="saveBarangModal"
|
||||
#default="{ state: formState }"
|
||||
>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="jenisBarangDetail"
|
||||
label="Jenis Barang"
|
||||
v-model="currentBarang.jenisBarangDetail"
|
||||
:options="jenisBarangDetailOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Jenis Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="tandaBarang"
|
||||
label="Tanda Barang"
|
||||
v-model="currentBarang.tandaBarang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Tanda Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="text"
|
||||
name="keadaanBarang"
|
||||
label="Keadaan Barang"
|
||||
v-model="currentBarang.keadaanBarang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Keadaan Barang diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="number"
|
||||
name="kuantitiBarang"
|
||||
label="Kuantiti Barang"
|
||||
v-model="currentBarang.kuantitiBarang"
|
||||
validation="required|number|min:1"
|
||||
:validation-messages="{
|
||||
required: 'Kuantiti Barang diperlukan',
|
||||
number: 'Kuantiti Barang mesti nombor',
|
||||
min: 'Kuantiti Barang mesti sekurang-kurangnya 1',
|
||||
}"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
name="jenisBarangSiber"
|
||||
label="Jenis Barang Siber"
|
||||
v-model="currentBarang.jenisBarangSiber"
|
||||
:options="jenisBarangSiberOptions"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Jenis Barang Siber diperlukan',
|
||||
}"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end gap-2 mt-4">
|
||||
<rs-button
|
||||
type="button"
|
||||
btn-type="reset"
|
||||
@click="cancelBarangModal"
|
||||
variant="danger"
|
||||
>Batal</rs-button
|
||||
>
|
||||
<rs-button
|
||||
type="submit"
|
||||
btn-type="submit"
|
||||
variant="success"
|
||||
:disabled="!formState.valid"
|
||||
>Simpan</rs-button
|
||||
>
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const noSiri = ref(route.params.noSiri);
|
||||
|
||||
// Form data refs
|
||||
const namaPemohon = ref("");
|
||||
const pangkatPemohon = ref("");
|
||||
const noPegawaiPemohon = ref("");
|
||||
const namaPenghantar = ref("");
|
||||
const pangkatPenghantar = ref("");
|
||||
const noPegawaiPenghantar = ref("");
|
||||
const ringkasanKenyataanKes = ref("");
|
||||
const bilangan = ref(0);
|
||||
const barangList = ref([]);
|
||||
const noKertasSiasatan = ref("");
|
||||
const noLaporanPolis = ref("");
|
||||
const tarikhTemujanji = ref("");
|
||||
const slotMasa = ref("");
|
||||
|
||||
// State for single checkbox
|
||||
const isPenghantarSameAsPemohon = ref(false);
|
||||
|
||||
// Remove computed properties and add watch
|
||||
watch(isPenghantarSameAsPemohon, (newValue) => {
|
||||
if (newValue) {
|
||||
namaPenghantar.value = namaPemohon.value;
|
||||
pangkatPenghantar.value = pangkatPemohon.value;
|
||||
noPegawaiPenghantar.value = noPegawaiPemohon.value;
|
||||
} else {
|
||||
namaPenghantar.value = "";
|
||||
pangkatPenghantar.value = "";
|
||||
noPegawaiPenghantar.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
// Update these to be reactive refs
|
||||
const jenisBarangDetailOptions = ref([]);
|
||||
const jenisBarangSiberOptions = ref([]);
|
||||
|
||||
// Fetch lookup data from API
|
||||
const fetchLookupData = async (type) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/lookup?type=${type}`);
|
||||
if (response.statusCode === 200) {
|
||||
// Data return with value and label
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${type} lookup data:`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch existing data
|
||||
const fetchExistingData = async (noSiri) => {
|
||||
try {
|
||||
const response = await $fetch(`/api/permohonan/${noSiri}`);
|
||||
if (response.statusCode === 200) {
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching existing data:", error);
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Gagal mendapatkan data permohonan.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const existingData = await fetchExistingData(noSiri.value);
|
||||
|
||||
if (existingData) {
|
||||
// Set the form values based on the existingData
|
||||
namaPemohon.value = existingData.namaPemohon;
|
||||
pangkatPemohon.value = existingData.pangkatPemohon;
|
||||
noPegawaiPemohon.value = existingData.noPegawaiPemohon;
|
||||
namaPenghantar.value = existingData.namaPenghantar;
|
||||
pangkatPenghantar.value = existingData.pangkatPenghantar;
|
||||
noPegawaiPenghantar.value = existingData.noPegawaiPenghantar;
|
||||
ringkasanKenyataanKes.value = existingData.ringkasanKenyataanKes;
|
||||
bilangan.value = existingData.bilangan;
|
||||
barangList.value = existingData.barangList;
|
||||
noKertasSiasatan.value = existingData.noKertasSiasatan;
|
||||
noLaporanPolis.value = existingData.noLaporanPolis;
|
||||
tarikhTemujanji.value = existingData.tarikhTemujanji;
|
||||
slotMasa.value = existingData.slotMasa;
|
||||
isPenghantarSameAsPemohon.value = existingData.isPenghantarSameAsPemohon;
|
||||
}
|
||||
|
||||
// Fetch lookup data
|
||||
jenisBarangDetailOptions.value = await fetchLookupData("jenis_barang");
|
||||
jenisBarangSiberOptions.value = await fetchLookupData("jenis_barang_siber");
|
||||
});
|
||||
|
||||
// Barang modal state
|
||||
const isBarangModalOpen = ref(false);
|
||||
const editingBarangIndex = ref(null);
|
||||
const currentBarang = ref({
|
||||
jenisBarangDetail: "",
|
||||
tandaBarang: "",
|
||||
keadaanBarang: "",
|
||||
kuantitiBarang: 1,
|
||||
jenisBarangSiber: "",
|
||||
});
|
||||
|
||||
// Barang modal functions
|
||||
const openBarangModal = () => {
|
||||
editingBarangIndex.value = null;
|
||||
currentBarang.value = {
|
||||
jenisBarangDetail: "",
|
||||
tandaBarang: "",
|
||||
keadaanBarang: "",
|
||||
kuantitiBarang: 1,
|
||||
jenisBarangSiber: "",
|
||||
};
|
||||
isBarangModalOpen.value = true;
|
||||
};
|
||||
|
||||
const editBarang = (index) => {
|
||||
editingBarangIndex.value = index;
|
||||
currentBarang.value = { ...barangList.value[index] };
|
||||
isBarangModalOpen.value = true;
|
||||
};
|
||||
|
||||
const removeBarang = (index) => {
|
||||
barangList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const cancelBarangModal = () => {
|
||||
isBarangModalOpen.value = false;
|
||||
};
|
||||
|
||||
const saveBarangModal = () => {
|
||||
if (editingBarangIndex.value === null) {
|
||||
barangList.value.push({
|
||||
...currentBarang.value,
|
||||
jenisBarangDetailLabel: getJenisBarangLabel(
|
||||
currentBarang.value.jenisBarangDetail
|
||||
),
|
||||
});
|
||||
} else {
|
||||
barangList.value[editingBarangIndex.value] = {
|
||||
...currentBarang.value,
|
||||
jenisBarangDetailLabel: getJenisBarangLabel(
|
||||
currentBarang.value.jenisBarangDetail
|
||||
),
|
||||
};
|
||||
}
|
||||
isBarangModalOpen.value = false;
|
||||
};
|
||||
|
||||
const navigateBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const isFormValid = () => {
|
||||
const requiredFields = [
|
||||
namaPemohon,
|
||||
pangkatPemohon,
|
||||
noPegawaiPemohon,
|
||||
namaPenghantar,
|
||||
pangkatPenghantar,
|
||||
noPegawaiPenghantar,
|
||||
ringkasanKenyataanKes,
|
||||
bilangan,
|
||||
noKertasSiasatan,
|
||||
noLaporanPolis,
|
||||
tarikhTemujanji,
|
||||
slotMasa,
|
||||
];
|
||||
|
||||
const areRequiredFieldsFilled = requiredFields.every(
|
||||
(field) => field.value !== "" && field.value !== 0
|
||||
);
|
||||
|
||||
// const areBarangFieldsValid = barangList.value.every((barang) =>
|
||||
// Object.values(barang).every((value) => value !== "" && value !== 0)
|
||||
// );
|
||||
|
||||
return areRequiredFieldsFilled && barangList.value.length > 0;
|
||||
};
|
||||
|
||||
const simpan = async () => {
|
||||
const isDraft = true; // Simpan means saving as draft
|
||||
await submitData(isDraft);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
const isDraft = false; // Submit means final submission
|
||||
await submitData(isDraft);
|
||||
};
|
||||
|
||||
const submitData = async (isDraft) => {
|
||||
if (isFormValid()) {
|
||||
try {
|
||||
const response = await $fetch(`/api/permohonan/${noSiri.value}`, {
|
||||
method: "PUT",
|
||||
body: {
|
||||
namaPemohon: namaPemohon.value,
|
||||
pangkatPemohon: pangkatPemohon.value,
|
||||
noPegawaiPemohon: noPegawaiPemohon.value,
|
||||
namaPenghantar: namaPenghantar.value,
|
||||
pangkatPenghantar: pangkatPenghantar.value,
|
||||
noPegawaiPenghantar: noPegawaiPenghantar.value,
|
||||
isPenghantarSameAsPemohon: isPenghantarSameAsPemohon.value,
|
||||
ringkasanKenyataanKes: ringkasanKenyataanKes.value,
|
||||
bilangan: bilangan.value.toString(),
|
||||
barangList: barangList.value,
|
||||
noKertasSiasatan: noKertasSiasatan.value,
|
||||
noLaporanPolis: noLaporanPolis.value,
|
||||
tarikhTemujanji: tarikhTemujanji.value,
|
||||
slotMasa: slotMasa.value,
|
||||
isDraft: isDraft,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
await $swal.fire({
|
||||
title: "Berjaya!",
|
||||
text: isDraft
|
||||
? "Permohonan telah berjaya disimpan sebagai draf."
|
||||
: "Permohonan telah berjaya dikemaskini.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
|
||||
// Redirect to senarai page after successful submission
|
||||
if (!isDraft) {
|
||||
router.push("/permohonan-temujanji/senarai");
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: error.message || "Gagal mengemaskini permohonan. Sila cuba lagi.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$swal.fire({
|
||||
title: "Ralat!",
|
||||
text: "Sila isi semua medan yang diperlukan dan tambah sekurang-kurangnya satu barang.",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getJenisBarangLabel = (value) => {
|
||||
const option = jenisBarangDetailOptions.value.find(
|
||||
(opt) => opt.__original === value
|
||||
);
|
||||
return option ? option.label : value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
@ -1,171 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Header with title and "Permohonan Baru" button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1>Senarai Permohonan</h1>
|
||||
<!-- Button to navigate to the "Permohonan Baru" page -->
|
||||
<rs-button @click="permohonanBaru()" variant="primary" class="mt-2">
|
||||
Permohonan Baru
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Table displaying the list of permohonan -->
|
||||
<rs-card class="mt-4 py-2">
|
||||
<rs-table
|
||||
:data="tableData"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: true,
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<!-- Table Headers -->
|
||||
<template v-slot:header>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>No Siri</th>
|
||||
<th>Tarikh & Masa</th>
|
||||
<th>Status</th>
|
||||
<th>Butiran</th>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<!-- Table Data Rows -->
|
||||
<template v-slot:no="data">
|
||||
{{ data.text }}
|
||||
</template>
|
||||
<template v-slot:noSiri="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:tarikhMasa="data">
|
||||
{{ data.text || "N/A" }}
|
||||
</template>
|
||||
<template v-slot:status="data">
|
||||
<rs-badge
|
||||
:variant="
|
||||
data.text === 'Aktif' || data.text === 'Sah'
|
||||
? 'success'
|
||||
: 'danger'
|
||||
"
|
||||
>
|
||||
{{ data.text || "N/A" }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<!-- Actions for each permohonan -->
|
||||
<template v-slot:butiran="data">
|
||||
<div
|
||||
class="flex flex-wrap gap-2"
|
||||
v-if="data.value.status === 'Permohonan Draf'"
|
||||
>
|
||||
<!-- Button to navigate to the "Kemaskini" page for the selected permohonan -->
|
||||
<rs-button
|
||||
@click="kemaskini(data.value.noSiri)"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Kemaskini"
|
||||
>
|
||||
<Icon name="ic:baseline-edit" size="1.2rem" />
|
||||
</rs-button>
|
||||
|
||||
<!-- Button to delete the selected permohonan -->
|
||||
<rs-button
|
||||
@click="hapus(data.value.noSiri)"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
class="p-1"
|
||||
title="Hapus"
|
||||
>
|
||||
<Icon name="ic:baseline-delete" size="1.2rem" />
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- If permohonan has been confirmed or submitted -->
|
||||
<div v-else>
|
||||
<span>{{
|
||||
data.value.status === "Sah"
|
||||
? "Permohonan telah disahkan"
|
||||
: "Permohonan telah dihantar"
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</rs-table>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
// Reactive variable to store table data
|
||||
const tableData = ref([]);
|
||||
|
||||
// Fetch permohonan list from API
|
||||
const fetchPermohonan = async () => {
|
||||
try {
|
||||
const response = await $fetch("/api/permohonan");
|
||||
if (response.statusCode === 200) {
|
||||
// Populate tableData with the fetched permohonan list
|
||||
tableData.value = response.data;
|
||||
} else {
|
||||
console.error(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching permohonan data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to navigate to the "Permohonan Baru" page
|
||||
const permohonanBaru = () => {
|
||||
navigateTo("/permohonan-temujanji/baru");
|
||||
};
|
||||
|
||||
// Function to navigate to the "Kemaskini" page for a specific permohonan
|
||||
const kemaskini = (noSiri) => {
|
||||
navigateTo(`/permohonan-temujanji/kemaskini/${noSiri}`);
|
||||
};
|
||||
|
||||
// Function to delete a permohonan by its noSiri
|
||||
const hapus = async (noSiri) => {
|
||||
const confirmation = await $swal.fire({
|
||||
title: "Anda pasti?",
|
||||
text: "Anda tidak akan dapat memulihkan semula data ini!",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#3085d6",
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Ya, hapuskan!",
|
||||
cancelButtonText: "Batal",
|
||||
});
|
||||
|
||||
if (confirmation.isConfirmed) {
|
||||
try {
|
||||
const response = await $fetch(`/api/permohonan/${noSiri}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
// Remove the deleted permohonan from tableData
|
||||
tableData.value = tableData.value.filter(
|
||||
(row) => row.noSiri !== noSiri
|
||||
);
|
||||
$swal.fire("Dihapuskan!", response.message, "success");
|
||||
} else {
|
||||
$swal.fire("Error!", response.message, "error");
|
||||
}
|
||||
} catch (error) {
|
||||
$swal.fire("Error!", "Failed to delete permohonan.", "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the permohonan list when the component is mounted
|
||||
onMounted(() => {
|
||||
fetchPermohonan();
|
||||
});
|
||||
</script>
|
@ -1,233 +0,0 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Kemaskini Daftar FR2",
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Kemaskini Daftar FR2",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const caseId = ref("");
|
||||
const equipmentCondition = ref(false);
|
||||
const officerQualification = ref(false);
|
||||
const methodApplicability = ref(false);
|
||||
const externalSupport = ref(false);
|
||||
const taskAcceptance = ref("");
|
||||
const officerComments = ref("");
|
||||
const showForm = ref(false);
|
||||
|
||||
const verifyCase = async () => {
|
||||
if (!caseId.value) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "ID Kes diperlukan",
|
||||
text: "Sila masukkan ID Kes untuk menyemak.",
|
||||
timer: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "ID Kes disahkan",
|
||||
text: "Maklumat kes telah berjaya diambil.",
|
||||
timer: 3000,
|
||||
});
|
||||
showForm.value = true;
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mengesahkan ID Kes",
|
||||
text: "Sila cuba lagi atau hubungi pentadbir sistem.",
|
||||
timer: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateForm = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Rekod telah berjaya dikemas kini",
|
||||
text: "Maklumat telah dikemas kini dalam sistem.",
|
||||
timer: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mengemas kini rekod",
|
||||
text: "Sila cuba lagi atau hubungi pentadbir sistem.",
|
||||
timer: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Rekod telah berjaya disahkan",
|
||||
text: "Maklumat telah disahkan dan disimpan dalam sistem.",
|
||||
timer: 3000,
|
||||
});
|
||||
router.push("/dashboard");
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mengesahkan rekod",
|
||||
text: "Sila cuba lagi atau hubungi pentadbir sistem.",
|
||||
timer: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
if (
|
||||
!equipmentCondition.value ||
|
||||
!officerQualification.value ||
|
||||
!methodApplicability.value ||
|
||||
!taskAcceptance.value
|
||||
) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Borang tidak lengkap",
|
||||
text: "Sila lengkapkan semua maklumat yang diperlukan.",
|
||||
timer: 3000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (taskAcceptance.value === "Tidak" && !officerComments.value) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Komen diperlukan",
|
||||
text: "Sila berikan komen kerana tugas tidak diterima.",
|
||||
timer: 3000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<LayoutsBreadcrumb class="mb-6" />
|
||||
|
||||
<RsCard class="mb-6 shadow-lg">
|
||||
<template #header>
|
||||
<h2 class="text-2xl">Kemaskini Daftar FR2</h2>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">
|
||||
Pengesahan ID Kes
|
||||
</h3>
|
||||
<div class="flex flex-col gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="caseId"
|
||||
label="ID Kes"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan ID Kes',
|
||||
}"
|
||||
v-model="caseId"
|
||||
class="flex-grow"
|
||||
:classes="{
|
||||
outer: 'mb-0',
|
||||
}"
|
||||
/>
|
||||
<RsButton @click="verifyCase" variant="primary" class="mb-1"
|
||||
>Semak</RsButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormKit
|
||||
v-if="showForm"
|
||||
type="form"
|
||||
@submit="submitForm"
|
||||
:actions="false"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-4 text-gray-700">Borang FR2</h3>
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="equipmentCondition"
|
||||
label="Peralatan dalam keadaan baik untuk analisis/pemeriksaan"
|
||||
v-model="equipmentCondition"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="officerQualification"
|
||||
label="Pegawai berkelayakan untuk analisis/pemeriksaan"
|
||||
v-model="officerQualification"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="methodApplicability"
|
||||
label="Kaedah boleh dilaksanakan dan diterima untuk analisis"
|
||||
v-model="methodApplicability"
|
||||
/>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="externalSupport"
|
||||
label="Sokongan luar diperlukan"
|
||||
v-model="externalSupport"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="taskAcceptance"
|
||||
label="Penerimaan Tugas"
|
||||
:options="[
|
||||
{ label: 'Ya', value: 'Ya' },
|
||||
{ label: 'Tidak', value: 'Tidak' },
|
||||
]"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih penerimaan tugas',
|
||||
}"
|
||||
v-model="taskAcceptance"
|
||||
/>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="officerComments"
|
||||
label="Komen Pegawai"
|
||||
v-model="officerComments"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end space-x-4">
|
||||
<RsButton @click="updateForm" variant="secondary"
|
||||
>Kemas Kini</RsButton
|
||||
>
|
||||
<!-- <RsButton type="submit" variant="primary">Sahkan</RsButton> -->
|
||||
</div>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
||||
</RsCard>
|
||||
</div>
|
||||
</template>
|
@ -1,334 +0,0 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Permohonan Online",
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Permohonan Online",
|
||||
type: "current",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { $swal } = useNuxtApp();
|
||||
|
||||
const applicantRank = ref("");
|
||||
const applicantName = ref("");
|
||||
const applicantOfficerNumber = ref("");
|
||||
const sameAsSender = ref(false);
|
||||
const showConfirmationModal = ref(false);
|
||||
const showDateTimeModal = ref(false);
|
||||
const availableTimeSlots = ref([]);
|
||||
const formData = ref({});
|
||||
|
||||
onMounted(() => {
|
||||
// Fetch applicant details from the system
|
||||
fetchApplicantDetails();
|
||||
});
|
||||
|
||||
const fetchApplicantDetails = async () => {
|
||||
try {
|
||||
// Simulated fetch of applicant details
|
||||
applicantRank.value = "Inspector";
|
||||
applicantName.value = "John Doe";
|
||||
applicantOfficerNumber.value = "123456";
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mendapatkan maklumat pemohon",
|
||||
text: "Sila cuba lagi.",
|
||||
timer: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSenderDetails = (value) => {
|
||||
sameAsSender.value = value;
|
||||
};
|
||||
|
||||
const submitForm = (formData) => {
|
||||
// Store form data for later use
|
||||
formData.value = formData;
|
||||
showConfirmationModal.value = true;
|
||||
};
|
||||
|
||||
const confirmSubmission = async () => {
|
||||
showConfirmationModal.value = false;
|
||||
try {
|
||||
// Simulated form submission
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Permohonan pemeriksaan forensik telah dihantar",
|
||||
timer: 5000,
|
||||
});
|
||||
showDateTimeModal.value = true;
|
||||
fetchAvailableTimeSlots();
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal menghantar permohonan",
|
||||
text: "Sila cuba lagi.",
|
||||
timer: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAvailableTimeSlots = async () => {
|
||||
try {
|
||||
// Simulated fetch of available time slots
|
||||
availableTimeSlots.value = [
|
||||
{ label: "9:00 AM", value: "09:00" },
|
||||
{ label: "10:00 AM", value: "10:00" },
|
||||
{ label: "11:00 AM", value: "11:00" },
|
||||
];
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mendapatkan slot masa yang tersedia",
|
||||
text: "Sila cuba lagi.",
|
||||
timer: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDateTimeSlot = async () => {
|
||||
try {
|
||||
// Simulated confirmation of date and time slot
|
||||
const data = { caseId: "FOR-2023-001" };
|
||||
$swal.fire({
|
||||
icon: "success",
|
||||
title: "Janji temu telah disahkan",
|
||||
text: `Nombor rujukan kes: ${data.caseId}`,
|
||||
timer: 5000,
|
||||
});
|
||||
showDateTimeModal.value = false;
|
||||
router.push("/dashboard"); // Redirect to dashboard or confirmation page
|
||||
} catch (error) {
|
||||
$swal.fire({
|
||||
icon: "error",
|
||||
title: "Gagal mengesahkan janji temu",
|
||||
text: "Sila cuba lagi.",
|
||||
timer: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb class="mb-6" />
|
||||
|
||||
<FormKit type="form" @submit="submitForm" :actions="false">
|
||||
<!-- Butir Permohonan Card -->
|
||||
<RsCard class="mb-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold">Butir Permohonan</h2>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Butir-butir Pemohon</h3>
|
||||
<div class="bg-gray-100 p-4 rounded-lg">
|
||||
<p>
|
||||
<span class="font-medium">Nama:</span> {{ applicantName }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">Pangkat:</span> {{ applicantRank }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-medium">Nombor Pegawai:</span>
|
||||
{{ applicantOfficerNumber }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">
|
||||
Butir-butir Penghantar Barang Kes
|
||||
</h3>
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
name="sameAsSender"
|
||||
label="Penghantar Barang Kes Sama Dengan Pemohon"
|
||||
v-model="sameAsSender"
|
||||
/>
|
||||
|
||||
<div v-if="!sameAsSender" class="mt-4 space-y-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
name="senderName"
|
||||
label="Nama Penghantar Barang Kes"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan nama penghantar barang kes',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="senderRank"
|
||||
label="Pangkat Penghantar Barang Kes"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan pangkat penghantar barang kes',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="senderOfficerNumber"
|
||||
label="Nombor Pegawai Penghantar Barang Kes"
|
||||
validation="required|number|length:6"
|
||||
:validation-messages="{
|
||||
required:
|
||||
'Sila masukkan nombor pegawai penghantar barang kes',
|
||||
number: 'Nombor pegawai mesti dalam bentuk nombor',
|
||||
length: 'Nombor pegawai mesti mempunyai 6 digit',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsCard>
|
||||
|
||||
<!-- Maklumat Kes Card -->
|
||||
<RsCard class="mb-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold">Maklumat Kes</h2>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Maklumat Barang Kes</h3>
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="select"
|
||||
name="jenisBarang"
|
||||
label="Jenis Barang"
|
||||
:options="[
|
||||
{ label: 'Pasport', value: 'pasport' },
|
||||
{ label: 'Malpass', value: 'malpass' },
|
||||
{ label: 'Cap Keselamatan', value: 'capKeselamatan' },
|
||||
{ label: 'Cap Jari', value: 'capJari' },
|
||||
{ label: 'Pemeriksaan Siber', value: 'pemeriksaanSiber' },
|
||||
{ label: 'Tulisan Tangan', value: 'tulisanTangan' },
|
||||
{ label: 'I-Kad', value: 'iKad' },
|
||||
{ label: 'Lain-lain', value: 'lainLain' },
|
||||
]"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih jenis barang',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="tandaBarang"
|
||||
label="Tanda Barang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan tanda barang',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="keadaanBarang"
|
||||
label="Keadaan Barang"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan keadaan barang',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="number"
|
||||
name="kuantitiBarang"
|
||||
label="Kuantiti Barang"
|
||||
validation="required|number|min:1"
|
||||
:validation-messages="{
|
||||
required: 'Sila masukkan kuantiti barang',
|
||||
number: 'Kuantiti mesti dalam bentuk nombor',
|
||||
min: 'Kuantiti mesti sekurang-kurangnya 1',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-lg font-medium mb-3">Maklumat Tambahan</h3>
|
||||
<div class="space-y-4">
|
||||
<FormKit
|
||||
type="textarea"
|
||||
name="ringkasanKes"
|
||||
label="Ringkasan Kenyataan Kes"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="nomorKertasSiasatan"
|
||||
label="Nombor Kertas Siasatan"
|
||||
/>
|
||||
<FormKit
|
||||
type="text"
|
||||
name="nomorLaporanPolis"
|
||||
label="Nombor Laporan Polis"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RsCard>
|
||||
|
||||
<div class="text-center">
|
||||
<FormKit
|
||||
type="submit"
|
||||
label="Hantar Permohonan"
|
||||
input-class="text-white font-bold py-2 px-4 rounded w-full"
|
||||
/>
|
||||
</div>
|
||||
</FormKit>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<RsModal v-model="showConfirmationModal" title="Pengesahan Permohonan">
|
||||
<template #body>
|
||||
<p>Adakah anda pasti untuk menghantar permohonan ini?</p>
|
||||
</template>
|
||||
<template #footer>
|
||||
<RsButton @click="showConfirmationModal = false" variant="secondary"
|
||||
>Kembali</RsButton
|
||||
>
|
||||
<RsButton @click="confirmSubmission" variant="primary">Sahkan</RsButton>
|
||||
</template>
|
||||
</RsModal>
|
||||
|
||||
<!-- Date and Time Slot Selection Modal -->
|
||||
<RsModal v-model="showDateTimeModal" title="Pilih Tarikh dan Slot Masa">
|
||||
<template #body>
|
||||
<FormKit
|
||||
type="date"
|
||||
name="appointmentDate"
|
||||
label="Tarikh Janji Temu"
|
||||
validation="required|after:today"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih tarikh janji temu',
|
||||
after: 'Tarikh janji temu mestilah selepas tarikh hari ini',
|
||||
}"
|
||||
/>
|
||||
<FormKit
|
||||
type="select"
|
||||
name="timeSlot"
|
||||
label="Slot Masa"
|
||||
:options="availableTimeSlots"
|
||||
validation="required"
|
||||
:validation-messages="{
|
||||
required: 'Sila pilih slot masa',
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<RsButton @click="showDateTimeModal = false" variant="secondary"
|
||||
>Kembali</RsButton
|
||||
>
|
||||
<RsButton @click="confirmDateTimeSlot" variant="primary"
|
||||
>Sahkan</RsButton
|
||||
>
|
||||
</template>
|
||||
</RsModal>
|
||||
</div>
|
||||
</template>
|
@ -16,168 +16,16 @@ model audit {
|
||||
auditCreatedDate DateTime? @db.DateTime(0)
|
||||
}
|
||||
|
||||
model document {
|
||||
documentID Int @id @default(autoincrement())
|
||||
userID Int?
|
||||
documentName String? @db.VarChar(255)
|
||||
documentURL String? @db.VarChar(255)
|
||||
documentType String? @db.VarChar(255)
|
||||
documentExtension String? @db.VarChar(255)
|
||||
imageMIMEType String? @db.VarChar(255)
|
||||
documentSize Int?
|
||||
documentStatus String? @default("ACTIVE") @db.VarChar(255)
|
||||
documentCreatedDate String? @db.VarChar(255)
|
||||
documentModifiedDate String? @db.VarChar(255)
|
||||
user user? @relation(fields: [userID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "document_ibfk_1")
|
||||
permohonan_forensik_checking permohonan_forensik_checking[]
|
||||
report report[]
|
||||
report_doc_support report_doc_support[]
|
||||
temujanji_temujanji_gambarSubjekTodocument temujanji[] @relation("temujanji_gambarSubjekTodocument")
|
||||
temujanji_temujanji_gambarCapJariTodocument temujanji[] @relation("temujanji_gambarCapJariTodocument")
|
||||
temujanji_detail temujanji_detail[]
|
||||
temujanji_log temujanji_log[]
|
||||
|
||||
@@index([userID], map: "userID")
|
||||
}
|
||||
|
||||
model lookup {
|
||||
lookupID Int @id @default(autoincrement())
|
||||
lookupOrder Int?
|
||||
lookupTitle String? @db.VarChar(255)
|
||||
lookupRefCode String? @db.VarChar(255)
|
||||
lookupValue String? @db.VarChar(255)
|
||||
lookupType String? @db.VarChar(255)
|
||||
lookupStatus String? @db.VarChar(255)
|
||||
lookupCreatedDate DateTime? @db.DateTime(0)
|
||||
lookupModifiedDate DateTime? @db.DateTime(0)
|
||||
permohonan permohonan[]
|
||||
permohonan_jenis_barang permohonan_jenis_barang[]
|
||||
permohonan_penolakan permohonan_penolakan[]
|
||||
report_report_dapatanTolookup report[] @relation("report_dapatanTolookup")
|
||||
report_report_jenis_barangTolookup report[] @relation("report_jenis_barangTolookup")
|
||||
}
|
||||
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model notifikasi {
|
||||
notifikasiID Int @id @default(autoincrement())
|
||||
penghantarID Int
|
||||
penerimaID Int
|
||||
body String @db.Text
|
||||
status Int
|
||||
}
|
||||
|
||||
model pemohon {
|
||||
id Int @id @default(autoincrement())
|
||||
userID Int
|
||||
pangkat_pemohon String @db.VarChar(255)
|
||||
no_pegawai_pemohon String @db.VarChar(255)
|
||||
user user @relation(fields: [userID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "pemohon_ibfk_1")
|
||||
permohonan permohonan[]
|
||||
temujanji temujanji[]
|
||||
|
||||
@@index([userID], map: "userID")
|
||||
}
|
||||
|
||||
model penghantar {
|
||||
id Int @id @default(autoincrement())
|
||||
nama_penghantar String @db.VarChar(255)
|
||||
pangkat_penghantar String @db.VarChar(255)
|
||||
no_pegawai_penghantar String @db.VarChar(255)
|
||||
permohonan permohonan[]
|
||||
}
|
||||
|
||||
model permohonan {
|
||||
id Int @id @default(autoincrement())
|
||||
no_siri String @unique(map: "Permohonan_no_siri_key") @db.VarChar(255)
|
||||
pemohonID Int?
|
||||
penghantar_sama_dengan_pemohon Int?
|
||||
penghantarID Int?
|
||||
status_permohonan String @db.VarChar(255)
|
||||
ringkasan_kenyataan_kes String? @db.Text
|
||||
bilangan Int?
|
||||
jenis_barang Int?
|
||||
tanda_barang String? @db.VarChar(255)
|
||||
keadaan_barang String? @db.VarChar(255)
|
||||
kuantiti_barang Int?
|
||||
jenis_barang_details String? @db.Text
|
||||
no_laporan_polis String? @db.VarChar(255)
|
||||
no_kertas_siasatan String? @db.VarChar(255)
|
||||
tarikh_temujanji DateTime? @db.DateTime(0)
|
||||
slot_masa DateTime? @db.Time(0)
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
lookup lookup? @relation(fields: [jenis_barang], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_ibfk_3")
|
||||
pemohon pemohon? @relation(fields: [pemohonID], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "permohonan_ibfk_2")
|
||||
penghantar penghantar? @relation(fields: [penghantarID], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "permohonan_ibfk_1")
|
||||
permohonan_assign_forensik permohonan_assign_forensik[]
|
||||
permohonan_jenis_barang permohonan_jenis_barang[]
|
||||
permohonan_penerimaan permohonan_penerimaan?
|
||||
permohonan_semakan permohonan_semakan?
|
||||
report report[]
|
||||
|
||||
@@index([pemohonID], map: "idx_pemohon")
|
||||
@@index([penghantarID], map: "idx_penghantar")
|
||||
@@index([jenis_barang], map: "jenis_barang")
|
||||
}
|
||||
|
||||
model permohonan_assign_forensik {
|
||||
assignID Int @id @default(autoincrement())
|
||||
permohonanID Int
|
||||
pegawai_forensikID Int
|
||||
permohonan permohonan @relation(fields: [permohonanID], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_assign_forensik_ibfk_2")
|
||||
user user @relation(fields: [pegawai_forensikID], references: [userID], onUpdate: Restrict, map: "permohonan_assign_forensik_ibfk_3")
|
||||
permohonan_forensik_checking permohonan_forensik_checking[]
|
||||
|
||||
@@index([pegawai_forensikID], map: "pegawai_forensikID")
|
||||
@@index([permohonanID], map: "permohonanID")
|
||||
}
|
||||
|
||||
model permohonan_penerimaan {
|
||||
penerimaanID Int @id @default(autoincrement())
|
||||
permohonanID Int @unique(map: "permohonanID")
|
||||
peralatan_keadaan_baik Int
|
||||
pegawai_berkelayakan Int
|
||||
kaedah_dpt_dilakukan Int
|
||||
subkontrak_diperlukan Int
|
||||
tugasan_diterima Int
|
||||
ulasan_pegawai String? @db.Text
|
||||
create_at DateTime @db.DateTime(0)
|
||||
diterima_oleh Int
|
||||
permohonan permohonan @relation(fields: [permohonanID], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_penerimaan_ibfk_1")
|
||||
user user @relation(fields: [diterima_oleh], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_penerimaan_ibfk_2")
|
||||
|
||||
@@index([diterima_oleh], map: "diterima_oleh")
|
||||
}
|
||||
|
||||
model permohonan_penolakan {
|
||||
penolakanID Int @id @default(autoincrement())
|
||||
permohonanID Int @unique(map: "permohonanID")
|
||||
sebab_penolakan Int
|
||||
lain_sebab String? @db.VarChar(255)
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
ditolak_oleh Int?
|
||||
lookup lookup @relation(fields: [sebab_penolakan], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_penolakan_ibfk_1")
|
||||
user user? @relation(fields: [ditolak_oleh], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_penolakan_ibfk_2")
|
||||
|
||||
@@index([ditolak_oleh], map: "ditolak_oleh")
|
||||
@@index([sebab_penolakan], map: "sebab_penolakan")
|
||||
}
|
||||
|
||||
model permohonan_semakan {
|
||||
semakanID Int @id @default(autoincrement())
|
||||
permohonanID Int @unique(map: "permohonanID")
|
||||
peralatan_keadaan_baik Int?
|
||||
pegawai_berkelayakan Int?
|
||||
kaedah_dpt_dilakukan Int?
|
||||
subkontrak_diperlukan Int?
|
||||
tugasan_diterima Int?
|
||||
ulasan_pegawai String? @db.Text
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
disemak_oleh Int?
|
||||
permohonan permohonan @relation(fields: [permohonanID], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_semakan_ibfk_1")
|
||||
user user? @relation(fields: [disemak_oleh], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_semakan_ibfk_2")
|
||||
|
||||
@@index([disemak_oleh], map: "disemak_oleh")
|
||||
lookupID Int @id @default(autoincrement())
|
||||
lookupOrder Int?
|
||||
lookupTitle String? @db.VarChar(255)
|
||||
lookupRefCode String? @db.VarChar(255)
|
||||
lookupValue String? @db.VarChar(255)
|
||||
lookupType String? @db.VarChar(255)
|
||||
lookupStatus String? @db.VarChar(255)
|
||||
lookupCreatedDate DateTime? @db.DateTime(0)
|
||||
lookupModifiedDate DateTime? @db.DateTime(0)
|
||||
}
|
||||
|
||||
model role {
|
||||
@ -190,30 +38,18 @@ model role {
|
||||
userrole userrole[]
|
||||
}
|
||||
|
||||
model status {
|
||||
statusID Int @id @default(autoincrement())
|
||||
status_name String @db.VarChar(255)
|
||||
}
|
||||
|
||||
model user {
|
||||
userID Int @id @default(autoincrement())
|
||||
userSecretKey String? @db.VarChar(255)
|
||||
userUsername String? @db.VarChar(255)
|
||||
userPassword String? @db.VarChar(255)
|
||||
userFullName String? @db.VarChar(255)
|
||||
userEmail String? @db.VarChar(255)
|
||||
userPhone String? @db.VarChar(255)
|
||||
userStatus String? @db.VarChar(255)
|
||||
userCreatedDate DateTime? @db.DateTime(0)
|
||||
userModifiedDate DateTime? @db.DateTime(0)
|
||||
document document[]
|
||||
pemohon pemohon[]
|
||||
permohonan_approval permohonan_approval[]
|
||||
permohonan_assign_forensik permohonan_assign_forensik[]
|
||||
permohonan_penerimaan permohonan_penerimaan[]
|
||||
permohonan_penolakan permohonan_penolakan[]
|
||||
permohonan_semakan permohonan_semakan[]
|
||||
userrole userrole[]
|
||||
userID Int @id @default(autoincrement())
|
||||
userSecretKey String? @db.VarChar(255)
|
||||
userUsername String? @db.VarChar(255)
|
||||
userPassword String? @db.VarChar(255)
|
||||
userFullName String? @db.VarChar(255)
|
||||
userEmail String? @db.VarChar(255)
|
||||
userPhone String? @db.VarChar(255)
|
||||
userStatus String? @db.VarChar(255)
|
||||
userCreatedDate DateTime? @db.DateTime(0)
|
||||
userModifiedDate DateTime? @db.DateTime(0)
|
||||
userrole userrole[]
|
||||
}
|
||||
|
||||
model userrole {
|
||||
@ -228,191 +64,14 @@ model userrole {
|
||||
@@index([userRoleUserID], map: "FK_userrole_user")
|
||||
}
|
||||
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model permohonan_approval {
|
||||
approvalID Int @id @default(autoincrement())
|
||||
permohonanID Int
|
||||
approve_by Int
|
||||
approval_status Int
|
||||
ulasan String? @db.Text
|
||||
approval_date DateTime @db.DateTime(0)
|
||||
user user @relation(fields: [approve_by], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_approval_ibfk_1")
|
||||
|
||||
@@index([approve_by], map: "userID")
|
||||
}
|
||||
|
||||
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
|
||||
model permohonan_forensik_checking {
|
||||
checkingID Int @id @default(autoincrement())
|
||||
assignID Int
|
||||
gambar Int?
|
||||
ulasan String? @db.Text
|
||||
dapatan String @db.VarChar(255)
|
||||
create_at DateTime @db.DateTime(0)
|
||||
permohonan_assign_forensik permohonan_assign_forensik @relation(fields: [assignID], references: [assignID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_forensik_checking_ibfk_1")
|
||||
document document? @relation(fields: [gambar], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_forensik_checking_ibfk_2")
|
||||
|
||||
@@index([assignID], map: "assignID")
|
||||
@@index([gambar], map: "gambar")
|
||||
}
|
||||
|
||||
model permohonan_jenis_barang {
|
||||
barangID Int @id @default(autoincrement())
|
||||
permohonanID Int
|
||||
jenis_barang Int
|
||||
barang_status String @default("ACTIVE") @db.VarChar(255)
|
||||
lookup lookup @relation(fields: [jenis_barang], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_jenis_barang_ibfk_1")
|
||||
permohonan permohonan @relation(fields: [permohonanID], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "permohonan_jenis_barang_ibfk_2")
|
||||
|
||||
@@index([jenis_barang], map: "jenis_barang")
|
||||
@@index([permohonanID], map: "permohonanID")
|
||||
}
|
||||
|
||||
model permohonan_tanda_barang {
|
||||
tandaID Int @id @default(autoincrement())
|
||||
tanda_name String @db.VarChar(255)
|
||||
tanda_status String @default("ACTIVE") @db.VarChar(255)
|
||||
}
|
||||
|
||||
model report {
|
||||
reportID Int @id @default(autoincrement())
|
||||
permohonanID Int
|
||||
jenis_barang Int
|
||||
tanda_barang String? @db.VarChar(255)
|
||||
keadaan_barang String? @db.VarChar(255)
|
||||
kuantiti_barang Int?
|
||||
peralatan String? @db.VarChar(255)
|
||||
langkah_langkah String? @db.VarChar(255)
|
||||
gambarID Int?
|
||||
ulasan String? @db.Text
|
||||
dapatan Int?
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
create_by Int
|
||||
document document? @relation(fields: [gambarID], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "report_ibfk_2")
|
||||
lookup_report_dapatanTolookup lookup? @relation("report_dapatanTolookup", fields: [dapatan], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "report_ibfk_3")
|
||||
lookup_report_jenis_barangTolookup lookup @relation("report_jenis_barangTolookup", fields: [jenis_barang], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "report_ibfk_4")
|
||||
permohonan permohonan @relation(fields: [permohonanID], references: [id], onDelete: Cascade, onUpdate: Restrict, map: "report_ibfk_1")
|
||||
report_doc_support report_doc_support[]
|
||||
|
||||
@@index([dapatan], map: "dapatan")
|
||||
@@index([gambarID], map: "gambarID")
|
||||
@@index([jenis_barang], map: "jenis_barang")
|
||||
@@index([permohonanID], map: "permohonanID")
|
||||
}
|
||||
|
||||
model report_doc_support {
|
||||
report_attachID Int @id @default(autoincrement())
|
||||
reportID Int
|
||||
documentID Int
|
||||
document document @relation(fields: [documentID], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "report_doc_support_ibfk_1")
|
||||
report report @relation(fields: [reportID], references: [reportID], onDelete: NoAction, onUpdate: NoAction, map: "report_doc_support_ibfk_2")
|
||||
|
||||
@@index([documentID], map: "documentID")
|
||||
@@index([reportID], map: "reportID")
|
||||
}
|
||||
|
||||
model temujanji {
|
||||
temujanjiID Int @id @default(autoincrement())
|
||||
noSiri String @db.VarChar(255)
|
||||
temujanjiDetailID Int?
|
||||
pemohonID Int
|
||||
jenisSemakan String @db.VarChar(255)
|
||||
tarikh DateTime @db.Date
|
||||
masa DateTime @db.Time(0)
|
||||
status String? @db.VarChar(255)
|
||||
gambarSubjek Int?
|
||||
gambarCapJari Int?
|
||||
create_at DateTime? @default(now()) @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
document_temujanji_gambarSubjekTodocument document? @relation("temujanji_gambarSubjekTodocument", fields: [gambarSubjek], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "fk_gambarSubjek")
|
||||
document_temujanji_gambarCapJariTodocument document? @relation("temujanji_gambarCapJariTodocument", fields: [gambarCapJari], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "fk_gambarCapJari")
|
||||
pemohon pemohon @relation(fields: [pemohonID], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "temujanji_ibfk_1")
|
||||
temujanji_detail temujanji_detail? @relation(fields: [temujanjiDetailID], references: [temujanjiDetailID], onDelete: NoAction, onUpdate: NoAction, map: "temujanji_ibfk_4")
|
||||
temujanji_log temujanji_log[]
|
||||
|
||||
@@index([gambarCapJari], map: "idx_gambarCapJari")
|
||||
@@index([gambarSubjek], map: "idx_gambarSubjek")
|
||||
@@index([pemohonID], map: "idx_pemohonID")
|
||||
@@index([temujanjiDetailID], map: "temujanjiDetailID")
|
||||
}
|
||||
|
||||
model temujanji_detail {
|
||||
temujanjiDetailID Int @id @default(autoincrement())
|
||||
negara String? @db.VarChar(255)
|
||||
namaPemilik String? @db.VarChar(255)
|
||||
noDokumen String? @db.VarChar(255)
|
||||
kewarganegaraan String? @db.VarChar(255)
|
||||
tarikhLahir DateTime? @db.Date
|
||||
jantina String? @db.VarChar(255)
|
||||
tarikhLuputDokumen DateTime? @db.Date
|
||||
skorPersamaanMuka Decimal? @db.Decimal(10, 2)
|
||||
skorPersamaanCapJari Decimal? @db.Decimal(10, 2)
|
||||
umur Int?
|
||||
tinggi Decimal? @db.Decimal(10, 2)
|
||||
warnaRambut String? @db.VarChar(255)
|
||||
bangsa String? @db.VarChar(255)
|
||||
etnik String? @db.VarChar(255)
|
||||
bentukKepala String? @db.VarChar(255)
|
||||
mata String? @db.VarChar(255)
|
||||
telinga String? @db.VarChar(255)
|
||||
hidung String? @db.VarChar(255)
|
||||
mulut String? @db.VarChar(255)
|
||||
parut String? @db.VarChar(255)
|
||||
sejarahPerjalanan String? @db.VarChar(255)
|
||||
persamaanTandaTangan String? @db.VarChar(255)
|
||||
pemeriksaanLain String? @db.VarChar(255)
|
||||
dapatan String? @db.VarChar(255)
|
||||
laporanSystemTdb Int?
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
temujanji temujanji[]
|
||||
document document? @relation(fields: [laporanSystemTdb], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "temujanji_detail_ibfk_2")
|
||||
|
||||
@@index([laporanSystemTdb], map: "idx_laporanSystemTdb")
|
||||
}
|
||||
|
||||
model temujanji_log {
|
||||
temujanjiLogID Int @id @default(autoincrement())
|
||||
temujanjiID Int
|
||||
pemohonID Int?
|
||||
jenisSemakan String? @db.VarChar(255)
|
||||
tarikh DateTime? @db.Date
|
||||
masa DateTime? @db.Time(0)
|
||||
gambarSubjek Int?
|
||||
gambarCapJari Int?
|
||||
negara String? @db.VarChar(255)
|
||||
namaPemilik String? @db.VarChar(255)
|
||||
noDokumen String? @db.VarChar(255)
|
||||
kewarganegaraan String? @db.VarChar(255)
|
||||
tarikhLahir DateTime? @db.Date
|
||||
jantina String? @db.VarChar(255)
|
||||
tarikhLuputDokumen DateTime? @db.Date
|
||||
skorPersamaanMuka Decimal? @db.Decimal(10, 2)
|
||||
skorPersamaanCapJari Decimal? @db.Decimal(10, 2)
|
||||
umur Int?
|
||||
tinggi Decimal? @db.Decimal(10, 2)
|
||||
warnaRambut String? @db.VarChar(255)
|
||||
bangsa String? @db.VarChar(255)
|
||||
etnik String? @db.VarChar(255)
|
||||
bentukKepala String? @db.VarChar(255)
|
||||
mata String? @db.VarChar(255)
|
||||
telinga String? @db.VarChar(255)
|
||||
hidung String? @db.VarChar(255)
|
||||
mulut String? @db.VarChar(255)
|
||||
parut String? @db.VarChar(255)
|
||||
sejarahPerjalanan String? @db.VarChar(255)
|
||||
persamaanTandaTangan String? @db.VarChar(255)
|
||||
pemeriksaanLain String? @db.VarChar(255)
|
||||
dapatan String? @db.VarChar(255)
|
||||
laporanSystemTdb Int?
|
||||
create_at DateTime? @db.DateTime(0)
|
||||
modified_at DateTime? @db.DateTime(0)
|
||||
document document? @relation(fields: [laporanSystemTdb], references: [documentID], onDelete: NoAction, onUpdate: NoAction, map: "temujanji_log_ibfk_1")
|
||||
temujanji temujanji @relation(fields: [temujanjiID], references: [temujanjiID], onDelete: NoAction, onUpdate: NoAction, map: "temujanji_log_ibfk_2")
|
||||
|
||||
@@index([gambarCapJari], map: "gambarCapJari")
|
||||
@@index([gambarSubjek], map: "gambarSubjek")
|
||||
@@index([laporanSystemTdb], map: "laporanSystemTdb")
|
||||
@@index([pemohonID], map: "pemohonID")
|
||||
@@index([temujanjiID], map: "temujanjiID")
|
||||
model customer {
|
||||
cust_id Int @id @default(autoincrement())
|
||||
cust_name String? @db.VarChar(255)
|
||||
cust_username String? @db.VarChar(255)
|
||||
cust_ic_number String? @db.VarChar(255)
|
||||
cust_address String? @db.VarChar(255)
|
||||
cust_dob DateTime? @db.Date
|
||||
cust_gender String? @db.VarChar(255)
|
||||
cust_status Int?
|
||||
cust_created_datetime DateTime? @db.DateTime(0)
|
||||
}
|
||||
|
@ -14,40 +14,170 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// run linter
|
||||
const code = body.code;
|
||||
const validateNitroCode = (code) => {
|
||||
// Check if this is a server route file
|
||||
const isServerRoute = code.includes("defineEventHandler");
|
||||
|
||||
// enable babel-eslint parser and requireConfigFile: false
|
||||
// ignore code export default defineEventHandler((event) => {
|
||||
if (isServerRoute) {
|
||||
let lineNumber = 1;
|
||||
|
||||
const eslint = new ESLint({
|
||||
overrideConfig: {
|
||||
parser: "@babel/eslint-parser",
|
||||
extends: ["@kiwicom"],
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
// 1. Validate event handler structure
|
||||
if (!code.includes("export default defineEventHandler")) {
|
||||
throw {
|
||||
message:
|
||||
"Nitro route handlers must use 'export default defineEventHandler'",
|
||||
line: 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Check for proper request handling
|
||||
const hasRequestBody = code.includes("await readBody(event)");
|
||||
const hasRequestQuery = code.includes("getQuery(event)");
|
||||
const usesEventWithoutImport =
|
||||
code.includes("event.") && !hasRequestBody && !hasRequestQuery;
|
||||
|
||||
if (usesEventWithoutImport) {
|
||||
// Find the line where event is improperly used
|
||||
const lines = code.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (
|
||||
lines[i].includes("event.") &&
|
||||
!lines[i].includes("readBody") &&
|
||||
!lines[i].includes("getQuery")
|
||||
) {
|
||||
throw {
|
||||
message:
|
||||
"Use 'readBody(event)' for POST data or 'getQuery(event)' for query parameters",
|
||||
line: i + 1,
|
||||
column: lines[i].indexOf("event."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validate response structure
|
||||
const responseRegex = /return\s+{([^}]+)}/g;
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = responseRegex.exec(code)) !== null) {
|
||||
lineNumber += (code.slice(lastIndex, match.index).match(/\n/g) || [])
|
||||
.length;
|
||||
lastIndex = match.index;
|
||||
|
||||
const responseContent = match[1];
|
||||
|
||||
// Check for required response properties
|
||||
if (!responseContent.includes("statusCode")) {
|
||||
throw {
|
||||
message: "API responses must include a 'statusCode' property",
|
||||
line: lineNumber,
|
||||
column: match.index - code.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate status code usage
|
||||
const statusMatch = responseContent.match(/statusCode:\s*(\d+)/);
|
||||
if (statusMatch) {
|
||||
const statusCode = parseInt(statusMatch[1]);
|
||||
if (![200, 201, 400, 401, 403, 404, 500].includes(statusCode)) {
|
||||
throw {
|
||||
message: `Invalid status code: ${statusCode}. Use standard HTTP status codes.`,
|
||||
line: lineNumber,
|
||||
column: statusMatch.index,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check error handling
|
||||
if (code.includes("try") && !code.includes("catch")) {
|
||||
throw {
|
||||
message:
|
||||
"Missing error handling. Add a catch block for try statements.",
|
||||
line:
|
||||
code.split("\n").findIndex((line) => line.includes("try")) + 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Validate async/await usage
|
||||
const asyncLines = code.match(/async.*=>/g) || [];
|
||||
const awaitLines = code.match(/await\s+/g) || [];
|
||||
|
||||
if (awaitLines.length > 0 && asyncLines.length === 0) {
|
||||
throw {
|
||||
message: "Using 'await' requires an async function",
|
||||
line:
|
||||
code.split("\n").findIndex((line) => line.includes("await")) + 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// // 6. Check for proper imports
|
||||
// const requiredImports = new Set();
|
||||
// if (hasRequestBody) requiredImports.add("readBody");
|
||||
// if (hasRequestQuery) requiredImports.add("getQuery");
|
||||
|
||||
// const importLines = code.match(/import.*from/g) || [];
|
||||
// requiredImports.forEach((imp) => {
|
||||
// if (!importLines.some((line) => line.includes(imp))) {
|
||||
// throw {
|
||||
// message: `Missing import for '${imp}' utility`,
|
||||
// line: 1,
|
||||
// column: 0,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
validateNitroCode(code);
|
||||
|
||||
const eslint = new ESLint({
|
||||
overrideConfig: {
|
||||
parser: "@babel/eslint-parser",
|
||||
extends: ["@kiwicom"],
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
useEslintrc: false,
|
||||
});
|
||||
useEslintrc: false,
|
||||
});
|
||||
|
||||
const results = await eslint.lintText(code);
|
||||
const results = await eslint.lintText(code);
|
||||
|
||||
if (results[0].messages.length > 0) {
|
||||
const messages = results[0].messages[0];
|
||||
if (results[0].messages.length > 0) {
|
||||
const messages = results[0].messages[0];
|
||||
|
||||
if (messages.fatal === true) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: messages,
|
||||
};
|
||||
}
|
||||
|
||||
if (messages.fatal === true) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
statusCode: 200,
|
||||
message: "Good Linter test",
|
||||
data: messages,
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Good Linter test",
|
||||
data: messages,
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: {
|
||||
message: error.message,
|
||||
line: error.line || 1,
|
||||
column: error.column || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -8,8 +8,8 @@ export default defineEventHandler(async (event) => {
|
||||
const codeDefault = `
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
// const query = await getQuery(event); || Get Params from URL
|
||||
// const body = await readBody(event); || Get Body Data
|
||||
// const query = await getQuery(event); // Get Params from URL
|
||||
// const body = await readBody(event); // Get Body Data
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
@ -1,4 +1,3 @@
|
||||
// import esline vue
|
||||
import { ESLint } from "eslint";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@ -12,9 +11,366 @@ export default defineEventHandler(async (event) => {
|
||||
};
|
||||
}
|
||||
|
||||
// run linter
|
||||
const code = body.code;
|
||||
|
||||
// Extract script and template content once
|
||||
const scriptContent =
|
||||
code.match(/<script\b[^>]*>([\s\S]*?)<\/script>/)?.[1] || "";
|
||||
const templateContent = code.match(/<template>([\s\S]*)<\/template>/)?.[1];
|
||||
|
||||
// Validate FormKit inputs
|
||||
const validateFormKit = (content) => {
|
||||
// List of valid FormKit input types
|
||||
const validFormKitTypes = [
|
||||
"text",
|
||||
"email",
|
||||
"url",
|
||||
"tel",
|
||||
"password",
|
||||
"number",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"time",
|
||||
"month",
|
||||
"week",
|
||||
"search",
|
||||
"color",
|
||||
"file",
|
||||
"range",
|
||||
"checkbox",
|
||||
"radio",
|
||||
"select",
|
||||
"textarea",
|
||||
"submit",
|
||||
"button",
|
||||
];
|
||||
|
||||
// Find all FormKit components
|
||||
const formKitRegex = /<FormKit[^>]*>/g;
|
||||
let formKitMatch;
|
||||
|
||||
// Start counting from template tag
|
||||
let lineNumber = content
|
||||
.slice(0, content.indexOf("<template"))
|
||||
.split("\n").length;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((formKitMatch = formKitRegex.exec(content)) !== null) {
|
||||
// Calculate correct line number including the lines before template
|
||||
lineNumber += (
|
||||
content.slice(lastIndex, formKitMatch.index).match(/\n/g) || []
|
||||
).length;
|
||||
lastIndex = formKitMatch.index;
|
||||
|
||||
const formKitTag = formKitMatch[0];
|
||||
|
||||
// Extract type attribute
|
||||
const typeMatch = formKitTag.match(/type=["']([^"']+)["']/);
|
||||
if (!typeMatch) {
|
||||
throw {
|
||||
message: "FormKit component missing required 'type' attribute",
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
|
||||
const inputType = typeMatch[1];
|
||||
if (!validFormKitTypes.includes(inputType)) {
|
||||
throw {
|
||||
message: `Invalid FormKit type: "${inputType}". Please use a valid input type.`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
|
||||
// Check for options in select, radio, and checkbox types
|
||||
if (["select", "radio", "checkbox"].includes(inputType)) {
|
||||
// Look for :options or v-model
|
||||
const hasOptions =
|
||||
formKitTag.includes(":options=") || formKitTag.includes("v-model=");
|
||||
const hasSlotContent =
|
||||
content
|
||||
.slice(
|
||||
formKitMatch.index,
|
||||
content.indexOf(">", formKitMatch.index)
|
||||
)
|
||||
.includes(">") &&
|
||||
content
|
||||
.slice(
|
||||
formKitMatch.index,
|
||||
content.indexOf("</FormKit>", formKitMatch.index)
|
||||
)
|
||||
.includes("<option");
|
||||
|
||||
if (!hasOptions && !hasSlotContent) {
|
||||
throw {
|
||||
message: `FormKit ${inputType} requires options. Add :options prop or option slots.`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add new function to validate mustache syntax
|
||||
const validateMustacheSyntax = (content) => {
|
||||
const stack = [];
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] === "\n") {
|
||||
lineNumber++;
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
|
||||
if (content[i] === "{" && content[i + 1] === "{") {
|
||||
stack.push({
|
||||
position: i,
|
||||
line: lineNumber,
|
||||
column: i - lastIndex,
|
||||
});
|
||||
i++; // Skip next '{'
|
||||
} else if (content[i] === "}" && content[i + 1] === "}") {
|
||||
if (stack.length === 0) {
|
||||
throw {
|
||||
message:
|
||||
"Unexpected closing mustache brackets '}}' without matching opening brackets",
|
||||
line: lineNumber,
|
||||
column: i - lastIndex,
|
||||
};
|
||||
}
|
||||
stack.pop();
|
||||
i++; // Skip next '}'
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
const unclosed = stack[0];
|
||||
throw {
|
||||
message:
|
||||
"Unclosed mustache brackets '{{'. Missing closing brackets '}}",
|
||||
line: unclosed.line,
|
||||
column: unclosed.column,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Check template content and FormKit validation
|
||||
if (templateContent) {
|
||||
try {
|
||||
validateMustacheSyntax(templateContent);
|
||||
validateFormKit(templateContent);
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Syntax Error",
|
||||
data: {
|
||||
message: error.message,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Check for undefined variables
|
||||
const definedVariables = new Set();
|
||||
|
||||
// Add common Vue variables
|
||||
const commonVueVars = [
|
||||
"$route",
|
||||
"$router",
|
||||
"$refs",
|
||||
"$emit",
|
||||
"$slots",
|
||||
"$attrs",
|
||||
];
|
||||
commonVueVars.forEach((v) => definedVariables.add(v));
|
||||
|
||||
// Extract refs and other variables from script
|
||||
const refRegex = /(?:const|let|var)\s+(\w+)\s*=/g;
|
||||
let varMatch;
|
||||
while ((varMatch = refRegex.exec(scriptContent)) !== null) {
|
||||
definedVariables.add(varMatch[1]);
|
||||
}
|
||||
|
||||
// Extract defineProps if any
|
||||
const propsMatch = scriptContent.match(/defineProps\(\s*{([^}]+)}\s*\)/);
|
||||
if (propsMatch) {
|
||||
const propsContent = propsMatch[1];
|
||||
const propNames = propsContent.match(/(\w+)\s*:/g);
|
||||
propNames?.forEach((prop) => {
|
||||
definedVariables.add(prop.replace(":", "").trim());
|
||||
});
|
||||
}
|
||||
|
||||
// Check template for undefined variables
|
||||
const mustacheRegex = /{{([^}]+)}}/g;
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
let mustacheMatch;
|
||||
|
||||
while ((mustacheMatch = mustacheRegex.exec(templateContent)) !== null) {
|
||||
// Calculate line number
|
||||
lineNumber += (
|
||||
templateContent.slice(lastIndex, mustacheMatch.index).match(/\n/g) ||
|
||||
[]
|
||||
).length;
|
||||
lastIndex = mustacheMatch.index;
|
||||
|
||||
const expression = mustacheMatch[1].trim();
|
||||
// Split expression and check each variable
|
||||
const variables = expression.split(/[\s.()[\]]+/);
|
||||
|
||||
for (const variable of variables) {
|
||||
// Skip numbers, operators, and empty strings
|
||||
if (
|
||||
!variable ||
|
||||
variable.match(/^[\d+\-*/&|!%<>=?:]+$/) ||
|
||||
variable === "true" ||
|
||||
variable === "false"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!definedVariables.has(variable)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Reference Error",
|
||||
data: {
|
||||
message: `Variable "${variable}" is not defined`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
mustacheMatch.index -
|
||||
templateContent.lastIndexOf("\n", mustacheMatch.index),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate template structure
|
||||
const validateTemplateStructure = (code) => {
|
||||
// Check for root level template and script tags
|
||||
const rootTemplateCount = (
|
||||
code.match(/^[\s\S]*<template>[\s\S]*<\/template>/g) || []
|
||||
).length;
|
||||
const rootScriptCount = (
|
||||
code.match(/^[\s\S]*<script>[\s\S]*<\/script>/g) || []
|
||||
).length;
|
||||
|
||||
if (rootTemplateCount > 1 || rootScriptCount > 1) {
|
||||
throw new Error(
|
||||
"Vue components must have only one root <template> and one <script> tag"
|
||||
);
|
||||
}
|
||||
|
||||
// Extract template content for further validation
|
||||
const templateContent = code.match(
|
||||
/<template>([\s\S]*)<\/template>/
|
||||
)?.[1];
|
||||
if (templateContent) {
|
||||
const tagStack = [];
|
||||
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9:-]*)\s*([^>]*?)(\/?)>/g;
|
||||
let match;
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = tagRegex.exec(templateContent)) !== null) {
|
||||
const [fullTag, tagName, attributes, selfClosing] = match;
|
||||
|
||||
// Calculate line number
|
||||
lineNumber += (
|
||||
templateContent.slice(lastIndex, match.index).match(/\n/g) || []
|
||||
).length;
|
||||
lastIndex = match.index;
|
||||
|
||||
// Skip comments
|
||||
if (templateContent.slice(match.index).startsWith("<!--")) {
|
||||
const commentEnd = templateContent.indexOf("-->", match.index);
|
||||
if (commentEnd !== -1) {
|
||||
tagRegex.lastIndex = commentEnd + 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fullTag.endsWith(">")) {
|
||||
throw {
|
||||
message: `Malformed tag found: ${fullTag}`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
if (selfClosing || fullTag.endsWith("/>")) continue;
|
||||
|
||||
if (!fullTag.startsWith("</")) {
|
||||
tagStack.push({
|
||||
name: tagName,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent.lastIndexOf("\n", match.index),
|
||||
});
|
||||
} else {
|
||||
if (tagStack.length === 0) {
|
||||
throw {
|
||||
message: `Unexpected closing tag </${tagName}> found without matching opening tag`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
const lastTag = tagStack[tagStack.length - 1];
|
||||
if (lastTag.name !== tagName) {
|
||||
throw {
|
||||
message: `Mismatched tags: expected closing tag for "${lastTag.name}" but found "${tagName}"`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
tagStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if (tagStack.length > 0) {
|
||||
const unclosedTag = tagStack[tagStack.length - 1];
|
||||
throw {
|
||||
message: `Unclosed tag: ${unclosedTag.name}`,
|
||||
line: unclosedTag.line,
|
||||
column: unclosedTag.column,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
validateTemplateStructure(code);
|
||||
} catch (structureError) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Structure Error",
|
||||
data: {
|
||||
message: structureError.message,
|
||||
line: structureError.line || 1,
|
||||
column: structureError.column || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ESLint configuration
|
||||
const eslint = new ESLint({
|
||||
overrideConfig: {
|
||||
extends: ["plugin:vue/vue3-recommended"],
|
||||
@ -30,28 +386,41 @@ export default defineEventHandler(async (event) => {
|
||||
const results = await eslint.lintText(code);
|
||||
|
||||
if (results[0].messages.length > 0) {
|
||||
const messages = results[0].messages[0];
|
||||
const message = results[0].messages[0];
|
||||
|
||||
if (messages.fatal === true) {
|
||||
if (message.fatal === true) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: messages,
|
||||
data: {
|
||||
message: message.message,
|
||||
line: message.line,
|
||||
column: message.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Good Linter test",
|
||||
data: messages,
|
||||
data: {
|
||||
message: message.message,
|
||||
line: message.line,
|
||||
column: message.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code validation passed",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
errror: error,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -29,23 +29,14 @@ export default defineEventHandler(async (event) => {
|
||||
// Create the folder if doesn't exist
|
||||
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
||||
|
||||
// Create new file
|
||||
fs.writeFileSync(
|
||||
newFilePath,
|
||||
`<script setup>
|
||||
definePageMeta({
|
||||
title: "${
|
||||
body.formData.title ? body.formData.title : body.formData.name
|
||||
}",
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
);
|
||||
// Create template content
|
||||
const templateContent = buildNuxtTemplate({
|
||||
title: body.formData.title || body.formData.name,
|
||||
name: body.formData.name,
|
||||
});
|
||||
|
||||
// Write file with template
|
||||
fs.writeFileSync(newFilePath, templateContent);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import navigationData from "~/navigation";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@ -11,15 +12,50 @@ export default defineEventHandler(async (event) => {
|
||||
// Delete path
|
||||
fs.rmSync(filePath, { recursive: true, force: true });
|
||||
|
||||
// Remove menu from navigation
|
||||
removeMenuFromNavigation(body.filePath);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Menu successfully added!",
|
||||
message: "Menu successfully deleted and removed from navigation!",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function removeMenuFromNavigation(menuPath) {
|
||||
const removeMenuItem = (items) => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].path === menuPath) {
|
||||
items.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
if (items[i].child && items[i].child.length > 0) {
|
||||
if (removeMenuItem(items[i].child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
navigationData.forEach((section) => {
|
||||
if (section.child) {
|
||||
removeMenuItem(section.child);
|
||||
}
|
||||
});
|
||||
|
||||
// Save updated navigation data
|
||||
const navigationFilePath = path.join(process.cwd(), "navigation", "index.js");
|
||||
const navigationContent = `export default ${JSON.stringify(
|
||||
navigationData,
|
||||
null,
|
||||
2
|
||||
)};`;
|
||||
fs.writeFileSync(navigationFilePath, navigationContent, "utf8");
|
||||
}
|
||||
|
@ -4,76 +4,62 @@ import path from "path";
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body.filePath != body.formData.path) {
|
||||
try {
|
||||
// Check if last character is not slash
|
||||
if (body.filePath.slice(-1) != "/") {
|
||||
body.filePath = body.filePath + "/";
|
||||
}
|
||||
// Normalize paths
|
||||
const oldPath = body.filePath.endsWith("/")
|
||||
? body.filePath
|
||||
: body.filePath + "/";
|
||||
const newPath = body.formData.path.endsWith("/")
|
||||
? body.formData.path
|
||||
: body.formData.path + "/";
|
||||
|
||||
// Get old file path
|
||||
const oldFilePath = path.join(
|
||||
process.cwd() + "pages",
|
||||
body.filePath + "index.vue"
|
||||
);
|
||||
// Get file paths
|
||||
const oldFilePath = path.join(process.cwd(), "pages", oldPath, "index.vue");
|
||||
const newFilePath = path.join(process.cwd(), "pages", newPath, "index.vue");
|
||||
|
||||
// Check if last character is not a slash
|
||||
if (body.formData.path.slice(-1) != "/") {
|
||||
body.formData.path = body.formData.path + "/";
|
||||
}
|
||||
try {
|
||||
// Create template content
|
||||
const templateContent = buildNuxtTemplate({
|
||||
title: body.formData.title || body.formData.name,
|
||||
name: body.formData.name,
|
||||
});
|
||||
|
||||
// Get new file path
|
||||
const newFilePath = path.join(
|
||||
process.cwd(),
|
||||
"pages",
|
||||
body.formData.path,
|
||||
"index.vue"
|
||||
);
|
||||
|
||||
// Create the folder if doesn't exist
|
||||
if (oldPath !== newPath) {
|
||||
// Create the new folder if it doesn't exist
|
||||
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
||||
|
||||
// Create new file
|
||||
fs.writeFileSync(
|
||||
newFilePath,
|
||||
`<script setup>
|
||||
definePageMeta({
|
||||
title: "${
|
||||
body.formData.title ? body.formData.title : body.formData.name
|
||||
}",
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
</div>
|
||||
</template>
|
||||
`
|
||||
);
|
||||
// Write the new file
|
||||
fs.writeFileSync(newFilePath, templateContent);
|
||||
|
||||
// copy old file to new file
|
||||
fs.copyFile(oldFilePath, newFilePath, (err) => {
|
||||
if (err) throw err;
|
||||
console.log("successfully copied old file to new file");
|
||||
});
|
||||
// Delete the old file
|
||||
fs.unlinkSync(oldFilePath);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Menu successfully saved",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error,
|
||||
};
|
||||
// Remove empty directories
|
||||
let dirToCheck = path.dirname(oldFilePath);
|
||||
while (dirToCheck !== path.join(process.cwd(), "pages")) {
|
||||
if (fs.readdirSync(dirToCheck).length === 0) {
|
||||
fs.rmdirSync(dirToCheck);
|
||||
dirToCheck = path.dirname(dirToCheck);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Update existing file
|
||||
fs.writeFileSync(oldFilePath, templateContent);
|
||||
}
|
||||
|
||||
// fs.writeFile;
|
||||
return {
|
||||
statusCode: 200,
|
||||
message:
|
||||
oldPath !== newPath
|
||||
? "Menu successfully moved and updated"
|
||||
: "Menu successfully updated",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "null",
|
||||
};
|
||||
});
|
||||
|
36
server/api/devtool/orm/data/get.get.js
Normal file
36
server/api/devtool/orm/data/get.get.js
Normal file
@ -0,0 +1,36 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName } = getQuery(event);
|
||||
|
||||
if (!tableName) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Table name is required",
|
||||
};
|
||||
}
|
||||
|
||||
// const JSONSchemaTable = getPrismaSchemaTable(tableName);
|
||||
// console.log(JSONSchemaTable);
|
||||
|
||||
const getData = await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName}`);
|
||||
|
||||
if (getData.length === 0) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Data not found",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Data successfully fetched",
|
||||
data: getData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
37
server/api/devtool/orm/schema.get.js
Normal file
37
server/api/devtool/orm/schema.get.js
Normal file
@ -0,0 +1,37 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { type } = getQuery(event);
|
||||
|
||||
if (!type) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Type is required",
|
||||
};
|
||||
}
|
||||
|
||||
if (type !== "table" && type !== "field") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Invalid type",
|
||||
};
|
||||
}
|
||||
|
||||
let schema = null;
|
||||
if (type == "table") schema = getPrismaSchemaTable();
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Schema successfully fetched",
|
||||
data: schema,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
34
server/api/devtool/orm/studio.get.js
Normal file
34
server/api/devtool/orm/studio.get.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
let error = false;
|
||||
|
||||
// Run command yarn prisma studio
|
||||
exec("npx prisma studio", (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
error = true;
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
});
|
||||
|
||||
if (error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Prisma Studio successfully launched",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
106
server/api/devtool/orm/table/config/configuration.json
Normal file
106
server/api/devtool/orm/table/config/configuration.json
Normal file
@ -0,0 +1,106 @@
|
||||
{
|
||||
"columnTypes": [
|
||||
{
|
||||
"group": "Numbers",
|
||||
"options": [
|
||||
"TINYINT",
|
||||
"SMALLINT",
|
||||
"MEDIUMINT",
|
||||
"INT",
|
||||
"BIGINT",
|
||||
"DECIMAL",
|
||||
"FLOAT",
|
||||
"DOUBLE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Date and Time",
|
||||
"options": ["DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR"]
|
||||
},
|
||||
{
|
||||
"group": "Strings",
|
||||
"options": [
|
||||
"CHAR",
|
||||
"VARCHAR",
|
||||
"TINYTEXT",
|
||||
"TEXT",
|
||||
"MEDIUMTEXT",
|
||||
"LONGTEXT",
|
||||
"JSON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Lists",
|
||||
"options": ["ENUM", "SET"]
|
||||
},
|
||||
{
|
||||
"group": "Binary",
|
||||
"options": [
|
||||
"BIT",
|
||||
"BINARY",
|
||||
"VARBINARY",
|
||||
"TINYBLOB",
|
||||
"BLOB",
|
||||
"MEDIUMBLOB",
|
||||
"LONGBLOB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Geometry",
|
||||
"options": [
|
||||
"GEOMETRY",
|
||||
"POINT",
|
||||
"LINESTRING",
|
||||
"POLYGON",
|
||||
"MULTIPOINT",
|
||||
"MULTILINESTRING",
|
||||
"MULTIPOLYGON",
|
||||
"GEOMETRYCOLLECTION"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dataTypes": [
|
||||
"",
|
||||
"INT",
|
||||
"TINYINT",
|
||||
"SMALLINT",
|
||||
"MEDIUMINT",
|
||||
"BIGINT",
|
||||
"DECIMAL",
|
||||
"NUMERIC",
|
||||
"FLOAT",
|
||||
"DOUBLE",
|
||||
"CHAR",
|
||||
"VARCHAR",
|
||||
"TEXT",
|
||||
"ENUM",
|
||||
"SET",
|
||||
"BINARY",
|
||||
"VARBINARY",
|
||||
"BLOB",
|
||||
"DATE",
|
||||
"TIME",
|
||||
"DATETIME",
|
||||
"TIMESTAMP",
|
||||
"YEAR",
|
||||
"BOOL",
|
||||
"BOOLEAN",
|
||||
"JSON",
|
||||
"JSONB",
|
||||
"XML",
|
||||
"UUID",
|
||||
"GEOMETRY",
|
||||
"POINT",
|
||||
"LINESTRING",
|
||||
"POLYGON"
|
||||
],
|
||||
"tableField": [
|
||||
"name",
|
||||
"type",
|
||||
"length",
|
||||
"defaultValue",
|
||||
"nullable",
|
||||
"primaryKey",
|
||||
"actions"
|
||||
]
|
||||
}
|
81
server/api/devtool/orm/table/config/index.get.js
Normal file
81
server/api/devtool/orm/table/config/index.get.js
Normal file
@ -0,0 +1,81 @@
|
||||
import fileConfig from "./configuration.json";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// read configuration file if it exists and return error if it doesn't
|
||||
if (!fileConfig) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Configuration file not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Get all tables with primary key
|
||||
const tables = await getAllTableWithPK();
|
||||
if (!tables) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please check your database connection",
|
||||
};
|
||||
}
|
||||
|
||||
// Remove columnTypes [{"group": "Foreign Keys", "options": [{"label": "TABLE_NAME (COLUMN_NAME)", "value": "TABLE_NAME"}]}] from fileconfig before appending
|
||||
fileConfig.columnTypes = fileConfig.columnTypes.filter(
|
||||
(columnType) => columnType.group !== "Foreign Keys"
|
||||
);
|
||||
|
||||
// Append columnTypes from fileconfig with tables
|
||||
fileConfig.columnTypes.push({
|
||||
...tables,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Configuration file successfully loaded",
|
||||
data: fileConfig,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function getAllTableWithPK() {
|
||||
try {
|
||||
const tables = await prisma.$queryRaw` SELECT
|
||||
table_name,
|
||||
column_name
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND column_key = 'PRI'`;
|
||||
|
||||
if (!tables) return false;
|
||||
|
||||
// Reformat to {group: "table_name", options: [{label: "TABLE_NAME (COLUMN_NAME)", value: "TABLE_NAME"}]}
|
||||
const remapTables = tables.reduce((acc, table) => {
|
||||
const group = "Foreign Keys";
|
||||
const option = {
|
||||
label: `${table.TABLE_NAME} (${table.COLUMN_NAME})`,
|
||||
value: `[[${table.TABLE_NAME}]]`,
|
||||
};
|
||||
const existingGroup = acc.find((item) => item.group === group);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.options.push(option);
|
||||
} else {
|
||||
acc.push({ group, options: [option] });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return remapTables[0];
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
171
server/api/devtool/orm/table/create/index.post.js
Normal file
171
server/api/devtool/orm/table/create/index.post.js
Normal file
@ -0,0 +1,171 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName, tableSchema, autoIncrementColumn } =
|
||||
await readBody(event);
|
||||
|
||||
if (!tableName || !tableSchema) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
// Create Table
|
||||
const isTableCreated = await createTable(
|
||||
tableName,
|
||||
tableSchema,
|
||||
autoIncrementColumn
|
||||
);
|
||||
|
||||
if (isTableCreated.statusCode !== 200)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: isTableCreated.message,
|
||||
};
|
||||
|
||||
// Run Prisma Command
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table Created",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function createTable(tableName, tableSchema) {
|
||||
try {
|
||||
let rawSchema = ``;
|
||||
for (let i = 0; i < tableSchema.length; i++) {
|
||||
const column = tableSchema[i];
|
||||
|
||||
// Sanitize rawSchema
|
||||
if (column.type.includes("[[") && column.type.includes("]]")) {
|
||||
const FKTableName = column.type.replace("[[", "").replace("]]", "");
|
||||
const primaryKey = await prisma.$queryRawUnsafe(
|
||||
"SHOW COLUMNS from " + FKTableName + " where `Key` = 'PRI'"
|
||||
);
|
||||
|
||||
rawSchema += `${column.name} INT NOT NULL, FOREIGN KEY (${column.name}) REFERENCES ${FKTableName}(${primaryKey[0].Field})`;
|
||||
} else {
|
||||
rawSchema += `${column.name}
|
||||
${column.type}${column.length ? "(" + column.length + ")" : ""}
|
||||
${column.defaultValue ? " DEFAULT " + column.defaultValue : ""}
|
||||
${column.nullable ? " NULL" : " NOT NULL "}
|
||||
${column.primaryKey ? " PRIMARY KEY AUTO_INCREMENT" : ""}`;
|
||||
}
|
||||
|
||||
if (i < tableSchema.length - 1) rawSchema += ", ";
|
||||
}
|
||||
|
||||
const sqlStatement = `CREATE TABLE ${tableName} (${rawSchema})`;
|
||||
console.log(sqlStatement);
|
||||
|
||||
const createTable = await prisma.$queryRawUnsafe(sqlStatement);
|
||||
if (!createTable)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Table Creation Failed",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table Created",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
|
||||
// Get Message
|
||||
if (error.message.includes("already exists")) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: `Table '${tableName}' already exists!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.message.includes("1064")) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please ensure the SQL syntax is correct!",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Table Creation Failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function runPrismaCommand() {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
90
server/api/devtool/orm/table/delete/[table]/index.delete.js
Normal file
90
server/api/devtool/orm/table/delete/[table]/index.delete.js
Normal file
@ -0,0 +1,90 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const tableName = event.context.params.table;
|
||||
|
||||
try {
|
||||
// Drop the table
|
||||
await prisma.$executeRawUnsafe(`DROP TABLE IF EXISTS ${tableName}`);
|
||||
|
||||
// Run Prisma Command to update the schema
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed after table deletion",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: `Table '${tableName}' has been successfully deleted.`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting table:", error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: `Failed to delete table '${tableName}'. Error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runPrismaCommand() {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
113
server/api/devtool/orm/table/modify/get.get.js
Normal file
113
server/api/devtool/orm/table/modify/get.get.js
Normal file
@ -0,0 +1,113 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName } = getQuery(event);
|
||||
|
||||
if (!tableName) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Table name is required",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await prisma.$queryRaw`SELECT DATABASE() AS db_name`;
|
||||
// console.log(result[0].db_name);
|
||||
|
||||
if (result.length === 0) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please check your database connection",
|
||||
};
|
||||
}
|
||||
|
||||
let sqlRaw = ` SELECT
|
||||
c.COLUMN_NAME,
|
||||
c.DATA_TYPE,
|
||||
c.CHARACTER_MAXIMUM_LENGTH,
|
||||
c.COLUMN_DEFAULT,
|
||||
c.IS_NULLABLE,
|
||||
c.COLUMN_KEY,
|
||||
kcu.REFERENCED_TABLE_NAME,
|
||||
kcu.REFERENCED_COLUMN_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON
|
||||
c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND
|
||||
c.TABLE_NAME = kcu.TABLE_NAME AND
|
||||
c.COLUMN_NAME = kcu.COLUMN_NAME
|
||||
WHERE
|
||||
c.TABLE_SCHEMA = '${result[0].db_name}' AND
|
||||
c.TABLE_NAME = '${tableName}';`;
|
||||
|
||||
// console.log(sqlRaw);
|
||||
|
||||
const getTableDetails = await prisma.$queryRawUnsafe(sqlRaw);
|
||||
// console.log(getTableDetails);
|
||||
|
||||
/*
|
||||
[{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "PID",
|
||||
"nullable": "",
|
||||
"primaryKey": true,
|
||||
"type": "INT"
|
||||
},
|
||||
{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "Pproduct",
|
||||
"nullable": true,
|
||||
"primaryKey": "",
|
||||
"type": "VARCHAR"
|
||||
},
|
||||
{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "userID",
|
||||
"nullable": "",
|
||||
"primaryKey": "",
|
||||
"type": "[[user]]"
|
||||
}]
|
||||
*/
|
||||
|
||||
let tableDetailsData = [];
|
||||
|
||||
// Loop through the result and convert bigInt to number
|
||||
for (let i = 0; i < getTableDetails.length; i++) {
|
||||
const table = getTableDetails[i];
|
||||
|
||||
tableDetailsData.push({
|
||||
name: table.COLUMN_NAME,
|
||||
type: table.REFERENCED_TABLE_NAME
|
||||
? `[[${table.REFERENCED_TABLE_NAME}]]`
|
||||
: table.DATA_TYPE.toUpperCase(),
|
||||
length: bigIntToNumber(table.CHARACTER_MAXIMUM_LENGTH),
|
||||
defaultValue: table.COLUMN_DEFAULT,
|
||||
nullable: table.IS_NULLABLE === "YES",
|
||||
primaryKey: table.COLUMN_KEY === "PRI",
|
||||
actions: {},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
data: tableDetailsData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function bigIntToNumber(bigInt) {
|
||||
if (bigInt === null) return null;
|
||||
return Number(bigInt.toString());
|
||||
}
|
191
server/api/devtool/orm/table/modify/index.post.js
Normal file
191
server/api/devtool/orm/table/modify/index.post.js
Normal file
@ -0,0 +1,191 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName, tableSchema, autoIncrementColumn } =
|
||||
await readBody(event);
|
||||
|
||||
if (!tableName || !tableSchema) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
// Get existing table structure
|
||||
const existingColumns = await prisma.$queryRaw`
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${tableName}
|
||||
`;
|
||||
|
||||
// Compare and modify table structure
|
||||
for (const column of tableSchema) {
|
||||
const existingColumn = existingColumns.find(
|
||||
(c) => c.COLUMN_NAME === column.name
|
||||
);
|
||||
|
||||
if (existingColumn) {
|
||||
// Modify existing column
|
||||
await modifyColumn(tableName, column, existingColumn);
|
||||
} else {
|
||||
// Add new column
|
||||
await addColumn(tableName, column);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove columns that are not in the new schema
|
||||
for (const existingColumn of existingColumns) {
|
||||
if (!tableSchema.find((c) => c.name === existingColumn.COLUMN_NAME)) {
|
||||
await removeColumn(tableName, existingColumn.COLUMN_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Update auto-increment column if necessary
|
||||
if (autoIncrementColumn) {
|
||||
await updateAutoIncrement(tableName, autoIncrementColumn);
|
||||
}
|
||||
|
||||
// Run Prisma Command to update the schema
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table modified successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function modifyColumn(tableName, newColumn, existingColumn) {
|
||||
let alterStatement = `ALTER TABLE ${tableName} MODIFY COLUMN ${newColumn.name} ${newColumn.type}`;
|
||||
|
||||
if (newColumn.length) {
|
||||
alterStatement += `(${newColumn.length})`;
|
||||
}
|
||||
|
||||
alterStatement += newColumn.nullable ? " NULL" : " NOT NULL";
|
||||
|
||||
if (newColumn.defaultValue) {
|
||||
alterStatement += ` DEFAULT ${newColumn.defaultValue}`;
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(alterStatement);
|
||||
}
|
||||
|
||||
async function addColumn(tableName, column) {
|
||||
let alterStatement = `ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`;
|
||||
|
||||
if (column.length) {
|
||||
alterStatement += `(${column.length})`;
|
||||
}
|
||||
|
||||
alterStatement += column.nullable ? " NULL" : " NOT NULL";
|
||||
|
||||
if (column.defaultValue) {
|
||||
alterStatement += ` DEFAULT ${column.defaultValue}`;
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(alterStatement);
|
||||
}
|
||||
|
||||
async function removeColumn(tableName, columnName) {
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE ${tableName} DROP COLUMN ${columnName}`
|
||||
);
|
||||
}
|
||||
|
||||
async function updateAutoIncrement(tableName, autoIncrementColumn) {
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE ${tableName} MODIFY ${autoIncrementColumn} INT AUTO_INCREMENT`
|
||||
);
|
||||
}
|
||||
|
||||
async function runPrismaCommand(retries = 3) {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function spawnCommand(command, args, cwd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(command, args, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -3,7 +3,11 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
try {
|
||||
// Check if the role already exists
|
||||
const allRole = await prisma.role.findMany();
|
||||
const allRole = await prisma.role.findMany({
|
||||
where: {
|
||||
roleStatus: "ACTIVE",
|
||||
},
|
||||
});
|
||||
|
||||
const roleExist = allRole.find((role) => {
|
||||
return role?.roleName.toLowerCase() === body?.name.toLowerCase();
|
||||
@ -16,71 +20,63 @@ export default defineEventHandler(async (event) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (body.module == "user") {
|
||||
// add new role
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
roleName: body.name,
|
||||
roleDescription: body.description || "",
|
||||
roleStatus: "ACTIVE",
|
||||
roleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
// add new role
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
roleName: body.name,
|
||||
roleDescription: body.description || "",
|
||||
roleStatus: "ACTIVE",
|
||||
roleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (role) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully added!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} else if (body.module == "role") {
|
||||
// add new role
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
roleName: body.name,
|
||||
roleDescription: body.description || "",
|
||||
roleStatus: "ACTIVE",
|
||||
roleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
if (role) {
|
||||
// Add User to the role if users are provided
|
||||
if (body.users && Array.isArray(body.users)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.users.map(async (el) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (role) {
|
||||
// Add User to the role
|
||||
body.users.forEach(async (el) => {
|
||||
// Select user where username
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: role.roleID,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
if (!user) return;
|
||||
|
||||
// Add UserRole
|
||||
const userRole = await prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: role.roleID,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully added!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
data: {
|
||||
role,
|
||||
assignedUsers: validUserRoles.length,
|
||||
totalUsers: body.users.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully added!",
|
||||
data: { role },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
@ -14,39 +14,53 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
|
||||
if (role) {
|
||||
// Delete all user role
|
||||
const deleteUserRole = await prisma.userrole.deleteMany({
|
||||
// Delete all user roles for this role
|
||||
await prisma.userrole.deleteMany({
|
||||
where: {
|
||||
userRoleRoleID: body.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (deleteUserRole) {
|
||||
// Add User to the role
|
||||
body.users.forEach(async (el) => {
|
||||
// Select user where username
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
// Add User to the role if users are provided
|
||||
if (body.users && Array.isArray(body.users)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.users.map(async (el) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return;
|
||||
if (user) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: body.id,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
// Add UserRole
|
||||
const userRole = await prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: body.id,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully edited!",
|
||||
data: {
|
||||
role,
|
||||
assignedUsers: validUserRoles.length,
|
||||
totalUsers: body.users.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully edited!",
|
||||
data: { role },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
@ -8,7 +8,11 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
try {
|
||||
// Get user from database
|
||||
const allUser = await prisma.user.findMany();
|
||||
const allUser = await prisma.user.findMany({
|
||||
where: {
|
||||
userStatus: "ACTIVE",
|
||||
},
|
||||
});
|
||||
|
||||
// Check if the user already exists
|
||||
const userExist = allUser.find((user) => {
|
||||
@ -30,86 +34,67 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
);
|
||||
|
||||
// If role is not empty
|
||||
if (body.module == "user") {
|
||||
if (body.role.length == 0) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Please select at least one role",
|
||||
};
|
||||
}
|
||||
// Add New User
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
userSecretKey: secretKey,
|
||||
userUsername: body.username,
|
||||
userPassword: password,
|
||||
userFullName: body?.fullname || "",
|
||||
userEmail: body?.email || "",
|
||||
userPhone: body?.phone || "",
|
||||
userStatus: "ACTIVE",
|
||||
userCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
body.role.forEach((el) => {
|
||||
// Check if roleID is valid for each role
|
||||
if (!checkRoleID(el.value)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Role ID is not valid",
|
||||
};
|
||||
}
|
||||
});
|
||||
if (user) {
|
||||
// Add user roles if provided
|
||||
if (body.role && Array.isArray(body.role)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.role.map(async (role) => {
|
||||
const existingRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
roleID: role.value,
|
||||
},
|
||||
});
|
||||
|
||||
// Add New User
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
userSecretKey: secretKey,
|
||||
userUsername: body.username,
|
||||
userPassword: password,
|
||||
userFullName: body?.fullname || "",
|
||||
userEmail: body?.email || "",
|
||||
userPhone: body?.phone || "",
|
||||
userStatus: "ACTIVE",
|
||||
userCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
if (existingRole) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: role.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
if (user) {
|
||||
// Add user role
|
||||
body.role.forEach(async (el) => {
|
||||
const userRole = await prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: el.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User successfully added!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} else if (body.module == "role") {
|
||||
// Add New User
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
userSecretKey: secretKey,
|
||||
userUsername: body.username,
|
||||
userPassword: password,
|
||||
userFullName: body?.fullname || "",
|
||||
userEmail: body?.email || "",
|
||||
userPhone: body?.phone || "",
|
||||
userStatus: "ACTIVE",
|
||||
userCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User successfully added!",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
data: {
|
||||
user,
|
||||
assignedRoles: validUserRoles.length,
|
||||
totalRoles: body.role.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User successfully added!",
|
||||
data: { user },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
if (user.count > 0) {
|
||||
const getUserID = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: body.username,
|
||||
@ -24,34 +24,59 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
|
||||
if (getUserID) {
|
||||
// Delete all user role
|
||||
const userRole = await prisma.userrole.deleteMany({
|
||||
// Delete all user roles
|
||||
await prisma.userrole.deleteMany({
|
||||
where: {
|
||||
userRoleUserID: getUserID.userID,
|
||||
},
|
||||
});
|
||||
|
||||
if (userRole) {
|
||||
const userRoleList = body.role;
|
||||
// Add new user roles
|
||||
if (body.role && Array.isArray(body.role)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.role.map(async (role) => {
|
||||
const existingRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
roleID: role.value,
|
||||
},
|
||||
});
|
||||
|
||||
// Add new user role
|
||||
userRoleList.forEach(async (role) => {
|
||||
const userRole = await prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: getUserID.userID,
|
||||
userRoleRoleID: role.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
if (existingRole) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: getUserID.userID,
|
||||
userRoleRoleID: role.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User updated successfully",
|
||||
data: {
|
||||
assignedRoles: validUserRoles.length,
|
||||
totalRoles: body.role.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User updated successfully",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "User not found",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
|
Loading…
x
Reference in New Issue
Block a user