add latest devtools

This commit is contained in:
Md Afiq Iskandar 2024-11-15 19:00:12 +08:00
parent 924e424251
commit 10a3209021
53 changed files with 3707 additions and 5472 deletions

View File

@ -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"],
// },
// },
// },
];

View File

@ -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>

View File

@ -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>

View 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,
};

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
}

View File

@ -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) {

View File

@ -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,

View File

@ -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,
};
}
});

View File

@ -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,

View File

@ -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");
}

View File

@ -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",
};
});

View 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",
};
}
});

View 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",
};
}
});

View 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",
};
}
});

View 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"
]
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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());
}

View 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}`));
}
});
});
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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,