DONE: fix error on top-menu to for mobile view
@@ -15,4 +15,6 @@
|
||||
[ ] boleh print semua borang
|
||||
[ ] jana surat lepas lulus anggota
|
||||
|
||||
##
|
||||
## Present to Boss (2/7/2026)
|
||||
[ ] discuss logo baru MyKOPKB
|
||||
[ ] layout footer untuk surat rasmi
|
||||
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 204 KiB |
@@ -7,10 +7,16 @@ RUN npm install -g pnpm && \
|
||||
pnpm install --frozen-lockfile --ignore-scripts && \
|
||||
pnpm rebuild esbuild
|
||||
COPY fe/ ./
|
||||
ARG VITE_APP_NAME="MyKOPKB"
|
||||
ARG VITE_APP_VERSION="1.0"
|
||||
ARG VITE_API_BASE_URL=https://api.koppkb.com
|
||||
ARG VITE_APP_URL=https://anggota.koppkb.com
|
||||
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||
ENV VITE_APP_URL=$VITE_APP_URL
|
||||
ENV VITE_APP_NAME=$VITE_APP_NAME
|
||||
ENV VITE_APP_VERSION=$VITE_APP_VERSION
|
||||
|
||||
# RUN pnpm run typecheck # uncomment this to run typecheck
|
||||
RUN pnpm exec vite build --mode=production
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 902 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="311.899" height="291.707" viewBox="0 0 311.899 291.707">
|
||||
<g id="Icon_ionic-ios-apps" data-name="Icon ionic-ios-apps" transform="translate(-4.57 -4.492)">
|
||||
<path id="Path_9" data-name="Path 9" d="M305.678,64.318,184.327,9a65.721,65.721,0,0,0-47.842,0L15.22,64.318c-14.293,6.5-14.293,17.117,0,23.639l120.221,54.826a69.059,69.059,0,0,0,50.033,0L305.687,87.956C319.971,81.46,319.971,70.822,305.678,64.318Z" transform="translate(0.07)" fill="#fff" opacity="0.999"/>
|
||||
<g id="Group_1" data-name="Group 1" transform="translate(4.533 135.941)">
|
||||
<path id="Path_10" data-name="Path 10" d="M108.2,53.266,44,24a10.438,10.438,0,0,0-8.553,0L12.99,34.227c-11.32,5.145-11.32,13.556,0,18.721l95.21,43.42a54.692,54.692,0,0,0,39.625,0l95.2-43.42c11.32-5.148,11.32-13.556,0-18.721L220.582,24a10.438,10.438,0,0,0-8.553,0l-64.2,29.268a54.692,54.692,0,0,1-39.625,0Z" transform="translate(28.02 60.176)" fill="#fff" opacity="0.5"/>
|
||||
<path id="Path_11" data-name="Path 11" d="M274.692,27.406l-23.1-10.493a11.652,11.652,0,0,0-9.618,0l-78.68,35.625a64.317,64.317,0,0,1-37.812,0L46.815,16.914a11.652,11.652,0,0,0-9.618,0L14.108,27.407c-12.819,5.825-12.819,15.351,0,21.2L121.925,97.783a61.937,61.937,0,0,0,44.874,0L274.615,48.608C287.51,42.773,287.51,33.232,274.692,27.406Z" transform="translate(11.592 -15.875)" fill="#fff" opacity="0.75"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -1,11 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="280.056" height="304.433" viewBox="0 0 280.056 304.433">
|
||||
<g id="Group_2" data-name="Group 2" transform="translate(-104.693 -1.265)">
|
||||
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.104" height="33.821" rx="0.751" transform="matrix(0.788, -0.616, 0.616, 0.788, 250.1, 52.551)" fill="#3f3d56"/>
|
||||
<path id="Rectangle_80" data-name="Rectangle 80" d="M12.78,0H124.507a12.78,12.78,0,0,1,12.78,12.78V266.236a12.78,12.78,0,0,1-12.78,12.78H12.78A12.78,12.78,0,0,1,0,266.236V12.78A12.78,12.78,0,0,1,12.78,0Z" transform="matrix(0.788, -0.616, 0.616, 0.788, 104.693, 85.834)" fill="#475569"/>
|
||||
<path id="Path_176" data-name="Path 176" d="M109.443,0H93.695V1.894a8.976,8.976,0,0,1-8.981,8.976H34.63a8.976,8.976,0,0,1-8.981-8.976V0H10.9A10.9,10.9,0,0,0,0,10.9V253.6a10.9,10.9,0,0,0,10.9,10.9h98.54a10.9,10.9,0,0,0,10.9-10.9V10.922A10.9,10.9,0,0,0,109.443,0Z" transform="matrix(0.788, -0.616, 0.616, 0.788, 116.052, 86.183)" fill="#f1f5f9"/>
|
||||
<rect id="Rectangle_81" data-name="Rectangle 81" width="19.171" height="3.891" rx="1.269" transform="matrix(0.788, -0.616, 0.616, 0.788, 153.48, 58.292)" fill="#e6e8ec"/>
|
||||
<circle id="Ellipse_5" data-name="Ellipse 5" cx="2.209" cy="2.209" r="2.209" transform="matrix(0.788, -0.616, 0.616, 0.788, 170.71, 44.496)" fill="#e6e8ec"/>
|
||||
<circle id="Ellipse_11" data-name="Ellipse 11" cx="25.971" cy="25.971" r="25.971" transform="matrix(0.788, -0.616, 0.616, 0.788, 210.99, 151.214)" fill="#fff"/>
|
||||
<path id="Path_222" data-name="Path 222" d="M69.422,34.712A34.711,34.711,0,1,1,34.711,0,34.711,34.711,0,0,1,69.422,34.712ZM30.7,53.085,56.448,27.337a2.237,2.237,0,0,0,0-3.168L53.28,21.024a2.233,2.233,0,0,0-3.164,0l-21,21L19.31,32.2a2.246,2.246,0,0,0-3.168,0l-3.168,3.187a2.247,2.247,0,0,0,0,3.168L27.532,53.1a2.237,2.237,0,0,0,3.168,0Z" transform="matrix(0.788, -0.616, 0.616, 0.788, 196.054, 147.189)" fill="#94a3b8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,32 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="251.014" height="317.588" viewBox="0 0 251.014 317.588">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" x1="0.505" y1="1.007" x2="0.505" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0" stop-color="gray" stop-opacity="0.251"/>
|
||||
<stop offset="0.54" stop-color="gray" stop-opacity="0.122"/>
|
||||
<stop offset="1" stop-color="gray" stop-opacity="0.102"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Group_1" data-name="Group 1" transform="translate(-125.954 18.154)">
|
||||
<rect id="Rectangle_77" data-name="Rectangle 77" width="3.785" height="18.307" rx="1.276" transform="translate(287.945 23.197) rotate(-11)" fill="#3f3d56"/>
|
||||
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.265" height="31.863" rx="1.276" transform="translate(294.207 56.13) rotate(-11)" fill="#3f3d56"/>
|
||||
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.059" height="32.137" rx="1.276" transform="translate(302.481 98.339) rotate(-11)" fill="#3f3d56"/>
|
||||
<path id="Path_190" data-name="Path 190" d="M0,0A.7.7,0,0,0,.83.107,2.943,2.943,0,0,1,0,0Z" transform="translate(269.32 191.355) rotate(-11)" fill="url(#linear-gradient)"/>
|
||||
<path id="Path_192" data-name="Path 192" d="M43.471,0l-.819,4.463L42.02,7.877l-1.15,6.245L29.721,21.927s-1-.86-2.663-2.216C22.489,15.948,12.885,8.288,5.834,4.175A23.753,23.753,0,0,0,0,1.487Z" transform="translate(259.796 205.712) rotate(-11)" fill="#f8d4d4"/>
|
||||
<path id="Path_194" data-name="Path 194" d="M44.318,12.282s13,2.231,13.761-3.32S51.388.41,45.072.037.849,2.268.849,2.268s-6.686,8.918,17.462,10.41Z" transform="translate(249.226 198.636) rotate(-11)" fill="#1e293b"/>
|
||||
<rect id="Rectangle_82" data-name="Rectangle 82" width="68.005" height="80.639" transform="translate(221.269 29.959) rotate(-11)" fill="#334155"/>
|
||||
<path id="Path_198" data-name="Path 198" d="M25.9,6.7a10.913,10.913,0,0,0-4.142,2.932C14.9,16.42,5.088,32.343,5.088,32.343-7.462,17.39,6.426,5.582,12.345,1.523,13.785.541,14.751,0,14.751,0Z" transform="translate(142.668 179.037) rotate(-11)" fill="#f8d4d4"/>
|
||||
<path id="Path_199" data-name="Path 199" d="M36.043,0s3.346,23.41,14.865,28.984S36.043,44.976,36.043,44.976L0,31.588S20.44,24.9,16.35,5.2Z" transform="translate(236.89 52.85) rotate(-11)" fill="#f8d4d4"/>
|
||||
<path id="Path_200" data-name="Path 200" d="M43.508,0l-.815,4.448L42.03,7.875,40.88,14.12l-11.149,7.8s-1-.853-2.663-2.208C22.5,15.947,12.9,8.286,5.834,4.174A23.6,23.6,0,0,0,0,1.486Z" transform="translate(218.498 199.899) rotate(-11)" fill="#f8d4d4"/>
|
||||
<path id="Path_202" data-name="Path 202" d="M43.03,14.445S54.7,14.527,55.4,8.96,49.041.408,43.03.035.5,2.439.5,2.439s-5.181,10.515,17.79,12Z" transform="translate(207.79 192.853) rotate(-11)" fill="#334155"/>
|
||||
<path id="Path_204" data-name="Path 204" d="M89.962,101.836c-.51-.251-1.021-.51-1.523-.761l-1.371-1.729-17.98-22.8v.4c-.122,3.808-1.416,33.98-11.514,36.767-10.776,2.97-23.775,3.716-29.7-16.35S11.119,33.446,0,30.842L16.54,17.279s10.951,12.809,15.413,25.07c-.167-.761-7.73-36.1-1.523-42.349H84.694s8.377,23.859,14.119,35.678c4.47,9.177,11.179,30.873,15.5,45.555C118.588,95.728,103.563,108.53,89.962,101.836Z" transform="translate(242.03 186.268) rotate(-11)" fill="#475569"/>
|
||||
<path id="Path_205" data-name="Path 205" d="M60.518,52.029C52.141,56.986,15.366,54.965,3.4,54.161L.14,53.925c-.35,0,.053-.2.053-.2l.335-3.374L9.449,27.69,57.2,0c-.968,2.917-1.287,7.38-1.211,12.489C56.208,28.908,60.518,52.029,60.518,52.029Z" transform="translate(260.759 136.409) rotate(-11)" opacity="0.1"/>
|
||||
<path id="Path_206" data-name="Path 206" d="M60.916,52.005C52.539,56.963,15.764,54.941,3.8,54.138L.54,53.909,0,53.863l.594-.152L3.53,52.95,9.843,27.682,57.591,0C56.624,2.917,56.3,7.38,56.38,12.489,56.6,28.893,60.916,52.005,60.916,52.005Z" transform="translate(260.16 135.409) rotate(-11)" fill="#cbd5e1"/>
|
||||
<path id="Path_208" data-name="Path 208" d="M2.406,0,13.555,6.686A10.914,10.914,0,0,0,9.413,9.618C8.484,6.907,2.653,3.122,0,1.508,1.439.541,2.406,0,2.406,0Z" transform="translate(154.789 176.698) rotate(-11)" opacity="0.1"/>
|
||||
<path id="Path_209" data-name="Path 209" d="M.761.107A.662.662,0,0,1,0,0,2.642,2.642,0,0,0,.761.107Z" transform="translate(268.311 189.076) rotate(-11)" opacity="0.1"/>
|
||||
<path id="Path_210" data-name="Path 210" d="M135.295,56.833s-10.037,1.112-17.1-6.321c0,0-29.7,8.552-39.76,27.872S0,135.614,0,135.614s11.9,6.686,10.776,10.029L55.744,119.63s15.992-11.514,20.432-14.119,28.992-18.581,34.193,24.895,0,38.275,0,38.275,7.433,2.6,18.208-18.208,44.215-25.641,44.215-25.641l1.858-21.917s6.633-14.53,1.523-20.561a12.946,12.946,0,0,1-2.978-6.29c-.845-5.567-.313-12.841,11.521-20.548,18.208-11.9,37.109-18.43,29.657-27.4s-55.228-42.823-55.228-42.823L148.124,2.658S192,26.711,191.085,30.723c-7.252,5.121-31.631,15.312-45.785,14.961C145.3,45.684,147.928,56.46,135.295,56.833Z" transform="translate(128.76 43.985) rotate(-11)" fill="#94a3b8"/>
|
||||
<path id="Path_211" data-name="Path 211" d="M22.2,9.081A72.58,72.58,0,0,1,20.143,0L.449,5.2A21.591,21.591,0,0,1,0,16.565,21.179,21.179,0,0,0,22.2,9.078Z" transform="translate(252.344 50.975) rotate(-11)" opacity="0.1"/>
|
||||
<circle id="Ellipse_10" data-name="Ellipse 10" cx="21.179" cy="21.179" r="21.179" transform="translate(232.292 28.197) rotate(-11)" fill="#f8d4d4"/>
|
||||
<path id="Path_213" data-name="Path 213" d="M0,0S12.261,24.156,8.544,31.581" transform="translate(312.28 207.976) rotate(-11)" opacity="0.1"/>
|
||||
<path id="Path_221" data-name="Path 221" d="M57.637.16C63.515.83,69.1,3.35,74.886,4.729a8.56,8.56,0,0,0,4.463.167c2.246-.693,4.074-3.046,6.4-2.978,1.434.046,2.7,1.021,3.884,1.957a47.776,47.776,0,0,1,5.331,4.752c3.99,4.333,6.465,10.167,10.1,14.881S119.433,31.9,124.6,29.733c-3.513,13.712-20.022,24.138-32.488,24.991-4.973.341-10.068-.693-14.881.761a12.894,12.894,0,0,1-3.755.914c-2.772,0-5-2.543-6.732-5.018C62.7,45.563,59.494,38.96,54.932,33.652c-1.523-1.759-3.282-3.427-5.414-3.746s-4.089.7-5.985,1.683c-3.13,1.622-6.328,3.3-8.757,6.092S30.815,44.6,31.82,48.343c.541,2.026,1.775,3.854,1.8,5.97.046,3.381-2.936,5.849-3.526,9.138a34.266,34.266,0,0,0-.145,4.158c-.259,4.506-3.209,8.193-6.282,11.034-1.2,1.1-2.526,2.285-2.879,3.983-.236,1.165,0,2.39-.152,3.572-.449,2.924-3.366,4.409-5.948,4.79A17.157,17.157,0,0,1,5.763,89.93c3.61-1.707,5.834-6.564,5-10.913-1.15-6.032-7.212-10.365-6.691-16.5a19.119,19.119,0,0,1,1.676-5.429A49.745,49.745,0,0,0,8.6,46.8a17.334,17.334,0,0,0-.175-8.2c-.761-2.361-2.353-4.241-3.7-6.237A35.466,35.466,0,0,1,.441,23.578a6.177,6.177,0,0,1-.41-2.909A6,6,0,0,1,1.9,17.661c3.313-3.482,6.854-7.1,11.294-8.1A25.42,25.42,0,0,1,19.6,9.322,75.68,75.68,0,0,0,30.741,8.85c3.046-.358,8.193-.312,10.776-2.285,2.406-1.844,3.762-4.158,6.724-5.331A19.8,19.8,0,0,1,57.641.16Z" transform="translate(202.236 5.622) rotate(-11)" fill="#475569"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -19,6 +19,7 @@ import { useRoleSwitcher } from '@/composables/useRoleSwitcher'
|
||||
interface Props {
|
||||
class?: string
|
||||
boxClass?: string
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -61,7 +62,9 @@ const handleLogout = async (event: MouseEvent) => {
|
||||
|
||||
<template>
|
||||
<div v-bind="$attrs" :class="[
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0 group-hover/profile:visible group-hover/profile:opacity-100 group-hover/profile:scale-100 group-hover/profile:delay-200',
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0',
|
||||
'xl:group-hover/profile:visible xl:group-hover/profile:opacity-100 xl:group-hover/profile:scale-100 xl:group-hover/profile:delay-200',
|
||||
props.open && 'max-xl:visible max-xl:opacity-100 max-xl:scale-100',
|
||||
props.class,
|
||||
]">
|
||||
<Box
|
||||
|
||||
@@ -10,12 +10,19 @@ interface Props {
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const authStore = useAuthStore()
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
if (!window.matchMedia('(max-width: 1279px)').matches) return
|
||||
event.stopPropagation()
|
||||
open.value = !open.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['side-menu__account group/profile transition-[width]', props.class]">
|
||||
<div :class="['flex cursor-pointer items-center transition', props.innerClass]">
|
||||
<div :class="['flex cursor-pointer items-center transition', props.innerClass]" @click="handleClick">
|
||||
<div :class="[
|
||||
'relative flex-none overflow-hidden rounded-full border-4',
|
||||
props.avatarClass ?? 'h-10 w-10 border-background/20 dark:border-foreground/20',
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
interface Props {
|
||||
class?: string
|
||||
boxClass?: string
|
||||
open?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -74,7 +75,9 @@ async function handleMarkAllAsRead(event: MouseEvent) {
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
:class="[
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0 group-hover/notifications:visible group-hover/notifications:opacity-100 group-hover/notifications:scale-100 group-hover/notifications:delay-200',
|
||||
'invisible opacity-0 scale-95 transition-all duration-200 delay-0',
|
||||
'xl:group-hover/notifications:visible xl:group-hover/notifications:opacity-100 xl:group-hover/notifications:scale-100 xl:group-hover/notifications:delay-200',
|
||||
props.open && 'max-xl:visible max-xl:opacity-100 max-xl:scale-100',
|
||||
props.class,
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -118,7 +118,7 @@ const markersData = {
|
||||
</SectionContent>
|
||||
<PreviewCode title="components/ui/map/Map.vue">
|
||||
{{`
|
||||
<script setup lang="ts">
|
||||
<${""}script setup lang="ts">
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import maplibregl, { type MapOptions } from "maplibre-gl";
|
||||
import { ref, onMounted } from "vue";
|
||||
@@ -318,7 +318,7 @@ const toggleFullscreen = () => {
|
||||
}
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
};
|
||||
</script>
|
||||
</${""}script>
|
||||
|
||||
<template>
|
||||
<div data-scope="map" data-part="root" ref="mapRef" :class="cn(map, className)">
|
||||
|
||||
@@ -141,11 +141,6 @@ const mainMenu: (string | Menu)[] = [
|
||||
route_name: 'crud-data-list',
|
||||
title: 'Data List',
|
||||
},
|
||||
{
|
||||
icon: 'CircleGauge',
|
||||
route_name: 'crud-form',
|
||||
title: 'Form',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -268,12 +263,6 @@ const mainMenu: (string | Menu)[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...authMenu,
|
||||
{
|
||||
icon: 'CircleGauge',
|
||||
route_name: 'error-page',
|
||||
title: 'Error Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
'UI COMPONENTS',
|
||||
|
||||
@@ -513,7 +513,7 @@ function stepLabelClass(stepId: number) {
|
||||
<div
|
||||
class="before:bg-foreground/10 relative flex flex-col justify-center px-5 before:absolute before:bottom-0 before:top-0 before:mt-6 before:hidden before:h-0.5 before:w-[69%] sm:px-10 lg:flex-row before:lg:block">
|
||||
<div v-for="step in steps" :key="step.id" class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
||||
<Button :class="stepButtonClass(step.id)" :variant="step.id === currentStep ? 'default' : 'ghost'">
|
||||
<Button :class="stepButtonClass(step.id)" :variant="step.id === currentStep ? 'primary' : 'ghost'">
|
||||
{{ step.id }}
|
||||
</Button>
|
||||
<div :class="stepLabelClass(step.id)">
|
||||
@@ -810,7 +810,7 @@ function stepLabelClass(stepId: number) {
|
||||
<Input id="employer_letter" type="file" accept=".pdf,.jpg,.jpeg,.png"
|
||||
@change="handleFileChange('employer_letter', $event)" />
|
||||
<FieldError v-if="fieldErrors['documents.employer_letter']">{{ fieldErrors['documents.employer_letter']
|
||||
}}</FieldError>
|
||||
}}</FieldError>
|
||||
</Field>
|
||||
|
||||
<div class="col-span-12 mt-2 rounded-lg border border-foreground/10 bg-foreground/5 p-4">
|
||||
|
||||
@@ -27,6 +27,12 @@ import {
|
||||
submitBoardReview,
|
||||
submitManagementReview,
|
||||
} from '../services/membership-application.service'
|
||||
import {
|
||||
reviewDecisionBadgeLook,
|
||||
reviewDecisionBadgeVariant,
|
||||
statusBadgeLook,
|
||||
statusBadgeVariant,
|
||||
} from '../utils/membership-application-badge.utils'
|
||||
import type {
|
||||
BoardReviewDecision,
|
||||
ManagementReviewDecision,
|
||||
@@ -90,13 +96,6 @@ function statusLabel(status: MembershipApplicationStatus): string {
|
||||
return labels[status] ?? status
|
||||
}
|
||||
|
||||
function statusBadgeVariant(status: MembershipApplicationStatus) {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'MANAGEMENT_REJECTED') return 'danger'
|
||||
if (status === 'PENDING_BOARD' || status === 'PENDING_NOTIFICATION') return 'pending'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function getWorkflowProgress(status: MembershipApplicationStatus) {
|
||||
switch (status) {
|
||||
case 'SUBMITTED':
|
||||
@@ -307,20 +306,6 @@ function reviewDecisionLabel(decision: string | null, stage: string): string {
|
||||
return decision
|
||||
}
|
||||
|
||||
function reviewDecisionBadgeVariant(decision: string | null, stage: string) {
|
||||
if (stage === 'MANAGEMENT') {
|
||||
if (decision === 'APPROVED') return 'success'
|
||||
if (decision === 'REJECTED') return 'danger'
|
||||
}
|
||||
|
||||
if (stage === 'BOARD') {
|
||||
if (decision === 'PASS') return 'success'
|
||||
if (decision === 'FAIL') return 'danger'
|
||||
}
|
||||
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function reviewTimelineDotClass(review: MembershipApplicationReviewDetail): string {
|
||||
const variant = reviewDecisionBadgeVariant(review.decision, review.stage)
|
||||
|
||||
@@ -500,7 +485,8 @@ onUnmounted(() => {
|
||||
<div class="text-xl font-semibold">{{ application.application_number }}</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge :variant="statusBadgeVariant(application.status)" class="whitespace-nowrap">
|
||||
<Badge :variant="statusBadgeVariant(application.status)" :look="statusBadgeLook(application.status)"
|
||||
class="whitespace-nowrap">
|
||||
{{ statusLabel(application.status) }}
|
||||
</Badge>
|
||||
<Badge v-if="application.board_result"
|
||||
@@ -539,7 +525,7 @@ onUnmounted(() => {
|
||||
<div v-for="step in WORKFLOW_STEPS" :key="step.id"
|
||||
class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
||||
<Button type="button" :class="workflowStepButtonClass(step.id)"
|
||||
:variant="step.id === workflowProgress?.currentStep && application.status !== 'COMPLETED' && !workflowProgress?.failed ? 'default' : 'ghost'"
|
||||
:variant="step.id === workflowProgress?.currentStep && application.status !== 'COMPLETED' && !workflowProgress?.failed ? 'primary' : 'ghost'"
|
||||
disabled>
|
||||
{{ step.id }}
|
||||
</Button>
|
||||
@@ -840,7 +826,11 @@ onUnmounted(() => {
|
||||
{{ formatDateTime(review.reviewed_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<Badge :variant="reviewDecisionBadgeVariant(review.decision, review.stage)" class="whitespace-nowrap">
|
||||
<Badge
|
||||
:variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
|
||||
:look="reviewDecisionBadgeLook(review.decision, review.stage)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
{{ reviewDecisionLabel(review.decision, review.stage) }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,12 @@ import {
|
||||
updateMembershipApplication,
|
||||
uploadMembershipApplicationDocument,
|
||||
} from '../services/membership-application.service'
|
||||
import {
|
||||
reviewDecisionBadgeLook,
|
||||
reviewDecisionBadgeVariant,
|
||||
statusBadgeLook,
|
||||
statusBadgeVariant,
|
||||
} from '../utils/membership-application-badge.utils'
|
||||
import type {
|
||||
DocumentUploadType,
|
||||
MembershipApplicationDetail,
|
||||
@@ -166,13 +172,6 @@ function statusLabel(status: MembershipApplicationStatus): string {
|
||||
return labels[status] ?? status
|
||||
}
|
||||
|
||||
function statusBadgeVariant(status: MembershipApplicationStatus) {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'MANAGEMENT_REJECTED') return 'danger'
|
||||
if (status === 'PENDING_BOARD' || status === 'PENDING_NOTIFICATION') return 'pending'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function workflowStepButtonClass(stepId: number) {
|
||||
const progress = workflowProgress.value
|
||||
if (!progress) return 'mx-2 size-12 rounded-full shadow-none bg-background border border-foreground/15'
|
||||
@@ -268,20 +267,6 @@ function reviewDecisionLabel(decision: string | null, stage: string): string {
|
||||
return decision
|
||||
}
|
||||
|
||||
function reviewDecisionBadgeVariant(decision: string | null, stage: string) {
|
||||
if (stage === 'MANAGEMENT') {
|
||||
if (decision === 'APPROVED') return 'success'
|
||||
if (decision === 'REJECTED') return 'danger'
|
||||
}
|
||||
|
||||
if (stage === 'BOARD') {
|
||||
if (decision === 'PASS') return 'success'
|
||||
if (decision === 'FAIL') return 'danger'
|
||||
}
|
||||
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function reviewTimelineDotClass(review: MembershipApplicationReviewDetail): string {
|
||||
const variant = reviewDecisionBadgeVariant(review.decision, review.stage)
|
||||
|
||||
@@ -543,12 +528,8 @@ onUnmounted(() => {
|
||||
{{ application.application_number }} · {{ application.applicant?.name ?? '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
look="outline"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
@click="router.push({ name: 'view-membership-application', params: { id: applicationId } })"
|
||||
>
|
||||
<Button look="outline" variant="secondary" type="button"
|
||||
@click="router.push({ name: 'view-membership-application', params: { id: applicationId } })">
|
||||
Lihat
|
||||
</Button>
|
||||
<Button look="outline" variant="secondary" type="button"
|
||||
@@ -581,7 +562,8 @@ onUnmounted(() => {
|
||||
<div class="text-xl font-semibold">{{ application.application_number }}</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge :variant="statusBadgeVariant(application.status)" class="whitespace-nowrap">
|
||||
<Badge :variant="statusBadgeVariant(application.status)" :look="statusBadgeLook(application.status)"
|
||||
class="whitespace-nowrap">
|
||||
{{ statusLabel(application.status) }}
|
||||
</Badge>
|
||||
<Badge v-if="application.board_result"
|
||||
@@ -620,7 +602,7 @@ onUnmounted(() => {
|
||||
<div v-for="step in WORKFLOW_STEPS" :key="step.id"
|
||||
class="z-10 flex flex-1 items-center lg:block lg:text-center">
|
||||
<Button type="button" :class="workflowStepButtonClass(step.id)"
|
||||
:variant="step.id === workflowProgress?.currentStep && application.status !== 'COMPLETED' && !workflowProgress?.failed ? 'default' : 'ghost'"
|
||||
:variant="step.id === workflowProgress?.currentStep && application.status !== 'COMPLETED' && !workflowProgress?.failed ? 'primary' : 'ghost'"
|
||||
disabled>
|
||||
{{ step.id }}
|
||||
</Button>
|
||||
@@ -635,29 +617,39 @@ onUnmounted(() => {
|
||||
<Box raised="single" class="w-full p-0">
|
||||
<div class="w-full px-5 py-4">
|
||||
<TabsList class="mb-0 flex w-full">
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="personal">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="personal">
|
||||
Maklumat Peribadi
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="contact">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="contact">
|
||||
Hubungan & Alamat
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="employment">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="employment">
|
||||
Pekerjaan & Caruman
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="heirs">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="heirs">
|
||||
Waris
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="references">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="references">
|
||||
Pencadang & Penyokong
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm" value="documents">
|
||||
<TabsTrigger
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="documents">
|
||||
Dokumen
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
v-if="application.reviews.length"
|
||||
<TabsTrigger v-if="application.reviews.length"
|
||||
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
|
||||
value="reviews"
|
||||
>
|
||||
value="reviews">
|
||||
Sejarah Semakan
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -679,28 +671,25 @@ onUnmounted(() => {
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="ic_number">No. Kad Pengenalan</FieldLabel>
|
||||
<Input id="ic_number" v-model="form.applicant.ic_number" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.ic_number']">{{ fieldErrors['applicant.ic_number'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.ic_number']">{{ fieldErrors['applicant.ic_number'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="birth_date">Tarikh Lahir</FieldLabel>
|
||||
<Input id="birth_date" v-model="form.applicant.birth_date" type="date" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.birth_date']">{{ fieldErrors['applicant.birth_date'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.birth_date']">{{ fieldErrors['applicant.birth_date'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="birth_place">Tempat Lahir</FieldLabel>
|
||||
<Input id="birth_place" v-model="form.applicant.birth_place" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.birth_place']">{{ fieldErrors['applicant.birth_place'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.birth_place']">{{ fieldErrors['applicant.birth_place'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel>Jantina</FieldLabel>
|
||||
<SelectRoot
|
||||
:key="`gender-${form.applicant.gender}`"
|
||||
class="w-full"
|
||||
:collection="genderCollection"
|
||||
:default-value="genderInitial"
|
||||
:disabled="!canEdit"
|
||||
@value-change="setGenderValue"
|
||||
>
|
||||
<SelectRoot :key="`gender-${form.applicant.gender}`" class="w-full" :collection="genderCollection"
|
||||
:default-value="genderInitial" :disabled="!canEdit" @value-change="setGenderValue">
|
||||
<SelectControl>
|
||||
<SelectTrigger :aria-invalid="!!fieldErrors['applicant.gender']">
|
||||
<SelectValueText placeholder="Pilih jantina" />
|
||||
@@ -719,14 +708,9 @@ onUnmounted(() => {
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel>Status Perkahwinan</FieldLabel>
|
||||
<SelectRoot
|
||||
:key="`marriage-${form.applicant.marriage_status}`"
|
||||
class="w-full"
|
||||
:collection="marriageStatusCollection"
|
||||
:default-value="marriageStatusInitial"
|
||||
:disabled="!canEdit"
|
||||
@value-change="setMarriageStatusValue"
|
||||
>
|
||||
<SelectRoot :key="`marriage-${form.applicant.marriage_status}`" class="w-full"
|
||||
:collection="marriageStatusCollection" :default-value="marriageStatusInitial" :disabled="!canEdit"
|
||||
@value-change="setMarriageStatusValue">
|
||||
<SelectControl>
|
||||
<SelectTrigger :aria-invalid="!!fieldErrors['applicant.marriage_status']">
|
||||
<SelectValueText placeholder="Pilih status" />
|
||||
@@ -741,7 +725,8 @@ onUnmounted(() => {
|
||||
</SelectItemGroup>
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
<FieldError v-if="fieldErrors['applicant.marriage_status']">{{ fieldErrors['applicant.marriage_status'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.marriage_status']">{{ fieldErrors['applicant.marriage_status'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -756,12 +741,14 @@ onUnmounted(() => {
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="phone_number">No. Telefon</FieldLabel>
|
||||
<Input id="phone_number" v-model="form.applicant.phone_number" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.phone_number']">{{ fieldErrors['applicant.phone_number'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.phone_number']">{{ fieldErrors['applicant.phone_number'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="office_number">No. Pejabat</FieldLabel>
|
||||
<Input id="office_number" v-model="form.applicant.office_number" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.office_number']">{{ fieldErrors['applicant.office_number'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.office_number']">{{ fieldErrors['applicant.office_number'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="postcode">Poskod</FieldLabel>
|
||||
@@ -776,51 +763,51 @@ onUnmounted(() => {
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="employer_name">Nama Majikan</FieldLabel>
|
||||
<Input id="employer_name" v-model="form.applicant.employer_name" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.employer_name']">{{ fieldErrors['applicant.employer_name'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.employer_name']">{{ fieldErrors['applicant.employer_name'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="current_position">Jawatan Semasa</FieldLabel>
|
||||
<Input id="current_position" v-model="form.applicant.current_position" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.current_position']">{{ fieldErrors['applicant.current_position'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.current_position']">{{ fieldErrors['applicant.current_position']
|
||||
}}</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12">
|
||||
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
|
||||
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address']
|
||||
}}</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="start_work_date">Tarikh Mula Berkhidmat</FieldLabel>
|
||||
<Input id="start_work_date" v-model="form.applicant.start_work_date" type="date" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.start_work_date']">{{ fieldErrors['applicant.start_work_date'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.start_work_date']">{{ fieldErrors['applicant.start_work_date'] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="stock_monthly_contribution">Caruman Saham (RM)</FieldLabel>
|
||||
<Input id="stock_monthly_contribution" v-model="form.applicant.stock_monthly_contribution" type="number"
|
||||
min="0" step="0.01" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.stock_monthly_contribution']">{{ fieldErrors['applicant.stock_monthly_contribution'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.stock_monthly_contribution']">{{
|
||||
fieldErrors['applicant.stock_monthly_contribution'] }}</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-4">
|
||||
<FieldLabel for="fee_monthly_contribution">Caruman Yuran (RM)</FieldLabel>
|
||||
<Input id="fee_monthly_contribution" v-model="form.applicant.fee_monthly_contribution" type="number"
|
||||
min="0" step="0.01" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors['applicant.fee_monthly_contribution']">{{ fieldErrors['applicant.fee_monthly_contribution'] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors['applicant.fee_monthly_contribution']">{{
|
||||
fieldErrors['applicant.fee_monthly_contribution'] }}</FieldError>
|
||||
</Field>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="heirs" class="mt-6">
|
||||
<div class="space-y-4">
|
||||
<div v-for="(heir, index) in form.heirs" :key="index"
|
||||
class="rounded-lg border border-foreground/10 p-4">
|
||||
<div v-for="(heir, index) in form.heirs" :key="index" class="rounded-lg border border-foreground/10 p-4">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="font-medium">Waris {{ index + 1 }}</div>
|
||||
<Button
|
||||
v-if="canEdit && form.heirs.length > 1"
|
||||
type="button"
|
||||
look="outline"
|
||||
size="sm"
|
||||
@click="removeHeir(index)"
|
||||
>
|
||||
<Button v-if="canEdit && form.heirs.length > 1" type="button" look="outline" size="sm"
|
||||
@click="removeHeir(index)">
|
||||
Buang
|
||||
</Button>
|
||||
</div>
|
||||
@@ -828,23 +815,21 @@ onUnmounted(() => {
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel :for="`heir-name-${index}`">Nama</FieldLabel>
|
||||
<Input :id="`heir-name-${index}`" v-model="heir.name" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.name`]">{{ fieldErrors[`heirs.${index}.name`] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.name`]">{{ fieldErrors[`heirs.${index}.name`] }}
|
||||
</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel :for="`heir-ic-${index}`">No. Kad Pengenalan</FieldLabel>
|
||||
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">{{ fieldErrors[`heirs.${index}.ic_number`] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">{{ fieldErrors[`heirs.${index}.ic_number`]
|
||||
}}</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel>Hubungan</FieldLabel>
|
||||
<SelectRoot
|
||||
:key="`heir-relationship-${index}-${heir.relationship}`"
|
||||
class="w-full"
|
||||
<SelectRoot :key="`heir-relationship-${index}-${heir.relationship}`" class="w-full"
|
||||
:collection="relationshipCollection"
|
||||
:default-value="apiValueToLabel(RELATIONSHIP_OPTIONS, heir.relationship)"
|
||||
:disabled="!canEdit"
|
||||
@value-change="(details) => setHeirRelationshipValue(index, details)"
|
||||
>
|
||||
:default-value="apiValueToLabel(RELATIONSHIP_OPTIONS, heir.relationship)" :disabled="!canEdit"
|
||||
@value-change="(details) => setHeirRelationshipValue(index, details)">
|
||||
<SelectControl>
|
||||
<SelectTrigger :aria-invalid="!!fieldErrors[`heirs.${index}.relationship`]">
|
||||
<SelectValueText placeholder="Pilih hubungan" />
|
||||
@@ -859,12 +844,14 @@ onUnmounted(() => {
|
||||
</SelectItemGroup>
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.relationship`]">{{ fieldErrors[`heirs.${index}.relationship`] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.relationship`]">{{
|
||||
fieldErrors[`heirs.${index}.relationship`] }}</FieldError>
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel :for="`heir-phone-${index}`">No. Telefon</FieldLabel>
|
||||
<Input :id="`heir-phone-${index}`" v-model="heir.phone_number" type="text" :disabled="!canEdit" />
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.phone_number`]">{{ fieldErrors[`heirs.${index}.phone_number`] }}</FieldError>
|
||||
<FieldError v-if="fieldErrors[`heirs.${index}.phone_number`]">{{
|
||||
fieldErrors[`heirs.${index}.phone_number`] }}</FieldError>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,15 +869,9 @@ onUnmounted(() => {
|
||||
<div class="grid grid-cols-12 gap-4 gap-y-5">
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="proposer_ic_number">No. KP Pencadang</FieldLabel>
|
||||
<Input
|
||||
id="proposer_ic_number"
|
||||
v-model="form.references.proposer.ic_number"
|
||||
type="text"
|
||||
placeholder="Contoh: 900101011234"
|
||||
:disabled="!canEdit || referenceLookupLoading.proposer"
|
||||
@input="handleReferenceIcInput('proposer')"
|
||||
@blur="lookupReference('proposer')"
|
||||
/>
|
||||
<Input id="proposer_ic_number" v-model="form.references.proposer.ic_number" type="text"
|
||||
placeholder="Contoh: 900101011234" :disabled="!canEdit || referenceLookupLoading.proposer"
|
||||
@input="handleReferenceIcInput('proposer')" @blur="lookupReference('proposer')" />
|
||||
<p v-if="form.references.proposer.name" class="mt-1 text-sm text-success">
|
||||
{{ form.references.proposer.name }}
|
||||
</p>
|
||||
@@ -900,15 +881,9 @@ onUnmounted(() => {
|
||||
</Field>
|
||||
<Field class="col-span-12 sm:col-span-6">
|
||||
<FieldLabel for="supporter_ic_number">No. KP Penyokong</FieldLabel>
|
||||
<Input
|
||||
id="supporter_ic_number"
|
||||
v-model="form.references.supporter.ic_number"
|
||||
type="text"
|
||||
placeholder="Contoh: 850505055678"
|
||||
:disabled="!canEdit || referenceLookupLoading.supporter"
|
||||
@input="handleReferenceIcInput('supporter')"
|
||||
@blur="lookupReference('supporter')"
|
||||
/>
|
||||
<Input id="supporter_ic_number" v-model="form.references.supporter.ic_number" type="text"
|
||||
placeholder="Contoh: 850505055678" :disabled="!canEdit || referenceLookupLoading.supporter"
|
||||
@input="handleReferenceIcInput('supporter')" @blur="lookupReference('supporter')" />
|
||||
<p v-if="form.references.supporter.name" class="mt-1 text-sm text-success">
|
||||
{{ form.references.supporter.name }}
|
||||
</p>
|
||||
@@ -925,11 +900,7 @@ onUnmounted(() => {
|
||||
Muat naik fail baharu untuk menggantikan dokumen sedia ada.
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="type in DOCUMENT_UPLOAD_TYPES"
|
||||
:key="type"
|
||||
class="rounded-lg border border-foreground/10 p-4"
|
||||
>
|
||||
<div v-for="type in DOCUMENT_UPLOAD_TYPES" :key="type" class="rounded-lg border border-foreground/10 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-medium">{{ DOCUMENT_TYPE_LABELS[type] }}</div>
|
||||
@@ -939,23 +910,15 @@ onUnmounted(() => {
|
||||
<div v-else class="mt-1 text-sm opacity-70">Tiada dokumen dimuat naik.</div>
|
||||
</div>
|
||||
<div v-if="documentsByType[type]" class="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
look="outline"
|
||||
size="sm"
|
||||
<Button type="button" look="outline" size="sm"
|
||||
:disabled="previewLoading && previewDocument?.id === documentsByType[type]?.id"
|
||||
@click="documentsByType[type] && handleViewDocument(documentsByType[type]!)"
|
||||
>
|
||||
@click="documentsByType[type] && handleViewDocument(documentsByType[type]!)">
|
||||
<Eye class="mr-2 size-4" />
|
||||
Lihat
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
look="outline"
|
||||
size="sm"
|
||||
<Button type="button" look="outline" size="sm"
|
||||
:disabled="downloadingDocumentId === documentsByType[type]?.id"
|
||||
@click="documentsByType[type] && handleDownloadDocument(documentsByType[type]!)"
|
||||
>
|
||||
@click="documentsByType[type] && handleDownloadDocument(documentsByType[type]!)">
|
||||
<Download class="mr-2 size-4" />
|
||||
Muat Turun
|
||||
</Button>
|
||||
@@ -965,13 +928,8 @@ onUnmounted(() => {
|
||||
<FieldLabel :for="`document-${type}`">
|
||||
{{ documentsByType[type] ? 'Ganti Dokumen' : 'Muat Naik Dokumen' }}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
:id="`document-${type}`"
|
||||
type="file"
|
||||
:accept="documentAccept(type)"
|
||||
:disabled="uploadingDocumentType === type"
|
||||
@change="handleDocumentUpload(type, $event)"
|
||||
/>
|
||||
<Input :id="`document-${type}`" type="file" :accept="documentAccept(type)"
|
||||
:disabled="uploadingDocumentType === type" @change="handleDocumentUpload(type, $event)" />
|
||||
<FieldError v-if="fieldErrors[`documents.${type}`]">{{ fieldErrors[`documents.${type}`] }}</FieldError>
|
||||
<p v-if="uploadingDocumentType === type" class="mt-1 text-sm opacity-70">Memuat naik...</p>
|
||||
</Field>
|
||||
@@ -981,19 +939,11 @@ onUnmounted(() => {
|
||||
|
||||
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
|
||||
<div class="relative ms-3 ps-8">
|
||||
<div
|
||||
v-for="(review, index) in sortedReviews"
|
||||
:key="review.id"
|
||||
class="relative pb-8 last:pb-0"
|
||||
>
|
||||
<span
|
||||
class="absolute -start-8 top-1.5 flex size-3.5 rounded-full border-2 ring-4 ring-background"
|
||||
:class="reviewTimelineDotClass(review)"
|
||||
/>
|
||||
<span
|
||||
v-if="index < sortedReviews.length - 1"
|
||||
class="absolute -start-[1.375rem] top-5 h-[calc(100%-0.25rem)] w-px bg-foreground/15"
|
||||
/>
|
||||
<div v-for="(review, index) in sortedReviews" :key="review.id" class="relative pb-8 last:pb-0">
|
||||
<span class="absolute -inset-s-8 top-1.5 flex size-3.5 rounded-full border-2 ring-4 ring-background"
|
||||
:class="reviewTimelineDotClass(review)" />
|
||||
<span v-if="index < sortedReviews.length - 1"
|
||||
class="absolute -inset-s-5.5 top-5 h-[calc(100%-0.25rem)] w-px bg-foreground/15" />
|
||||
|
||||
<div class="rounded-lg border border-foreground/10 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||
@@ -1003,10 +953,8 @@ onUnmounted(() => {
|
||||
{{ formatDateTime(review.reviewed_at) }}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
<Badge :variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
|
||||
:look="reviewDecisionBadgeLook(review.decision, review.stage)" class="whitespace-nowrap">
|
||||
{{ reviewDecisionLabel(review.decision, review.stage) }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,11 @@ import axios from 'axios'
|
||||
import { useMembershipApplicationList } from '../composables/useMembershipApplicationList'
|
||||
import { usePermissions } from '@/composables/usePermissions'
|
||||
import { batchCompleteMembershipApplications } from '../services/membership-application.service'
|
||||
import {
|
||||
boardResultBadgeVariant,
|
||||
statusBadgeLook,
|
||||
statusBadgeVariant,
|
||||
} from '../utils/membership-application-badge.utils'
|
||||
import type {
|
||||
BatchCompleteFailedItem,
|
||||
BatchCompleteResponse,
|
||||
@@ -203,25 +208,12 @@ function statusLabel(status: MembershipApplicationStatus): string {
|
||||
return labels[status] ?? status
|
||||
}
|
||||
|
||||
function statusBadgeVariant(status: MembershipApplicationStatus) {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'MANAGEMENT_REJECTED') return 'danger'
|
||||
if (status === 'PENDING_BOARD' || status === 'PENDING_NOTIFICATION') return 'pending'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function boardResultLabel(result: MembershipApplicationBoardResult | null): string {
|
||||
if (result === 'PASS') return 'Lulus'
|
||||
if (result === 'FAIL') return 'Gagal'
|
||||
return '-'
|
||||
}
|
||||
|
||||
function boardResultBadgeVariant(result: MembershipApplicationBoardResult | null) {
|
||||
if (result === 'PASS') return 'success'
|
||||
if (result === 'FAIL') return 'danger'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
function formatSubmittedAt(value: string | null): string {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('DD/MM/YYYY HH:mm')
|
||||
@@ -238,44 +230,44 @@ function goToApplicationEdit(id: string) {
|
||||
const headers = computed<TableHeader[]>(() => {
|
||||
const base: TableHeader[] = [
|
||||
{ title: 'Bil.', key: '#', sortable: false },
|
||||
{ title: 'No. Permohonan', key: 'application_number', sortable: true },
|
||||
{
|
||||
title: 'Nama Pemohon',
|
||||
key: 'applicant_name',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.name ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Emel',
|
||||
key: 'applicant_email',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.email ?? '',
|
||||
},
|
||||
{
|
||||
title: 'No. IC',
|
||||
key: 'applicant_ic_number',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.ic_number ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
sortable: true,
|
||||
exportValue: (item) => statusLabel(item.status),
|
||||
},
|
||||
{
|
||||
title: 'Keputusan Lembaga',
|
||||
key: 'board_result',
|
||||
sortable: false,
|
||||
exportValue: (item) => boardResultLabel(item.board_result),
|
||||
},
|
||||
{
|
||||
title: 'Tarikh Hantar',
|
||||
key: 'submitted_at',
|
||||
sortable: true,
|
||||
exportValue: (item) => formatSubmittedAt(item.submitted_at),
|
||||
},
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
{ title: 'No. Permohonan', key: 'application_number', sortable: true },
|
||||
{
|
||||
title: 'Nama Pemohon',
|
||||
key: 'applicant_name',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.name ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Emel',
|
||||
key: 'applicant_email',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.email ?? '',
|
||||
},
|
||||
{
|
||||
title: 'No. IC',
|
||||
key: 'applicant_ic_number',
|
||||
sortable: false,
|
||||
exportValue: (item) => item.applicant?.ic_number ?? '',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
key: 'status',
|
||||
sortable: true,
|
||||
exportValue: (item) => statusLabel(item.status),
|
||||
},
|
||||
{
|
||||
title: 'Keputusan Lembaga',
|
||||
key: 'board_result',
|
||||
sortable: false,
|
||||
exportValue: (item) => boardResultLabel(item.board_result),
|
||||
},
|
||||
{
|
||||
title: 'Tarikh Hantar',
|
||||
key: 'submitted_at',
|
||||
sortable: true,
|
||||
exportValue: (item) => formatSubmittedAt(item.submitted_at),
|
||||
},
|
||||
{ title: 'Tindakan', key: 'actions', sortable: false },
|
||||
]
|
||||
|
||||
if (canBatchComplete.value) {
|
||||
@@ -321,41 +313,20 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<AlertCloseTrigger @click="error = null" />
|
||||
</AlertRoot>
|
||||
|
||||
<DataTable
|
||||
:headers="headers"
|
||||
:items="applications"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:current-sort="sortBy"
|
||||
show-pagination
|
||||
exportable
|
||||
export-file-name="permohonan-keahlian"
|
||||
v-model:page="page"
|
||||
v-model:items-per-page="itemsPerPage"
|
||||
@update:sort-by="handleSortUpdate"
|
||||
>
|
||||
<DataTable :headers="headers" :items="applications" :loading="loading" :pagination="pagination"
|
||||
:current-sort="sortBy" show-pagination exportable export-file-name="permohonan-keahlian" v-model:page="page"
|
||||
v-model:items-per-page="itemsPerPage" @update:sort-by="handleSortUpdate">
|
||||
<template #toolbar>
|
||||
<div class="flex w-full flex-wrap items-center gap-3">
|
||||
<div class="relative w-full max-w-md flex-1">
|
||||
<Search
|
||||
class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Input
|
||||
v-model="search"
|
||||
type="search"
|
||||
placeholder="Cari no. permohonan, nama, emel, IC..."
|
||||
class="w-full pl-9"
|
||||
aria-label="Cari permohonan keahlian"
|
||||
/>
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
|
||||
aria-hidden="true" />
|
||||
<Input v-model="search" type="search" placeholder="Cari no. permohonan, nama, emel, IC..."
|
||||
class="w-full pl-9" aria-label="Cari permohonan keahlian" />
|
||||
</div>
|
||||
|
||||
<SelectRoot
|
||||
class="w-full sm:w-56"
|
||||
:collection="statusFilterCollection"
|
||||
:default-value="statusFilterInitial"
|
||||
@value-change="setStatusFilterValue"
|
||||
>
|
||||
<SelectRoot class="w-full sm:w-56" :collection="statusFilterCollection" :default-value="statusFilterInitial"
|
||||
@value-change="setStatusFilterValue">
|
||||
<SelectControl>
|
||||
<SelectTrigger aria-label="Tapis status">
|
||||
<SelectValueText placeholder="Semua Status" />
|
||||
@@ -364,11 +335,7 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<SelectContent>
|
||||
<SelectItemGroup>
|
||||
<SelectItemGroupLabel>Status</SelectItemGroupLabel>
|
||||
<SelectItem
|
||||
v-for="item in statusFilterCollection.items"
|
||||
:key="item.label"
|
||||
:item="item"
|
||||
>
|
||||
<SelectItem v-for="item in statusFilterCollection.items" :key="item.label" :item="item">
|
||||
<SelectItemText>{{ item.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
</SelectItemGroup>
|
||||
@@ -376,21 +343,13 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</SelectRoot>
|
||||
|
||||
<template v-if="canBatchComplete">
|
||||
<Button
|
||||
type="button"
|
||||
look="outline"
|
||||
variant="secondary"
|
||||
<Button type="button" look="outline" variant="secondary"
|
||||
:disabled="!selectableApplications.length || loading"
|
||||
@click="toggleSelectAllOnPage(!allSelectableSelected)"
|
||||
>
|
||||
@click="toggleSelectAllOnPage(!allSelectableSelected)">
|
||||
{{ allSelectableSelected ? 'Nyahpilih Halaman' : 'Pilih Halaman' }}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
:disabled="!selectedIds.length || loading || batchSubmitting"
|
||||
@click="openBatchConfirm"
|
||||
>
|
||||
<Button type="button" variant="primary" :disabled="!selectedIds.length || loading || batchSubmitting"
|
||||
@click="openBatchConfirm">
|
||||
Selesaikan Terpilih ({{ selectedIds.length }})
|
||||
</Button>
|
||||
</template>
|
||||
@@ -398,11 +357,9 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</template>
|
||||
|
||||
<template v-if="canBatchComplete" #item.select="{ item }">
|
||||
<CheckboxRoot
|
||||
v-if="isSelectable(item as MembershipApplicationListItem)"
|
||||
<CheckboxRoot v-if="isSelectable(item as MembershipApplicationListItem)"
|
||||
:checked="isSelected((item as MembershipApplicationListItem).id)"
|
||||
@checked-change="({ checked }) => toggleSelection((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
@checked-change="({ checked }) => toggleSelection((item as MembershipApplicationListItem).id)">
|
||||
<CheckboxControl />
|
||||
</CheckboxRoot>
|
||||
</template>
|
||||
@@ -422,6 +379,7 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<template #item.status="{ item }">
|
||||
<Badge
|
||||
:variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
|
||||
:look="statusBadgeLook((item as MembershipApplicationListItem).status)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
{{ statusLabel((item as MembershipApplicationListItem).status) }}
|
||||
@@ -429,11 +387,9 @@ const headers = computed<TableHeader[]>(() => {
|
||||
</template>
|
||||
|
||||
<template #item.board_result="{ item }">
|
||||
<Badge
|
||||
v-if="(item as MembershipApplicationListItem).board_result"
|
||||
<Badge v-if="(item as MembershipApplicationListItem).board_result"
|
||||
:variant="boardResultBadgeVariant((item as MembershipApplicationListItem).board_result)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
class="whitespace-nowrap">
|
||||
{{ boardResultLabel((item as MembershipApplicationListItem).board_result) }}
|
||||
</Badge>
|
||||
<span v-else class="opacity-50">-</span>
|
||||
@@ -445,36 +401,21 @@ const headers = computed<TableHeader[]>(() => {
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="hasPermission('lihat permohonan keahlian')"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-green-600 text-white"
|
||||
title="Lihat butiran permohonan"
|
||||
@click="goToApplicationDetail((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
<Button v-if="hasPermission('lihat permohonan keahlian')" type="button" variant="ghost" size="sm"
|
||||
class="bg-green-600 text-white" title="Lihat butiran permohonan"
|
||||
@click="goToApplicationDetail((item as MembershipApplicationListItem).id)">
|
||||
<Eye class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="hasPermission('kemaskini permohonan keahlian')"
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="bg-blue-600 text-white"
|
||||
title="Kemaskini permohonan"
|
||||
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)"
|
||||
>
|
||||
<Button v-if="hasPermission('kemaskini permohonan keahlian')" type="button" variant="ghost" size="sm"
|
||||
class="bg-blue-600 text-white" title="Kemaskini permohonan"
|
||||
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)">
|
||||
<Pencil class="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<DialogRoot
|
||||
:open="batchConfirmOpen"
|
||||
@openChange="(details) => { batchConfirmOpen = details.open }"
|
||||
>
|
||||
<DialogRoot :open="batchConfirmOpen" @openChange="(details) => { batchConfirmOpen = details.open }">
|
||||
<DialogContent>
|
||||
<div class="p-5 text-center">
|
||||
<div class="mt-2 text-2xl font-medium">Selesaikan Permohonan Terpilih?</div>
|
||||
@@ -486,13 +427,8 @@ const headers = computed<TableHeader[]>(() => {
|
||||
<DialogCloseTrigger class="mr-2 w-32" :disabled="batchSubmitting">
|
||||
Batal
|
||||
</DialogCloseTrigger>
|
||||
<Button
|
||||
class="w-32"
|
||||
type="button"
|
||||
variant="primary"
|
||||
:disabled="batchSubmitting"
|
||||
@click="confirmBatchComplete"
|
||||
>
|
||||
<Button class="w-32" type="button" variant="primary" :disabled="batchSubmitting"
|
||||
@click="confirmBatchComplete">
|
||||
{{ batchSubmitting ? 'Memproses...' : 'Sahkan' }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { BadgeVariants } from '@/components/ui/styles/badge.styles'
|
||||
import type {
|
||||
MembershipApplicationBoardResult,
|
||||
MembershipApplicationStatus,
|
||||
} from '../types/membership-application.types'
|
||||
|
||||
export function statusBadgeVariant(
|
||||
status: MembershipApplicationStatus,
|
||||
): NonNullable<BadgeVariants['variant']> {
|
||||
if (status === 'COMPLETED') return 'success'
|
||||
if (status === 'MANAGEMENT_REJECTED') return 'danger'
|
||||
if (status === 'PENDING_BOARD' || status === 'PENDING_NOTIFICATION') return 'pending'
|
||||
return 'ghost'
|
||||
}
|
||||
|
||||
export function statusBadgeLook(
|
||||
status: MembershipApplicationStatus,
|
||||
): BadgeVariants['look'] {
|
||||
if (
|
||||
status === 'COMPLETED' ||
|
||||
status === 'MANAGEMENT_REJECTED' ||
|
||||
status === 'PENDING_BOARD' ||
|
||||
status === 'PENDING_NOTIFICATION'
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
export function boardResultBadgeVariant(
|
||||
result: MembershipApplicationBoardResult | null,
|
||||
): NonNullable<BadgeVariants['variant']> {
|
||||
if (result === 'PASS') return 'success'
|
||||
if (result === 'FAIL') return 'danger'
|
||||
return 'ghost'
|
||||
}
|
||||
|
||||
export function reviewDecisionBadgeVariant(
|
||||
decision: string | null,
|
||||
stage: string,
|
||||
): NonNullable<BadgeVariants['variant']> {
|
||||
if (stage === 'MANAGEMENT') {
|
||||
if (decision === 'APPROVED') return 'success'
|
||||
if (decision === 'REJECTED') return 'danger'
|
||||
}
|
||||
|
||||
if (stage === 'BOARD') {
|
||||
if (decision === 'PASS') return 'success'
|
||||
if (decision === 'FAIL') return 'danger'
|
||||
}
|
||||
|
||||
return 'ghost'
|
||||
}
|
||||
|
||||
export function reviewDecisionBadgeLook(
|
||||
decision: string | null,
|
||||
stage: string,
|
||||
): BadgeVariants['look'] {
|
||||
if (
|
||||
(stage === 'MANAGEMENT' && (decision === 'APPROVED' || decision === 'REJECTED')) ||
|
||||
(stage === 'BOARD' && (decision === 'PASS' || decision === 'FAIL'))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return 'outline'
|
||||
}
|
||||
@@ -156,9 +156,8 @@ onMounted(async () => {
|
||||
<div
|
||||
class="mt-6 flex flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-0 lg:pt-0">
|
||||
<div
|
||||
class="relative aspect-[1.75/1] w-full max-w-[17rem] overflow-hidden rounded-2xl bg-gradient-to-br from-primary via-primary/95 to-primary/75 p-4 text-primary-foreground shadow-lg ring-1 ring-white/20 sm:max-w-xs sm:p-5"
|
||||
role="img"
|
||||
aria-label="Kad digital anggota">
|
||||
class="relative aspect-1.75/1 w-full max-w-68 overflow-hidden rounded-2xl bg-linear-to-br from-primary via-primary/95 to-primary/75 p-4 text-primary-foreground shadow-lg ring-1 ring-white/20 sm:max-w-xs sm:p-5"
|
||||
role="img" aria-label="Kad digital anggota">
|
||||
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
|
||||
<div class="pointer-events-none absolute -right-10 -top-10 size-36 rounded-full bg-white/10" />
|
||||
<div class="pointer-events-none absolute -bottom-12 -left-8 size-40 rounded-full bg-white/5" />
|
||||
@@ -212,9 +211,6 @@ onMounted(async () => {
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="2">
|
||||
<Lucide class="mr-2 size-4" icon="Lock" /> Kata Laluan
|
||||
</TabsTrigger>
|
||||
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="4">
|
||||
<Lucide class="mr-2 size-4" icon="MoreHorizontal" /> Lain-lain
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</Box>
|
||||
@@ -238,357 +234,6 @@ onMounted(async () => {
|
||||
<TabsContent value="6" class="mt-8">
|
||||
<HeirTab embedded />
|
||||
</TabsContent>
|
||||
<!-- Perkhidmatan -->
|
||||
<TabsContent value="4" class="mt-8">
|
||||
<div class="grid grid-cols-12 gap-x-6 gap-y-8">
|
||||
<!-- BEGIN: Latest Uploads -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-5 sm:py-3">
|
||||
<h2 class="mr-auto text-base font-medium">Latest Uploads</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="all">All Files</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 hidden sm:flex">All Files</Button>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<FileIcon class="w-12" variant="directory" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Documentation</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">40 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<FileIcon class="w-12 text-xs" variant="file" type="MP3" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Celine Dion - Ashes</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">40 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<FileIcon class="w-12" variant="empty-directory" />
|
||||
<div class="ml-4">
|
||||
<a class="font-medium" href="">Resources</a>
|
||||
<div class="mt-0.5 text-xs opacity-70">0 KB</div>
|
||||
</div>
|
||||
<MenuRoot class="w-auto ml-auto">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="share">
|
||||
<Lucide class="mr-2 size-4" icon="Users" /> Share File
|
||||
</MenuItem>
|
||||
<MenuItem value="delete">
|
||||
<Lucide class="mr-2 size-4" icon="Trash" /> Delete
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Latest Uploads -->
|
||||
<!-- BEGIN: Work In Progress -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<TabsRoot defaultValue="wip-0">
|
||||
<div class="relative flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Work In Progress</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="new">New</MenuItem>
|
||||
<MenuItem value="last-week">Last Week</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<TabsList class="absolute inset-y-0 h-[2.8rem] right-5 my-auto hidden w-auto sm:flex">
|
||||
<TabsTrigger value="wip-0">New</TabsTrigger>
|
||||
<TabsTrigger value="wip-1">Last Week</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="wip-0" class="p-5">
|
||||
<div>
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Pending Tasks</div>
|
||||
<div>20%</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="50" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Completed Tasks</div>
|
||||
<div>2 / 20</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="25" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<div class="flex">
|
||||
<div class="mr-auto">Tasks In Progress</div>
|
||||
<div>42</div>
|
||||
</div>
|
||||
<ProgressRoot :defaultValue="75" class="mt-2">
|
||||
<ProgressTrack>
|
||||
<ProgressRange />
|
||||
</ProgressTrack>
|
||||
</ProgressRoot>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<Button variant="ghost" class="border border-foreground/15 shadow-none mx-auto mt-5 inline-block"
|
||||
as="a" href="">
|
||||
View More Details
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</Box>
|
||||
<!-- END: Work In Progress -->
|
||||
<!-- BEGIN: Daily Sales -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-5 sm:py-3">
|
||||
<h2 class="mr-auto text-base font-medium">Daily Sales</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="download">
|
||||
<Lucide class="mr-2 size-4" icon="File" /> Download Excel
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 hidden sm:flex">
|
||||
<Lucide class="mr-2 size-4" icon="File" /> Download Excel
|
||||
</Button>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div v-for="(faker, index) in fakers.slice(0, 3)" :key="index" :class="{ 'mt-5': index > 0 }"
|
||||
class="relative flex items-center">
|
||||
<AvatarRoot class="size-12 bg-background rounded-full">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['photos'][0]" />
|
||||
</AvatarRoot>
|
||||
<div class="ml-4 mr-auto">
|
||||
<a class="font-medium" href="">{{ faker['users'][0]!['name'] }}</a>
|
||||
<div class="mr-5 opacity-70 sm:mr-5">
|
||||
{{
|
||||
index === 0
|
||||
? 'Bootstrap 4 HTML Admin Template'
|
||||
: index === 1
|
||||
? 'Tailwind Admin Dashboard Template'
|
||||
: 'Vuejs HTML Admin Template'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-medium">
|
||||
{{ index === 0 ? '+$19' : index === 1 ? '+$25' : '+$21' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
<!-- END: Daily Sales -->
|
||||
<!-- BEGIN: Latest Tasks -->
|
||||
<Box raised="single" class="col-span-12 p-0 lg:col-span-6">
|
||||
<TabsRoot defaultValue="lt-0">
|
||||
<div class="relative flex items-center border-b border-foreground/15 p-5">
|
||||
<h2 class="mr-auto text-base font-medium">Latest Tasks</h2>
|
||||
<MenuRoot class="w-auto ml-auto sm:hidden">
|
||||
<MenuTrigger as-child>
|
||||
<a class="block size-5" href="#">
|
||||
<Lucide class="size-5 opacity-70" icon="MoreHorizontal" />
|
||||
</a>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="new">New</MenuItem>
|
||||
<MenuItem value="last-week">Last Week</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<TabsList class="absolute inset-y-0 h-[2.8rem] right-5 my-auto hidden w-auto sm:flex">
|
||||
<TabsTrigger value="lt-0">New</TabsTrigger>
|
||||
<TabsTrigger value="lt-1">Last Week</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<TabsContent value="lt-0">
|
||||
<div class="flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Create New Campaign</a>
|
||||
<div class="opacity-70">10:00 AM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Meeting With Client</a>
|
||||
<div class="opacity-70">02:00 PM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center">
|
||||
<div class="border-l-4 border-primary/20 pl-4">
|
||||
<a class="font-medium" href="">Create New Repository</a>
|
||||
<div class="opacity-70">04:00 PM</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<SwitchRoot>
|
||||
<SwitchControl />
|
||||
</SwitchRoot>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</TabsRoot>
|
||||
</Box>
|
||||
<!-- END: Latest Tasks -->
|
||||
<Box raised="single" class="col-span-12 p-0">
|
||||
<CarouselRoot :default-page="0" :slide-count="fakers.slice(0, 5).length">
|
||||
<div class="flex items-center border-b border-foreground/15 px-5 py-3">
|
||||
<h2 class="mr-auto text-base font-medium">New Products</h2>
|
||||
<CarouselPrevTrigger as-child>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15 mr-2">
|
||||
<Lucide class="size-4" icon="ChevronLeft" />
|
||||
</Button>
|
||||
</CarouselPrevTrigger>
|
||||
<CarouselNextTrigger as-child>
|
||||
<Button variant="ghost" class="shadow-none border border-foreground/15">
|
||||
<Lucide class="size-4" icon="ChevronRight" />
|
||||
</Button>
|
||||
</CarouselNextTrigger>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<CarouselItemGroup class="py-5">
|
||||
<CarouselItem v-for="(faker, index) in fakers.slice(0, 5)" :key="index" :index="index" class="px-5"
|
||||
as-child>
|
||||
<div>
|
||||
<div class="flex flex-col items-center pb-5 lg:flex-row">
|
||||
<div class="flex flex-col items-center pr-5 sm:flex-row lg:border-r border-foreground/15">
|
||||
<div class="sm:mr-5">
|
||||
<AvatarRoot class="size-20 bg-background rounded-full">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['images'][0]" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
<div class="mr-auto mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<a class="text-lg font-medium" href="">
|
||||
{{ faker['products'][0]!['name'] }}
|
||||
</a>
|
||||
<div class="mt-1 opacity-70 sm:mt-0">
|
||||
{{ faker['news'][0]!['shortContent'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mt-6 flex w-full flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-4 lg:mt-0 lg:w-auto lg:border-t-0 lg:pt-0">
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][0] }}</div>
|
||||
<div class="opacity-70">Orders</div>
|
||||
</div>
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][1] }}k</div>
|
||||
<div class="opacity-70">Purchases</div>
|
||||
</div>
|
||||
<div class="w-20 rounded-md py-3 text-center">
|
||||
<div class="text-xl font-medium">{{ faker['totals'][0] }}</div>
|
||||
<div class="opacity-70">Reviews</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center border-t border-foreground/15 pt-5 sm:flex-row">
|
||||
<div
|
||||
class="flex w-full items-center justify-center border-b border-foreground/15 pb-5 sm:w-auto sm:justify-start sm:border-b-0 sm:pb-0">
|
||||
<Badge look="outline" class="mr-3 px-3 py-2">{{
|
||||
faker['dates'][0]
|
||||
}}</Badge>
|
||||
<div class="opacity-70">Date of Release</div>
|
||||
</div>
|
||||
<div class="mt-5 flex sm:ml-auto sm:mt-0">
|
||||
<Button variant="ghost"
|
||||
class="border border-foreground/15 shadow-none ml-auto">Preview</Button>
|
||||
<Button variant="ghost" class="border border-foreground/15 shadow-none ml-2">Details</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</CarouselItemGroup>
|
||||
</div>
|
||||
</CarouselRoot>
|
||||
</Box>
|
||||
<!-- END: New Products -->
|
||||
</div>
|
||||
</TabsContent>
|
||||
</TabsRoot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -95,11 +95,6 @@ const router = createRouter({
|
||||
name: 'post',
|
||||
component: () => import('../views/Post.vue'),
|
||||
},
|
||||
{
|
||||
path: 'crud-data-list',
|
||||
name: 'crud-data-list',
|
||||
component: () => import('../views/CrudDataList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'crud-form',
|
||||
name: 'crud-form',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useBreadcrumb } from '@/composables/useBreadcrumb'
|
||||
import { useNotifications } from '@/modules/notification'
|
||||
import '@/assets/css/themes/enigma/side-menu.css'
|
||||
import logo from '@/assets/images/logo-kopkb.svg'
|
||||
import logo from '@/assets/images/logonew-mykopkb.svg'
|
||||
import { useSideMenu } from '@/composables/useSideMenu'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
@@ -40,9 +40,59 @@ const { unreadCount, fetchNotifications } = useNotifications()
|
||||
const appName = import.meta.env.VITE_APP_NAME
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION
|
||||
|
||||
const notificationsOpen = ref(false)
|
||||
const profileOpen = ref(false)
|
||||
const sidebarProfileOpen = ref(false)
|
||||
const notificationsRef = ref<HTMLElement | null>(null)
|
||||
const profileRef = ref<HTMLElement | null>(null)
|
||||
const sidebarProfileRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function toggleNotifications() {
|
||||
profileOpen.value = false
|
||||
sidebarProfileOpen.value = false
|
||||
notificationsOpen.value = !notificationsOpen.value
|
||||
}
|
||||
|
||||
function toggleProfile() {
|
||||
notificationsOpen.value = false
|
||||
sidebarProfileOpen.value = false
|
||||
profileOpen.value = !profileOpen.value
|
||||
}
|
||||
|
||||
function closeDropdowns() {
|
||||
notificationsOpen.value = false
|
||||
profileOpen.value = false
|
||||
sidebarProfileOpen.value = false
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
const target = event.target as Node
|
||||
|
||||
if (
|
||||
notificationsRef.value?.contains(target) ||
|
||||
profileRef.value?.contains(target) ||
|
||||
sidebarProfileRef.value?.contains(target)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
closeDropdowns()
|
||||
}
|
||||
|
||||
watch(sidebarProfileOpen, (isOpen) => {
|
||||
if (!isOpen) return
|
||||
notificationsOpen.value = false
|
||||
profileOpen.value = false
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
authStore.fetchSession()
|
||||
fetchNotifications()
|
||||
document.addEventListener('click', handleDocumentClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleDocumentClick)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -76,6 +126,7 @@ onMounted(() => {
|
||||
'z-20 relative w-[320px] duration-100 transition-[width] group-[.side-menu--collapsed]:xl:w-[165px] group-[.side-menu--collapsed]:group-hover:xl:w-[320px] h-screen flex flex-col-reverse xl:flex-col',
|
||||
'before:absolute before:inset-y-0 before:w-px before:mt-23 before:bg-foreground/11 dark:before:bg-foreground/10 before:right-0 before:mr-8 before:hidden xl:before:block',
|
||||
]">
|
||||
<!-- Logo -->
|
||||
<div
|
||||
class="relative z-10 mt-3 hidden h-[90px] w-[320px] flex-none items-center overflow-hidden pl-8 pr-14 duration-100 xl:flex group-[.side-menu--collapsed]:group-hover:xl:w-[320px] group-[.side-menu--collapsed]:xl:w-[165px]">
|
||||
<a class="relative flex items-center transition-[margin] duration-100 xl:ml-1.5 group-[.side-menu--collapsed]:group-hover:xl:ml-1.5 group-[.side-menu--collapsed]:xl:ml-8"
|
||||
@@ -91,13 +142,15 @@ onMounted(() => {
|
||||
<Lucide icon="ChevronLeft" />
|
||||
</button>
|
||||
</div>
|
||||
<AccountTrigger class="relative transition-[width] xl:mb-2 xl:mr-8"
|
||||
innerClass="border-foreground/[.11] dark:border-foreground/[.11] border-t py-3 pl-6 pr-5 opacity-90 hover:opacity-100 xl:border-b xl:border-t-0 xl:pb-6 xl:pl-8 xl:pt-2.5"
|
||||
avatarClass="h-11 w-11 border-background/70 dark:border-foreground/20 shadow-sm"
|
||||
textClass="text-primary dark:text-foreground w-full">
|
||||
<AccountDropdown class="absolute top-0 left-full origin-bottom-left"
|
||||
boxClass="absolute bottom-0 left-[100%] ml-2 xl:bottom-auto xl:top-0" />
|
||||
</AccountTrigger>
|
||||
<div ref="sidebarProfileRef">
|
||||
<AccountTrigger v-model:open="sidebarProfileOpen" class="relative transition-[width] xl:mb-2 xl:mr-8"
|
||||
innerClass="border-foreground/[.11] dark:border-foreground/[.11] border-t py-3 pl-6 pr-5 opacity-90 hover:opacity-100 xl:border-b xl:border-t-0 xl:pb-6 xl:pl-8 xl:pt-2.5"
|
||||
avatarClass="h-11 w-11 border-background/70 dark:border-foreground/20 shadow-sm"
|
||||
textClass="text-primary dark:text-foreground w-full">
|
||||
<AccountDropdown :open="sidebarProfileOpen" class="absolute top-0 left-full origin-bottom-left"
|
||||
boxClass="absolute bottom-0 left-[100%] ml-2 xl:bottom-auto xl:top-0" />
|
||||
</AccountTrigger>
|
||||
</div>
|
||||
|
||||
<ScrollAreaRoot class="flex-1 min-h-0">
|
||||
<ScrollAreaViewport :class="[
|
||||
@@ -145,29 +198,26 @@ onMounted(() => {
|
||||
<Breadcrumb v-if="breadcrumbItems.length"
|
||||
class="mr-auto hidden xl:flex [&_ol]:text-(--color-nav-foreground)/70 [&_li]:last:text-(--color-nav-foreground)/90 [&_li]:hover:text-(--color-nav-foreground)/90"
|
||||
:items="breadcrumbItems" />
|
||||
<div
|
||||
<div ref="notificationsRef"
|
||||
class="group/notifications relative flex size-10 flex-none cursor-pointer items-center justify-center rounded-xl border border-(--color-nav-foreground)/30 bg-(--color-nav-foreground)/10 transition-colors hover:bg-(--color-nav-foreground)/20"
|
||||
>
|
||||
<Lucide
|
||||
class="size-[22px] stroke-[1.75] [--color:var(--color-nav-foreground)]"
|
||||
icon="Bell"
|
||||
/>
|
||||
<span
|
||||
v-if="unreadCount > 0"
|
||||
class="bg-danger text-background ring-background absolute -right-1.5 -top-1.5 flex min-w-5 items-center justify-center rounded-full px-1 py-0.5 text-[11px] font-semibold leading-none ring-2"
|
||||
>
|
||||
@click.stop="toggleNotifications">
|
||||
<Lucide class="size-[22px] stroke-[1.75] [--color:var(--color-nav-foreground)]" icon="Bell" />
|
||||
<span v-if="unreadCount > 0"
|
||||
class="bg-danger text-background ring-background absolute -right-1.5 -top-1.5 flex min-w-5 items-center justify-center rounded-full px-1 py-0.5 text-[11px] font-semibold leading-none ring-2">
|
||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||
</span>
|
||||
<NotificationDropdown class="absolute right-0 top-full mt-2 origin-top-right"
|
||||
<NotificationDropdown :open="notificationsOpen"
|
||||
class="absolute right-0 top-full mt-2 origin-top-right"
|
||||
boxClass="absolute right-0 top-0 -mr-0.5 -mt-0.5" />
|
||||
</div>
|
||||
<div class="group/profile relative size-9 flex-none">
|
||||
<div ref="profileRef" class="group/profile relative size-9 flex-none cursor-pointer"
|
||||
@click.stop="toggleProfile">
|
||||
<AvatarRoot class="ring-(--color)/40 size-full [--color:var(--color-nav-foreground)] rounded-full">
|
||||
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
|
||||
<AvatarImage v-if="authStore.userImageUrl" :src="authStore.userImageUrl"
|
||||
:alt="authStore.userName" />
|
||||
</AvatarRoot>
|
||||
<AccountDropdown class="absolute right-0 top-full mt-2 origin-top-right"
|
||||
<AccountDropdown :open="profileOpen" class="absolute right-0 top-full mt-2 origin-top-right"
|
||||
boxClass="absolute right-0 top-0 -mr-0.5 -mt-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import fakers from '@/utils/faker'
|
||||
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Box } from '@/components/ui/box'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Lucide } from '@/components/ui/lucide'
|
||||
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
|
||||
|
||||
import {
|
||||
PaginationContext,
|
||||
PaginationRoot,
|
||||
PaginationItem,
|
||||
PaginationPrevTrigger,
|
||||
PaginationNextTrigger,
|
||||
PaginationEllipsis,
|
||||
} from '@/components/ui/pagination'
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
const deleteConfirmationOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Data List Layout</h2>
|
||||
<div class="mt-5 grid grid-cols-12 gap-6">
|
||||
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
|
||||
<Button class="mr-2" look="outline" variant="primary"> Add New Product </Button>
|
||||
<MenuRoot class="w-auto">
|
||||
<MenuTrigger as-child>
|
||||
<Box class="px-2" as-child>
|
||||
<Button variant="ghost" class="box flex items-center">
|
||||
<span class="flex h-5 w-5 items-center justify-center">
|
||||
<Lucide class="h-4 w-4" icon="Plus" />
|
||||
</span>
|
||||
</Button>
|
||||
</Box>
|
||||
</MenuTrigger>
|
||||
<MenuPositioner>
|
||||
<MenuContent class="w-40">
|
||||
<MenuItem value="print">
|
||||
<Lucide class="mr-2 h-4 w-4" icon="Printer" /> Print
|
||||
</MenuItem>
|
||||
<MenuItem value="excel">
|
||||
<Lucide class="mr-2 h-4 w-4" icon="FileText" /> Export to Excel
|
||||
</MenuItem>
|
||||
<MenuItem value="pdf">
|
||||
<Lucide class="mr-2 h-4 w-4" icon="FileText" /> Export to PDF
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</MenuPositioner>
|
||||
</MenuRoot>
|
||||
<div class="mx-auto hidden opacity-70 md:block">Showing 1 to 10 of 150 entries</div>
|
||||
<div class="mt-3 w-full sm:ml-auto sm:mt-0 sm:w-auto md:ml-0">
|
||||
<div class="relative w-56">
|
||||
<Input class="w-56 pr-10" type="text" placeholder="Search..." />
|
||||
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- BEGIN: Data List -->
|
||||
<div class="col-span-12 overflow-auto lg:overflow-visible">
|
||||
<Table class="-mt-2" variant="boxed">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead> IMAGES </TableHead>
|
||||
<TableHead> PRODUCT NAME </TableHead>
|
||||
<TableHead class="text-center"> STOCK </TableHead>
|
||||
<TableHead class="text-center"> STATUS </TableHead>
|
||||
<TableHead class="text-center"> ACTIONS </TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="(faker, index) in fakers.slice(0, 9)" :key="index">
|
||||
<TableCell>
|
||||
<div class="flex">
|
||||
<AvatarRoot class="size-11 bg-background rounded-full">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['images'][0]" :alt="'Uploaded at ' + faker['dates'][0]" />
|
||||
</AvatarRoot>
|
||||
<AvatarRoot class="size-11 bg-background rounded-full -ms-5">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['images'][1]" :alt="'Uploaded at ' + faker['dates'][1]" />
|
||||
</AvatarRoot>
|
||||
<AvatarRoot class="size-11 bg-background rounded-full -ms-5">
|
||||
<AvatarFallback>IM</AvatarFallback>
|
||||
<AvatarImage :src="faker['images'][2]" :alt="'Uploaded at ' + faker['dates'][2]" />
|
||||
</AvatarRoot>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<a class="whitespace-nowrap font-medium" href="">
|
||||
{{ faker['products'][0]!['name'] }}
|
||||
</a>
|
||||
<div class="mt-0.5 whitespace-nowrap text-xs opacity-70">
|
||||
{{ faker['products'][0]!['category'] }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
{{ faker['stocks'][0] }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center justify-center"
|
||||
:class="faker['trueFalse'][0] ? 'text-success' : 'text-danger'">
|
||||
<Lucide class="mr-2 h-4 w-4" icon="CheckSquare" />
|
||||
{{ faker['trueFalse'][0] ? 'Active' : 'Inactive' }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex items-center justify-center">
|
||||
<a class="mr-3 flex items-center" href="">
|
||||
<Lucide class="mr-1 h-4 w-4" icon="CheckSquare" />
|
||||
Edit
|
||||
</a>
|
||||
<a class="text-danger flex items-center" href="#" @click.prevent="deleteConfirmationOpen = true">
|
||||
<Lucide class="mr-1 h-4 w-4" icon="Trash" />
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<!-- END: Data List -->
|
||||
<!-- BEGIN: Pagination -->
|
||||
<div class="col-span-12 flex flex-wrap items-center sm:flex-row sm:flex-nowrap">
|
||||
<PaginationRoot :count="150" :pageSize="10" :siblingCount="1" class="w-full sm:mr-auto sm:w-auto">
|
||||
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
|
||||
<PaginationContext v-slot="{ pagination }">
|
||||
<template v-for="(page, index) in pagination?.pages" :key="index">
|
||||
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
|
||||
{{ page.value }}
|
||||
</PaginationItem>
|
||||
<PaginationEllipsis v-else :index="index" />
|
||||
</template>
|
||||
</PaginationContext>
|
||||
<PaginationNextTrigger>Next</PaginationNextTrigger>
|
||||
</PaginationRoot>
|
||||
<NativeSelect class="mt-3 w-20 sm:mt-0">
|
||||
<NativeSelectOption value="10">10</NativeSelectOption>
|
||||
<NativeSelectOption value="25">25</NativeSelectOption>
|
||||
<NativeSelectOption value="35">35</NativeSelectOption>
|
||||
<NativeSelectOption value="50">50</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
<!-- END: Pagination -->
|
||||
</div>
|
||||
<!-- BEGIN: Delete Confirmation Modal -->
|
||||
<DialogRoot :open="deleteConfirmationOpen" @openChange="(details) => (deleteConfirmationOpen = details.open)">
|
||||
<DialogContent>
|
||||
<div class="p-5 text-center">
|
||||
<Lucide class="text-danger mx-auto mt-3 size-16 stroke-1" icon="CircleX" />
|
||||
<div class="mt-5 text-2xl font-medium">Are you sure?</div>
|
||||
<div class="mt-2 opacity-70">
|
||||
Do you really want to delete these records? <br />
|
||||
This process cannot be undone.
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-5 pb-8 text-center">
|
||||
<DialogCloseTrigger class="mr-2 w-24"> Cancel </DialogCloseTrigger>
|
||||
<Button class="w-24" type="button" variant="danger" look="outline"> Delete </Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
<!-- END: Delete Confirmation Modal -->
|
||||
</div>
|
||||
</template>
|
||||