- 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.
308 lines
10 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|