335 lines
11 KiB
Vue
335 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { Head } from '@inertiajs/vue3';
|
|
import {
|
|
BellRing,
|
|
Camera,
|
|
CheckCircle2,
|
|
FlaskConical,
|
|
MapPin,
|
|
XCircle,
|
|
} from '@lucide/vue';
|
|
import { onMounted, ref, watch } from 'vue';
|
|
import { toast } from 'vue-sonner';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { usePushNotification } from '@/composables/usePushNotification';
|
|
import AccountLayout from '@/layouts/AccountLayout.vue';
|
|
|
|
type PermissionState = 'granted' | 'denied' | 'prompt' | 'unsupported';
|
|
|
|
interface PermissionItem {
|
|
key: 'notification' | 'camera' | 'location';
|
|
label: string;
|
|
description: string;
|
|
icon: typeof BellRing;
|
|
state: PermissionState;
|
|
requesting: boolean;
|
|
testing: boolean;
|
|
}
|
|
|
|
const { isSupported, isSubscribed, isLoading, subscribe, unsubscribe } =
|
|
usePushNotification();
|
|
|
|
watch(isSubscribed, (subscribed) => {
|
|
const notif = permissions.value.find((p) => p.key === 'notification');
|
|
|
|
if (!notif) {
|
|
return;
|
|
}
|
|
|
|
if (subscribed) {
|
|
notif.state = 'granted';
|
|
} else if (isSupported.value) {
|
|
notif.state = 'prompt';
|
|
}
|
|
});
|
|
|
|
const permissions = ref<PermissionItem[]>([
|
|
{
|
|
key: 'notification',
|
|
label: 'Notifikasi',
|
|
description:
|
|
'Izinkan aplikasi mengirimkan notifikasi push ke perangkat.',
|
|
icon: BellRing,
|
|
state: 'prompt',
|
|
requesting: false,
|
|
testing: false,
|
|
},
|
|
{
|
|
key: 'camera',
|
|
label: 'Kamera',
|
|
description: 'Izinkan kamera untuk melakukan presensi.',
|
|
icon: Camera,
|
|
state: 'prompt',
|
|
requesting: false,
|
|
testing: false,
|
|
},
|
|
{
|
|
key: 'location',
|
|
label: 'Lokasi',
|
|
description: 'Izinkan lokasi untuk melakukan presensi.',
|
|
icon: MapPin,
|
|
state: 'prompt',
|
|
requesting: false,
|
|
testing: false,
|
|
},
|
|
]);
|
|
|
|
async function checkPermissions() {
|
|
for (const perm of permissions.value) {
|
|
if (perm.key === 'notification') {
|
|
if (!isSupported.value) {
|
|
perm.state = 'unsupported';
|
|
} else if (isSubscribed.value) {
|
|
perm.state = 'granted';
|
|
} else {
|
|
perm.state = 'prompt';
|
|
}
|
|
} else if (perm.key === 'camera') {
|
|
if (!navigator.mediaDevices?.getUserMedia) {
|
|
perm.state = 'unsupported';
|
|
} else {
|
|
try {
|
|
const result = await navigator.permissions.query({
|
|
name: 'camera' as PermissionName,
|
|
});
|
|
perm.state = result.state as PermissionState;
|
|
} catch {
|
|
perm.state = 'prompt';
|
|
}
|
|
}
|
|
} else if (perm.key === 'location') {
|
|
if (!navigator.geolocation) {
|
|
perm.state = 'unsupported';
|
|
} else {
|
|
try {
|
|
const result = await navigator.permissions.query({
|
|
name: 'geolocation',
|
|
});
|
|
perm.state = result.state as PermissionState;
|
|
} catch {
|
|
perm.state = 'prompt';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function requestPermission(perm: PermissionItem) {
|
|
if (
|
|
perm.requesting ||
|
|
perm.state === 'denied' ||
|
|
perm.state === 'unsupported'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
perm.requesting = true;
|
|
|
|
try {
|
|
if (perm.key === 'notification') {
|
|
await subscribe();
|
|
|
|
if (isSubscribed.value) {
|
|
perm.state = 'granted';
|
|
} else if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
perm.state = 'denied';
|
|
} else {
|
|
perm.state = 'prompt';
|
|
}
|
|
} else if (perm.key === 'camera') {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
});
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
perm.state = 'granted';
|
|
} else if (perm.key === 'location') {
|
|
await new Promise<void>((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(
|
|
() => resolve(),
|
|
() => reject(),
|
|
);
|
|
});
|
|
perm.state = 'granted';
|
|
}
|
|
} catch {
|
|
perm.state = 'denied';
|
|
} finally {
|
|
perm.requesting = false;
|
|
}
|
|
}
|
|
|
|
async function handleUnsubscribeNotification(perm: PermissionItem) {
|
|
perm.requesting = true;
|
|
|
|
try {
|
|
await unsubscribe();
|
|
perm.state = 'prompt';
|
|
} catch {
|
|
// state unchanged on error
|
|
} finally {
|
|
perm.requesting = false;
|
|
}
|
|
}
|
|
|
|
async function testPermission(perm: PermissionItem) {
|
|
perm.testing = true;
|
|
|
|
try {
|
|
if (perm.key === 'notification') {
|
|
if ('serviceWorker' in navigator) {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
await registration.showNotification('Tes Notifikasi', {
|
|
body: 'Notifikasi berhasil diterima oleh perangkat Anda.',
|
|
icon: '/favicon.ico',
|
|
});
|
|
} else {
|
|
new Notification('Tes Notifikasi', {
|
|
body: 'Notifikasi berhasil diterima oleh perangkat Anda.',
|
|
icon: '/favicon.ico',
|
|
});
|
|
}
|
|
|
|
toast.success('Notifikasi tes berhasil dikirim.');
|
|
} else if (perm.key === 'camera') {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
});
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
toast.success('Kamera berhasil diakses.');
|
|
} else if (perm.key === 'location') {
|
|
await new Promise<void>((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
toast.success(
|
|
`Lokasi berhasil diambil: ${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`,
|
|
);
|
|
resolve();
|
|
},
|
|
() => reject(new Error('Gagal mengambil lokasi.')),
|
|
);
|
|
});
|
|
}
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : 'Tes gagal.');
|
|
} finally {
|
|
perm.testing = false;
|
|
}
|
|
}
|
|
|
|
const stateLabel: Record<PermissionState, string> = {
|
|
granted: 'Diizinkan',
|
|
denied: 'Ditolak',
|
|
prompt: 'Belum Diizinkan',
|
|
unsupported: 'Tidak Didukung',
|
|
};
|
|
|
|
const stateClass: Record<PermissionState, string> = {
|
|
granted: 'text-green-600 dark:text-green-400',
|
|
denied: 'text-red-500 dark:text-red-400',
|
|
prompt: 'text-muted-foreground',
|
|
unsupported: 'text-muted-foreground',
|
|
};
|
|
|
|
onMounted(() => {
|
|
checkPermissions();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Izin" />
|
|
|
|
<AccountLayout>
|
|
<div class="grid gap-4">
|
|
<Card v-for="perm in permissions" :key="perm.key">
|
|
<CardContent>
|
|
<div class="flex items-start gap-4">
|
|
<div
|
|
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"
|
|
>
|
|
<component :is="perm.icon" class="size-5" />
|
|
</div>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<p class="text-sm font-semibold">
|
|
{{ perm.label }}
|
|
</p>
|
|
<span
|
|
:class="[
|
|
'inline-flex items-center gap-1 text-xs font-medium',
|
|
stateClass[perm.state],
|
|
]"
|
|
>
|
|
<CheckCircle2
|
|
v-if="perm.state === 'granted'"
|
|
class="size-3.5"
|
|
/>
|
|
<XCircle
|
|
v-else-if="perm.state === 'denied'"
|
|
class="size-3.5"
|
|
/>
|
|
{{ stateLabel[perm.state] }}
|
|
</span>
|
|
</div>
|
|
<p class="mt-0.5 text-sm text-muted-foreground">
|
|
{{ perm.description }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex shrink-0 gap-2">
|
|
<template v-if="perm.state === 'granted'">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="perm.testing"
|
|
@click="testPermission(perm)"
|
|
>
|
|
<FlaskConical class="size-3.5" />
|
|
{{ perm.testing ? 'Mencoba...' : 'Tes' }}
|
|
</Button>
|
|
<Button
|
|
v-if="perm.key === 'notification'"
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="isLoading || perm.requesting"
|
|
@click="handleUnsubscribeNotification(perm)"
|
|
>
|
|
{{
|
|
perm.requesting || isLoading
|
|
? 'Memproses...'
|
|
: 'Nonaktifkan'
|
|
}}
|
|
</Button>
|
|
</template>
|
|
|
|
<Button
|
|
v-else-if="perm.state === 'prompt'"
|
|
size="sm"
|
|
:disabled="perm.requesting || (perm.key === 'notification' && isLoading)"
|
|
@click="requestPermission(perm)"
|
|
>
|
|
{{ (perm.requesting || (perm.key === 'notification' && isLoading)) ? 'Meminta...' : 'Izinkan' }}
|
|
</Button>
|
|
|
|
<span
|
|
v-else-if="perm.state === 'denied'"
|
|
class="text-xs text-muted-foreground"
|
|
>
|
|
Ditolak di browser
|
|
</span>
|
|
|
|
<span
|
|
v-else-if="perm.state === 'unsupported'"
|
|
class="text-xs text-muted-foreground"
|
|
>
|
|
—
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AccountLayout>
|
|
</template>
|