diff --git a/app/Http/Controllers/api/AuthController.php b/app/Http/Controllers/api/AuthController.php index 473fc1c..a803815 100644 --- a/app/Http/Controllers/api/AuthController.php +++ b/app/Http/Controllers/api/AuthController.php @@ -76,7 +76,7 @@ class AuthController extends Controller $user = User::where('mobile', $request->mobile)->first(); if (! $user) { - return response()->json(['error' => 'User not found'], 404); + return response()->json(['error' => 'کاربر یافت نشد'], 404); } $latestCode = UserCode::where('user_id', $user->id) diff --git a/app/Http/Controllers/api/BylawApiController.php b/app/Http/Controllers/api/BylawApiController.php new file mode 100644 index 0000000..0442c16 --- /dev/null +++ b/app/Http/Controllers/api/BylawApiController.php @@ -0,0 +1,226 @@ +map(function ($category) { + return [ + 'id' => $category->id, + 'name' => $category->name, + ]; + }); + + return $this->success($categories, 'Success', 'Bylaw Categories List'); + } + + /** + * Get all Bylaw Legal Authorities + */ + public function authorities() + { + $authorities = BylawLegalAuthority::all()->map(function ($authority) { + return [ + 'id' => $authority->id, + 'name' => $authority->name, + 'authority_type' => $authority->authority_type, + ]; + }); + + return $this->success($authorities, 'Success', 'Bylaw Authorities List'); + } + + /** + * Get Paginated Bylaws (Filtered & Sorted by Approval Date) + */ + public function index(Request $request) + { + $request->validate([ + 'category_id' => 'nullable|exists:bylaw_subject_categories,id', + 'approval_authority_id' => 'nullable|exists:bylaw_legal_authorities,id', + 'bylaw_type' => 'nullable|in:executive,delegated,independent', + 'sort' => 'nullable|in:asc,desc', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true); + + if ($request->filled('category_id')) { + $query->where('category_id', $request->category_id); + } + + if ($request->filled('approval_authority_id')) { + $query->where('approval_authority_id', $request->approval_authority_id); + } + + if ($request->filled('bylaw_type')) { + $query->where('bylaw_type', $request->bylaw_type); + } + + $sortOrder = $request->input('sort', 'desc'); + $query->orderBy('approval_date', $sortOrder); + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $paginator = $query->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatBylaw($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Bylaws Fetched'); + } + + /** + * Search Bylaws by Keyword + */ + public function search(Request $request) + { + $request->validate([ + 'search' => 'nullable|string', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true); + + if ($request->filled('search')) { + $searchTerm = $request->input('search'); + + $query->where(function ($q) use ($searchTerm) { + $q->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('description', 'LIKE', "%{$searchTerm}%") + ->orWhere('promulgation_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('official_gazette_number', 'LIKE', "%{$searchTerm}%"); + }); + } + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + + // Default search sort to newest first + $paginator = $query->orderBy('approval_date', 'desc')->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatBylaw($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Search Results Fetched'); + } + + /** + * Contextual Pager API + */ + public function pager($id) + { + $current = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true) + ->find($id); + + if (!$current) { + return $this->failed([], [ + 'title' => 'خطا', + 'message' => 'آیین‌نامه مورد نظر یافت نشد.' + ], 404); + } + + $previousItem = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true) + ->where(function ($query) use ($current) { + if ($current->approval_date) { + $query->where('approval_date', '<', $current->approval_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('approval_date', '=', $current->approval_date) + ->where('id', '<', $current->id); + }); + } else { + $query->whereNull('approval_date')->where('id', '<', $current->id); + } + }) + ->orderBy('approval_date', 'desc') + ->orderBy('id', 'desc') + ->first(); + + $nextItem = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true) + ->where(function ($query) use ($current) { + if ($current->approval_date) { + $query->where('approval_date', '>', $current->approval_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('approval_date', '=', $current->approval_date) + ->where('id', '>', $current->id); + }); + } else { + $query->whereNotNull('approval_date') + ->orWhere(function ($subQuery) use ($current) { + $subQuery->whereNull('approval_date')->where('id', '>', $current->id); + }); + } + }) + ->orderBy('approval_date', 'asc') + ->orderBy('id', 'asc') + ->first(); + + $results = array_values(array_filter([ + $previousItem ? $this->formatBylaw($previousItem) : null, + $this->formatBylaw($current), + $nextItem ? $this->formatBylaw($nextItem) : null + ])); + + return $this->success($results, 'Success', 'Pager Window Details'); + } + + /** + * Helper to uniformly format the Bylaw Document payload + */ + private function formatBylaw($item) + { + return [ + 'id' => $item->id, + 'title' => $item->title, + 'category_id' => $item->category_id, + 'category_name' => optional($item->category)->name, + 'approval_authority_name' => optional($item->approvalAuthority)->name, + 'approval_date' => $item->approval_date, + 'promulgation_authority_name'=> optional($item->promulgationAuthority)->name, + 'promulgation_date' => $item->promulgation_date, + 'promulgation_number' => $item->promulgation_number, + 'official_gazette_number' => $item->official_gazette_number, + 'official_gazette_date' => $item->official_gazette_date, + 'execution_date' => $item->execution_date, + 'bylaw_type' => $item->bylaw_type, + 'global_status' => $item->global_status, + 'pdf_file_url' => $item->pdf_file_path ? asset('storage/' . $item->pdf_file_path) : null, + 'description' => $item->description, + ]; + } +} diff --git a/app/Http/Controllers/api/GlobalSearchApiController.php b/app/Http/Controllers/api/GlobalSearchApiController.php new file mode 100644 index 0000000..405992d --- /dev/null +++ b/app/Http/Controllers/api/GlobalSearchApiController.php @@ -0,0 +1,275 @@ +validate([ + 'search' => 'nullable|string', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + $searchTerm = $request->input('search'); + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $page = (int) $request->input('page', 1); + + $results = collect(); + + if (!empty($searchTerm)) { + $limitPerTable = 100; // Performance safeguard + + // 1. Search Bylaw Documents + $bylaws = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) + ->where('is_published', true) + ->where(function ($q) use ($searchTerm) { + $q->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('description', 'LIKE', "%{$searchTerm}%") + ->orWhere('promulgation_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('official_gazette_number', 'LIKE', "%{$searchTerm}%"); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatBylaw($item); + $formatted['type'] = 'bylaw'; + return $formatted; + }); + $results = $results->merge($bylaws); + + // 2. Search Judicial Precedents + $judicials = JudicialPrecedent::with('category') + ->where(function ($q) use ($searchTerm) { + $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%"); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatJudicial($item); + $formatted['type'] = 'judicial_precedent'; + return $formatted; + }); + $results = $results->merge($judicials); + + // 3. Search Legal Opinions + $opinions = LegalOpinion::with('category') + ->where(function ($q) use ($searchTerm) { + $q->where('opinion_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%") + ->orWhere('issuing_authority', 'LIKE', "%{$searchTerm}%"); + })->take($limitPerTable)->get()->map(function($item) { + $formatted = $this->formatOpinion($item); + $formatted['type'] = 'legal_opinion'; + return $formatted; + }); + $results = $results->merge($opinions); + + // 4. Search Laws, Arts, and Structural Models + $structuralModels = [ + Law::class, Volum::class, Book::class, Division::class, + Section::class, Chapter::class, Part::class, Gate::class, + Branch::class, Art::class, + ]; + + foreach ($structuralModels as $modelClass) { + $query = $modelClass::query(); + $modelInstance = new $modelClass; + $table = $modelInstance->getTable(); + + if ($table === 'art') { + $query->where('title', 'LIKE', "%{$searchTerm}%") + ->orWhere('text', 'LIKE', "%{$searchTerm}%"); + } else { + $query->where('title', 'LIKE', "%{$searchTerm}%"); + } + + $items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table, $searchTerm) { + if ($table === 'art') { + $formatted = $this->formatArt($item, $searchTerm, $modelClass); + $formatted['type'] = 'art'; + } else { + $formatted = $this->formatStructure($item, $modelClass); + // Standardize the structural type names (laws, sections, etc.) + $formatted['type'] = ($table === 'section') ? 'sections' : $table; + } + return $formatted; + }); + + $results = $results->merge($items); + } + } + + // --- Execute Manual Pagination on the Combined Collection --- + $total = $results->count(); + // Slice the array for the exact page requested + $paginatedItems = $results->forPage($page, $perPage)->values(); + + // Build a proper Laravel paginator object so the response metadata remains identical + $paginator = new LengthAwarePaginator( + $paginatedItems, + $total, + $perPage, + $page, + ['path' => $request->url(), 'query' => $request->query()] + ); + + $responseData = [ + 'items' => $paginator->items(), + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Global Search Results Fetched'); + } + + /* ----------------------------------------------------------------- + * FORMATTING HELPERS + * ----------------------------------------------------------------- */ + + private function formatBylaw($item) + { + return [ + 'id' => $item->id, + 'title' => $item->title, + 'category_name' => optional($item->category)->name, + 'approval_authority_name' => optional($item->approvalAuthority)->name, + 'approval_date' => $item->approval_date, + 'promulgation_number' => $item->promulgation_number, + 'description' => $item->description, + ]; + } + + private function formatJudicial($item) + { + return [ + 'id' => $item->id, + 'title' => $item->subject, // Map subject to title for universal UI parsing + 'ruling_number' => $item->ruling_number, + 'ruling_date' => $item->ruling_date, + 'text' => $item->full_text, + 'issuing_authority' => $item->issuing_authority, + 'category_name' => optional($item->category)->name, + ]; + } + + private function formatOpinion($item) + { + return [ + 'id' => $item->id, + 'title' => $item->subject, // Map subject to title for universal UI parsing + 'opinion_number' => $item->opinion_number, + 'opinion_date' => $item->opinion_date, + 'text' => $item->full_text, + 'issuing_authority' => $item->issuing_authority, + 'category_name' => optional($item->category)->name, + ]; + } + + private function formatArt($item, $searchTerm, $modelClass) + { + $law = Law::find($item->law_id); + $isLocked = $this->isLockedForCurrentUser($law); + + // Text snippet highlighting logic (from ArtController) + $text = $item->text ?? ''; + $searchQuoted = preg_quote($searchTerm, '/'); + if (!empty($text) && preg_match('/\b(.{0,50})\b('.$searchQuoted.')\b(.{0,50})\b/i', $text, $matches)) { + $context = trim($matches[1] ?? '') . ' ' . $matches[2] . ' ' . trim($matches[3] ?? ''); + } else { + $context = $text; + } + + return [ + 'id' => $item->id, + 'title' => $item->title, + 'text' => $isLocked ? null : $context, + 'is_locked' => $isLocked, + 'route' => array_values($this->route($modelClass, $item)), + 'category' => optional($law->category)->name, + 'law' => $law?->title, + 'count_art' => $law?->arts?->count() ?? 0, + 'count_volums'=> $law?->volums?->count() ?? 0, + 'price' => $law?->price, + 'image' => $law?->image, + ]; + } + + private function formatStructure($item, $modelClass) + { + $law = Law::find($item?->law_id ?? $item->id); + $isLocked = $this->isLockedForCurrentUser($law); + + $payload = [ + 'id' => $item->id, + 'title' => $item->title, + 'number' => $item->number, + 'is_locked' => $isLocked, + 'route' => array_values($this->route($modelClass, $item)), + 'law' => $law?->title ?? '', + ]; + + if ((new $modelClass)->getTable() == 'laws') { + $payload['category'] = optional($law?->category)?->name ?? ''; + $payload['image'] = $law?->image; + } + + return $payload; + } + + /* ----------------------------------------------------------------- + * UTILITY METHODS + * ----------------------------------------------------------------- */ + + private function isLockedForCurrentUser(?Law $law): bool + { + return ! auth()->user()->isSubscriber() && (bool) $law?->is_locked; + } + + private function route($modelClass, $query) + { + if ((new $modelClass)->getTable() == 'art') { + $route = array_filter([ + Law::find($query->law_id)?->title, + Volum::find($query->volum_id)?->title, + Book::find($query->book_id)?->title, + Division::find($query->division_id)?->title, + Section::find($query->section_id)?->title, + Chapter::find($query->chapter_id)?->title, + Part::find($query->part_id)?->title, + Gate::find($query->gate_id)?->title, + Branch::find($query->branch_id)?->title, + ]); + return array_values($route); + } + + if ((new $modelClass)->getTable() == 'laws') { + return array_values(array_filter([Law::find($query->id)?->title])); + } + + return []; + } +} diff --git a/app/Http/Controllers/api/HomeController.php b/app/Http/Controllers/api/HomeController.php index d0c9cf1..b6a5ad5 100644 --- a/app/Http/Controllers/api/HomeController.php +++ b/app/Http/Controllers/api/HomeController.php @@ -19,6 +19,139 @@ class HomeController extends Controller { } + public function homeV2() + { + $user = auth()->user(); + $this->refreshMarketSubscriptionAfterResponse($user); + + $recent = RecentArt::query()->where('user_id', $user->id)->get()->map(function ($q) { + return [ + 'id' => $q->law?->id, + 'name' => $q->law?->title, + 'is_locked' => $q->law?->is_locked, + ]; + }); + + $laws = Law::orderBy('created_at')->get()->map(function ($q) { + return [ + 'id' => $q->id, + 'name' => $q->title, + 'is_locked' => $q->is_locked, + ]; + }); + + $categories = ['hagigi', 'kifari']; + $lawsByCategory = []; + + foreach ($categories as $category) { + $lawsByCategory[$category] = Law::whereHas('category', function ($q) use ($category) { + $q->where('type', $category); + })->get()->map(function ($q) { + return [ + 'id' => $q->id, + 'title' => $q->title, + 'is_locked' => $q->is_locked, + 'category_id' => $q->category_id, + 'price' => $q->price, + 'image' => $q->image, + 'type' => 'law', + ]; + }); + } + + $free_law = Law::where('is_locked', false)->orderBy('created_at')->get()->map(function ($q) { + return [ + 'id' => $q->id, + 'name' => $q->title, + 'is_locked' => $q->is_locked, + ]; + }); + + $freeSubscription = $user->userSubscribers() + ->whereHas('subscribe', function ($query) { + $query->where('is_free', true); + }) + ->where('expired_at', '>=', now()) + ->first(); + + $current_plan = null; + + if ($freeSubscription) { + $current_plan = [ + 'id' => $freeSubscription->id, + 'name' => $freeSubscription->subscribe->name, + 'price' => $freeSubscription->subscribe->price, + 'expired_day' => $freeSubscription->expired_at->diffInDays(now()), + 'is_free' => true, + ]; + } + + $latestSubscription = $user->userSubscribers() + ->where('expired_at', '>=', now()) + ->latest('expired_at') + ->first(); + + $expiredDays = UserSubscriber::query()->where('user_id', $user->id)->where('expired_at', '>=', now())->get()->sum(function ($subscriber) { + return $subscriber->expired_at->diffInDays(now()); + }); + + if ($latestSubscription) { + $current_plan = [ + 'id' => $latestSubscription->id, + 'name' => $latestSubscription->subscribe->name ?? 'Subscription', + 'price' => $latestSubscription->subscribe->price ?? 100, + 'expired_day' => $expiredDays, + 'is_free' => (bool)$latestSubscription->is_free, + ]; + } + + $unread_notifications_count = Notification::unreadForUser($user->id)->count(); + + $homeData = [ + 'recent' => $recent, + 'laws' => $lawsByCategory, + 'last_law' => $laws, + 'free' => $free_law, + 'current_plan' => $current_plan, + 'unread_notifications_count' => $unread_notifications_count, + ]; + + return $this->success([ + 'home' => $homeData, + 'hoqoqi_list' => $this->getCategoryList(1), // Category 1 + 'keyfari_list' => $this->getCategoryList(2), // Category 2 + ]); + } + + private function getCategoryList(int $categoryId) + { + $type = $categoryId === 1 ? 'hagigi' : 'kifari'; + + $categories = Category::with(['laws'])->where('type', $type)->get()->pluck('id'); + $items = Law::query()->whereIn('category_id', $categories)->get(); + + $data = []; + $user = auth()->user(); + + foreach ($items as $item) { + $data[] = [ + 'id' => $item->id, + 'title' => $item->title, + 'number' => $item->number, + 'type' => 'laws', + 'is_locked' => $user->isSubscriber() !== false ? false : Law::where('id', $item->id)->first()?->is_locked, + 'image' => $item?->image, + 'law' => $item?->title, + 'count_art' => $item->arts->count(), + 'count_volums' => $item->volums->count(), + 'price' => $item->price, + ]; + } + + return $data; + } + + public function index() { $user = auth()->user(); @@ -102,7 +235,7 @@ class HomeController extends Controller 'name' => $latestSubscription->subscribe->name ?? 'Subscription', 'price' => $latestSubscription->subscribe->price ?? 100, 'expired_day' => $expiredDays, - 'is_free' => (bool) $latestSubscription->is_free, + 'is_free' => (bool)$latestSubscription->is_free, ]; } @@ -123,7 +256,7 @@ class HomeController extends Controller $cacheKey = "home-market-refresh-user-{$user->id}"; $this->refreshMarketSubscription($user); - if (! Cache::add($cacheKey, true, now()->addHours(24))) { + if (!Cache::add($cacheKey, true, now()->addHours(24))) { return; } @@ -166,6 +299,6 @@ class HomeController extends Controller return true; } - return ! $subscriber->last_verified_at || $subscriber->last_verified_at->lessThan(now()->subHours(12)); + return !$subscriber->last_verified_at || $subscriber->last_verified_at->lessThan(now()->subHours(12)); } } diff --git a/app/Http/Controllers/api/JudicialPrecedentApiController.php b/app/Http/Controllers/api/JudicialPrecedentApiController.php new file mode 100644 index 0000000..0e2aa5f --- /dev/null +++ b/app/Http/Controllers/api/JudicialPrecedentApiController.php @@ -0,0 +1,160 @@ +map(function ($category) { + return [ + 'id' => $category->id, + 'name' => $category->name, + ]; + }); + + return $this->success($categories, 'Success', 'Categories List'); + } + + public function index(Request $request) + { + $request->validate([ + 'category_id' => 'nullable|exists:judicial_precedent_categories,id', + 'sort' => 'nullable|in:asc,desc', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = JudicialPrecedent::query(); + + if ($request->filled('category_id')) { + $query->where('category_id', $request->category_id); + } + + $sortOrder = $request->input('sort', 'desc'); + $query->orderBy('ruling_date', $sortOrder); + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $paginator = $query->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatItem($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Judicial Precedents Fetched'); + } + + /** + * Search Judicial Precedents by Keyword + */ + public function search(Request $request) + { + $request->validate([ + 'search' => 'nullable|string', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = JudicialPrecedent::query(); + + if ($request->filled('search')) { + $searchTerm = $request->input('search'); + + $query->where(function ($q) use ($searchTerm) { + $q->where('ruling_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%"); + }); + } + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $paginator = $query->orderBy('ruling_date', 'desc')->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatItem($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Search Results Fetched'); + } + + public function pager($id) + { + $current = JudicialPrecedent::find($id); + + if (!$current) { + return $this->failed([], [ + 'title' => 'خطا', + 'message' => 'رأی وحدت رویه مورد نظر یافت نشد.' + ], 404); + } + + $previousItem = JudicialPrecedent::where(function ($query) use ($current) { + $query->where('ruling_date', '<', $current->ruling_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('ruling_date', '=', $current->ruling_date) + ->where('id', '<', $current->id); + }); + }) + ->orderBy('ruling_date', 'desc') + ->orderBy('id', 'desc') + ->first(); + + $nextItem = JudicialPrecedent::where(function ($query) use ($current) { + $query->where('ruling_date', '>', $current->ruling_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('ruling_date', '=', $current->ruling_date) + ->where('id', '>', $current->id); + }); + }) + ->orderBy('ruling_date', 'asc') + ->orderBy('id', 'asc') + ->first(); + + $results = array_values(array_filter([ + $previousItem ? $this->formatItem($previousItem) : null, + $this->formatItem($current), + $nextItem ? $this->formatItem($nextItem) : null + ])); + + return $this->success($results, 'Success', 'Pager Window Details'); + } + + /** + * Helper method to format payload uniformly + */ + private function formatItem($item) + { + return [ + 'id' => $item->id, + 'ruling_number' => $item->ruling_number, + 'ruling_date' => $item->ruling_date, + 'subject' => $item->subject, + 'full_text' => $item->full_text, + 'issuing_authority' => $item->issuing_authority, + 'category_id' => $item->category_id, + 'category_name' => optional($item->category)->name, + ]; + } +} diff --git a/app/Http/Controllers/api/LegalOpinionApiController.php b/app/Http/Controllers/api/LegalOpinionApiController.php new file mode 100644 index 0000000..029a9a3 --- /dev/null +++ b/app/Http/Controllers/api/LegalOpinionApiController.php @@ -0,0 +1,172 @@ +map(function ($category) { + return [ + 'id' => $category->id, + 'name' => $category->name, + ]; + }); + + return $this->success($categories, 'Success', 'Categories List'); + } + + public function index(Request $request) + { + $request->validate([ + 'category_id' => 'nullable|exists:legal_opinion_categories,id', + 'sort' => 'nullable|in:asc,desc', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = LegalOpinion::query(); + + if ($request->filled('category_id')) { + $query->where('category_id', $request->category_id); + } + + $sortOrder = $request->input('sort', 'desc'); + $query->orderBy('opinion_date', $sortOrder); + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $paginator = $query->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatItem($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Legal Opinions Fetched'); + } + + /** + * Search Legal Opinions by Keyword + */ + public function search(Request $request) + { + $request->validate([ + 'search' => 'nullable|string', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + $query = LegalOpinion::query(); + + if ($request->filled('search')) { + $searchTerm = $request->input('search'); + + $query->where(function ($q) use ($searchTerm) { + $q->where('opinion_number', 'LIKE', "%{$searchTerm}%") + ->orWhere('subject', 'LIKE', "%{$searchTerm}%") + ->orWhere('full_text', 'LIKE', "%{$searchTerm}%") + ->orWhere('issuing_authority', 'LIKE', "%{$searchTerm}%"); + }); + } + + $perPage = min(max((int) $request->input('per_page', 15), 10), 100); + $paginator = $query->orderBy('opinion_date', 'desc')->paginate($perPage); + + $items = collect($paginator->items())->map(function ($item) { + return $this->formatItem($item); + }); + + $responseData = [ + 'items' => $items, + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + ]; + + return $this->success($responseData, 'Success', 'Search Results Fetched'); + } + + public function pager($id) + { + $current = LegalOpinion::find($id); + + if (!$current) { + return $this->failed([], [ + 'title' => 'خطا', + 'message' => 'نظریه قانونی مورد نظر یافت نشد.' + ], 404); + } + + $previousItem = LegalOpinion::where(function ($query) use ($current) { + if ($current->opinion_date) { + $query->where('opinion_date', '<', $current->opinion_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('opinion_date', '=', $current->opinion_date) + ->where('id', '<', $current->id); + }); + } else { + $query->whereNull('opinion_date')->where('id', '<', $current->id); + } + }) + ->orderBy('opinion_date', 'desc') + ->orderBy('id', 'desc') + ->first(); + + $nextItem = LegalOpinion::where(function ($query) use ($current) { + if ($current->opinion_date) { + $query->where('opinion_date', '>', $current->opinion_date) + ->orWhere(function ($subQuery) use ($current) { + $subQuery->where('opinion_date', '=', $current->opinion_date) + ->where('id', '>', $current->id); + }); + } else { + $query->whereNotNull('opinion_date') + ->orWhere(function ($subQuery) use ($current) { + $subQuery->whereNull('opinion_date')->where('id', '>', $current->id); + }); + } + }) + ->orderBy('opinion_date', 'asc') + ->orderBy('id', 'asc') + ->first(); + + $results = array_values(array_filter([ + $previousItem ? $this->formatItem($previousItem) : null, + $this->formatItem($current), + $nextItem ? $this->formatItem($nextItem) : null + ])); + + return $this->success($results, 'Success', 'Pager Window Details'); + } + + /** + * Helper method to format payload uniformly + */ + private function formatItem($item) + { + return [ + 'id' => $item->id, + 'opinion_number' => $item->opinion_number, + 'opinion_date' => $item->opinion_date, + 'subject' => $item->subject, + 'full_text' => $item->full_text, + 'issuing_authority' => $item->issuing_authority, + 'category_id' => $item->category_id, + 'category_name' => optional($item->category)->name, + ]; + } +} diff --git a/app/Http/Controllers/api/NoteController.php b/app/Http/Controllers/api/NoteController.php index 00d86fa..16e0ece 100644 --- a/app/Http/Controllers/api/NoteController.php +++ b/app/Http/Controllers/api/NoteController.php @@ -29,7 +29,7 @@ class NoteController extends Controller ] ); - return $this->success([], 'successfully created.'); + return $this->success([], 'با موفقیت اضافه شد'); } public function update($id, Request $request) @@ -45,7 +45,7 @@ class NoteController extends Controller 'color_code' => $validated['color_code'], ]); - return $this->success([], 'successfully updated.'); + return $this->success([], 'بروز شد'); } public function destroy($id) @@ -53,6 +53,6 @@ class NoteController extends Controller $note = Note::findOrFail($id); $note->delete(); - return $this->success([], 'successfully deleted.'); + return $this->success([], 'حذف شد'); } } diff --git a/app/Http/Controllers/api/PayController.php b/app/Http/Controllers/api/PayController.php index 82aa9bc..627523c 100644 --- a/app/Http/Controllers/api/PayController.php +++ b/app/Http/Controllers/api/PayController.php @@ -72,14 +72,14 @@ class PayController extends Controller 'expired_at' => $expiredAt, ]); - return $this->success([], 'Payment Successful', 'Subscription successfully activated.'); + return $this->success([], 'موفق', 'اشتراک با موفقیت فعال شد'); } - return $this->failed([], ['title' => 'Payment Failed', 'message' => 'Payment verification failed.']); + return $this->failed([], ['title' => 'پرداخت ناموفق', 'message' => 'پرداخت ناموفق']); } catch (\Exception $e) { Log::info('error in callback function', ['error' => $e->getMessage()]); - return $this->failed([], ['title' => 'Payment Failed', 'message' => 'Payment verification failed.']); + return $this->failed([], ['title' => 'پرداخت ناموفق', 'message' => 'پرداخت ناموفق']); } } diff --git a/app/Http/Controllers/api/SubscribePlanController.php b/app/Http/Controllers/api/SubscribePlanController.php index 9e34eca..15158ad 100644 --- a/app/Http/Controllers/api/SubscribePlanController.php +++ b/app/Http/Controllers/api/SubscribePlanController.php @@ -62,7 +62,7 @@ class SubscribePlanController extends Controller ->first(); if ($activeSubscription && ! $activeSubscription?->subscribe?->is_free && ! $subscribePlan->is_free) { - return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'You already have an active subscription. Please wait until it expires.']); + return $this->failed(null, ['title' => 'اشتراک', 'message' => 'شما درحال حاظر اشتراک فعال دارید']); } if ($subscribePlan->is_free) { @@ -72,7 +72,7 @@ class SubscribePlanController extends Controller })->exists(); if ($hasUsedFreePlan) { - return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'You have already used the free plan.']); + return $this->failed(null, ['title' => 'اشتراک', 'message' => 'شما قبلا اشتراک رایگان فعال کردید']); } $expiredAt = now()->addDays($subscribePlan->expired_day + 1); @@ -82,7 +82,7 @@ class SubscribePlanController extends Controller 'expired_at' => $expiredAt, ]); - return $this->success(null, 'Subscribe Plan', 'Free Subscribe Plan Successfully Activated'); + return $this->success(null, 'اشتراک', 'اشتراک رایگان شما فعال شد'); } try { @@ -133,7 +133,7 @@ class SubscribePlanController extends Controller ->exists(); if ($hasUsedFreePlan) { - return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'You have already used the free plan.']); + return $this->failed(null, ['title' => 'اشتراک', 'message' => 'شما قبلا اشتراک رایگان فعال کردید']); } $expiredAt = $activeSubscription ? $activeSubscription->expired_at->addDays($subscribePlan->expired_day) : now()->addDays($subscribePlan->expired_day); @@ -144,7 +144,7 @@ class SubscribePlanController extends Controller 'is_free' => true, ]); - return $this->success(null, 'Subscribe Plan', 'Free Subscribe Plan Successfully Activated'); + return $this->success(null, 'اشتراک', 'اشتراک رایگان فعال شد'); } $subscription_id = $request->input('subscription_id'); @@ -177,7 +177,7 @@ class SubscribePlanController extends Controller 'is_free' => false, ]); - return $this->success(null, 'Subscribe Plan', 'Subscription successfully activated.'); + return $this->success(null, 'اشتراک', 'اشتراک شما فعال شد'); } catch (\Exception $e) { Log::error('Error in subscription', ['Error' => $e->getMessage()]); diff --git a/package-lock.json b/package-lock.json index fa36f41..0a9cc40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "persian-ping", + "name": "law-api", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/routes/api.php b/routes/api.php index 9ba1a90..4e070a2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -30,6 +30,7 @@ Route::prefix('v1')->group(function () { Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::get('home', [HomeController::class, 'index']); + Route::post('home-v2', [HomeController::class, 'homeV2']); Route::get('notifications', [NotificationController::class, 'index']); Route::get('art', [ArtController::class, 'index']); Route::get('categories', [CategoriesController::class, 'index']); @@ -69,4 +70,26 @@ Route::prefix('v1')->middleware('auth:sanctum')->group(function () { Route::delete('folders/{id}/art/{art_id}', [FolderController::class, 'delete_art']); Route::post('suggestions', [SuggestionController::class, 'index']); + + + Route::get('judicial-precedents/categories', [\App\Http\Controllers\api\JudicialPrecedentApiController::class, 'categories']); + Route::get('judicial-precedents', [\App\Http\Controllers\api\JudicialPrecedentApiController::class, 'index']); + Route::get('judicial-precedents/{id}/pager', [\App\Http\Controllers\api\JudicialPrecedentApiController::class, 'pager']); + Route::post('judicial-precedents/search', [\App\Http\Controllers\api\JudicialPrecedentApiController::class, 'search']); // NEW + + Route::get('legal-opinions/categories', [\App\Http\Controllers\api\LegalOpinionApiController::class, 'categories']); + Route::get('legal-opinions', [\App\Http\Controllers\api\LegalOpinionApiController::class, 'index']); + Route::get('legal-opinions/{id}/pager', [\App\Http\Controllers\api\LegalOpinionApiController::class, 'pager']); + Route::post('legal-opinions/search', [\App\Http\Controllers\api\LegalOpinionApiController::class, 'search']); // NEW + + Route::get('bylaws/categories', [\App\Http\Controllers\api\BylawApiController::class, 'categories']); + Route::get('bylaws/authorities', [\App\Http\Controllers\api\BylawApiController::class, 'authorities']); + Route::get('bylaws', [\App\Http\Controllers\api\BylawApiController::class, 'index']); + Route::get('bylaws/{id}/pager', [\App\Http\Controllers\api\BylawApiController::class, 'pager']); + Route::post('bylaws/search', [\App\Http\Controllers\api\BylawApiController::class, 'search']); + + + + Route::post('global-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'search']); + });