dstpabuaran.com/resources/js/pages/settings/permissions.tsx
Yoga Pangestu e7aa582572 feat: add cutting management functionality with CRUD operations
- Implemented CuttingIndex component for listing and managing cuttings.
- Added routes for cutting management in web.php.
- Created CuttingTest for testing cutting-related features including authorization, validation, and stock management.
- Updated roles create and edit pages to include necessary imports.
- Refactored settings and profile pages to streamline imports.
- Enhanced permissions checks for cutting management actions.
2026-08-04 02:24:11 +07:00

308 lines
10 KiB
TypeScript

import { Head, usePage } from '@inertiajs/react';
import { Bell, Camera, MapPin } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Switch } from '@/components/ui/switch';
import { usePushNotification } from '@/hooks/use-push-notification';
import { edit } from '@/routes/permissions';
type PermissionStatus = 'granted' | 'denied' | 'prompt' | 'unknown';
const isPWA = () => {
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as { standalone?: boolean }).standalone === true
);
};
const getPlatform = (): 'android' | 'ios' | 'desktop' => {
const ua = navigator.userAgent.toLowerCase();
if (/android/.test(ua)) {
return 'android';
}
if (/iphone|ipad|ipod/.test(ua)) {
return 'ios';
}
return 'desktop';
};
const getDeniedMessage = (appName: string): string => {
if (!isPWA()) {
return 'Izin ditolak. Klik ikon gembok di address bar untuk mengaktifkannya.';
}
const platform = getPlatform();
switch (platform) {
case 'android':
return `Izin ditolak. Buka Pengaturan > Aplikasi > ${appName} > Izin untuk mengaktifkannya.`;
case 'ios':
return `Izin ditolak. Buka Pengaturan > ${appName} > Safari > Izin untuk mengaktifkannya.`;
default:
return `Izin ditolak. Buka Pengaturan sistem untuk mengaktifkan izin ${appName}.`;
}
};
function getInitialNotificationPermission(): PermissionStatus {
if (!('Notification' in window)) {
return 'denied';
}
return Notification.permission as PermissionStatus;
}
function getInitialCameraPermission(): PermissionStatus {
if (!navigator.mediaDevices) {
return 'denied';
}
return 'unknown';
}
function getInitialLocationPermission(): PermissionStatus {
if (!('geolocation' in navigator)) {
return 'denied';
}
return 'unknown';
}
export default function Permissions() {
const { vapidPublicKey } = usePage().props as { vapidPublicKey?: string };
const [notifications, setNotifications] = useState<PermissionStatus>(
getInitialNotificationPermission,
);
const [camera, setCamera] = useState<PermissionStatus>(
getInitialCameraPermission,
);
const [location, setLocation] = useState<PermissionStatus>(
getInitialLocationPermission,
);
const [isSubscribing, setIsSubscribing] = useState(false);
const { isSupported, requestPermission, subscribe, unsubscribe } =
usePushNotification();
const appName = 'DST Collection';
const checkCameraPermission = useCallback(async () => {
if (!navigator.mediaDevices) {
setCamera('denied');
return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
});
stream.getTracks().forEach((track) => track.stop());
setCamera('granted');
} catch {
if ('permissions' in navigator) {
try {
const result = await navigator.permissions.query({
name: 'camera' as PermissionName,
});
setCamera(result.state as PermissionStatus);
} catch {
setCamera('denied');
}
} else {
setCamera('denied');
}
}
}, []);
const checkLocationPermission = useCallback(async () => {
if (!('geolocation' in navigator)) {
setLocation('denied');
return;
}
navigator.geolocation.getCurrentPosition(
() => setLocation('granted'),
async () => {
if ('permissions' in navigator) {
try {
const result = await navigator.permissions.query({
name: 'geolocation',
});
setLocation(result.state as PermissionStatus);
} catch {
setLocation('denied');
}
} else {
setLocation('denied');
}
},
);
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- permission check on mount is safe
checkCameraPermission();
checkLocationPermission();
}, [checkCameraPermission, checkLocationPermission]);
const handleNotifications = async (checked: boolean) => {
if (isSubscribing) {
return;
}
if (checked) {
if (!isSupported || !('Notification' in window)) {
return;
}
if (Notification.permission === 'denied') {
setNotifications('denied');
return;
}
setIsSubscribing(true);
try {
const result = await requestPermission();
setNotifications(result);
if (result === 'granted' && vapidPublicKey) {
await subscribe(vapidPublicKey);
}
} finally {
setIsSubscribing(false);
}
} else {
await unsubscribe();
setNotifications('prompt');
}
};
const handleCamera = async (checked: boolean) => {
if (checked) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
});
stream.getTracks().forEach((track) => track.stop());
setCamera('granted');
} catch {
setCamera('denied');
}
} else {
setCamera('prompt');
}
};
const handleLocation = async (checked: boolean) => {
if (checked) {
navigator.geolocation.getCurrentPosition(
() => setLocation('granted'),
() => setLocation('denied'),
);
} else {
setLocation('prompt');
}
};
const isDenied = (status: PermissionStatus) => status === 'denied';
const deniedMessage = getDeniedMessage(appName);
return (
<>
<Head title="Izin" />
<h1 className="sr-only">Izin</h1>
<div className="flex h-full flex-1 flex-col gap-4 overflow-x-auto px-4 md:px-6">
<Alert
variant={
isDenied(notifications) ? 'destructive' : 'default'
}
>
<Bell className="h-4 w-4" />
<div className="flex items-center justify-between">
<div className="flex-1">
<AlertTitle>Notifikasi</AlertTitle>
<AlertDescription>
Izinkan aplikasi mengirimkan notifikasi push ke
perangkat Anda.
</AlertDescription>
{isDenied(notifications) && (
<p className="mt-1 text-sm text-destructive">
{deniedMessage}
</p>
)}
</div>
<div className="flex items-center gap-2">
<Switch
checked={notifications === 'granted'}
onCheckedChange={handleNotifications}
disabled={
isDenied(notifications) || isSubscribing
}
/>
</div>
</div>
</Alert>
<Alert variant={isDenied(camera) ? 'destructive' : 'default'}>
<Camera className="h-4 w-4" />
<div className="flex items-center justify-between">
<div className="flex-1">
<AlertTitle>Kamera</AlertTitle>
<AlertDescription>
Izinkan aplikasi mengakses kamera perangkat Anda
untuk mengambil foto atau video.
</AlertDescription>
{isDenied(camera) && (
<p className="mt-1 text-sm text-destructive">
{deniedMessage}
</p>
)}
</div>
<div className="flex items-center gap-2">
<Switch
checked={camera === 'granted'}
onCheckedChange={handleCamera}
disabled={isDenied(camera)}
/>
</div>
</div>
</Alert>
<Alert variant={isDenied(location) ? 'destructive' : 'default'}>
<MapPin className="h-4 w-4" />
<div className="flex items-center justify-between">
<div className="flex-1">
<AlertTitle>Lokasi</AlertTitle>
<AlertDescription>
Izinkan aplikasi mengakses lokasi perangkat Anda
untuk menyediakan layanan berbasis lokasi.
</AlertDescription>
{isDenied(location) && (
<p className="mt-1 text-sm text-destructive">
{deniedMessage}
</p>
)}
</div>
<div className="flex items-center gap-2">
<Switch
checked={location === 'granted'}
onCheckedChange={handleLocation}
disabled={isDenied(location)}
/>
</div>
</div>
</Alert>
</div>
</>
);
}