with('specializations'); // Filter by specialization if provided if ($request->has('specialization_id') && $request->specialization_id) { $query->whereHas('specializations', function ($q) use ($request) { $q->where('tailor_specializations.id', $request->specialization_id); }); } // Get user's current peminatan if logged in as customer $userPreferred = []; $user = Auth::user(); if ($user && $user->role === 'pelanggan') { $userPreferred = $user->preferredSpecializations->pluck('id')->toArray(); } // Get tailors $tailors = $query->get(); // Add distance if customer has coordinates if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude) { foreach ($tailors as $tailor) { if ($tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } } // Sort by distance if available $tailors = $tailors->sortBy('distance'); } return $this->sendResponse([ 'tailors' => $tailors->values(), 'user_preferred' => $userPreferred ], 'Data penjahit berhasil diambil'); } catch (\Exception $e) { return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mencari penjahit: ' . $e->getMessage()], 500); } } /** * Get recommended tailors based on customer preferences using Content-Based Filtering */ public function getRecommended() { try { // Log untuk debug \Log::info('Accessing getRecommended method'); $user = Auth::user(); // Check if user is customer if (!$user || $user->role !== 'pelanggan') { \Log::warning('User not authorized: ', ['user' => $user ? $user->toArray() : 'null']); return $this->sendError('Unauthorized.', ['error' => 'Anda harus login sebagai pelanggan'], 403); } // Get user's preferred specializations $preferredSpecIds = $user->preferredSpecializations->pluck('id')->toArray(); \Log::info('User preferred specializations: ', ['preferred' => $preferredSpecIds]); // Get all available specializations for feature vector preparation $allSpecializations = TailorSpecialization::all()->pluck('id')->toArray(); // Get all tailors $allTailors = User::where('role', 'penjahit') ->with(['specializations', 'ratings']) ->get(); if (empty($preferredSpecIds)) { // If no preferences, return popular tailors \Log::info('No preferences found, returning popular tailors'); $tailors = User::where('role', 'penjahit') ->withCount([ 'bookings' => function ($query) { $query->where('status', 'selesai'); } ]) ->orderBy('bookings_count', 'desc') ->with(['specializations', 'ratings']) ->limit(10) ->get(); } else { // Mengubah preferensi user menjadi vektor $userProfile = $this->createFeatureVector($preferredSpecIds, $allSpecializations); // Calculate similarity scores for all tailors $tailorsWithScores = []; foreach ($allTailors as $tailor) { $tailorSpecIds = $tailor->specializations->pluck('id')->toArray(); $tailorProfile = $this->createFeatureVector($tailorSpecIds, $allSpecializations); // Calculate cosine similarity between user and tailor profiles $similarityScore = $this->calculateCosineSimilarity($userProfile, $tailorProfile); // Only include tailors with some similarity if ($similarityScore > 0) { $tailor->similarity_score = $similarityScore; $tailorsWithScores[] = $tailor; } } // Sort tailors by similarity score $tailors = collect($tailorsWithScores)->sortByDesc('similarity_score'); // If no similar tailors found, return popular ones if ($tailors->isEmpty()) { $tailors = User::where('role', 'penjahit') ->withCount([ 'bookings' => function ($query) { $query->where('status', 'selesai'); } ]) ->orderBy('bookings_count', 'desc') ->with(['specializations', 'ratings']) ->limit(10) ->get(); } } \Log::info('Tailors found: ', ['count' => $tailors->count()]); // Add distance and rating information foreach ($tailors as $tailor) { // Calculate distance if coordinates available if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } // Calculate average rating $ratings = $tailor->ratings; if ($ratings->count() > 0) { $tailor->rating_info = [ 'average_rating' => round($ratings->avg('rating'), 1), 'total_reviews' => $ratings->count(), 'reviews' => $ratings->map(function ($rating) { return [ 'rating' => $rating->rating, 'review' => $rating->review, 'created_at' => $rating->created_at, 'customer' => [ 'id' => $rating->customer->id, 'name' => $rating->customer->name, 'profile_photo' => $rating->customer->profile_photo ] ]; }) ]; } else { $tailor->rating_info = [ 'average_rating' => 0, 'total_reviews' => 0, 'reviews' => [] ]; } } // If user has location, use hybrid approach combining similarity and distance if ($user->latitude && $user->longitude) { // Normalize scores for hybrid ranking $maxDistance = $tailors->max('distance') ?: 1; $tailors = $tailors->map(function ($tailor) use ($maxDistance) { // Calculate normalized distance score (1 when closest, 0 when furthest) if ($tailor->distance !== null) { $tailor->distance_score = 1 - ($tailor->distance / $maxDistance); } else { $tailor->distance_score = 0; } // Hybrid score (70% similarity, 30% proximity) $tailor->hybrid_score = isset($tailor->similarity_score) ? ($tailor->similarity_score * 0.7) + ($tailor->distance_score * 0.3) : $tailor->distance_score; return $tailor; })->sortByDesc('hybrid_score'); } return $this->sendResponse([ 'tailors' => $tailors->values(), 'user_preferred' => $preferredSpecIds ], 'Rekomendasi penjahit berhasil diambil'); } catch (\Exception $e) { \Log::error('Content-Based Filtering error: ' . $e->getMessage() . "\n" . $e->getTraceAsString()); return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mendapatkan rekomendasi: ' . $e->getMessage()], 500); } } /** * Create binary feature vector based on specializations */ private function createFeatureVector($specializations, $allSpecializations) { $vector = []; foreach ($allSpecializations as $specId) { $vector[$specId] = in_array($specId, $specializations) ? 1 : 0; } return $vector; } /** * Calculate cosine similarity between two feature vectors */ private function calculateCosineSimilarity($vectorA, $vectorB) { $dotProduct = 0; $magnitudeA = 0; $magnitudeB = 0; foreach ($vectorA as $key => $valueA) { $valueB = $vectorB[$key] ?? 0; $dotProduct += $valueA * $valueB; $magnitudeA += $valueA * $valueA; $magnitudeB += $valueB * $valueB; } $magnitudeA = sqrt($magnitudeA); $magnitudeB = sqrt($magnitudeB); if ($magnitudeA == 0 || $magnitudeB == 0) { return 0; } return $dotProduct / ($magnitudeA * $magnitudeB); } /** * Get dashboard data including nearby and recommended tailors */ public function getDashboardData(Request $request) { try { $user = Auth::user();//Periksa apakah user yang login adalah pelanggan? if (!$user || !$user->isPelanggan()) { return $this->sendError('Unauthorized.', [], 401); } // Get nearby tailors- Mendapatkan Penjahit terdekat $nearbyTailors = User::where('role', 'penjahit') ->whereNotNull('latitude') ->whereNotNull('longitude') ->with(['specializations', 'services']) ->get() ->map(function ($tailor) use ($user) { if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } return $tailor; }) ->sortBy('distance') ->take(5); // Get recommended tailors based on user's preferences $preferredSpecIds = $user->preferredSpecializations() ->select('tailor_specializations.id') ->pluck('tailor_specializations.id') ->toArray(); $recommendedTailors = collect([]); if (!empty($preferredSpecIds)) { $recommendedTailors = User::where('role', 'penjahit') ->with(['specializations', 'services']) ->whereHas('specializations', function ($query) use ($preferredSpecIds) { $query->whereIn('tailor_specializations.id', $preferredSpecIds); }) ->withCount(['bookings' => function ($query) { $query->where('status', 'selesai'); }]) ->orderBy('bookings_count', 'desc') ->take(5) ->get() ->map(function ($tailor) use ($user) { if ($user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } return $tailor; }); } return $this->sendResponse([ 'nearby_tailors' => $nearbyTailors->values(), 'recommended_tailors' => $recommendedTailors->values(), 'user_preferences' => $user->preferredSpecializations ], 'Dashboard data retrieved successfully.'); } catch (\Exception $e) { \Log::error('Dashboard error: ' . $e->getMessage()); return $this->sendError('Error retrieving dashboard data.', ['error' => $e->getMessage()], 500); } } /** * Calculate distance between two points in kilometers * Fungsi untuk perhitungan Jarak untuk mencari penjahit Terdekat */ private function calculateDistance($lat1, $lon1, $lat2, $lon2) { $earthRadius = 6371; // Jari-jari bumi dalam kilometer $latDelta = deg2rad($lat2 - $lat1); $lonDelta = deg2rad($lon2 - $lon1); $a = sin($latDelta / 2) * sin($latDelta / 2) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($lonDelta / 2) * sin($lonDelta / 2); $c = 2 * atan2(sqrt($a), sqrt(1 - $a)); return round($earthRadius * $c, 2); } /** * Get detailed information about a tailor */ public function getTailorDetail($id) { try { // Log untuk debug \Log::info('Accessing getTailorDetail method', ['id' => $id]); $tailor = User::where('id', $id) ->where('role', 'penjahit') ->with([ 'specializations', 'services' => function($query) { $query->where('is_available', true); }, 'gallery' ]) ->withCount([ 'bookings as completed_orders' => function($query) { $query->where('status', 'selesai'); } ]) ->first(); \Log::info('Tailor query result:', ['tailor' => $tailor ? 'found' : 'not found']); if (!$tailor) { return $this->sendError('Not found.', ['error' => 'Penjahit tidak ditemukan'], 404); } // Get average rating $avgRating = DB::table('tailor_ratings') ->where('tailor_id', $id) ->avg('rating'); $tailor->average_rating = round($avgRating ?? 0, 1); // Calculate distance if user is logged in and has coordinates $user = Auth::user(); if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude && $tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } // Get gallery photos $gallery = \App\Models\TailorGallery::where('user_id', $id) ->orderBy('created_at', 'desc') ->get(); // Prepare response data $responseData = $tailor->toArray(); $responseData['gallery'] = $gallery; return $this->sendResponse($responseData, 'Detail penjahit berhasil diambil'); } catch (\Exception $e) { \Log::error('Error in getTailorDetail', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mengambil detail penjahit: ' . $e->getMessage()], 500); } } /** * Search tailors by name * * @param string $name * @return \Illuminate\Http\Response */ public function searchByName($name) { try { if (!$name) { return $this->sendError('Error.', ['error' => 'Parameter nama diperlukan'], 400); } $query = User::where('role', 'penjahit') ->where('name', 'LIKE', "%{$name}%") ->with('specializations'); // Get tailors $tailors = $query->get(); // Add distance if customer is logged in and has coordinates $user = Auth::user(); if ($user && $user->role === 'pelanggan' && $user->latitude && $user->longitude) { foreach ($tailors as $tailor) { if ($tailor->latitude && $tailor->longitude) { $tailor->distance = $this->calculateDistance( $user->latitude, $user->longitude, $tailor->latitude, $tailor->longitude ); } else { $tailor->distance = null; } } // Sort by distance if available $tailors = $tailors->sortBy('distance'); } return $this->sendResponse([ 'tailors' => $tailors->values() ], 'Data penjahit berhasil diambil'); } catch (\Exception $e) { return $this->sendError('Error.', ['error' => 'Terjadi kesalahan saat mencari penjahit: ' . $e->getMessage()], 500); } } }