308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
'use client'
|
||
|
||
import React, { useEffect, useState } from 'react'
|
||
import { useSession } from 'next-auth/react'
|
||
import { useRouter } from 'next/navigation'
|
||
import Link from 'next/link'
|
||
import Cookies from 'js-cookie'
|
||
|
||
interface Address {
|
||
id: string
|
||
street: string
|
||
city: string
|
||
postcode: string
|
||
latitude: number
|
||
longitude: number
|
||
order: number
|
||
}
|
||
|
||
interface Route {
|
||
id: string
|
||
createdAt: string
|
||
status: string
|
||
addresses: Address[]
|
||
}
|
||
|
||
interface RouteClientProps {
|
||
routeId: string
|
||
}
|
||
|
||
// İki nokta arası mesafeyi hesapla (Haversine formülü)
|
||
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||
const R = 6371; // Dünya'nın yarıçapı (km)
|
||
const dLat = toRad(lat2 - lat1);
|
||
const dLon = toRad(lon2 - lon1);
|
||
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
||
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||
return Math.round(R * c * 10) / 10; // 1 ondalık basamak
|
||
}
|
||
|
||
function toRad(value: number): number {
|
||
return value * Math.PI / 180;
|
||
}
|
||
|
||
// Toplam mesafeyi hesapla
|
||
function calculateTotalDistance(addresses: any[]): number {
|
||
let total = 0;
|
||
const sortedAddresses = addresses.sort((a, b) => a.order - b.order);
|
||
|
||
for (let i = 0; i < sortedAddresses.length - 1; i++) {
|
||
const current = sortedAddresses[i];
|
||
const next = sortedAddresses[i + 1];
|
||
total += calculateDistance(
|
||
current.latitude,
|
||
current.longitude,
|
||
next.latitude,
|
||
next.longitude
|
||
);
|
||
}
|
||
|
||
return Math.round(total * 10) / 10; // 1 ondalık basamak
|
||
}
|
||
|
||
export default function RouteClient({ routeId }: RouteClientProps) {
|
||
const router = useRouter()
|
||
const { data: session, status: sessionStatus } = useSession()
|
||
const [route, setRoute] = useState<Route | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [messages, setMessages] = useState<any>(null)
|
||
const [visitedAddresses, setVisitedAddresses] = useState<Set<string>>(new Set())
|
||
|
||
useEffect(() => {
|
||
const loadMessages = async () => {
|
||
const lang = Cookies.get('NEXT_LOCALE') || 'tr'
|
||
const messages = await import(`@/messages/${lang}.json`)
|
||
setMessages(messages.default)
|
||
}
|
||
loadMessages()
|
||
}, [])
|
||
|
||
useEffect(() => {
|
||
fetchRoute()
|
||
}, [routeId])
|
||
|
||
const fetchRoute = async () => {
|
||
try {
|
||
setLoading(true)
|
||
const response = await fetch(`/api/routes/${routeId}`, {
|
||
credentials: 'include'
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(messages?.routeDetails?.error || 'Rota yüklenirken bir hata oluştu')
|
||
}
|
||
|
||
const data = await response.json()
|
||
setRoute(data)
|
||
setError(null)
|
||
} catch (err) {
|
||
console.error('Rota getirme hatası:', err)
|
||
setError(err instanceof Error ? err.message : messages?.routeDetails?.error || 'Rota yüklenirken bir hata oluştu')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const startRoute = async () => {
|
||
try {
|
||
const response = await fetch(`/api/routes/${routeId}/start`, {
|
||
method: 'POST',
|
||
credentials: 'include'
|
||
})
|
||
|
||
if (!response.ok) {
|
||
throw new Error(messages?.routeDetails?.startError || 'Rota başlatılırken bir hata oluştu')
|
||
}
|
||
|
||
await fetchRoute()
|
||
} catch (err) {
|
||
console.error('Rota başlatma hatası:', err)
|
||
setError(err instanceof Error ? err.message : messages?.routeDetails?.startError || 'Rota başlatılırken bir hata oluştu')
|
||
}
|
||
}
|
||
|
||
const handleAddressClick = async (addressId: string) => {
|
||
try {
|
||
const address = route?.addresses.find(a => a.id === addressId);
|
||
if (!address) return;
|
||
|
||
// Google Maps URL'sini oluştur
|
||
const googleMapsUrl = `https://www.google.com/maps/place/${encodeURIComponent(
|
||
`${address.street}, ${address.postcode} ${address.city}`
|
||
)}`;
|
||
|
||
// Yeni sekmede Google Maps'i aç
|
||
window.open(googleMapsUrl, '_blank', 'noopener,noreferrer');
|
||
|
||
// Ziyaret edilen adresleri güncelle
|
||
setVisitedAddresses(prev => new Set(Array.from(prev).concat(addressId)));
|
||
|
||
// Geçmişe kaydet
|
||
await fetch('/api/history', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
routeId,
|
||
addressId,
|
||
action: 'ADDRESS_VIEWED'
|
||
})
|
||
});
|
||
} catch (error) {
|
||
console.error('Adres görüntüleme hatası:', error);
|
||
}
|
||
};
|
||
|
||
if (!messages || sessionStatus === 'loading' || loading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-b-2 border-indigo-500"></div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (sessionStatus === 'unauthenticated') {
|
||
router.push('/auth/login')
|
||
return null
|
||
}
|
||
|
||
if (error || !route) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center">
|
||
<div className="text-center">
|
||
<h2 className="text-2xl font-bold mb-4">{messages.common.error}</h2>
|
||
<p className="text-red-600">{error || messages.routeDetails.notFound}</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-100 py-6">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between items-center">
|
||
<div className="flex items-center space-x-4">
|
||
<Link
|
||
href="/dashboard/routes"
|
||
className="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600"
|
||
>
|
||
{messages.common.back}
|
||
</Link>
|
||
<h1 className="text-3xl font-bold text-gray-900">{messages.routeDetails.title}</h1>
|
||
</div>
|
||
{route?.status === 'CREATED' && (
|
||
<button
|
||
onClick={startRoute}
|
||
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700"
|
||
>
|
||
{messages.routeDetails.startDrive}
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-8 bg-white shadow overflow-hidden sm:rounded-lg">
|
||
<div className="px-4 py-5 sm:px-6">
|
||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||
{messages.routeDetails.routeInfo}
|
||
</h3>
|
||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
||
{messages.routeDetails.createdAt}: {new Date(route.createdAt).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="border-t border-gray-200">
|
||
<dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
|
||
<div className="sm:col-span-1">
|
||
<dt className="text-sm font-medium text-gray-500">
|
||
{String(messages?.routeDetails?.status || 'Durum')}
|
||
</dt>
|
||
<dd className="mt-1 text-sm text-gray-900">{route?.status}</dd>
|
||
</div>
|
||
<div className="sm:col-span-1">
|
||
<dt className="text-sm font-medium text-gray-500">
|
||
{String(messages?.routeDetails?.addressCount || 'Adres Sayısı')}
|
||
</dt>
|
||
<dd className="mt-1 text-sm text-gray-900">
|
||
{route?.addresses.length}
|
||
</dd>
|
||
</div>
|
||
<div className="sm:col-span-1">
|
||
<dt className="text-sm font-medium text-gray-500">
|
||
{String(messages?.routeDetails?.optimizationStatus || 'Optimizasyon Durumu')}
|
||
</dt>
|
||
<dd className="mt-1 text-sm text-gray-900">
|
||
{route?.status === 'OPTIMIZED'
|
||
? String(messages?.routeDetails?.status?.optimized || 'Optimize Edilmiş')
|
||
: String(messages?.routeDetails?.status?.notOptimized || 'Optimize Edilmemiş')}
|
||
</dd>
|
||
</div>
|
||
<div className="sm:col-span-1">
|
||
<dt className="text-sm font-medium text-gray-500">
|
||
{String(messages?.routeDetails?.totalDistance || 'Toplam Mesafe')}
|
||
</dt>
|
||
<dd className="mt-1 text-sm text-gray-900">
|
||
{calculateTotalDistance(route.addresses)} km
|
||
</dd>
|
||
</div>
|
||
<div className="sm:col-span-1">
|
||
<dt className="text-sm font-medium text-gray-500">
|
||
{String(messages?.routeDetails?.estimatedDuration)}
|
||
</dt>
|
||
<dd className="mt-1 text-sm text-gray-900">
|
||
{Math.round(calculateTotalDistance(route.addresses) / 30 * 60)} dakika
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-8">
|
||
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
||
<div className="px-4 py-5 sm:px-6">
|
||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||
{messages.routeDetails.addresses}
|
||
</h3>
|
||
</div>
|
||
<div className="border-t border-gray-200">
|
||
<div className="space-y-4">
|
||
{route?.addresses.map((address, index) => (
|
||
<div
|
||
key={address.id}
|
||
onClick={() => handleAddressClick(address.id)}
|
||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg cursor-pointer hover:bg-gray-100"
|
||
>
|
||
<div className="flex items-center">
|
||
<span className="text-gray-500 mr-4">{index + 1}.</span>
|
||
<span className="text-gray-900">
|
||
{address.street}, {address.postcode} {address.city}
|
||
</span>
|
||
</div>
|
||
{visitedAddresses.has(address.id) && (
|
||
<svg
|
||
className="h-5 w-5 text-green-500"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M5 13l4 4L19 7"
|
||
/>
|
||
</svg>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|