From a173f17cae2669f1306b09d060667be6e958eeae Mon Sep 17 00:00:00 2001 From: "m.razaghi" Date: Tue, 14 Jul 2026 00:10:53 +0330 Subject: [PATCH] updated apis --- .../Controllers/api/BylawApiController.php | 33 +++++----- .../Controllers/api/BylawNoteController.php | 55 ++++++++++++++++ .../api/GlobalSearchApiController.php | 62 +++++++------------ .../api/JudicialPrecedentApiController.php | 38 ++++++------ .../api/JudicialPrecedentNoteController.php | 57 +++++++++++++++++ .../api/LegalOpinionApiController.php | 55 ++++++++-------- .../api/LegalOpinionNoteController.php | 56 +++++++++++++++++ app/Models/BylawNote.php | 11 ++++ app/Models/JudicialPrecedentNote.php | 11 ++++ app/Models/LegalOpinionNote.php | 11 ++++ routes/api.php | 17 +++++ 11 files changed, 304 insertions(+), 102 deletions(-) create mode 100644 app/Http/Controllers/api/BylawNoteController.php create mode 100644 app/Http/Controllers/api/JudicialPrecedentNoteController.php create mode 100644 app/Http/Controllers/api/LegalOpinionNoteController.php create mode 100644 app/Models/BylawNote.php create mode 100644 app/Models/JudicialPrecedentNote.php create mode 100644 app/Models/LegalOpinionNote.php diff --git a/app/Http/Controllers/api/BylawApiController.php b/app/Http/Controllers/api/BylawApiController.php index a52c5df..ca5c674 100644 --- a/app/Http/Controllers/api/BylawApiController.php +++ b/app/Http/Controllers/api/BylawApiController.php @@ -79,7 +79,6 @@ class BylawApiController extends Controller $perPage = min(max((int) $request->input('per_page', 15), 10), 100); $paginator = $query->paginate($perPage); - // واکشی دسته‌جمعی و ایمنِ گره‌ها با استفاده از Eloquent $docIds = collect($paginator->items())->pluck('id')->map(fn($id) => (int)$id)->toArray(); $nodesByDoc = collect(); @@ -121,6 +120,12 @@ class BylawApiController extends Controller $formatted = $this->formatBylaw($bylaw); + // --- FETCH NOTE EXACTLY LIKE ART CONTROLLER --- + $formatted['note'] = \App\Models\BylawNote::select('id', 'note', 'color_code', 'created_at') + ->where('user_id', auth()->user()->id) + ->where('bylaw_document_id', $bylaw->id) + ->get(); + if (!$formatted['is_locked']) { $eloquentNodes = BylawNode::with('type') ->where('document_id', $bylaw->id) @@ -340,7 +345,15 @@ class BylawApiController extends Controller $results = collect($itemsToFormat)->map(function($item) use ($nodesByDoc) { $itemNodes = $nodesByDoc->get((int)$item->id, collect()); - return $this->formatBylaw($item, $itemNodes); + $formatted = $this->formatBylaw($item, $itemNodes); + + // --- FETCH NOTE EXACTLY LIKE ART CONTROLLER --- + $formatted['note'] = \App\Models\BylawNote::select('id', 'note', 'color_code', 'created_at') + ->where('user_id', auth()->user()->id) + ->where('bylaw_document_id', $item->id) + ->get(); + + return $formatted; })->values()->toArray(); return $this->success($results, 'Success', 'Pager Window Details'); @@ -354,22 +367,16 @@ class BylawApiController extends Controller return (bool) ($item->is_locked ?? false); } - /** - * متد قدرتمند برای استخراج ایمن متون - */ private function formatBylaw($item, $itemNodes = null) { $isLocked = $this->isLockedForCurrentUser($item); - // واکشی در صورت خالی بودن کالکشن if ($itemNodes === null) { $itemNodes = BylawNode::where('document_id', $item->id)->orderBy('sort_order', 'asc')->get(); } - // استخراج محتوا و تبدیل امنِ Encoding (ضد باگِ JSON Encode لاراول) - $nodeContentAsDescription = $itemNodes->pluck('node_content') + $nodeContentAsDescription = collect($itemNodes)->pluck('node_content') ->map(function ($text) { - // اطمینان از حذف فاصله‌ها و اعمال انکودینگ استاندارد برای جلوگیری از باگ‌های نامرئی $safeText = trim((string)$text); return mb_convert_encoding($safeText, 'UTF-8', 'UTF-8'); }) @@ -378,7 +385,6 @@ class BylawApiController extends Controller }) ->implode("\n\n"); - // اگر همچنان خالی ماند (فقط در صورتی که دیتابیس واقعاً رکورد نداشته باشد) if (empty($nodeContentAsDescription)) { $nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8'); } @@ -401,13 +407,8 @@ class BylawApiController extends Controller 'bylaw_type' => $item->bylaw_type, 'global_status' => $item->global_status, 'pdf_file_url' => $isLocked ? null : ($item->pdf_file_path ? asset('storage/' . $item->pdf_file_path) : null), - - // نتیجه استخراج ۱۰۰٪ در اینجا می‌نشیند 'description' => $isLocked ? null : $nodeContentAsDescription, - - // 🚨 پارامتر عیب‌یابی: این عدد نشان می‌دهد چند گره متصل به این فایل در دیتابیس پیدا شد - 'debug_node_count' => $itemNodes->count(), - + 'debug_node_count' => collect($itemNodes)->count(), 'is_locked' => $isLocked, ]; } diff --git a/app/Http/Controllers/api/BylawNoteController.php b/app/Http/Controllers/api/BylawNoteController.php new file mode 100644 index 0000000..1408db2 --- /dev/null +++ b/app/Http/Controllers/api/BylawNoteController.php @@ -0,0 +1,55 @@ +validate([ + 'bylaw_document_id' => 'required', + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + BylawNote::query()->create([ + 'user_id' => auth()->user()->id, + 'bylaw_document_id' => $validated['bylaw_document_id'], + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'با موفقیت اضافه شد'); + } + + public function update($id, Request $request) + { + $validated = $request->validate([ + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + $note = BylawNote::findOrFail($id); + $note->update([ + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'بروز شد'); + } + + public function destroy($id) + { + $note = BylawNote::findOrFail($id); + $note->delete(); + + return $this->success([], 'حذف شد'); + } +} diff --git a/app/Http/Controllers/api/GlobalSearchApiController.php b/app/Http/Controllers/api/GlobalSearchApiController.php index f4afe07..6e53f12 100644 --- a/app/Http/Controllers/api/GlobalSearchApiController.php +++ b/app/Http/Controllers/api/GlobalSearchApiController.php @@ -90,7 +90,7 @@ class GlobalSearchApiController extends Controller } // ========================================================= - // 3. Search Bylaw Documents (With 100% Bulletproof Node Fetching) + // 3. Search Bylaw Documents // ========================================================= $bylawsQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) ->where('is_published', true) @@ -103,7 +103,7 @@ class GlobalSearchApiController extends Controller $subQuery->select('document_id') ->from('bylaw_nodes') ->where('node_content', 'LIKE', "%{$searchTerm}%") - ->orWhere('title', 'LIKE', "%{$searchTerm}%"); // Fixed: Title added! + ->orWhere('title', 'LIKE', "%{$searchTerm}%"); }); })->take($limitPerTable)->get(); @@ -265,7 +265,7 @@ class GlobalSearchApiController extends Controller } } - // 3. Bylaw Documents (With 100% Bulletproof Node Fetching) + // 3. Bylaw Documents $bylawsQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']) ->where('is_published', true) ->where(function ($q) use ($searchTerm) { @@ -284,7 +284,7 @@ class GlobalSearchApiController extends Controller $totalCount += $bylawsQuery->count(); $bylawsRaw = $bylawsQuery->take($limitPerTable)->get(); - // Safely bulk-fetch nodes using DB facade + // Bulk-fetch nodes safely using DB facade $docIds = $bylawsRaw->pluck('id')->toArray(); $allNodes = collect(); if (!empty($docIds)) { @@ -346,46 +346,35 @@ class GlobalSearchApiController extends Controller private function isLockedForCurrentUserGeneric($item): bool { - return ! auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false); + if (auth()->check() && auth()->user() && method_exists(auth()->user(), 'isSubscriber')) { + return !auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false); + } + return (bool) ($item->is_locked ?? false); } /** - * BULLETPROOF DESCRIPTION GENERATOR (Matches BylawApiController Exactly) + * Exact 1:1 match with BylawApiController's encoding approach */ private function formatBylaw($item, $itemNodes = null) { $isLocked = $this->isLockedForCurrentUserGeneric($item); - $description = $item->description; - // Check if description is practically empty (less than 5 chars = garbage HTML) - $cleanDesc = trim(str_replace([' ', '‌', "\r", "\n", "\t", ' '], '', strip_tags($description ?? ''))); + if ($itemNodes === null) { + $itemNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->orderBy('sort_order', 'asc')->get(); + } - if (strlen($cleanDesc) < 5) { + $nodeContentAsDescription = collect($itemNodes)->pluck('node_content') + ->map(function ($text) { + $safeText = trim((string)$text); + return mb_convert_encoding($safeText, 'UTF-8', 'UTF-8'); + }) + ->filter(function ($text) { + return $text !== ''; + }) + ->implode("\n\n"); - // Absolute Safety Net: If somehow $itemNodes is still null or empty array, FORCE a direct query. - if ($itemNodes === null || (is_object($itemNodes) && $itemNodes->isEmpty())) { - // Verify against DB just in case - $dbNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->count(); - if ($dbNodes > 0) { - $itemNodes = DB::table('bylaw_nodes') - ->where('document_id', $item->id) - ->orderBy('sort_order', 'asc') - ->get(); - } - } - - $nodeContents = []; - if ($itemNodes) { - foreach ($itemNodes as $node) { - // EXPLICITLY GET node_content - $content = trim(strip_tags($node->node_content ?? '')); - if (!empty($content)) { - $nodeContents[] = $content; - } - } - } - - $description = implode("\n\n", $nodeContents); + if (empty($nodeContentAsDescription)) { + $nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8'); } return [ @@ -395,7 +384,7 @@ class GlobalSearchApiController extends Controller 'approval_authority_name' => optional($item->approvalAuthority)->name, 'approval_date' => $item->approval_date, 'promulgation_number' => $item->promulgation_number, - 'description' => $isLocked ? null : $description, + 'description' => $isLocked ? null : $nodeContentAsDescription, 'is_locked' => $isLocked, ]; } @@ -432,9 +421,6 @@ class GlobalSearchApiController extends Controller ]; } - /** - * Exact 1:1 match with ArtController format mapping - */ private function formatArt($item, $searchTerm, $modelClass) { $law = $item->law ?? Law::find($item->law_id); diff --git a/app/Http/Controllers/api/JudicialPrecedentApiController.php b/app/Http/Controllers/api/JudicialPrecedentApiController.php index 98732d5..a7f7541 100644 --- a/app/Http/Controllers/api/JudicialPrecedentApiController.php +++ b/app/Http/Controllers/api/JudicialPrecedentApiController.php @@ -146,38 +146,38 @@ class JudicialPrecedentApiController extends Controller $current = JudicialPrecedent::find($id); if (!$current) { - return $this->failed([], [ - 'title' => 'خطا', - 'message' => 'رأی وحدت رویه مورد نظر یافت نشد.' - ], 404); + 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); + $subQuery->where('ruling_date', '=', $current->ruling_date)->where('id', '<', $current->id); }); - }) - ->orderBy('ruling_date', 'desc') - ->orderBy('id', 'desc') - ->first(); + })->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); + $subQuery->where('ruling_date', '=', $current->ruling_date)->where('id', '>', $current->id); }); - }) - ->orderBy('ruling_date', 'asc') - ->orderBy('id', 'asc') - ->first(); + })->orderBy('ruling_date', 'asc')->orderBy('id', 'asc')->first(); + + // Apply format and add Notes + $formatWithNote = function($item) { + if (!$item) return null; + $formatted = $this->formatItem($item); + $formatted['note'] = \App\Models\JudicialPrecedentNote::select('id', 'note', 'color_code', 'created_at') + ->where('user_id', auth()->user()->id) + ->where('judicial_precedent_id', $item->id) + ->get(); + return $formatted; + }; $results = array_values(array_filter([ - $previousItem ? $this->formatItem($previousItem) : null, - $this->formatItem($current), - $nextItem ? $this->formatItem($nextItem) : null + $previousItem ? $formatWithNote($previousItem) : null, + $formatWithNote($current), + $nextItem ? $formatWithNote($nextItem) : null ])); return $this->success($results, 'Success', 'Pager Window Details'); diff --git a/app/Http/Controllers/api/JudicialPrecedentNoteController.php b/app/Http/Controllers/api/JudicialPrecedentNoteController.php new file mode 100644 index 0000000..5634ea0 --- /dev/null +++ b/app/Http/Controllers/api/JudicialPrecedentNoteController.php @@ -0,0 +1,57 @@ +validate([ + 'judicial_precedent_id' => 'required', + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + JudicialPrecedentNote::query()->create([ + 'user_id' => auth()->id(), + 'judicial_precedent_id' => $validated['judicial_precedent_id'], + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'با موفقیت اضافه شد'); + } + + public function update($id, Request $request) + { + $validated = $request->validate([ + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + // SECURITY FIX: Only allow the owner of the note to update it + $note = JudicialPrecedentNote::where('user_id', auth()->id())->findOrFail($id); + + $note->update([ + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'بروز شد'); + } + + public function destroy($id) + { + $note = JudicialPrecedentNote::where('user_id', auth()->id())->findOrFail($id); + $note->delete(); + + return $this->success([], 'حذف شد'); + } +} diff --git a/app/Http/Controllers/api/LegalOpinionApiController.php b/app/Http/Controllers/api/LegalOpinionApiController.php index a6ff5e6..fe4e933 100644 --- a/app/Http/Controllers/api/LegalOpinionApiController.php +++ b/app/Http/Controllers/api/LegalOpinionApiController.php @@ -147,49 +147,46 @@ class LegalOpinionApiController extends Controller $current = LegalOpinion::find($id); if (!$current) { - return $this->failed([], [ - 'title' => 'خطا', - 'message' => 'نظریه قانونی مورد نظر یافت نشد.' - ], 404); + 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); - }); + $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(); + })->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); - }); + $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); - }); + $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(); + })->orderBy('opinion_date', 'asc')->orderBy('id', 'asc')->first(); + + // Apply format and add Notes + $formatWithNote = function($item) { + if (!$item) return null; + $formatted = $this->formatItem($item); + $formatted['note'] = \App\Models\LegalOpinionNote::select('id', 'note', 'color_code', 'created_at') + ->where('user_id', auth()->user()->id) + ->where('legal_opinion_id', $item->id) + ->get(); + return $formatted; + }; $results = array_values(array_filter([ - $previousItem ? $this->formatItem($previousItem) : null, - $this->formatItem($current), - $nextItem ? $this->formatItem($nextItem) : null + $previousItem ? $formatWithNote($previousItem) : null, + $formatWithNote($current), + $nextItem ? $formatWithNote($nextItem) : null ])); return $this->success($results, 'Success', 'Pager Window Details'); diff --git a/app/Http/Controllers/api/LegalOpinionNoteController.php b/app/Http/Controllers/api/LegalOpinionNoteController.php new file mode 100644 index 0000000..81ae189 --- /dev/null +++ b/app/Http/Controllers/api/LegalOpinionNoteController.php @@ -0,0 +1,56 @@ +validate([ + 'legal_opinion_id' => 'required', + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + LegalOpinionNote::query()->create([ + 'user_id' => auth()->id(), // Cleaner way to get the user ID + 'legal_opinion_id' => $validated['legal_opinion_id'], + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'با موفقیت اضافه شد'); + } + + public function update($id, Request $request) + { + $validated = $request->validate([ + 'note' => 'required', + 'color_code' => 'nullable', + ]); + + $note = LegalOpinionNote::where('user_id', auth()->id())->findOrFail($id); + + $note->update([ + 'note' => $validated['note'], + 'color_code' => $validated['color_code'], + ]); + + return $this->success([], 'بروز شد'); + } + + public function destroy($id) + { + $note = LegalOpinionNote::where('user_id', auth()->id())->findOrFail($id); + $note->delete(); + + return $this->success([], 'حذف شد'); + } +} diff --git a/app/Models/BylawNote.php b/app/Models/BylawNote.php new file mode 100644 index 0000000..d7d8668 --- /dev/null +++ b/app/Models/BylawNote.php @@ -0,0 +1,11 @@ +middleware('auth:sanctum')->group(function () { // Global Search Route::post('global-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'search']); Route::post('global-search/fast-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'fast_search']); // NEW + + // Bylaw Notes + Route::post('/bylaw-notes', [\App\Http\Controllers\api\BylawNoteController::class, 'create']); + Route::put('/bylaw-notes/{id}', [\App\Http\Controllers\api\BylawNoteController::class, 'update']); + Route::delete('/bylaw-notes/{id}', [\App\Http\Controllers\api\BylawNoteController::class, 'destroy']); + + // Judicial Precedent Notes + Route::post('/judicial-notes', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'create']); + Route::put('/judicial-notes/{id}', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'update']); + Route::delete('/judicial-notes/{id}', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'destroy']); + + // Legal Opinion Notes + Route::post('/opinion-notes', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'create']); + Route::put('/opinion-notes/{id}', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'update']); + Route::delete('/opinion-notes/{id}', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'destroy']); + + });