Compare commits
126 Commits
34c80e87ac
...
clean-main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b5d218f42 | ||
|
|
0771bb16f2 | ||
|
|
85e1ca9d89 | ||
|
|
db3f6d6553 | ||
|
|
c73990b629 | ||
|
|
6a204c69ac | ||
|
|
641332e5ba | ||
|
|
76d9246e0e | ||
|
|
7cb3978775 | ||
|
|
f31f0806ae | ||
|
|
a6b2a1ba30 | ||
|
|
f84ef8c067 | ||
|
|
4c6eb52716 | ||
|
|
b1c5345d22 | ||
|
|
8b853a6f8f | ||
|
|
16fd292976 | ||
|
|
464ed71360 | ||
|
|
27b365c7e2 | ||
|
|
452bd77f84 | ||
|
|
f358c151af | ||
|
|
94aab63db6 | ||
|
|
5d392ff50b | ||
|
|
4e5ef028dc | ||
|
|
500ef019a3 | ||
|
|
def51f6c63 | ||
|
|
88813a4f86 | ||
|
|
b041736a22 | ||
|
|
c3f53f08f2 | ||
|
|
ab0ab84a49 | ||
|
|
dda5a87980 | ||
|
|
fcaa51626d | ||
|
|
d7f6131a33 | ||
|
|
52a13dcf6e | ||
|
|
ec76bc9737 | ||
|
|
255889b202 | ||
|
|
f801ca9eed | ||
|
|
1be9ed98b8 | ||
|
|
6c157e79e4 | ||
|
|
47dfbba165 | ||
|
|
bd4789a85c | ||
|
|
01d6584e71 | ||
|
|
2d6766760f | ||
|
|
bb69e7f953 | ||
|
|
d705c7c56d | ||
|
|
768a0aaeb0 | ||
|
|
0fffd2eb3d | ||
|
|
041a3e7bd5 | ||
|
|
af21c1c77e | ||
|
|
5aa2447e23 | ||
|
|
74ead9d0ed | ||
|
|
aad799d29f | ||
|
|
6e68ed45e4 | ||
|
|
067c1d2351 | ||
|
|
0ae3343b17 | ||
|
|
516bc4b26b | ||
|
|
d77605db75 | ||
|
|
a2799a8d1b | ||
|
|
86c68951bb | ||
|
|
cf0f8f0c3f | ||
|
|
209b1f2530 | ||
|
|
a6ae5724f6 | ||
|
|
a173f17cae | ||
|
|
39e85df27a | ||
|
|
2d6ba2a70d | ||
|
|
abdfe190a7 | ||
|
|
032ae6ebf8 | ||
|
|
2474f268d1 | ||
|
|
7f744beec4 | ||
|
|
7a987a33c9 | ||
|
|
7ca903ec3c | ||
|
|
56e57719eb | ||
|
|
680cf9c4fd | ||
|
|
375e3b166c | ||
|
|
2fab528d0a | ||
|
|
677e101181 | ||
|
|
e041b85e74 | ||
|
|
aad67585ef | ||
| 4f94af7fc4 | |||
|
|
dcea6423ff | ||
| ee8d9cdb8f | |||
|
|
51c4570f97 | ||
| 6062d0b3a0 | |||
|
|
19db0fd24d | ||
|
|
4d15014621 | ||
|
|
6c0b1f83f2 | ||
|
|
cad3fa9e1f | ||
|
|
1a3f41ca8a | ||
|
|
e4dad6f9a4 | ||
| 16e68c3048 | |||
|
|
10463e2f71 | ||
| ed95f5b3ee | |||
|
|
1eedb30acc | ||
| 0962801c96 | |||
|
|
b2ac3d6ec8 | ||
| ecccab9edc | |||
|
|
737f06f68e | ||
| 370f4c0a78 | |||
|
|
a95826e5e5 | ||
|
|
398fcb4eec | ||
| 4214a4b007 | |||
|
|
9d124a07c1 | ||
|
|
ffea0e3c11 | ||
| 2a6313802b | |||
|
|
3b9fcd76df | ||
| d47f148f12 | |||
|
|
d19d4b8667 | ||
|
|
3b47d78ee8 | ||
|
|
71a6f683a7 | ||
|
|
71dc7da8a4 | ||
| 4ed77dd060 | |||
|
|
b7f139b935 | ||
| 0b6851ebe2 | |||
| 80a271d325 | |||
|
|
83ff22b5a4 | ||
| d27c32c918 | |||
|
|
ce5ccc00a1 | ||
| f242b8936a | |||
| 9cf6957675 | |||
| 2e26bd229b | |||
| e695a80e36 | |||
| ece9e6e017 | |||
| c3d798689c | |||
| 2796ce7de7 | |||
| c05587474f | |||
| 8fda92ead5 | |||
| ae522c5640 |
31
.env
Normal file
31
.env
Normal file
@@ -0,0 +1,31 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://law.ghaafapp.ir
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=error
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=law
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=sH1382@@
|
||||
|
||||
BROADCAST_DRIVER=pusher
|
||||
CACHE_DRIVER=file
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
|
||||
PUSHER_APP_ID=2184469
|
||||
PUSHER_APP_KEY=ecd88d278bd51427da98
|
||||
PUSHER_APP_SECRET=3b1cb32eb9ddda591f65
|
||||
PUSHER_HOST=
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
PUSHER_APP_CLUSTER=eu
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,7 +5,6 @@
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
@@ -17,4 +16,4 @@ yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
*.DS_Store
|
||||
*.DS_Store
|
||||
|
||||
@@ -31,37 +31,41 @@ class AddSubscriptionDays extends Command
|
||||
|
||||
if ($days <= 0) {
|
||||
$this->error('Days must be a positive integer.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($onlyWithoutSubscription && !$allUsers) {
|
||||
if ($onlyWithoutSubscription && ! $allUsers) {
|
||||
$this->error('The --only-without-subscription option can only be used with --all-users.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($allUsers) {
|
||||
$planId = $this->option('plan-id');
|
||||
if (!$planId) {
|
||||
if (! $planId) {
|
||||
$this->error('When using --all-users you must provide --plan-id for users without a subscription.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$plan = SubscribePlan::find($planId);
|
||||
if (!$plan) {
|
||||
if (! $plan) {
|
||||
$this->error('Subscribe plan not found for the given --plan-id.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
if ($allUsers) {
|
||||
if ($onlyWithoutSubscription) {
|
||||
$this->info("Granting {$days} day(s) only to users without a subscription." . ($dryRun ? ' (dry-run)' : ''));
|
||||
$this->info("Granting {$days} day(s) only to users without a subscription.".($dryRun ? ' (dry-run)' : ''));
|
||||
} else {
|
||||
$this->info("Granting {$days} day(s) to every user." . ($dryRun ? ' (dry-run)' : ''));
|
||||
$this->info("Granting {$days} day(s) to every user.".($dryRun ? ' (dry-run)' : ''));
|
||||
}
|
||||
} else {
|
||||
$scopeLabel = $includeExpired ? 'all' : 'active';
|
||||
$this->info("Extending {$scopeLabel} subscriptions by {$days} day(s)." . ($dryRun ? ' (dry-run)' : ''));
|
||||
$this->info("Extending {$scopeLabel} subscriptions by {$days} day(s).".($dryRun ? ' (dry-run)' : ''));
|
||||
}
|
||||
|
||||
$touched = 0;
|
||||
@@ -100,7 +104,7 @@ class AddSubscriptionDays extends Command
|
||||
});
|
||||
} else {
|
||||
$query = UserSubscriber::query();
|
||||
if (!$includeExpired) {
|
||||
if (! $includeExpired) {
|
||||
$query->where('expired_at', '>', now());
|
||||
}
|
||||
|
||||
|
||||
32
app/Events/MatchEnded.php
Normal file
32
app/Events/MatchEnded.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MatchEnded implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $matchId;
|
||||
|
||||
public function __construct($matchId)
|
||||
{
|
||||
$this->matchId = $matchId;
|
||||
}
|
||||
|
||||
public function broadcastOn()
|
||||
{
|
||||
// ارسال پیام به کانال پرایوت بازی که هر دو نفر داخل آن هستند
|
||||
return new PrivateChannel('pvp-match.' . $this->matchId);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'match.ended';
|
||||
}
|
||||
}
|
||||
35
app/Events/MatchStarted.php
Normal file
35
app/Events/MatchStarted.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MatchStarted implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $matchData;
|
||||
public $ticketId;
|
||||
|
||||
public function __construct($matchData, $ticketId)
|
||||
{
|
||||
$this->matchData = $matchData;
|
||||
$this->ticketId = $ticketId;
|
||||
}
|
||||
|
||||
public function broadcastOn()
|
||||
{
|
||||
// ارسال پیام به کانال پرایوت مختص تیکت کاربری که در صف انتظار بود
|
||||
return new PrivateChannel('matchmaking.' . $this->ticketId);
|
||||
}
|
||||
|
||||
public function broadcastAs()
|
||||
{
|
||||
// نام ایونتی که کاتلین باید به آن گوش دهد
|
||||
return 'match.started';
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,9 @@ class ArtController extends Controller
|
||||
$laws = Law::all();
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
$branches = Branch::all();
|
||||
$branches = Branch::all();
|
||||
|
||||
return view('admin.art.create', compact('chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections','branches','divisions'));
|
||||
return view('admin.art.create', compact('chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections', 'branches', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -64,7 +64,7 @@ class ArtController extends Controller
|
||||
'law_id' => 'required',
|
||||
'division_id' => 'nullable',
|
||||
'branch_id' => 'nullable',
|
||||
'is_free' => 'nullable'
|
||||
'is_free' => 'nullable',
|
||||
]);
|
||||
|
||||
$validated['is_free'] = $request->input('is_free') ? true : false;
|
||||
@@ -84,10 +84,9 @@ class ArtController extends Controller
|
||||
$laws = Law::all();
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
$branches = Branch::all();
|
||||
$branches = Branch::all();
|
||||
|
||||
|
||||
return view('admin.art.update', compact('art', 'chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections','branches','divisions'));
|
||||
return view('admin.art.update', compact('art', 'chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections', 'branches', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Art $art)
|
||||
@@ -106,14 +105,14 @@ class ArtController extends Controller
|
||||
'law_id' => 'required',
|
||||
'division_id' => 'nullable',
|
||||
'branch_id' => 'nullable',
|
||||
'is_free' => 'nullable'
|
||||
'is_free' => 'nullable',
|
||||
]);
|
||||
|
||||
$validated['is_free'] = $request->input('is_free') ? true : false;
|
||||
|
||||
$art->update($validated);
|
||||
|
||||
return redirect(route('art.edit',$art->id));
|
||||
return redirect(route('art.edit', $art->id));
|
||||
}
|
||||
|
||||
public function destroy(Art $art)
|
||||
|
||||
@@ -15,7 +15,7 @@ class BookController extends Controller
|
||||
$query = Book::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
|
||||
@@ -21,7 +21,7 @@ class BranchController extends Controller
|
||||
$query = Branch::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -41,7 +41,7 @@ class BranchController extends Controller
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.branch.create', compact('chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections','divisions'));
|
||||
return view('admin.branch.create', compact('chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -75,7 +75,7 @@ class BranchController extends Controller
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.branch.update', compact('branch', 'chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections','divisions'));
|
||||
return view('admin.branch.update', compact('branch', 'chapters', 'parts', 'gates', 'books', 'volums', 'laws', 'sections', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Branch $branch)
|
||||
@@ -83,7 +83,7 @@ class BranchController extends Controller
|
||||
|
||||
$validated = $request->validate([
|
||||
'title' => 'required',
|
||||
'number' => 'required',
|
||||
'number' => 'required',
|
||||
'chapter_id' => 'nullable',
|
||||
'part_id' => 'nullable',
|
||||
'gate_id' => 'nullable',
|
||||
@@ -104,5 +104,5 @@ class BranchController extends Controller
|
||||
$branch->delete();
|
||||
|
||||
return redirect(route('branch.index'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawCodificationLink;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawCodificationLinkController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$items = BylawCodificationLink::latest('created_at')->paginate(15);
|
||||
|
||||
return view('admin.bylaw-codification-link.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.bylaw-codification-link.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate(['source_entity_type' => 'required|in:bylaw_node,bylaw_doc,law_doc,court_verdict,advisory_opinion', 'source_entity_id' => 'required|integer', 'target_entity_type' => 'required|in:bylaw_node,bylaw_doc,law_doc,court_verdict,advisory_opinion', 'target_entity_id' => 'required|integer', 'link_type' => 'required|in:repeals_explicitly,repeals_implicitly,annuls_entirely,annuls_partially,amends,adds_to,interprets,is_executive_bylaw_of', 'effective_date' => 'nullable|date', 'justification' => 'nullable|string']);
|
||||
$data['created_at'] = now();
|
||||
BylawCodificationLink::create($data);
|
||||
|
||||
return redirect(route('bylaw-codification-link.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawCodificationLink $bylawCodificationLink)
|
||||
{
|
||||
return view('admin.bylaw-codification-link.update', compact('bylawCodificationLink'));
|
||||
}
|
||||
|
||||
public function show(BylawCodificationLink $bylawCodificationLink)
|
||||
{
|
||||
return redirect(route('bylaw-codification-link.edit', $bylawCodificationLink));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawCodificationLink $bylawCodificationLink)
|
||||
{
|
||||
$data = $request->validate(['source_entity_type' => 'required|in:bylaw_node,bylaw_doc,law_doc,court_verdict,advisory_opinion', 'source_entity_id' => 'required|integer', 'target_entity_type' => 'required|in:bylaw_node,bylaw_doc,law_doc,court_verdict,advisory_opinion', 'target_entity_id' => 'required|integer', 'link_type' => 'required|in:repeals_explicitly,repeals_implicitly,annuls_entirely,annuls_partially,amends,adds_to,interprets,is_executive_bylaw_of', 'effective_date' => 'nullable|date', 'justification' => 'nullable|string']);
|
||||
$bylawCodificationLink->update($data);
|
||||
|
||||
return redirect(route('bylaw-codification-link.edit', $bylawCodificationLink));
|
||||
}
|
||||
|
||||
public function destroy(BylawCodificationLink $bylawCodificationLink)
|
||||
{
|
||||
$bylawCodificationLink->delete();
|
||||
|
||||
return redirect(route('bylaw-codification-link.index'));
|
||||
}
|
||||
}
|
||||
118
app/Http/Controllers/Admin/BylawDocumentController.php
Normal file
118
app/Http/Controllers/Admin/BylawDocumentController.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawDocument;
|
||||
use App\Models\BylawLegalAuthority;
|
||||
use App\Models\BylawSubjectCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class BylawDocumentController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority']);
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', "%{$request->q}%");
|
||||
}
|
||||
$items = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('admin.bylaw-document.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$categories = BylawSubjectCategory::all();
|
||||
$authorities = BylawLegalAuthority::all();
|
||||
|
||||
return view('admin.bylaw-document.create', compact('categories', 'authorities'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => 'required|string|max:1000',
|
||||
'category_id' => 'nullable|exists:bylaw_subject_categories,id',
|
||||
'approval_authority_id' => 'nullable|exists:bylaw_legal_authorities,id',
|
||||
'approval_date' => 'nullable|date',
|
||||
'promulgation_authority_id' => 'nullable|exists:bylaw_legal_authorities,id',
|
||||
'promulgation_date' => 'nullable|date',
|
||||
'promulgation_number' => 'nullable|string|max:100',
|
||||
'official_gazette_number' => 'nullable|string|max:100',
|
||||
'official_gazette_date' => 'nullable|date',
|
||||
'execution_date' => 'nullable|date',
|
||||
'bylaw_type' => 'required|in:executive,delegated,independent',
|
||||
'global_status' => 'required|in:valid,explicitly_repealed,implicitly_repealed,annulled,expired,draft',
|
||||
'pdf_file' => 'nullable|file|mimes:pdf|max:20480',
|
||||
'pdf_file_path' => 'nullable|string|max:500',
|
||||
'is_published' => 'nullable|boolean',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('pdf_file')) {
|
||||
$data['pdf_file_path'] = $request->file('pdf_file')->store('pdfs', 'public');
|
||||
}
|
||||
|
||||
$data['is_published'] = $request->boolean('is_published');
|
||||
BylawDocument::create($data);
|
||||
|
||||
return redirect(route('bylaw-document.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawDocument $bylawDocument)
|
||||
{
|
||||
$categories = BylawSubjectCategory::all();
|
||||
$authorities = BylawLegalAuthority::all();
|
||||
|
||||
return view('admin.bylaw-document.update', compact('bylawDocument', 'categories', 'authorities'));
|
||||
}
|
||||
|
||||
public function show(BylawDocument $bylawDocument)
|
||||
{
|
||||
return redirect(route('bylaw-document.edit', $bylawDocument));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawDocument $bylawDocument)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => 'required|string|max:1000',
|
||||
'category_id' => 'nullable|exists:bylaw_subject_categories,id',
|
||||
'approval_authority_id' => 'nullable|exists:bylaw_legal_authorities,id',
|
||||
'approval_date' => 'nullable|date',
|
||||
'promulgation_authority_id' => 'nullable|exists:bylaw_legal_authorities,id',
|
||||
'promulgation_date' => 'nullable|date',
|
||||
'promulgation_number' => 'nullable|string|max:100',
|
||||
'official_gazette_number' => 'nullable|string|max:100',
|
||||
'official_gazette_date' => 'nullable|date',
|
||||
'execution_date' => 'nullable|date',
|
||||
'bylaw_type' => 'required|in:executive,delegated,independent',
|
||||
'global_status' => 'required|in:valid,explicitly_repealed,implicitly_repealed,annulled,expired,draft',
|
||||
'pdf_file' => 'nullable|file|mimes:pdf|max:20480',
|
||||
'pdf_file_path' => 'nullable|string|max:500',
|
||||
'is_published' => 'nullable|boolean',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($request->hasFile('pdf_file')) {
|
||||
if ($bylawDocument->pdf_file_path && Storage::disk('public')->exists($bylawDocument->pdf_file_path)) {
|
||||
Storage::disk('public')->delete($bylawDocument->pdf_file_path);
|
||||
}
|
||||
|
||||
$data['pdf_file_path'] = $request->file('pdf_file')->store('pdfs', 'public');
|
||||
}
|
||||
|
||||
$data['is_published'] = $request->boolean('is_published');
|
||||
$bylawDocument->update($data);
|
||||
|
||||
return redirect(route('bylaw-document.edit', $bylawDocument));
|
||||
}
|
||||
|
||||
public function destroy(BylawDocument $bylawDocument)
|
||||
{
|
||||
$bylawDocument->delete();
|
||||
|
||||
return redirect(route('bylaw-document.index'));
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/Admin/BylawLegalAuthorityController.php
Normal file
59
app/Http/Controllers/Admin/BylawLegalAuthorityController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawLegalAuthority;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawLegalAuthorityController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = BylawLegalAuthority::query();
|
||||
if ($request->filled('q')) {
|
||||
$query->where('name', 'like', "%{$request->q}%");
|
||||
}
|
||||
$items = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('admin.bylaw-legal-authority.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.bylaw-legal-authority.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate(['name' => 'required|unique:bylaw_legal_authorities,name', 'authority_type' => 'required|in:executive,judicial,legislative,administrative,other', 'description' => 'nullable|string']);
|
||||
BylawLegalAuthority::create($data);
|
||||
|
||||
return redirect(route('bylaw-legal-authority.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawLegalAuthority $bylawLegalAuthority)
|
||||
{
|
||||
return view('admin.bylaw-legal-authority.update', compact('bylawLegalAuthority'));
|
||||
}
|
||||
|
||||
public function show(BylawLegalAuthority $bylawLegalAuthority)
|
||||
{
|
||||
return redirect(route('bylaw-legal-authority.edit', $bylawLegalAuthority));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawLegalAuthority $bylawLegalAuthority)
|
||||
{
|
||||
$data = $request->validate(['name' => 'required|unique:bylaw_legal_authorities,name,'.$bylawLegalAuthority->id, 'authority_type' => 'required|in:executive,judicial,legislative,administrative,other', 'description' => 'nullable|string']);
|
||||
$bylawLegalAuthority->update($data);
|
||||
|
||||
return redirect(route('bylaw-legal-authority.edit', $bylawLegalAuthority));
|
||||
}
|
||||
|
||||
public function destroy(BylawLegalAuthority $bylawLegalAuthority)
|
||||
{
|
||||
$bylawLegalAuthority->delete();
|
||||
|
||||
return redirect(route('bylaw-legal-authority.index'));
|
||||
}
|
||||
}
|
||||
57
app/Http/Controllers/Admin/BylawNodeController.php
Normal file
57
app/Http/Controllers/Admin/BylawNodeController.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawDocument;
|
||||
use App\Models\BylawNode;
|
||||
use App\Models\BylawNodeType;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawNodeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$items = BylawNode::with(['document', 'type'])->latest()->paginate(15);
|
||||
|
||||
return view('admin.bylaw-node.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.bylaw-node.create', ['documents' => BylawDocument::all(), 'types' => BylawNodeType::all(), 'parents' => BylawNode::all()]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate(['document_id' => 'required|exists:bylaw_documents,id', 'parent_id' => 'nullable|exists:bylaw_nodes,id', 'node_type_id' => 'required|exists:bylaw_node_types,id', 'node_identifier' => 'nullable|string|max:50', 'title' => 'nullable|string|max:500', 'node_content' => 'nullable|string', 'sort_order' => 'nullable|integer', 'valid_from' => 'nullable|date', 'valid_to' => 'nullable|date', 'node_status' => 'required|in:valid,repealed,annulled,amended,suspended']);
|
||||
BylawNode::create($data);
|
||||
|
||||
return redirect(route('bylaw-node.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawNode $bylawNode)
|
||||
{
|
||||
return view('admin.bylaw-node.update', ['bylawNode' => $bylawNode, 'documents' => BylawDocument::all(), 'types' => BylawNodeType::all(), 'parents' => BylawNode::where('id', '!=', $bylawNode->id)->get()]);
|
||||
}
|
||||
|
||||
public function show(BylawNode $bylawNode)
|
||||
{
|
||||
return redirect(route('bylaw-node.edit', $bylawNode));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawNode $bylawNode)
|
||||
{
|
||||
$data = $request->validate(['document_id' => 'required|exists:bylaw_documents,id', 'parent_id' => 'nullable|exists:bylaw_nodes,id', 'node_type_id' => 'required|exists:bylaw_node_types,id', 'node_identifier' => 'nullable|string|max:50', 'title' => 'nullable|string|max:500', 'node_content' => 'nullable|string', 'sort_order' => 'nullable|integer', 'valid_from' => 'nullable|date', 'valid_to' => 'nullable|date', 'node_status' => 'required|in:valid,repealed,annulled,amended,suspended']);
|
||||
$bylawNode->update($data);
|
||||
|
||||
return redirect(route('bylaw-node.edit', $bylawNode));
|
||||
}
|
||||
|
||||
public function destroy(BylawNode $bylawNode)
|
||||
{
|
||||
$bylawNode->delete();
|
||||
|
||||
return redirect(route('bylaw-node.index'));
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Admin/BylawNodeTypeController.php
Normal file
55
app/Http/Controllers/Admin/BylawNodeTypeController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawNodeType;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawNodeTypeController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$items = BylawNodeType::latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('admin.bylaw-node-type.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.bylaw-node-type.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate(['name' => 'required|unique:bylaw_node_types,name', 'hierarchy_level' => 'required|integer']);
|
||||
BylawNodeType::create($data);
|
||||
|
||||
return redirect(route('bylaw-node-type.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawNodeType $bylawNodeType)
|
||||
{
|
||||
return view('admin.bylaw-node-type.update', compact('bylawNodeType'));
|
||||
}
|
||||
|
||||
public function show(BylawNodeType $bylawNodeType)
|
||||
{
|
||||
return redirect(route('bylaw-node-type.edit', $bylawNodeType));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawNodeType $bylawNodeType)
|
||||
{
|
||||
$data = $request->validate(['name' => 'required|unique:bylaw_node_types,name,'.$bylawNodeType->id, 'hierarchy_level' => 'required|integer']);
|
||||
$bylawNodeType->update($data);
|
||||
|
||||
return redirect(route('bylaw-node-type.edit', $bylawNodeType));
|
||||
}
|
||||
|
||||
public function destroy(BylawNodeType $bylawNodeType)
|
||||
{
|
||||
$bylawNodeType->delete();
|
||||
|
||||
return redirect(route('bylaw-node-type.index'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawSubjectCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BylawSubjectCategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = BylawSubjectCategory::query();
|
||||
if ($request->filled('q')) {
|
||||
$query->where('name', 'like', "%{$request->q}%");
|
||||
} $items = $query->latest()->paginate(15)->withQueryString();
|
||||
|
||||
return view('admin.bylaw-subject-category.index', compact('items'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$parents = BylawSubjectCategory::all();
|
||||
|
||||
return view('admin.bylaw-subject-category.create', compact('parents'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->validate(['parent_id' => 'nullable|exists:bylaw_subject_categories,id', 'name' => 'required|string', 'slug' => 'nullable|string|unique:bylaw_subject_categories,slug', 'sort_order' => 'nullable|integer']);
|
||||
$data['slug'] = $data['slug'] ?? Str::slug($data['name']);
|
||||
BylawSubjectCategory::create($data);
|
||||
|
||||
return redirect(route('bylaw-subject-category.index'));
|
||||
}
|
||||
|
||||
public function edit(BylawSubjectCategory $bylawSubjectCategory)
|
||||
{
|
||||
$parents = BylawSubjectCategory::where('id', '!=', $bylawSubjectCategory->id)->get();
|
||||
|
||||
return view('admin.bylaw-subject-category.update', compact('bylawSubjectCategory', 'parents'));
|
||||
}
|
||||
|
||||
public function show(BylawSubjectCategory $bylawSubjectCategory)
|
||||
{
|
||||
return redirect(route('bylaw-subject-category.edit', $bylawSubjectCategory));
|
||||
}
|
||||
|
||||
public function update(Request $request, BylawSubjectCategory $bylawSubjectCategory)
|
||||
{
|
||||
$data = $request->validate(['parent_id' => 'nullable|exists:bylaw_subject_categories,id', 'name' => 'required|string', 'slug' => 'required|string|unique:bylaw_subject_categories,slug,'.$bylawSubjectCategory->id, 'sort_order' => 'nullable|integer']);
|
||||
$bylawSubjectCategory->update($data);
|
||||
|
||||
return redirect(route('bylaw-subject-category.edit', $bylawSubjectCategory));
|
||||
}
|
||||
|
||||
public function destroy(BylawSubjectCategory $bylawSubjectCategory)
|
||||
{
|
||||
$bylawSubjectCategory->delete();
|
||||
|
||||
return redirect(route('bylaw-subject-category.index'));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class CategoryController extends Controller
|
||||
$query = Category::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('name', 'like', '%' . $request->q . '%');
|
||||
$query->where('name', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
if ($request->filled('type')) {
|
||||
$query->where('type', $request->type);
|
||||
@@ -32,12 +32,11 @@ class CategoryController extends Controller
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
$validated = $request->validate([
|
||||
'name' => 'required',
|
||||
'type' => 'required'
|
||||
'type' => 'required',
|
||||
]);
|
||||
|
||||
|
||||
Category::create($validated);
|
||||
|
||||
return redirect(route('categories.index'));
|
||||
@@ -45,25 +44,25 @@ class CategoryController extends Controller
|
||||
|
||||
public function edit(Category $category)
|
||||
{
|
||||
return view('admin.categories.update',compact('category'));
|
||||
return view('admin.categories.update', compact('category'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Category $category)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
$validated = $request->validate([
|
||||
'name' => 'required',
|
||||
'type' => 'required'
|
||||
'type' => 'required',
|
||||
]);
|
||||
|
||||
$category->update($validated);
|
||||
|
||||
|
||||
return redirect(route('categories.index'));
|
||||
}
|
||||
|
||||
public function destroy(Category $category)
|
||||
{
|
||||
$category->delete();
|
||||
|
||||
|
||||
return redirect(route('categories.index'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class ChapterController extends Controller
|
||||
$query = Chapter::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -35,7 +35,7 @@ class ChapterController extends Controller
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.chapter.create', compact('sections', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.chapter.create', compact('sections', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -47,7 +47,7 @@ class ChapterController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
Chapter::query()->create($validated);
|
||||
@@ -63,7 +63,7 @@ class ChapterController extends Controller
|
||||
$laws = Law::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.chapter.update', compact('sections', 'chapter', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.chapter.update', compact('sections', 'chapter', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Chapter $chapter)
|
||||
@@ -75,7 +75,7 @@ class ChapterController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
$chapter->update($validated);
|
||||
|
||||
85
app/Http/Controllers/Admin/DailyQuestController.php
Normal file
85
app/Http/Controllers/Admin/DailyQuestController.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DailyQuest;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DailyQuestController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$quests = \App\Models\DailyQuest::latest()->paginate(15);
|
||||
|
||||
// Upgraded Bank of Smart Suggestions (Hardcore Focused)
|
||||
$suggestions = [
|
||||
// --- EXTREME CHALLENGES ---
|
||||
['title' => 'اسطوره لیگ: پیروزی در ۵۰ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 50, 'reward_xp' => 4000],
|
||||
['title' => 'ماراتن تستزنی: پاسخ به ۵۰۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 500, 'reward_xp' => 3500],
|
||||
['title' => 'کمالگرا: کسب نمره کامل در ۵ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 5, 'reward_xp' => 2500],
|
||||
|
||||
// --- HARD CHALLENGES ---
|
||||
['title' => 'مبارز خستگیناپذیر: پیروزی در ۲۰ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 20, 'reward_xp' => 1500],
|
||||
['title' => 'چالش حقوقدان: پاسخ به ۲۵۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 250, 'reward_xp' => 1200],
|
||||
['title' => 'دقت بالا: کسب نمره کامل در ۳ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 3, 'reward_xp' => 1200],
|
||||
['title' => 'پیروزی در ۱۰ دوئل متوالی (کاپ نقره)', 'quest_type' => 'duel_win', 'required_amount' => 10, 'reward_xp' => 700],
|
||||
|
||||
// --- MEDIUM CHALLENGES (For balance) ---
|
||||
['title' => 'گرمکردن: پیروزی در ۵ دوئل', 'quest_type' => 'duel_win', 'required_amount' => 5, 'reward_xp' => 300],
|
||||
['title' => 'مرور روزانه: پاسخ به ۵۰ تست', 'quest_type' => 'answer_tests', 'required_amount' => 50, 'reward_xp' => 250],
|
||||
['title' => 'کسب نمره کامل در ۱ آزمون', 'quest_type' => 'perfect_score', 'required_amount' => 1, 'reward_xp' => 350],
|
||||
];
|
||||
|
||||
return view('admin.daily-quests.index', compact('quests', 'suggestions'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.daily-quests.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'quest_type' => 'required|string|in:duel_win,answer_tests,perfect_score',
|
||||
'required_amount' => 'required|integer|min:1',
|
||||
'reward_xp' => 'required|integer|min:10',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active'); // <-- Catch the switch
|
||||
|
||||
DailyQuest::create($validated);
|
||||
|
||||
return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه با موفقیت ایجاد شد.');
|
||||
}
|
||||
|
||||
public function update(Request $request, DailyQuest $daily_quest)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'quest_type' => 'required|string|in:duel_win,answer_tests,perfect_score',
|
||||
'required_amount' => 'required|integer|min:1',
|
||||
'reward_xp' => 'required|integer|min:10',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active'); // <-- Catch the switch
|
||||
|
||||
$daily_quest->update($validated);
|
||||
|
||||
return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه با موفقیت ویرایش شد.');
|
||||
}
|
||||
|
||||
|
||||
public function edit(DailyQuest $daily_quest)
|
||||
{
|
||||
return view('admin.daily-quests.update', compact('daily_quest'));
|
||||
}
|
||||
|
||||
public function destroy(DailyQuest $daily_quest)
|
||||
{
|
||||
$daily_quest->delete();
|
||||
return redirect()->route('daily-quests.index')->with('success', 'چالش روزانه حذف شد.');
|
||||
}
|
||||
}
|
||||
108
app/Http/Controllers/Admin/DashboardController.php
Normal file
108
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\Suggestion;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\UserSubscriber;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// 1. Calculate General Statistics
|
||||
$totalUsers = User::count();
|
||||
|
||||
// 2. Safely Count Free Subscribers using the relationship
|
||||
$freeSubscribers = UserSubscriber::where('expired_at', '>=', now())
|
||||
->whereHas('subscribe', function (Builder $query) {
|
||||
$query->where('is_free', true);
|
||||
})
|
||||
->distinct('user_id')
|
||||
->count('user_id');
|
||||
|
||||
// 3. Get Paid Subscribers & Breakdown by Plan Type AND Market Provider
|
||||
$rawBreakdown = UserSubscriber::where('user_subscribers.expired_at', '>=', now())
|
||||
->join('subscribe_plans', 'user_subscribers.subscribe_plan_id', '=', 'subscribe_plans.id')
|
||||
->where('subscribe_plans.is_free', false) // Replaced redundant whereHas for better performance
|
||||
->selectRaw('subscribe_plans.name as plan_name, user_subscribers.market_provider, count(DISTINCT user_subscribers.user_id) as total')
|
||||
->groupBy('subscribe_plans.name', 'user_subscribers.market_provider')
|
||||
->get();
|
||||
|
||||
// 4. Safely group the results in PHP to fix split numbers (merging null, '', and 'web')
|
||||
$paidBreakdown = $rawBreakdown->map(function ($item) {
|
||||
$provider = strtolower(trim($item->market_provider));
|
||||
|
||||
// Normalize the provider names
|
||||
if (empty($provider) || $provider === 'web') {
|
||||
$providerName = 'سایت';
|
||||
} elseif ($provider === 'bazar') {
|
||||
$providerName = 'بازار';
|
||||
} elseif ($provider === 'myket') {
|
||||
$providerName = 'مایکت';
|
||||
} else {
|
||||
$providerName = ucfirst($provider); // Dynamic fallback for any future gateways
|
||||
}
|
||||
|
||||
return [
|
||||
'display_name' => $item->plan_name . ' (' . $providerName . ')',
|
||||
'total' => $item->total
|
||||
];
|
||||
})->groupBy('display_name')->map(function ($group, $key) {
|
||||
// Merge any split rows (e.g. 'web' + null) into one accurate total sum
|
||||
return (object) [
|
||||
'display_name' => $key,
|
||||
'total' => $group->sum('total')
|
||||
];
|
||||
})->values();
|
||||
|
||||
// Total paid is the exact sum of the merged breakdown
|
||||
$paidSubscribers = $paidBreakdown->sum('total');
|
||||
|
||||
// 4. Fetch Recent Data (Limit to 5)
|
||||
$recentSuggestions = Suggestion::with('user')->latest()->take(5)->get();
|
||||
|
||||
// Order tickets by updated_at so newly replied tickets jump to the top
|
||||
$recentTickets = Ticket::with('user')->latest('updated_at')->take(5)->get();
|
||||
|
||||
// 5. Prepare Chart Data (Strictly the last 7 calendar days)
|
||||
$chartDates = [];
|
||||
$newUsersData = [];
|
||||
$paidSubsData = [];
|
||||
$freeSubsData = [];
|
||||
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$date = now()->subDays($i);
|
||||
$dbDate = $date->format('Y-m-d');
|
||||
|
||||
// Format X-axis to Jalali for the chart labels
|
||||
$chartDates[] = \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d');
|
||||
|
||||
// Count records for this specific exact date
|
||||
$newUsersData[] = User::whereDate('created_at', $dbDate)->count();
|
||||
|
||||
$paidSubsData[] = UserSubscriber::whereDate('created_at', $dbDate)
|
||||
->whereHas('subscribe', fn($q) => $q->where('is_free', false))->count();
|
||||
|
||||
$freeSubsData[] = UserSubscriber::whereDate('created_at', $dbDate)
|
||||
->whereHas('subscribe', fn($q) => $q->where('is_free', true))->count();
|
||||
}
|
||||
|
||||
return view('admin.dashboard.index', compact(
|
||||
'totalUsers',
|
||||
'paidSubscribers',
|
||||
'freeSubscribers',
|
||||
'paidBreakdown',
|
||||
'recentSuggestions',
|
||||
'recentTickets',
|
||||
'chartDates',
|
||||
'newUsersData',
|
||||
'paidSubsData',
|
||||
'freeSubsData'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class DivisionController extends Controller
|
||||
$query = Division::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -55,7 +55,7 @@ class DivisionController extends Controller
|
||||
$volums = Volum::all();
|
||||
$laws = Law::all();
|
||||
|
||||
return view('admin.division.update', compact('division','books', 'volums', 'laws'));
|
||||
return view('admin.division.update', compact('division', 'books', 'volums', 'laws'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Division $division)
|
||||
|
||||
@@ -20,7 +20,7 @@ class GateController extends Controller
|
||||
$query = Gate::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -37,9 +37,9 @@ class GateController extends Controller
|
||||
$books = Book::all();
|
||||
$volums = Volum::all();
|
||||
$laws = Law::all();
|
||||
$divisions = Division::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.gate.create', compact('parts', 'chapters', 'sections', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.gate.create', compact('parts', 'chapters', 'sections', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -64,9 +64,9 @@ class GateController extends Controller
|
||||
$books = Book::all();
|
||||
$volums = Volum::all();
|
||||
$laws = Law::all();
|
||||
$divisions = Division::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.gate.update', compact('gate', 'parts', 'chapters', 'sections', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.gate.update', compact('gate', 'parts', 'chapters', 'sections', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Gate $gate)
|
||||
|
||||
@@ -40,7 +40,7 @@ class JudicialPrecedentCategoryController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|unique:judicial_precedent_categories,name'
|
||||
'name' => 'required|string|unique:judicial_precedent_categories,name',
|
||||
]);
|
||||
|
||||
JudicialPrecedentCategory::create($validated);
|
||||
@@ -70,7 +70,7 @@ class JudicialPrecedentCategoryController extends Controller
|
||||
public function update(Request $request, JudicialPrecedentCategory $judicialPrecedentCategory)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|unique:judicial_precedent_categories,name,' . $judicialPrecedentCategory->id
|
||||
'name' => 'required|string|unique:judicial_precedent_categories,name,'.$judicialPrecedentCategory->id,
|
||||
]);
|
||||
|
||||
$judicialPrecedentCategory->update($validated);
|
||||
@@ -84,6 +84,7 @@ class JudicialPrecedentCategoryController extends Controller
|
||||
public function destroy(JudicialPrecedentCategory $judicialPrecedentCategory)
|
||||
{
|
||||
$judicialPrecedentCategory->delete();
|
||||
|
||||
return redirect(route('judicial-precedent-category.index'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class JudicialPrecedentController extends Controller
|
||||
{
|
||||
$arts = Art::all();
|
||||
$categories = JudicialPrecedentCategory::all();
|
||||
|
||||
return view('admin.judicial-precedent.create', compact('arts', 'categories'));
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ class JudicialPrecedentController extends Controller
|
||||
'issuing_authority' => 'nullable|string',
|
||||
'category_id' => 'nullable|exists:judicial_precedent_categories,id',
|
||||
'art_ids' => 'nullable|array',
|
||||
'art_ids.*' => 'exists:arts,id'
|
||||
'art_ids.*' => 'exists:arts,id',
|
||||
]);
|
||||
|
||||
$judicialPrecedent = JudicialPrecedent::create([
|
||||
@@ -55,10 +56,10 @@ class JudicialPrecedentController extends Controller
|
||||
'subject' => $validated['subject'],
|
||||
'full_text' => $validated['full_text'],
|
||||
'issuing_authority' => $validated['issuing_authority'] ?? 'هیأت عمومی دیوان عالی کشور',
|
||||
'category_id' => $validated['category_id']
|
||||
'category_id' => $validated['category_id'],
|
||||
]);
|
||||
|
||||
if (!empty($validated['art_ids'])) {
|
||||
if (! empty($validated['art_ids'])) {
|
||||
$judicialPrecedent->arts()->attach($validated['art_ids']);
|
||||
}
|
||||
|
||||
@@ -70,25 +71,25 @@ class JudicialPrecedentController extends Controller
|
||||
$arts = Art::all();
|
||||
$categories = JudicialPrecedentCategory::all();
|
||||
$selectedArtIds = $judicialPrecedent->arts->pluck('id')->toArray();
|
||||
|
||||
|
||||
return view('admin.judicial-precedent.update', compact('judicialPrecedent', 'arts', 'categories', 'selectedArtIds'));
|
||||
}
|
||||
|
||||
public function update(Request $request, JudicialPrecedent $judicialPrecedent)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ruling_number' => 'required|unique:judicial_precedents,ruling_number,' . $judicialPrecedent->id,
|
||||
'ruling_number' => 'required|unique:judicial_precedents,ruling_number,'.$judicialPrecedent->id,
|
||||
'ruling_date' => 'required|date',
|
||||
'subject' => 'required|string',
|
||||
'full_text' => 'required|string',
|
||||
'issuing_authority' => 'nullable|string',
|
||||
'category_id' => 'nullable|exists:judicial_precedent_categories,id',
|
||||
'art_ids' => 'nullable|array',
|
||||
'art_ids.*' => 'exists:arts,id'
|
||||
'art_ids.*' => 'exists:arts,id',
|
||||
]);
|
||||
|
||||
$judicialPrecedent->update($validated);
|
||||
|
||||
|
||||
$judicialPrecedent->arts()->sync($validated['art_ids'] ?? []);
|
||||
|
||||
return redirect(route('judicial-precedent.edit', $judicialPrecedent->id));
|
||||
@@ -97,6 +98,7 @@ class JudicialPrecedentController extends Controller
|
||||
public function destroy(JudicialPrecedent $judicialPrecedent)
|
||||
{
|
||||
$judicialPrecedent->delete();
|
||||
|
||||
return redirect(route('judicial-precedent.index'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class LawController extends Controller
|
||||
$query = Law::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
if ($request->filled('locked')) {
|
||||
$query->where('is_locked', $request->locked === '1');
|
||||
@@ -46,8 +46,7 @@ class LawController extends Controller
|
||||
$validated['is_locked'] = $request->has('is_locked', false);
|
||||
|
||||
if ($request->hasFile('image') && $request->file('image')->isValid()) {
|
||||
dd($request->has('image'));
|
||||
$imageName = uniqid('image_', true) . '.' . $request->file('image')->getClientOriginalExtension();
|
||||
$imageName = uniqid('image_', true).'.'.$request->file('image')->getClientOriginalExtension();
|
||||
|
||||
$request->file('image')->move(public_path('images'), $imageName);
|
||||
|
||||
@@ -59,11 +58,10 @@ class LawController extends Controller
|
||||
return redirect()->route('law.index')->with('success', 'Law created successfully.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function edit(Law $law)
|
||||
{
|
||||
$categories = Category::all();
|
||||
|
||||
return view('admin.law.update', compact('law', 'categories'));
|
||||
}
|
||||
|
||||
@@ -80,7 +78,7 @@ class LawController extends Controller
|
||||
$validated['is_locked'] = $request->boolean('is_locked');
|
||||
|
||||
if ($request->hasFile('image') && $request->file('image')->isValid()) {
|
||||
$imageName = uniqid('image_', true) . '.' . $request->file('image')->getClientOriginalExtension();
|
||||
$imageName = uniqid('image_', true).'.'.$request->file('image')->getClientOriginalExtension();
|
||||
$request->file('image')->move(public_path('images'), $imageName);
|
||||
$validated['image'] = $imageName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LegalOpinionCategory;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegalOpinionCategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = LegalOpinionCategory::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = $request->q;
|
||||
$query->where('name', 'like', "%{$q}%");
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
$categories = $query->paginate($perPage)->withQueryString();
|
||||
|
||||
return view('admin.legal-opinion-category.index', compact('categories'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('admin.legal-opinion-category.create');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|unique:legal_opinion_categories,name',
|
||||
]);
|
||||
|
||||
LegalOpinionCategory::create($validated);
|
||||
|
||||
return redirect(route('legal-opinion-category.index'));
|
||||
}
|
||||
|
||||
public function edit(LegalOpinionCategory $legalOpinionCategory)
|
||||
{
|
||||
return view('admin.legal-opinion-category.update', compact('legalOpinionCategory'));
|
||||
}
|
||||
|
||||
public function show(LegalOpinionCategory $legalOpinionCategory)
|
||||
{
|
||||
return redirect(route('legal-opinion-category.edit', $legalOpinionCategory->id));
|
||||
}
|
||||
|
||||
public function update(Request $request, LegalOpinionCategory $legalOpinionCategory)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|unique:legal_opinion_categories,name,'.$legalOpinionCategory->id,
|
||||
]);
|
||||
|
||||
$legalOpinionCategory->update($validated);
|
||||
|
||||
return redirect(route('legal-opinion-category.edit', $legalOpinionCategory->id));
|
||||
}
|
||||
|
||||
public function destroy(LegalOpinionCategory $legalOpinionCategory)
|
||||
{
|
||||
$legalOpinionCategory->delete();
|
||||
|
||||
return redirect(route('legal-opinion-category.index'));
|
||||
}
|
||||
}
|
||||
126
app/Http/Controllers/Admin/LegalOpinionController.php
Normal file
126
app/Http/Controllers/Admin/LegalOpinionController.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Art;
|
||||
use App\Models\LegalOpinion;
|
||||
use App\Models\LegalOpinionCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LegalOpinionController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = LegalOpinion::with('category');
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = $request->q;
|
||||
$query->where(function ($qry) use ($q) {
|
||||
$qry->where('opinion_number', 'like', "%{$q}%")
|
||||
->orWhere('subject', 'like', "%{$q}%")
|
||||
->orWhere('full_text', 'like', "%{$q}%")
|
||||
->orWhere('issuing_authority', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
$legalOpinions = $query->latest()->paginate($perPage)->withQueryString();
|
||||
|
||||
return view('admin.legal-opinion.index', compact('legalOpinions'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$arts = Art::all();
|
||||
$categories = LegalOpinionCategory::all();
|
||||
|
||||
return view('admin.legal-opinion.create', compact('arts', 'categories'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'opinion_number' => [
|
||||
'required',
|
||||
Rule::unique('legal_opinions', 'opinion_number')->where('category_id', $request->input('category_id')),
|
||||
],
|
||||
'opinion_date' => 'nullable|date',
|
||||
'subject' => 'nullable|string|max:1000',
|
||||
'full_text' => 'required|string',
|
||||
'issuing_authority' => 'nullable|string',
|
||||
'category_id' => 'required|exists:legal_opinion_categories,id',
|
||||
'art_ids' => 'nullable|array',
|
||||
'art_ids.*' => 'exists:art,id',
|
||||
]);
|
||||
|
||||
$legalOpinion = LegalOpinion::create([
|
||||
'opinion_number' => $validated['opinion_number'],
|
||||
'opinion_date' => $validated['opinion_date'] ?? null,
|
||||
'subject' => $validated['subject'] ?? null,
|
||||
'full_text' => $validated['full_text'],
|
||||
'issuing_authority' => $validated['issuing_authority'] ?? null,
|
||||
'category_id' => $validated['category_id'],
|
||||
]);
|
||||
|
||||
if (! empty($validated['art_ids'])) {
|
||||
$legalOpinion->arts()->attach($validated['art_ids']);
|
||||
}
|
||||
|
||||
return redirect(route('legal-opinion.index'));
|
||||
}
|
||||
|
||||
public function edit(LegalOpinion $legalOpinion)
|
||||
{
|
||||
$arts = Art::all();
|
||||
$categories = LegalOpinionCategory::all();
|
||||
$selectedArtIds = $legalOpinion->arts->pluck('id')->toArray();
|
||||
|
||||
return view('admin.legal-opinion.update', compact('legalOpinion', 'arts', 'categories', 'selectedArtIds'));
|
||||
}
|
||||
|
||||
public function show(LegalOpinion $legalOpinion)
|
||||
{
|
||||
return redirect(route('legal-opinion.edit', $legalOpinion->id));
|
||||
}
|
||||
|
||||
public function update(Request $request, LegalOpinion $legalOpinion)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'opinion_number' => [
|
||||
'required',
|
||||
Rule::unique('legal_opinions', 'opinion_number')
|
||||
->where('category_id', $request->input('category_id'))
|
||||
->ignore($legalOpinion->id),
|
||||
],
|
||||
'opinion_date' => 'nullable|date',
|
||||
'subject' => 'nullable|string|max:1000',
|
||||
'full_text' => 'required|string',
|
||||
'issuing_authority' => 'nullable|string',
|
||||
'category_id' => 'required|exists:legal_opinion_categories,id',
|
||||
'art_ids' => 'nullable|array',
|
||||
'art_ids.*' => 'exists:art,id',
|
||||
]);
|
||||
|
||||
$legalOpinion->update([
|
||||
'opinion_number' => $validated['opinion_number'],
|
||||
'opinion_date' => $validated['opinion_date'] ?? null,
|
||||
'subject' => $validated['subject'] ?? null,
|
||||
'full_text' => $validated['full_text'],
|
||||
'issuing_authority' => $validated['issuing_authority'] ?? null,
|
||||
'category_id' => $validated['category_id'],
|
||||
]);
|
||||
|
||||
$legalOpinion->arts()->sync($validated['art_ids'] ?? []);
|
||||
|
||||
return redirect(route('legal-opinion.edit', $legalOpinion->id));
|
||||
}
|
||||
|
||||
public function destroy(LegalOpinion $legalOpinion)
|
||||
{
|
||||
$legalOpinion->delete();
|
||||
|
||||
return redirect(route('legal-opinion.index'));
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class NotificationController extends Controller
|
||||
public function store(StoreRequest $request)
|
||||
{
|
||||
Notification::create($request->validated());
|
||||
|
||||
return redirect()->route('notifications.index');
|
||||
}
|
||||
|
||||
@@ -47,12 +48,14 @@ class NotificationController extends Controller
|
||||
public function update(UpdateRequest $request, Notification $notification)
|
||||
{
|
||||
$notification->update($request->validated());
|
||||
|
||||
return redirect()->route('notifications.index');
|
||||
}
|
||||
|
||||
public function destroy(Notification $notification)
|
||||
{
|
||||
$notification->delete();
|
||||
|
||||
return redirect()->route('notifications.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class PartController extends Controller
|
||||
$query = Part::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -37,7 +37,7 @@ class PartController extends Controller
|
||||
$laws = Law::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.part.create', compact('chapters', 'sections', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.part.create', compact('chapters', 'sections', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -50,7 +50,7 @@ class PartController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
Part::query()->create($validated);
|
||||
@@ -67,7 +67,7 @@ class PartController extends Controller
|
||||
$laws = Law::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.part.update', compact('part', 'chapters', 'sections', 'books', 'volums', 'laws','divisions'));
|
||||
return view('admin.part.update', compact('part', 'chapters', 'sections', 'books', 'volums', 'laws', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Part $part)
|
||||
@@ -80,7 +80,7 @@ class PartController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
$part->update($validated);
|
||||
|
||||
47
app/Http/Controllers/Admin/QuizCategoryController.php
Normal file
47
app/Http/Controllers/Admin/QuizCategoryController.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QuizCategory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QuizCategoryController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Load categories and count how many quizzes are in each
|
||||
$categories = QuizCategory::withCount('quizzes')->latest()->get();
|
||||
return view('admin.quiz-categories.index', compact('categories'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
QuizCategory::create([
|
||||
'title' => $request->title,
|
||||
// Automatically generate a unique slug for URLs
|
||||
'slug' => Str::slug($request->title) . '-' . time(),
|
||||
'description' => $request->description,
|
||||
'is_active' => $request->has('is_active'),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'دستهبندی با موفقیت ایجاد شد.');
|
||||
}
|
||||
|
||||
public function destroy(QuizCategory $quiz_category)
|
||||
{
|
||||
// Check if the category has quizzes before deleting
|
||||
if ($quiz_category->quizzes()->count() > 0) {
|
||||
return back()->with('error', 'این دستهبندی شامل آزمون است و نمیتواند حذف شود. ابتدا آزمونها را حذف کنید.');
|
||||
}
|
||||
|
||||
$quiz_category->delete();
|
||||
return back()->with('success', 'دستهبندی با موفقیت حذف شد.');
|
||||
}
|
||||
}
|
||||
109
app/Http/Controllers/Admin/QuizController.php
Normal file
109
app/Http/Controllers/Admin/QuizController.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Quiz;
|
||||
use App\Models\QuizCategory;
|
||||
use App\Models\QuizSubject;
|
||||
use App\Models\Question;
|
||||
use App\Models\QuestionOption;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QuizController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$quizzes = Quiz::with('category')->withCount('questions')->latest()->paginate(15);
|
||||
return view('admin.quizzes.index', compact('quizzes'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
$categories = QuizCategory::where('is_active', true)->get();
|
||||
return view('admin.quizzes.create', compact('categories'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
// 1. Validate only the text and number fields
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:quiz_categories,id',
|
||||
'year' => 'nullable|integer',
|
||||
'type' => 'required|in:multiple_choice,descriptive,mixed',
|
||||
'duration_minutes' => 'required|integer|min:1',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// 2. Safely handle checkboxes and generate slug
|
||||
$validated['slug'] = \Illuminate\Support\Str::slug($validated['title']) . '-' . time();
|
||||
$validated['has_negative_score'] = $request->has('has_negative_score');
|
||||
$validated['is_free'] = $request->has('is_free');
|
||||
$validated['is_published'] = $request->has('is_published');
|
||||
|
||||
// 3. Create the quiz
|
||||
$quiz = Quiz::create($validated);
|
||||
|
||||
return redirect()->route('quizzes.questions.index', $quiz->id)
|
||||
->with('success', 'آزمون با موفقیت ایجاد شد. اکنون میتوانید سوالات را اضافه کنید.');
|
||||
}
|
||||
|
||||
public function destroy(Quiz $quiz)
|
||||
{
|
||||
$quiz->delete();
|
||||
return redirect()->route('quizzes.index')->with('success', 'آزمون حذف شد.');
|
||||
}
|
||||
|
||||
// --- Questions Management ---
|
||||
|
||||
public function questions(Quiz $quiz)
|
||||
{
|
||||
$quiz->load(['questions.options', 'questions.subject']);
|
||||
$subjects = QuizSubject::all();
|
||||
return view('admin.quizzes.questions', compact('quiz', 'subjects'));
|
||||
}
|
||||
|
||||
public function storeQuestion(Request $request, Quiz $quiz)
|
||||
{
|
||||
$request->validate([
|
||||
'question_text' => 'required|string',
|
||||
'type' => 'required|in:multiple_choice,descriptive',
|
||||
'subject_id' => 'nullable|exists:quiz_subjects,id',
|
||||
'score' => 'required|numeric|min:0.5',
|
||||
'solution_explanation' => 'nullable|string',
|
||||
// Options validation for multiple-choice questions
|
||||
'options' => 'required_if:type,multiple_choice|array|min:4|max:4',
|
||||
'options.*' => 'required_if:type,multiple_choice|string',
|
||||
'correct_option' => 'required_if:type,multiple_choice|integer|between:0,3',
|
||||
]);
|
||||
|
||||
$question = $quiz->questions()->create([
|
||||
'subject_id' => $request->subject_id,
|
||||
'question_text' => $request->question_text,
|
||||
'type' => $request->type,
|
||||
'score' => $request->score,
|
||||
'solution_explanation' => $request->solution_explanation,
|
||||
'order' => $quiz->questions()->count() + 1,
|
||||
]);
|
||||
|
||||
// Save options if question is multiple choice
|
||||
if ($request->type === 'multiple_choice' && $request->has('options')) {
|
||||
foreach ($request->options as $index => $optionText) {
|
||||
$question->options()->create([
|
||||
'option_text' => $optionText,
|
||||
'is_correct' => ($index == $request->correct_option),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'سوال با موفقیت ثبت شد.');
|
||||
}
|
||||
|
||||
public function destroyQuestion(Question $question)
|
||||
{
|
||||
$question->delete();
|
||||
return back()->with('success', 'سوال حذف شد.');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Admin/QuizSubjectController.php
Normal file
44
app/Http/Controllers/Admin/QuizSubjectController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\QuizSubject;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QuizSubjectController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Load subjects and count how many questions belong to each
|
||||
$subjects = QuizSubject::withCount('questions')->latest()->get();
|
||||
return view('admin.quiz-subjects.index', compact('subjects'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
QuizSubject::create([
|
||||
'title' => $request->title,
|
||||
// Generate a unique slug for potential API/URL usage
|
||||
'slug' => Str::slug($request->title) . '-' . time(),
|
||||
]);
|
||||
|
||||
return back()->with('success', 'درس با موفقیت اضافه شد.');
|
||||
}
|
||||
|
||||
public function destroy(QuizSubject $quiz_subject)
|
||||
{
|
||||
// Prevent deletion if questions are already assigned to this subject
|
||||
if ($quiz_subject->questions()->count() > 0) {
|
||||
return back()->with('error', 'این درس دارای سوال است و نمیتواند حذف شود. ابتدا سوالات مربوطه را ویرایش یا حذف کنید.');
|
||||
}
|
||||
|
||||
$quiz_subject->delete();
|
||||
return back()->with('success', 'درس با موفقیت حذف شد.');
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class SectionController extends Controller
|
||||
$query = Section::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
@@ -34,7 +34,7 @@ class SectionController extends Controller
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.section.create', compact('books', 'volums', 'laws','sections','divisions'));
|
||||
return view('admin.section.create', compact('books', 'volums', 'laws', 'sections', 'divisions'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -45,7 +45,7 @@ class SectionController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
section::query()->create($validated);
|
||||
@@ -60,8 +60,8 @@ class SectionController extends Controller
|
||||
$laws = Law::all();
|
||||
$sections = Section::all();
|
||||
$divisions = Division::all();
|
||||
|
||||
return view('admin.section.update', compact('section', 'books', 'volums', 'laws','sections','divisions'));
|
||||
|
||||
return view('admin.section.update', compact('section', 'books', 'volums', 'laws', 'sections', 'divisions'));
|
||||
}
|
||||
|
||||
public function update(Request $request, Section $section)
|
||||
@@ -72,7 +72,7 @@ class SectionController extends Controller
|
||||
'book_id' => 'nullable',
|
||||
'volum_id' => 'nullable',
|
||||
'law_id' => 'nullable',
|
||||
'division_id' => 'nullable'
|
||||
'division_id' => 'nullable',
|
||||
]);
|
||||
|
||||
$section->update($validated);
|
||||
|
||||
@@ -37,15 +37,15 @@ class SubscribePlanController extends Controller
|
||||
$validated['is_free'] = $request->has('is_free');
|
||||
|
||||
SubscribePlan::create($validated);
|
||||
return redirect()->route('subscribe-plans.index');
|
||||
}
|
||||
|
||||
return redirect()->route('subscribe-plans.index');
|
||||
}
|
||||
|
||||
public function edit(SubscribePlan $subscribePlan)
|
||||
{
|
||||
return view('admin.subscribe-plans.update', compact('subscribePlan'));
|
||||
}
|
||||
|
||||
|
||||
public function update(UpdateRequest $request, SubscribePlan $subscribePlan)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
@@ -53,12 +53,14 @@ class SubscribePlanController extends Controller
|
||||
$validated['is_free'] = $request->has('is_free');
|
||||
|
||||
$subscribePlan->update($validated);
|
||||
return redirect()->route('subscribe-plans.index');
|
||||
|
||||
return redirect()->route('subscribe-plans.index');
|
||||
}
|
||||
|
||||
public function destroy(SubscribePlan $subscribePlan)
|
||||
{
|
||||
$subscribePlan->delete();
|
||||
|
||||
return redirect()->route('subscribe-plans.index');
|
||||
}
|
||||
}
|
||||
|
||||
78
app/Http/Controllers/Admin/TicketController.php
Normal file
78
app/Http/Controllers/Admin/TicketController.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketMessage;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
// Eager load the user who created the ticket
|
||||
$query = Ticket::query()->with('user');
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$q = $request->q;
|
||||
$query->where(function ($qry) use ($q) {
|
||||
$qry->where('subject', 'like', "%{$q}%")
|
||||
->orWhereHas('user', fn ($u) => $u->where('mobile', 'like', "%{$q}%")
|
||||
->orWhere('name', 'like', "%{$q}%"));
|
||||
});
|
||||
}
|
||||
|
||||
// Optional: Filter by status (open, answered, user_reply, closed)
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
// Order by updated_at so tickets with new replies jump to the top
|
||||
$tickets = $query->latest('updated_at')->paginate($perPage)->withQueryString();
|
||||
|
||||
return view('admin.tickets.index', compact('tickets'));
|
||||
}
|
||||
|
||||
public function show(Ticket $ticket)
|
||||
{
|
||||
// Load messages and the user belonging to the ticket
|
||||
$ticket->load(['messages.user', 'user']);
|
||||
|
||||
return view('admin.tickets.show', compact('ticket'));
|
||||
}
|
||||
|
||||
public function reply(Request $request, Ticket $ticket)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'required|string',
|
||||
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120',
|
||||
]);
|
||||
|
||||
$attachmentPath = null;
|
||||
if ($request->hasFile('attachment')) {
|
||||
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||
}
|
||||
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => auth()->id(), // This will be the Admin's ID
|
||||
'message' => $request->message,
|
||||
'attachment_path' => $attachmentPath,
|
||||
]);
|
||||
|
||||
$ticket->update(['status' => 'answered']);
|
||||
|
||||
return redirect()->route('tickets.show', $ticket->id)
|
||||
->with('success', 'پاسخ شما با موفقیت ارسال شد.');
|
||||
}
|
||||
|
||||
public function close(Ticket $ticket)
|
||||
{
|
||||
$ticket->update(['status' => 'closed']);
|
||||
|
||||
return redirect()->route('tickets.index')
|
||||
->with('success', 'تیکت با موفقیت بسته شد.');
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,58 @@ class UsersController extends Controller
|
||||
return view('admin.users.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually assign a paid subscription to a user from the Admin Panel.
|
||||
*/
|
||||
public function addManualSubscription(Request $request)
|
||||
{
|
||||
// 1. Validate the input
|
||||
$request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'subscribe_plan_id' => 'required|exists:subscribe_plans,id',
|
||||
]);
|
||||
|
||||
$user = \App\Models\User::findOrFail($request->user_id);
|
||||
$plan = \App\Models\SubscribePlan::findOrFail($request->subscribe_plan_id);
|
||||
|
||||
// Prevent admins from accidentally adding free plans through this paid-only route
|
||||
if ($plan->is_free) {
|
||||
return back()->with('error', 'برای این عملیات باید یک اشتراک پولی انتخاب کنید.');
|
||||
}
|
||||
|
||||
// 2. Find the latest active PAID subscription to correctly stack the days
|
||||
$latestPaidSub = $user->userSubscribers()
|
||||
->where('expired_at', '>', now())
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', false);
|
||||
})
|
||||
->latest('expired_at')
|
||||
->first();
|
||||
|
||||
// If they have an active paid plan, start from its expiration. Otherwise, start from right now.
|
||||
$startDate = $latestPaidSub ? $latestPaidSub->expired_at : now();
|
||||
$expiredAt = $startDate->copy()->addDays($plan->expired_day);
|
||||
|
||||
// 3. CLEANUP: Instantly expire any active FREE plans so they don't pollute the database
|
||||
$user->userSubscribers()
|
||||
->where('expired_at', '>', now())
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
})
|
||||
->update(['expired_at' => now()]);
|
||||
|
||||
// 4. Create the manual paid subscription
|
||||
$user->userSubscribers()->create([
|
||||
'subscribe_plan_id' => $plan->id,
|
||||
'expired_at' => $expiredAt,
|
||||
'market_provider' => 'admin', // This will cleanly display as "(Admin)" in your dashboard charts!
|
||||
'is_free' => false,
|
||||
]);
|
||||
|
||||
// Redirect back with a success message for your blade view
|
||||
return back()->with('success', 'اشتراک پولی با موفقیت برای کاربر ثبت شد.');
|
||||
}
|
||||
|
||||
public function store(CreateUserRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
@@ -53,7 +105,30 @@ class UsersController extends Controller
|
||||
|
||||
public function edit(User $user)
|
||||
{
|
||||
return view('admin.users.update', compact('user'));
|
||||
$paidPlans = \App\Models\SubscribePlan::where('is_free', false)
|
||||
->where('is_active', true)
|
||||
->get();
|
||||
|
||||
return view('admin.users.update', compact('user', 'paidPlans'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly revoke a user's active subscriptions (Set to 0 days)
|
||||
*/
|
||||
public function revokeSubscription(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
]);
|
||||
|
||||
$user = \App\Models\User::findOrFail($request->user_id);
|
||||
|
||||
// Instantly expire ALL active subscriptions by setting them to 'now()'
|
||||
$user->userSubscribers()
|
||||
->where('expired_at', '>', now())
|
||||
->update(['expired_at' => now()]);
|
||||
|
||||
return back()->with('success', 'اشتراک کاربر با موفقیت صفر (لغو) شد.');
|
||||
}
|
||||
|
||||
public function update(UpdateUserRequest $request, User $user)
|
||||
|
||||
@@ -41,6 +41,7 @@ class VersionController extends Controller
|
||||
$validated['force_update'] = $validated['force_update'] == 'on' ? 1 : 0;
|
||||
}
|
||||
Version::create($validated);
|
||||
|
||||
return redirect(route('versions.index'));
|
||||
}
|
||||
|
||||
@@ -56,12 +57,14 @@ class VersionController extends Controller
|
||||
$validated['force_update'] = $validated['force_update'] == 'on' ? 1 : 0;
|
||||
}
|
||||
$version->update($validated);
|
||||
|
||||
return redirect(route('versions.index'));
|
||||
}
|
||||
|
||||
public function destroy(Version $version)
|
||||
{
|
||||
$version->delete();
|
||||
|
||||
return redirect(route('versions.index'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class VolumController extends Controller
|
||||
$query = Volum::query();
|
||||
|
||||
if ($request->filled('q')) {
|
||||
$query->where('title', 'like', '%' . $request->q . '%');
|
||||
$query->where('title', 'like', '%'.$request->q.'%');
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
|
||||
@@ -10,13 +10,13 @@ class SuggestionController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function index(SuggestionRequest $requst)
|
||||
public function index(SuggestionRequest $requst)
|
||||
{
|
||||
Suggestion::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'text' => $requst->text
|
||||
'text' => $requst->text,
|
||||
]);
|
||||
|
||||
return $this->success(null,'موفقیت','پیشنهاد شما با موفقیت برای مدیر ارسال شد');
|
||||
return $this->success(null, 'موفقیت', 'پیشنهاد شما با موفقیت برای مدیر ارسال شد');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,13 @@ class ArtController extends Controller
|
||||
|
||||
public function index(GetApiRequest $request)
|
||||
{
|
||||
$book = Book::find($request->book_id);
|
||||
$law = Law::find($book?->law_id);
|
||||
|
||||
if ($this->isLockedForCurrentUser($law)) {
|
||||
return $this->failed([], ['title' => 'Subscription Required', 'message' => 'This content requires an active subscription.'], 403);
|
||||
}
|
||||
|
||||
$arts = Art::with(['chapter', 'part', 'volum', 'law', 'book', 'section', 'gate'])->where('book_id', $request->book_id)->orderBy('number')->get();
|
||||
|
||||
$arts = $arts->map(function ($art) {
|
||||
@@ -78,19 +85,24 @@ class ArtController extends Controller
|
||||
{
|
||||
$art = Art::select('id', 'title', 'number', 'text', 'law_id')->find($id);
|
||||
|
||||
if (!$art) {
|
||||
if (! $art) {
|
||||
return $this->failed([], 'Art not found');
|
||||
}
|
||||
|
||||
$law = Law::find($art?->law_id);
|
||||
|
||||
if ($this->isLockedForCurrentUser($law)) {
|
||||
return $this->failed([], ['title' => 'Subscription Required', 'message' => 'This content requires an active subscription.'], 403);
|
||||
}
|
||||
|
||||
$art->is_like = $this->isLiked($art->id);
|
||||
$art->note = Note::select('id', 'note', 'color_code','created_at')->where('user_id', auth()->user()->id)->where('art_id', $id)->get();
|
||||
$art->note = Note::select('id', 'note', 'color_code', 'created_at')->where('user_id', auth()->id())->where('art_id', $id)->get();
|
||||
$art->highlight = \App\Models\ArtHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')->where('user_id', auth()->id())->where('art_id', $id)->get();
|
||||
$art->category = $law?->category?->name;
|
||||
$art->route = array_values($this->route(Art::class, $art));
|
||||
|
||||
$previousArt = Art::select('id', 'title', 'number', 'text', 'law_id')
|
||||
->where('law_id',$art->law_id)
|
||||
->where('law_id', $art->law_id)
|
||||
->where('number', '<', $art->number)
|
||||
->orderBy('number', 'desc')
|
||||
->first();
|
||||
@@ -98,13 +110,14 @@ class ArtController extends Controller
|
||||
if ($previousArt) {
|
||||
$previousLaw = Law::find($previousArt->law_id);
|
||||
$previousArt->is_like = $this->isLiked($previousArt->id);
|
||||
$previousArt->note = Note::select('id', 'note', 'color_code')->where('user_id', auth()->user()->id)->where('art_id', $previousArt->id)->get();
|
||||
$previousArt->note = Note::select('id', 'note', 'color_code')->where('user_id', auth()->id())->where('art_id', $previousArt->id)->get();
|
||||
$previousArt->highlight = \App\Models\ArtHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')->where('user_id', auth()->id())->where('art_id', $previousArt->id)->get();
|
||||
$previousArt->category = $previousLaw?->category?->name;
|
||||
$previousArt->route = array_values($this->route(Art::class, $previousArt));
|
||||
}
|
||||
|
||||
$nextArt = Art::select('id', 'title', 'number', 'text', 'law_id')
|
||||
->where('law_id',$art->law_id)
|
||||
->where('law_id', $art->law_id)
|
||||
->where('number', '>', $art->number)
|
||||
->orderBy('number')
|
||||
->first();
|
||||
@@ -112,7 +125,8 @@ class ArtController extends Controller
|
||||
if ($nextArt) {
|
||||
$nextLaw = Law::find($nextArt->law_id);
|
||||
$nextArt->is_like = $this->isLiked($nextArt->id);
|
||||
$nextArt->note = Note::select('id', 'note', 'color_code')->where('user_id', auth()->user()->id)->where('art_id', $nextArt->id)->get();
|
||||
$nextArt->note = Note::select('id', 'note', 'color_code')->where('user_id', auth()->id())->where('art_id', $nextArt->id)->get();
|
||||
$nextArt->highlight = \App\Models\ArtHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')->where('user_id', auth()->id())->where('art_id', $nextArt->id)->get();
|
||||
$nextArt->category = $nextLaw?->category?->name;
|
||||
$nextArt->route = array_values($this->route(Art::class, $nextArt));
|
||||
}
|
||||
@@ -144,7 +158,6 @@ class ArtController extends Controller
|
||||
return $this->success(null, 'Success', 'Art liked successfully');
|
||||
}
|
||||
|
||||
|
||||
private function isLiked($art_id): bool
|
||||
{
|
||||
return LikeArt::query()->where('art_id', $art_id)
|
||||
@@ -152,23 +165,29 @@ class ArtController extends Controller
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function isLockedForCurrentUser(?Law $law): bool
|
||||
{
|
||||
return ! auth()->user()->isSubscriber() && (bool) $law?->is_locked;
|
||||
}
|
||||
|
||||
public function likes()
|
||||
{
|
||||
$likes = LikeArt::query()->where('user_id', auth()->user()->id)
|
||||
->with('art')
|
||||
->with('art.law')
|
||||
->get()
|
||||
->map(function ($q) {
|
||||
$isLocked = $this->isLockedForCurrentUser($q->art?->law);
|
||||
|
||||
return [
|
||||
'id' => $q->art->id,
|
||||
'title' => $q->art->title,
|
||||
'text' => $q->art->text
|
||||
'text' => $isLocked ? null : $q->art->text,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success($likes);
|
||||
}
|
||||
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
@@ -183,7 +202,7 @@ class ArtController extends Controller
|
||||
'branch_id' => 'nullable|array',
|
||||
'category_id' => 'nullable|int',
|
||||
'per_page' => 'nullable|integer',
|
||||
'search' => 'nullable|string'
|
||||
'search' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$models = [
|
||||
@@ -196,7 +215,7 @@ class ArtController extends Controller
|
||||
Part::class,
|
||||
Gate::class,
|
||||
Branch::class,
|
||||
Art::class
|
||||
Art::class,
|
||||
];
|
||||
|
||||
$results = [];
|
||||
@@ -220,13 +239,13 @@ class ArtController extends Controller
|
||||
'part_id',
|
||||
'gate_id',
|
||||
'chapter_id',
|
||||
'branch_id'
|
||||
'branch_id',
|
||||
];
|
||||
|
||||
$model = new $modelClass;
|
||||
if ($model->getTable() == 'art') {
|
||||
$query->where(function ($query) use ($validated, $filters) {
|
||||
if (!empty($validated['search'])) {
|
||||
if (! empty($validated['search'])) {
|
||||
$query->where(function ($q) use ($validated) {
|
||||
$q->where('title', 'LIKE', "%{$validated['search']}%")
|
||||
->orWhere('text', 'LIKE', "%{$validated['search']}%");
|
||||
@@ -234,14 +253,13 @@ class ArtController extends Controller
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (!empty($validated[$filter])) {
|
||||
if (! empty($validated[$filter])) {
|
||||
$query->whereIn($filter, $validated[$filter]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!empty($validated['category_id'])) {
|
||||
if (! empty($validated['category_id'])) {
|
||||
$query->whereIn('law_id', Category::where('type', $this->convertValueTo($validated['category_id']))->get()->pluck('id'));
|
||||
}
|
||||
|
||||
@@ -250,19 +268,20 @@ class ArtController extends Controller
|
||||
return $items->map(function ($item) use ($validated, $modelClass) {
|
||||
$text = $item->text ?? '';
|
||||
$search = preg_quote($validated['search'] ?? '', '/');
|
||||
if (!empty($text) && preg_match('/\b(.{0,50})\b(' . $search . ')\b(.{0,50})\b/i', $text, $matches)) {
|
||||
$context = trim($matches[1] ?? '') . ' ' . $matches[2] . ' ' . trim($matches[3] ?? '');
|
||||
if (! empty($text) && preg_match('/\b(.{0,50})\b('.$search.')\b(.{0,50})\b/i', $text, $matches)) {
|
||||
$context = trim($matches[1] ?? '').' '.$matches[2].' '.trim($matches[3] ?? '');
|
||||
} else {
|
||||
$context = $text;
|
||||
}
|
||||
|
||||
$law = Law::find($item->law_id);
|
||||
$isLocked = $this->isLockedForCurrentUser($law);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'text' => $context,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'text' => $isLocked ? null : $context,
|
||||
'is_locked' => $isLocked,
|
||||
'type' => 'art',
|
||||
'route' => array_values($this->route($modelClass, $item)),
|
||||
'category' => optional($law->category)->name,
|
||||
@@ -270,11 +289,11 @@ class ArtController extends Controller
|
||||
'count_art' => $law->arts->count(),
|
||||
'count_volums' => $law->volums->count(),
|
||||
'price' => $law->price,
|
||||
'image' => $law->image
|
||||
'image' => $law->image,
|
||||
];
|
||||
})->toArray();
|
||||
} else {
|
||||
$query = $query->where('title', 'LIKE', '%' . ($validated['search'] ?? '') . '%');
|
||||
$query = $query->where('title', 'LIKE', '%'.($validated['search'] ?? '').'%');
|
||||
}
|
||||
|
||||
$items = $query->paginate($validated['per_page'] ?? 10);
|
||||
@@ -287,21 +306,21 @@ class ArtController extends Controller
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => (new $modelClass)->getTable(),
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'route' => array_values($this->route($modelClass, $item)),
|
||||
'category' => optional($law?->category)?->name ?? '',
|
||||
'image' => $law->image
|
||||
'image' => $law->image,
|
||||
];
|
||||
} else if ((new $modelClass)->getTable() == 'art') {
|
||||
} elseif ((new $modelClass)->getTable() == 'art') {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => (new $modelClass)->getTable(),
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'route' => array_values($this->route($modelClass, $item)),
|
||||
'law' => $law?->title ?? '',
|
||||
'image' => $law->image
|
||||
'image' => $law->image,
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
@@ -309,7 +328,7 @@ class ArtController extends Controller
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => (new $modelClass)->getTable(),
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : optional($law)->is_locked,
|
||||
'route' => array_values($this->route($modelClass, $item)),
|
||||
'law' => $law?->title ?? '',
|
||||
];
|
||||
@@ -317,8 +336,6 @@ class ArtController extends Controller
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function route($modelClass, $query)
|
||||
{
|
||||
if ((new $modelClass)->getTable() == 'art') {
|
||||
@@ -331,12 +348,14 @@ class ArtController extends Controller
|
||||
Chapter::find($query->chapter_id)?->title,
|
||||
Part::find($query->part_id)?->title,
|
||||
Gate::find($query->gate_id)?->title,
|
||||
Branch::find($query->branch_id)?->title
|
||||
Branch::find($query->branch_id)?->title,
|
||||
]);
|
||||
|
||||
$route = array_values($route);
|
||||
|
||||
return $route;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -346,7 +365,7 @@ class ArtController extends Controller
|
||||
|
||||
$models = [
|
||||
Law::class,
|
||||
Art::class
|
||||
Art::class,
|
||||
];
|
||||
|
||||
$results = [];
|
||||
@@ -355,7 +374,6 @@ class ArtController extends Controller
|
||||
$results = array_merge($results, $this->searchModelFastSearch($modelClass, $search));
|
||||
}
|
||||
|
||||
|
||||
$models_other = [
|
||||
Volum::class,
|
||||
Book::class,
|
||||
@@ -369,13 +387,12 @@ class ArtController extends Controller
|
||||
|
||||
$found = false;
|
||||
foreach ($models_other as $modelClass) {
|
||||
if (count($this->searchModelFastSearch($modelClass, $search)) > 0 && !$found) {
|
||||
if (count($this->searchModelFastSearch($modelClass, $search)) > 0 && ! $found) {
|
||||
$results = array_merge($results, $this->searchModelFastSearch($modelClass, $search));
|
||||
$found = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$models = [
|
||||
Law::class,
|
||||
Volum::class,
|
||||
@@ -386,7 +403,7 @@ class ArtController extends Controller
|
||||
Part::class,
|
||||
Gate::class,
|
||||
Branch::class,
|
||||
Art::class
|
||||
Art::class,
|
||||
];
|
||||
|
||||
$count_all = 0;
|
||||
@@ -395,7 +412,6 @@ class ArtController extends Controller
|
||||
$count_all += count($this->searchModel($modelClass, $search));
|
||||
}
|
||||
|
||||
|
||||
return $this->success(['items' => $results, 'count' => $count_all]);
|
||||
}
|
||||
|
||||
@@ -430,7 +446,7 @@ class ArtController extends Controller
|
||||
'title' => $q->title,
|
||||
'type' => $table === 'section' ? 'sections' : $table,
|
||||
'category' => $category,
|
||||
'law' => Law::where('id', $q->law_id)->first()?->title
|
||||
'law' => Law::where('id', $q->law_id)->first()?->title,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -439,7 +455,7 @@ class ArtController extends Controller
|
||||
'title' => $q->title,
|
||||
'type' => $table === 'section' ? 'sections' : $table,
|
||||
'category' => $category,
|
||||
'image' => Law::where('id', $q->id)->first()?->image
|
||||
'image' => Law::where('id', $q->id)->first()?->image,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -455,22 +471,24 @@ class ArtController extends Controller
|
||||
Chapter::find($q->chapter_id)?->title,
|
||||
Part::find($q->part_id)?->title,
|
||||
Gate::find($q->gate_id)?->title,
|
||||
Branch::find($q->branch_id)?->title
|
||||
Branch::find($q->branch_id)?->title,
|
||||
]);
|
||||
|
||||
$text = $q->text;
|
||||
$search = preg_quote($search, '/');
|
||||
if (preg_match('/\b(.{0,50})\b(' . $search . ')\b(.{0,50})\b/i', $text, $matches)) {
|
||||
$context = trim($matches[1]) . ' ' . $matches[2] . ' ' . trim($matches[3]);
|
||||
if (preg_match('/\b(.{0,50})\b('.$search.')\b(.{0,50})\b/i', $text, $matches)) {
|
||||
$context = trim($matches[1]).' '.$matches[2].' '.trim($matches[3]);
|
||||
} else {
|
||||
$context = $text;
|
||||
}
|
||||
|
||||
$isLocked = $this->isLockedForCurrentUser($law);
|
||||
|
||||
return [
|
||||
'id' => $q->id,
|
||||
'title' => $q->title,
|
||||
'text' => $context,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : $law->is_locked,
|
||||
'text' => $isLocked ? null : $context,
|
||||
'is_locked' => $isLocked,
|
||||
'type' => 'art',
|
||||
'route' => $route,
|
||||
'category' => $law?->category?->name,
|
||||
@@ -478,7 +496,7 @@ class ArtController extends Controller
|
||||
'count_art' => $law->arts->count(),
|
||||
'count_volum' => $law->volums->count(),
|
||||
'price' => $law->price,
|
||||
'image' => $law->image
|
||||
'image' => $law->image,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
58
app/Http/Controllers/api/ArtHighlightController.php
Normal file
58
app/Http/Controllers/api/ArtHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ArtHighlight;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ArtHighlightController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'art_id' => 'required',
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
ArtHighlight::create([
|
||||
'user_id' => auth()->id(),
|
||||
'art_id' => $validated['art_id'],
|
||||
'start_index' => $validated['start_index'],
|
||||
'end_index' => $validated['end_index'],
|
||||
'selected_text' => $validated['selected_text'],
|
||||
'color_code' => $validated['color_code'],
|
||||
]);
|
||||
|
||||
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||
}
|
||||
|
||||
public function update($id, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$highlight = ArtHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->update($validated);
|
||||
|
||||
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$highlight = ArtHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->delete();
|
||||
|
||||
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AuthRequest;
|
||||
use App\Models\User;
|
||||
use App\Models\UserCode;
|
||||
use App\Models\SubscribePlan;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -24,7 +25,7 @@ class AuthController extends Controller
|
||||
return User::create([
|
||||
'name' => 'Law',
|
||||
'mobile' => $request->mobile,
|
||||
'email' => 'law' . $request->mobile . '@law.com',
|
||||
'email' => 'law'.$request->mobile.'@law.com',
|
||||
'password' => Hash::make($request->mobile),
|
||||
]);
|
||||
});
|
||||
@@ -59,7 +60,10 @@ class AuthController extends Controller
|
||||
if ($response->successful()) {
|
||||
return $this->success([], 'موفق', 'کد ورود به شماره موبایل شما ارسال شد');
|
||||
} else {
|
||||
return $this->failed([], 'نا موفق', 'در ارسال کد به شماره شما مشکلی وجود دارد');
|
||||
return $this->failed([], [
|
||||
'title' => 'نا موفق',
|
||||
'message' => 'در ارسال کد به شماره شما مشکلی وجود دارد',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +77,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)
|
||||
@@ -88,6 +92,29 @@ class AuthController extends Controller
|
||||
if ($latestCode->code === $request->code) {
|
||||
$token = $user->createToken('mobile')->plainTextToken;
|
||||
|
||||
// --- AUTO 30-DAY SUBSCRIPTION LOGIC ---
|
||||
|
||||
// 1. Check if they have ever used a free plan before
|
||||
$hasUsedFreePlan = $user->userSubscribers()
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
})->exists();
|
||||
|
||||
// 2. If not, find the active free plan and assign it
|
||||
if (!$hasUsedFreePlan) {
|
||||
$freePlan = SubscribePlan::where('is_free', true)->where('is_active', true)->first();
|
||||
|
||||
if ($freePlan) {
|
||||
// We intentionally DO NOT pass 'is_free' => true here,
|
||||
// matching SubscribePlanController to ensure the frontend hides the banner!
|
||||
$user->userSubscribers()->create([
|
||||
'subscribe_plan_id' => $freePlan->id,
|
||||
'expired_at' => now()->addDays($freePlan->expired_day + 1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
// ----------------------------------------------
|
||||
|
||||
return $this->success(['token' => $token], 'ورود موفقیت امیز بود');
|
||||
} else {
|
||||
return $this->failed([], 'کد ورود اشتباه است', 404);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\BookRequest;
|
||||
use App\Models\Book;
|
||||
use App\Models\Law;
|
||||
use App\Models\Order;
|
||||
use App\Traits\BaseApiResponse;
|
||||
|
||||
class BookController extends Controller
|
||||
@@ -23,8 +22,8 @@ class BookController extends Controller
|
||||
$books = Book::where('volum_id', $validated['volum_id'])->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$books->getCollection()->transform(function ($section) {
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== true ? true : Law::where('is_locked',$section['law_id'])->first()?->is_locked ?? false;
|
||||
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $section['law_id'])->first()?->is_locked ?? false;
|
||||
|
||||
unset($section['law_id']);
|
||||
unset($section['volum_id']);
|
||||
|
||||
|
||||
424
app/Http/Controllers/api/BylawApiController.php
Normal file
424
app/Http/Controllers/api/BylawApiController.php
Normal file
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawDocument;
|
||||
use App\Models\BylawLegalAuthority;
|
||||
use App\Models\BylawSubjectCategory;
|
||||
use App\Models\BylawNode;
|
||||
use App\Models\BylawCodificationLink;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawApiController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function categories()
|
||||
{
|
||||
$categories = BylawSubjectCategory::all()->map(function ($category) {
|
||||
return [
|
||||
'id' => $category->id,
|
||||
'name' => $category->name,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success($categories, 'Success', 'Bylaw Categories List');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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_by' => 'nullable|in:date,number',
|
||||
'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);
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'promulgation_number' : 'approval_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $sortOrder);
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$docIds = collect($paginator->items())->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||
$nodesByDoc = collect();
|
||||
|
||||
if (!empty($docIds)) {
|
||||
$nodesByDoc = BylawNode::whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get()
|
||||
->groupBy(fn($node) => (int)$node->document_id);
|
||||
}
|
||||
|
||||
$items = collect($paginator->items())->map(function ($item) use ($nodesByDoc) {
|
||||
$itemNodes = $nodesByDoc->get((int)$item->id, collect());
|
||||
return $this->formatBylaw($item, $itemNodes);
|
||||
});
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
public function single($id)
|
||||
{
|
||||
$bylaw = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
||||
->where('is_published', true)
|
||||
->find($id);
|
||||
|
||||
if (!$bylaw) {
|
||||
return $this->failed([], [
|
||||
'title' => 'خطا',
|
||||
'message' => 'آییننامه مورد نظر یافت نشد.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
$formatted = $this->formatBylaw($bylaw);
|
||||
|
||||
$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();
|
||||
|
||||
$formatted['highlight'] = \App\Models\BylawHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')
|
||||
->where('user_id', auth()->id())
|
||||
->where('bylaw_document_id', $bylaw->id)
|
||||
->get();
|
||||
|
||||
|
||||
if (!$formatted['is_locked']) {
|
||||
$eloquentNodes = BylawNode::with('type')
|
||||
->where('document_id', $bylaw->id)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get();
|
||||
|
||||
$formatted['nodes'] = $eloquentNodes->map(function ($node) {
|
||||
return [
|
||||
'id' => $node->id,
|
||||
'parent_id' => $node->parent_id,
|
||||
'node_type' => optional($node->type)->name,
|
||||
'node_identifier' => $node->node_identifier,
|
||||
'title' => $node->title,
|
||||
'node_content' => $node->node_content,
|
||||
'sort_order' => $node->sort_order,
|
||||
'valid_from' => $node->valid_from,
|
||||
'valid_to' => $node->valid_to,
|
||||
'node_status' => $node->node_status,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$links = BylawCodificationLink::where('source_entity_type', 'bylaw_doc')
|
||||
->where('source_entity_id', $bylaw->id)
|
||||
->get()
|
||||
->map(function ($link) {
|
||||
return [
|
||||
'id' => $link->id,
|
||||
'target_entity_type' => $link->target_entity_type,
|
||||
'target_entity_id' => $link->target_entity_id,
|
||||
'link_type' => $link->link_type,
|
||||
'effective_date' => $link->effective_date,
|
||||
'justification' => $link->justification,
|
||||
];
|
||||
});
|
||||
|
||||
$formatted['links'] = $links->toArray();
|
||||
}
|
||||
|
||||
return $this->success($formatted, 'Success', 'Bylaw Document Details Fetched');
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'search' => 'nullable|string',
|
||||
'sort_by' => 'nullable|in:date,number',
|
||||
'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('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}%")
|
||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||
$subQuery->select('document_id')
|
||||
->from('bylaw_nodes')
|
||||
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'promulgation_number' : 'approval_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $sortOrder);
|
||||
}
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
$docIds = collect($paginator->items())->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||
$nodesByDoc = collect();
|
||||
|
||||
if (!empty($docIds)) {
|
||||
$nodesByDoc = BylawNode::whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get()
|
||||
->groupBy(fn($node) => (int)$node->document_id);
|
||||
}
|
||||
|
||||
$items = collect($paginator->items())->map(function ($item) use ($nodesByDoc) {
|
||||
$itemNodes = $nodesByDoc->get((int)$item->id, collect());
|
||||
return $this->formatBylaw($item, $itemNodes);
|
||||
});
|
||||
|
||||
$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 fast_search(Request $request)
|
||||
{
|
||||
$searchTerm = $request->input('search');
|
||||
|
||||
$query = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
||||
->where('is_published', true);
|
||||
|
||||
if (!empty($searchTerm)) {
|
||||
$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}%")
|
||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||
$subQuery->select('document_id')
|
||||
->from('bylaw_nodes')
|
||||
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
$items = $query->orderBy('approval_date', 'desc')->take(5)->get();
|
||||
|
||||
$docIds = $items->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||
$nodesByDoc = collect();
|
||||
|
||||
if (!empty($docIds)) {
|
||||
$nodesByDoc = BylawNode::whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get()
|
||||
->groupBy(fn($node) => (int)$node->document_id);
|
||||
}
|
||||
|
||||
$formattedItems = $items->map(function ($item) use ($nodesByDoc) {
|
||||
$itemNodes = $nodesByDoc->get((int)$item->id, collect());
|
||||
return $this->formatBylaw($item, $itemNodes);
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'items' => $formattedItems,
|
||||
'count' => $count
|
||||
], 'Success', 'Fast Search Results');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
$itemsToFormat = array_filter([$previousItem, $current, $nextItem]);
|
||||
|
||||
$docIds = collect($itemsToFormat)->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||
$nodesByDoc = collect();
|
||||
|
||||
if (!empty($docIds)) {
|
||||
$nodesByDoc = BylawNode::whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get()
|
||||
->groupBy(fn($node) => (int)$node->document_id);
|
||||
}
|
||||
|
||||
$results = collect($itemsToFormat)->map(function($item) use ($nodesByDoc) {
|
||||
$itemNodes = $nodesByDoc->get((int)$item->id, collect());
|
||||
$formatted = $this->formatBylaw($item, $itemNodes);
|
||||
|
||||
$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();
|
||||
|
||||
$formatted['highlight'] = \App\Models\BylawHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')
|
||||
->where('user_id', auth()->id())
|
||||
->where('bylaw_document_id', $item->id)
|
||||
->get();
|
||||
|
||||
return $formatted;
|
||||
})->values()->toArray();
|
||||
|
||||
return $this->success($results, 'Success', 'Pager Window Details');
|
||||
}
|
||||
|
||||
private function isLockedForCurrentUser($item): bool
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
$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");
|
||||
|
||||
if (empty($nodeContentAsDescription)) {
|
||||
$nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'category_id' => $item->category_id,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'approval_authority_id' => $item->approval_authority_id,
|
||||
'approval_authority_name' => optional($item->approvalAuthority)->name,
|
||||
'approval_date' => $item->approval_date,
|
||||
'promulgation_authority_id' => $item->promulgation_authority_id,
|
||||
'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' => $isLocked ? null : ($item->pdf_file_path ? asset(str_replace('storage/storage/', 'storage/', 'storage/' . $item->pdf_file_path)) : null),
|
||||
'description' => $isLocked ? null : $nodeContentAsDescription,
|
||||
'debug_node_count' => collect($itemNodes)->count(),
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/api/BylawHighlightController.php
Normal file
58
app/Http/Controllers/api/BylawHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawHighlight;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawHighlightController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'bylaw_id' => 'required', // Clean request parameter
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
BylawHighlight::create([
|
||||
'user_id' => auth()->id(),
|
||||
'bylaw_document_id' => $validated['bylaw_id'], // Mapped to DB column
|
||||
'start_index' => $validated['start_index'],
|
||||
'end_index' => $validated['end_index'],
|
||||
'selected_text' => $validated['selected_text'],
|
||||
'color_code' => $validated['color_code'],
|
||||
]);
|
||||
|
||||
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||
}
|
||||
|
||||
public function update($id, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$highlight = BylawHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->update($validated);
|
||||
|
||||
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$highlight = BylawHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->delete();
|
||||
|
||||
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/api/BylawNoteController.php
Normal file
56
app/Http/Controllers/api/BylawNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BylawNote;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BylawNoteController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'bylaw_id' => 'required', // Cleaned up for the frontend!
|
||||
'note' => 'required',
|
||||
'color_code' => 'nullable',
|
||||
]);
|
||||
|
||||
BylawNote::query()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'bylaw_document_id' => $validated['bylaw_id'], // Maps perfectly to the DB
|
||||
'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::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 = BylawNote::where('user_id', auth()->id())->findOrFail($id);
|
||||
$note->delete();
|
||||
|
||||
return $this->success([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,12 @@ class CategoriesController extends Controller
|
||||
use BaseApiResponse;
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
{
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$page = $request->input('page', 1);
|
||||
|
||||
$categories = Category::paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
|
||||
return $this->success($categories->items());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ChapterRequest;
|
||||
use App\Models\Chapter;
|
||||
use App\Models\Law;
|
||||
use App\Models\Order;
|
||||
use App\Traits\BaseApiResponse;
|
||||
|
||||
class ChapterController extends Controller
|
||||
@@ -20,11 +19,11 @@ class ChapterController extends Controller
|
||||
$perPage = $validated['per_page'] ?? 15;
|
||||
$page = $validated['page'] ?? 1;
|
||||
|
||||
$chapters = Chapter::where('book_id' , $validated['book_id'])->paginate($perPage, ['*'], 'page', $page);
|
||||
$chapters = Chapter::where('book_id', $validated['book_id'])->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$chapters->getCollection()->transform(function ($section) {
|
||||
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $section['law_id'])->first()?->is_locked;
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $section['law_id'])->first()?->is_locked;
|
||||
|
||||
unset($section['law_id']);
|
||||
unset($section['section_id']);
|
||||
|
||||
103
app/Http/Controllers/api/CurrencyController.php
Normal file
103
app/Http/Controllers/api/CurrencyController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\GoldCurrency;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CurrencyController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function goldPrices()
|
||||
{
|
||||
// 1. Check if we already updated the database today
|
||||
$latestUpdate = GoldCurrency::max('updated_at');
|
||||
$isUpdatedToday = $latestUpdate && Carbon::parse($latestUpdate)->isToday();
|
||||
|
||||
// 2. If not updated today, call the API
|
||||
if (!$isUpdatedToday) {
|
||||
$this->fetchAndCacheGoldPrices();
|
||||
}
|
||||
|
||||
// 3. Fetch data from our own database
|
||||
$goldCurrencies = GoldCurrency::all();
|
||||
|
||||
// 4. Debug check: If it is STILL empty, the API failed and we have no backup data yet.
|
||||
if ($goldCurrencies->isEmpty()) {
|
||||
// FIX: Pass an array to format the 'alert' as a JSON Object for Kotlin!
|
||||
return $this->failed(null, [
|
||||
'title' => 'خطا',
|
||||
'message' => 'ارتباط با سرور قیمتها برقرار نشد. لطفا بعدا تلاش کنید.'
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. Map the data
|
||||
$mappedCurrencies = $goldCurrencies->map(function ($currency) {
|
||||
return [
|
||||
'change_percent' => $currency->change_percent,
|
||||
'change_value' => $currency->change_value,
|
||||
'date' => $currency->updated_at->format('Y-m-d'),
|
||||
'name' => $currency->name,
|
||||
'name_en' => $currency->name_en,
|
||||
'price' => $currency->price,
|
||||
'symbol' => $currency->symbol,
|
||||
'time' => $currency->updated_at->format('H:i:s'),
|
||||
'time_unix' => $currency->time_unix,
|
||||
'unit' => $currency->unit,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'gold' => $mappedCurrencies
|
||||
], 'موفق', 'لیست قیمت طلا و سکه دریافت شد.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper to fetch from the external API and save to DB
|
||||
*/
|
||||
private function fetchAndCacheGoldPrices()
|
||||
{
|
||||
try {
|
||||
$url = 'https://BrsApi.ir/Api/Market/Gold_Currency.php?key=B727Ue3kBrbtCwHR3BmSVZKRRNF1sllp';
|
||||
|
||||
// Added withoutVerifying() for Iranian SSL issues and a User-Agent to bypass firewalls
|
||||
$response = Http::withoutVerifying()
|
||||
->withHeaders([
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->timeout(15)
|
||||
->get($url);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
if (isset($data['gold']) && is_array($data['gold'])) {
|
||||
foreach ($data['gold'] as $item) {
|
||||
GoldCurrency::updateOrCreate(
|
||||
['name_en' => $item['name_en']],
|
||||
[
|
||||
'name' => $item['name'],
|
||||
'price' => (int) $item['price'],
|
||||
'change_percent' => (float) $item['change_percent'],
|
||||
'change_value' => (int) $item['change_value'],
|
||||
'symbol' => $item['symbol'] ?? null,
|
||||
'unit' => $item['unit'] ?? null,
|
||||
'time_unix' => (int) $item['time_unix'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log::warning('BrsApi returned an error status: ' . $response->status() . ' - ' . $response->body());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to fetch from BrsApi: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ namespace App\Http\Controllers\api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderArt;
|
||||
use App\Models\Law;
|
||||
use App\Models\FolderBylaw;
|
||||
use App\Models\FolderJudicialPrecedent;
|
||||
use App\Models\FolderLegalOpinion;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FolderController extends Controller
|
||||
{
|
||||
@@ -15,29 +18,34 @@ class FolderController extends Controller
|
||||
|
||||
public function index()
|
||||
{
|
||||
$folder = Folder::query()->where('user_id', auth()->id())->get()
|
||||
// Eager load everything to prevent N+1 Database issues
|
||||
$folder = Folder::with(['arts', 'bylaws', 'judicialPrecedents', 'legalOpinions'])
|
||||
->where('user_id', auth()->id())
|
||||
->get()
|
||||
->map(function ($q) {
|
||||
return [
|
||||
"id" => $q->id,
|
||||
"name" => $q->name,
|
||||
"count_of_arts" => $q->arts->count(),
|
||||
"created_at" => $q->created_at,
|
||||
'id' => $q->id,
|
||||
'name' => $q->name,
|
||||
'count_of_arts' => $q->arts->count(),
|
||||
'count_of_bylaws' => $q->bylaws->count(),
|
||||
'count_of_judicials' => $q->judicialPrecedents->count(),
|
||||
'count_of_opinions' => $q->legalOpinions->count(),
|
||||
'created_at' => $q->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
|
||||
return $this->success($folder);
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validted = $request->validate([
|
||||
'name' => 'required'
|
||||
'name' => 'required',
|
||||
]);
|
||||
|
||||
Folder::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'name' => $validted['name']
|
||||
'user_id' => auth()->id(),
|
||||
'name' => $validted['name'],
|
||||
]);
|
||||
|
||||
return $this->success([], 'ایجاد شد', 'پوشه با موفقیت ایجاد شد.');
|
||||
@@ -46,50 +54,94 @@ class FolderController extends Controller
|
||||
public function assign(Request $request)
|
||||
{
|
||||
$validted = $request->validate([
|
||||
'folder_id' => 'required',
|
||||
'art_id' => 'required'
|
||||
'folder_id' => 'required',
|
||||
'art_id' => 'nullable',
|
||||
'bylaw_document_id' => 'nullable',
|
||||
'judicial_precedent_id' => 'nullable',
|
||||
'legal_opinion_id' => 'nullable',
|
||||
]);
|
||||
|
||||
if (Folder::query()->where('id', $validted['folder_id'])->count() == 0) {
|
||||
return $this->failed([], 'پوشه مورد نظر یافت نشد', 404);
|
||||
// 🚨 SECURITY FIX: Check if the folder exists AND belongs to the logged-in user
|
||||
if (Folder::where('id', $validted['folder_id'])->where('user_id', auth()->id())->count() == 0) {
|
||||
return $this->failed([], 'پوشه مورد نظر یافت نشد یا شما دسترسی ندارید', 404);
|
||||
}
|
||||
|
||||
if (FolderArt::query()->where('folder_id', $validted['folder_id'])->where('art_id', $validted['art_id'])->count() > 0) {
|
||||
return $this->failed([], 'ارت مورد نظر قبلا اضافه شده است', 400);
|
||||
if ($request->filled('art_id')) {
|
||||
if (FolderArt::where('folder_id', $validted['folder_id'])->where('art_id', $validted['art_id'])->exists()) {
|
||||
return $this->failed([], 'ارت مورد نظر قبلا اضافه شده است', 400);
|
||||
}
|
||||
FolderArt::create(['folder_id' => $validted['folder_id'], 'art_id' => $validted['art_id']]);
|
||||
}
|
||||
elseif ($request->filled('bylaw_document_id')) {
|
||||
if (FolderBylaw::where('folder_id', $validted['folder_id'])->where('bylaw_document_id', $validted['bylaw_document_id'])->exists()) {
|
||||
return $this->failed([], 'آییننامه قبلا اضافه شده است', 400);
|
||||
}
|
||||
FolderBylaw::create(['folder_id' => $validted['folder_id'], 'bylaw_document_id' => $validted['bylaw_document_id']]);
|
||||
}
|
||||
elseif ($request->filled('judicial_precedent_id')) {
|
||||
if (FolderJudicialPrecedent::where('folder_id', $validted['folder_id'])->where('judicial_precedent_id', $validted['judicial_precedent_id'])->exists()) {
|
||||
return $this->failed([], 'رأی قبلا اضافه شده است', 400);
|
||||
}
|
||||
FolderJudicialPrecedent::create(['folder_id' => $validted['folder_id'], 'judicial_precedent_id' => $validted['judicial_precedent_id']]);
|
||||
}
|
||||
elseif ($request->filled('legal_opinion_id')) {
|
||||
if (FolderLegalOpinion::where('folder_id', $validted['folder_id'])->where('legal_opinion_id', $validted['legal_opinion_id'])->exists()) {
|
||||
return $this->failed([], 'نظریه قبلا اضافه شده است', 400);
|
||||
}
|
||||
FolderLegalOpinion::create(['folder_id' => $validted['folder_id'], 'legal_opinion_id' => $validted['legal_opinion_id']]);
|
||||
}
|
||||
else {
|
||||
return $this->failed([], 'موردی برای افزودن یافت نشد', 400);
|
||||
}
|
||||
|
||||
FolderArt::create([
|
||||
'folder_id' => $validted['folder_id'],
|
||||
'art_id' => $validted['art_id']
|
||||
]);
|
||||
|
||||
return $this->success([], 'اضافه شد', 'ارت با موفیت اضافه شد');
|
||||
return $this->success([], 'اضافه شد', 'با موفیت اضافه شد');
|
||||
}
|
||||
|
||||
public function folder($id)
|
||||
{
|
||||
$arts = FolderArt::where('folder_id', $id)
|
||||
->with('arts.chapter', 'arts.part', 'arts.volum', 'arts.law', 'arts.book', 'arts.section', 'arts.gate')
|
||||
->get()
|
||||
->pluck('arts')
|
||||
->flatten();
|
||||
// Added 'arts.law.category' so we can fetch category_name without slowing down the DB
|
||||
$folder = Folder::with([
|
||||
'arts.chapter', 'arts.part', 'arts.volum', 'arts.law.category', 'arts.book', 'arts.section', 'arts.gate',
|
||||
'bylaws.category', 'bylaws.approvalAuthority', 'bylaws.promulgationAuthority',
|
||||
'judicialPrecedents.category',
|
||||
'legalOpinions.category'
|
||||
])->where('user_id', auth()->id())->find($id);
|
||||
|
||||
$arts = $arts->map(function ($art) {
|
||||
if (!$folder) {
|
||||
return $this->failed([], 'پوشه یافت نشد یا شما دسترسی ندارید', 404);
|
||||
}
|
||||
|
||||
$items = collect();
|
||||
|
||||
// 1. Process Arts
|
||||
$arts = $folder->arts->map(function ($art) {
|
||||
$text = $art->text ?? '';
|
||||
$shortText = '';
|
||||
if (strlen($text) > 50) {
|
||||
$shortText = substr($text, 0, 50);
|
||||
$shortText = substr($shortText, 0, strrpos($shortText, ' ')) . '...';
|
||||
$shortText = mb_substr($text, 0, 50);
|
||||
$shortText = mb_substr($shortText, 0, mb_strrpos($shortText, ' ')).'...';
|
||||
} else {
|
||||
$shortText = $text;
|
||||
}
|
||||
|
||||
$isLocked = ! auth()->user()->isSubscriber() && (bool) $art->law?->is_locked;
|
||||
|
||||
return [
|
||||
'id' => $art->id,
|
||||
'title' => $art->title,
|
||||
'text' => $shortText,
|
||||
'text' => $isLocked ? null : $shortText,
|
||||
'number' => $art->number,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $art->law->id)->first()?->is_locked,
|
||||
'is_locked' => $isLocked,
|
||||
'type' => 'art',
|
||||
'assigned_at' => $art->pivot->created_at,
|
||||
|
||||
'law_title' => $art->law?->title,
|
||||
|
||||
'law' => $art->law != null ? [
|
||||
'id' => $art->law->id,
|
||||
'title' => $art->law->title,
|
||||
'number' => $art->law->number,
|
||||
] : null,
|
||||
|
||||
'chapter' => $art->chapter != null ? [
|
||||
'id' => $art->chapter->id,
|
||||
'title' => $art->chapter->title,
|
||||
@@ -105,11 +157,6 @@ class FolderController extends Controller
|
||||
'title' => $art->volum->title,
|
||||
'number' => $art->volum->number,
|
||||
] : null,
|
||||
'law' => $art->law != null ? [
|
||||
'id' => $art->law->id,
|
||||
'title' => $art->law->title,
|
||||
'number' => $art->law->number,
|
||||
] : null,
|
||||
'book' => $art->book != null ? [
|
||||
'id' => $art->book->id,
|
||||
'title' => $art->book->title,
|
||||
@@ -127,25 +174,191 @@ class FolderController extends Controller
|
||||
] : null,
|
||||
];
|
||||
});
|
||||
$items = $items->concat($arts);
|
||||
|
||||
return $this->success($arts);
|
||||
// 2. Process Bylaws
|
||||
$bylawIds = $folder->bylaws->pluck('id')->toArray();
|
||||
$allBylawNodes = collect();
|
||||
if (!empty($bylawIds)) {
|
||||
$allBylawNodes = DB::table('bylaw_nodes')
|
||||
->whereIn('document_id', $bylawIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
$bylaws = $folder->bylaws->map(function ($bylaw) use ($allBylawNodes) {
|
||||
$itemNodes = $allBylawNodes->where('document_id', $bylaw->id);
|
||||
$formatted = $this->formatBylaw($bylaw, $itemNodes);
|
||||
$formatted['type'] = 'bylaw';
|
||||
$formatted['assigned_at'] = $bylaw->pivot->created_at;
|
||||
return $formatted;
|
||||
});
|
||||
$items = $items->concat($bylaws);
|
||||
|
||||
// 3. Process Judicial Precedents
|
||||
$judicials = $folder->judicialPrecedents->map(function ($judicial) {
|
||||
$formatted = $this->formatJudicial($judicial);
|
||||
$formatted['type'] = 'judicial_precedent';
|
||||
$formatted['assigned_at'] = $judicial->pivot->created_at;
|
||||
return $formatted;
|
||||
});
|
||||
$items = $items->concat($judicials);
|
||||
|
||||
// 4. Process Legal Opinions
|
||||
$opinions = $folder->legalOpinions->map(function ($opinion) {
|
||||
$formatted = $this->formatOpinion($opinion);
|
||||
$formatted['type'] = 'legal_opinion';
|
||||
$formatted['assigned_at'] = $opinion->pivot->created_at;
|
||||
return $formatted;
|
||||
});
|
||||
$items = $items->concat($opinions);
|
||||
|
||||
// Sort everything together based on when it was added to the folder!
|
||||
$sortedItems = $items->sortByDesc('assigned_at')->values()->toArray();
|
||||
|
||||
return $this->success($sortedItems);
|
||||
}
|
||||
|
||||
public function delete_folder($id)
|
||||
{
|
||||
if (Folder::query()->where('id', $id)->count() == 0) {
|
||||
return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد', 404);
|
||||
$folder = Folder::where('id', $id)->where('user_id', auth()->id())->first();
|
||||
|
||||
if (!$folder) {
|
||||
return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد یا شما دسترسی ندارید', 404);
|
||||
}
|
||||
Folder::query()->where('id', $id)->delete();
|
||||
|
||||
$folder->delete();
|
||||
return $this->success([], 'حذف شد', 'پوشه با موفقیت حذف شد');
|
||||
}
|
||||
|
||||
public function delete_art($id,$art_id)
|
||||
private function checkFolderOwnership($folder_id)
|
||||
{
|
||||
if (FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->count() == 0) {
|
||||
return $this->failed([], 'ارت مورد نظر یافت نشد', 404);
|
||||
return Folder::where('id', $folder_id)->where('user_id', auth()->id())->exists();
|
||||
}
|
||||
|
||||
public function delete_art($id, $art_id)
|
||||
{
|
||||
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||
|
||||
if (FolderArt::where('folder_id', $id)->where('art_id', $art_id)->count() == 0) {
|
||||
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||
}
|
||||
FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->delete();
|
||||
return $this->success([], 'حذف شد', 'ارت با موفقیت حذف شد');
|
||||
FolderArt::where('folder_id', $id)->where('art_id', $art_id)->delete();
|
||||
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||
}
|
||||
|
||||
public function delete_bylaw($id, $bylaw_id)
|
||||
{
|
||||
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||
|
||||
if (FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->count() == 0) {
|
||||
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||
}
|
||||
FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->delete();
|
||||
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||
}
|
||||
|
||||
public function delete_judicial($id, $judicial_id)
|
||||
{
|
||||
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||
|
||||
if (FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->count() == 0) {
|
||||
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||
}
|
||||
FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->delete();
|
||||
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||
}
|
||||
|
||||
public function delete_opinion($id, $opinion_id)
|
||||
{
|
||||
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||
|
||||
if (FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->count() == 0) {
|
||||
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||
}
|
||||
FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->delete();
|
||||
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
* FORMATTING HELPERS
|
||||
* ----------------------------------------------------------------- */
|
||||
|
||||
private function isLockedForCurrentUserGeneric($item): bool
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private function formatBylaw($item, $itemNodes = null)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
$description = $item->description;
|
||||
|
||||
$cleanDesc = trim(str_replace([' ', '‌', "\r", "\n", "\t", ' '], '', strip_tags($description ?? '')));
|
||||
|
||||
if (strlen($cleanDesc) < 5) {
|
||||
if ($itemNodes === null || (is_object($itemNodes) && $itemNodes->isEmpty())) {
|
||||
$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) {
|
||||
$content = trim(strip_tags($node->node_content ?? ''));
|
||||
if (!empty($content)) {
|
||||
$nodeContents[] = $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
$description = implode("\n\n", $nodeContents);
|
||||
}
|
||||
|
||||
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' => $isLocked ? null : mb_substr($description, 0, 100) . '...',
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatJudicial($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->subject,
|
||||
'ruling_number' => $item->ruling_number,
|
||||
'ruling_date' => $item->ruling_date,
|
||||
'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...',
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatOpinion($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->subject,
|
||||
'opinion_number' => $item->opinion_number,
|
||||
'opinion_date' => $item->opinion_date,
|
||||
'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...',
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ChapterRequest;
|
||||
use App\Models\Gate;
|
||||
use App\Models\Law;
|
||||
use App\Models\Order;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GateController extends Controller
|
||||
{
|
||||
@@ -23,15 +21,13 @@ class GateController extends Controller
|
||||
|
||||
$gates->getCollection()->transform(function ($gate) {
|
||||
unset($gate['book_id']);
|
||||
$gate['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $gate['law_id'])->first()?->is_locked;
|
||||
$gate['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $gate['law_id'])->first()?->is_locked;
|
||||
|
||||
unset($gate['law_id']);
|
||||
|
||||
return $gate;
|
||||
});
|
||||
|
||||
|
||||
|
||||
return $this->success($gates->items());
|
||||
}
|
||||
}
|
||||
|
||||
500
app/Http/Controllers/api/GlobalSearchApiController.php
Normal file
500
app/Http/Controllers/api/GlobalSearchApiController.php
Normal file
@@ -0,0 +1,500 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Art;
|
||||
use App\Models\Book;
|
||||
use App\Models\Branch;
|
||||
use App\Models\BylawDocument;
|
||||
use App\Models\Chapter;
|
||||
use App\Models\Division;
|
||||
use App\Models\Gate;
|
||||
use App\Models\JudicialPrecedent;
|
||||
use App\Models\Law;
|
||||
use App\Models\LegalOpinion;
|
||||
use App\Models\Part;
|
||||
use App\Models\Section;
|
||||
use App\Models\Volum;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GlobalSearchApiController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
/**
|
||||
* Massive Search across ALL data types
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'search' => 'nullable|string',
|
||||
'sort_by' => 'nullable|in:date,number',
|
||||
'sort' => 'nullable|in:asc,desc',
|
||||
'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);
|
||||
|
||||
$staticResults = collect(); // Arts and Structures (Always at the top)
|
||||
$sortableResults = collect(); // Bylaws, Opinions, Precedents (Sorted if requested)
|
||||
|
||||
if (!empty($searchTerm)) {
|
||||
$limitPerTable = 100;
|
||||
|
||||
// =========================================================
|
||||
// 1. Search Arts (Exact match to ArtController)
|
||||
// =========================================================
|
||||
$arts = Art::with(['law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
||||
->where(function ($q) use ($searchTerm) {
|
||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('text', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('number', 'LIKE', "%{$searchTerm}%");
|
||||
})
|
||||
->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
||||
$formatted = $this->formatArt($item, $searchTerm, Art::class);
|
||||
$formatted['type'] = 'art';
|
||||
return $formatted;
|
||||
});
|
||||
$staticResults = $staticResults->concat($arts);
|
||||
|
||||
// =========================================================
|
||||
// 2. Search Laws and Structural Models (Exact match to ArtController)
|
||||
// =========================================================
|
||||
$structuralModels = [
|
||||
Law::class, Volum::class, Book::class, Division::class,
|
||||
Section::class, Chapter::class, Part::class, Gate::class,
|
||||
Branch::class
|
||||
];
|
||||
|
||||
foreach ($structuralModels as $modelClass) {
|
||||
$query = $modelClass::query();
|
||||
$modelInstance = new $modelClass;
|
||||
$table = $modelInstance->getTable();
|
||||
|
||||
$query->where('title', 'LIKE', "%{$searchTerm}%");
|
||||
|
||||
$items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table) {
|
||||
$formatted = $this->formatStructure($item, $modelClass);
|
||||
$formatted['type'] = ($table === 'section') ? 'sections' : $table;
|
||||
return $formatted;
|
||||
});
|
||||
|
||||
$staticResults = $staticResults->concat($items);
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 3. Search Bylaw Documents
|
||||
// =========================================================
|
||||
$bylawsQuery = 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}%")
|
||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||
$subQuery->select('document_id')
|
||||
->from('bylaw_nodes')
|
||||
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
})->take($limitPerTable)->get();
|
||||
|
||||
// Safely bulk-fetch nodes using DB facade
|
||||
$docIds = $bylawsQuery->pluck('id')->toArray();
|
||||
$allNodes = collect();
|
||||
if (!empty($docIds)) {
|
||||
$allNodes = DB::table('bylaw_nodes')
|
||||
->whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
$bylaws = $bylawsQuery->map(function($item) use ($allNodes) {
|
||||
$itemNodes = $allNodes->where('document_id', $item->id);
|
||||
$formatted = $this->formatBylaw($item, $itemNodes);
|
||||
$formatted['type'] = 'bylaw';
|
||||
return $formatted;
|
||||
});
|
||||
$sortableResults = $sortableResults->concat($bylaws);
|
||||
|
||||
// =========================================================
|
||||
// 4. 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;
|
||||
});
|
||||
$sortableResults = $sortableResults->concat($judicials);
|
||||
|
||||
// =========================================================
|
||||
// 5. 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;
|
||||
});
|
||||
$sortableResults = $sortableResults->concat($opinions);
|
||||
}
|
||||
|
||||
// --- Apply Dynamic Memory Sorting ONLY to Legal Docs ---
|
||||
if ($request->has('sort_by') && $request->input('sort_by') != null) {
|
||||
$sortBy = $request->input('sort_by');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$sortCallback = function ($item) {
|
||||
return (string) ($item['promulgation_number'] ?? $item['ruling_number'] ?? $item['opinion_number'] ?? '0');
|
||||
};
|
||||
|
||||
if ($sortOrder === 'asc') {
|
||||
$sortableResults = $sortableResults->sortBy($sortCallback, SORT_NATURAL)->values();
|
||||
} else {
|
||||
$sortableResults = $sortableResults->sortByDesc($sortCallback, SORT_NATURAL)->values();
|
||||
}
|
||||
} elseif ($sortBy === 'date') {
|
||||
$sortCallback = function ($item) {
|
||||
return (string) ($item['approval_date'] ?? $item['ruling_date'] ?? $item['opinion_date'] ?? '1970-01-01');
|
||||
};
|
||||
|
||||
if ($sortOrder === 'asc') {
|
||||
$sortableResults = $sortableResults->sortBy($sortCallback)->values();
|
||||
} else {
|
||||
$sortableResults = $sortableResults->sortByDesc($sortCallback)->values();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine Results: Arts and Structures ALWAYS pin to the top!
|
||||
$results = $staticResults->concat($sortableResults);
|
||||
|
||||
// --- Execute Manual Pagination on the final Collection ---
|
||||
$total = $results->count();
|
||||
$paginatedItems = $results->forPage($page, $perPage)->values();
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level fast search for quick dropdown UI elements
|
||||
*/
|
||||
public function fast_search(Request $request)
|
||||
{
|
||||
$searchTerm = $request->input('search');
|
||||
$results = collect();
|
||||
$totalCount = 0;
|
||||
|
||||
$limitPerTable = 3;
|
||||
|
||||
if (!empty($searchTerm)) {
|
||||
|
||||
// 1. Search Arts FIRST
|
||||
$artQuery = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
||||
->where(function ($q) use ($searchTerm) {
|
||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('text', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('number', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
$totalCount += $artQuery->count();
|
||||
$arts = $artQuery->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
||||
$formatted = $this->formatArt($item, $searchTerm, Art::class);
|
||||
$formatted['type'] = 'art';
|
||||
return $formatted;
|
||||
});
|
||||
$results = $results->concat($arts);
|
||||
|
||||
// 2. Structural Models
|
||||
$structuralModels = [
|
||||
Law::class, Volum::class, Book::class, Division::class,
|
||||
Section::class, Chapter::class, Part::class, Gate::class,
|
||||
Branch::class
|
||||
];
|
||||
|
||||
foreach ($structuralModels as $modelClass) {
|
||||
$query = $modelClass::query();
|
||||
$modelInstance = new $modelClass;
|
||||
$table = $modelInstance->getTable();
|
||||
|
||||
$query->where('title', 'LIKE', "%{$searchTerm}%");
|
||||
|
||||
$totalCount += $query->count();
|
||||
|
||||
if ($results->count() < 15) {
|
||||
$items = $query->take($limitPerTable)->get()->map(function($item) use ($modelClass, $table) {
|
||||
$formatted = $this->formatStructure($item, $modelClass);
|
||||
$formatted['type'] = ($table === 'section') ? 'sections' : $table;
|
||||
return $formatted;
|
||||
});
|
||||
$results = $results->concat($items);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Bylaw Documents
|
||||
$bylawsQuery = 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}%")
|
||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||
$subQuery->select('document_id')
|
||||
->from('bylaw_nodes')
|
||||
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
});
|
||||
|
||||
$totalCount += $bylawsQuery->count();
|
||||
$bylawsRaw = $bylawsQuery->take($limitPerTable)->get();
|
||||
|
||||
// Bulk-fetch nodes safely using DB facade
|
||||
$docIds = $bylawsRaw->pluck('id')->toArray();
|
||||
$allNodes = collect();
|
||||
if (!empty($docIds)) {
|
||||
$allNodes = DB::table('bylaw_nodes')
|
||||
->whereIn('document_id', $docIds)
|
||||
->orderBy('sort_order', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
$bylaws = $bylawsRaw->map(function($item) use ($allNodes) {
|
||||
$itemNodes = $allNodes->where('document_id', $item->id);
|
||||
$formatted = $this->formatBylaw($item, $itemNodes);
|
||||
$formatted['type'] = 'bylaw';
|
||||
return $formatted;
|
||||
});
|
||||
$results = $results->concat($bylaws);
|
||||
|
||||
// 4. Judicial Precedents
|
||||
$judicialQuery = JudicialPrecedent::with('category')
|
||||
->where(function ($q) use ($searchTerm) {
|
||||
$q->where('ruling_number', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('subject', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('full_text', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
$totalCount += $judicialQuery->count();
|
||||
$judicials = $judicialQuery->take($limitPerTable)->get()->map(function($item) {
|
||||
$formatted = $this->formatJudicial($item);
|
||||
$formatted['type'] = 'judicial_precedent';
|
||||
return $formatted;
|
||||
});
|
||||
$results = $results->concat($judicials);
|
||||
|
||||
// 5. Legal Opinions
|
||||
$opinionQuery = 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}%");
|
||||
});
|
||||
$totalCount += $opinionQuery->count();
|
||||
$opinions = $opinionQuery->take($limitPerTable)->get()->map(function($item) {
|
||||
$formatted = $this->formatOpinion($item);
|
||||
$formatted['type'] = 'legal_opinion';
|
||||
return $formatted;
|
||||
});
|
||||
$results = $results->concat($opinions);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'items' => $results->take(15)->values(),
|
||||
'count' => $totalCount
|
||||
], 'Success', 'Global Fast Search Results');
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
* FORMATTING HELPERS
|
||||
* ----------------------------------------------------------------- */
|
||||
|
||||
private function isLockedForCurrentUserGeneric($item): bool
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact 1:1 match with BylawApiController's encoding approach
|
||||
*/
|
||||
private function formatBylaw($item, $itemNodes = null)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
|
||||
if ($itemNodes === null) {
|
||||
$itemNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->orderBy('sort_order', 'asc')->get();
|
||||
}
|
||||
|
||||
$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");
|
||||
|
||||
if (empty($nodeContentAsDescription)) {
|
||||
$nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8');
|
||||
}
|
||||
|
||||
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' => $isLocked ? null : $nodeContentAsDescription,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatJudicial($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->subject,
|
||||
'ruling_number' => $item->ruling_number,
|
||||
'ruling_date' => $item->ruling_date,
|
||||
'text' => $isLocked ? null : $item->full_text,
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatOpinion($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->subject,
|
||||
'opinion_number' => $item->opinion_number,
|
||||
'opinion_date' => $item->opinion_date,
|
||||
'text' => $isLocked ? null : $item->full_text,
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatArt($item, $searchTerm, $modelClass)
|
||||
{
|
||||
$law = $item->law ?? Law::find($item->law_id);
|
||||
$isLocked = ! auth()->user()->isSubscriber() && (bool) ($law?->is_locked ?? false);
|
||||
|
||||
$text = $item->text ?? '';
|
||||
$searchQuoted = preg_quote($searchTerm, '/');
|
||||
|
||||
if (!empty($searchTerm) && 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,
|
||||
'number' => $item->number,
|
||||
'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,
|
||||
'count_volum' => $law?->volums?->count() ?? 0,
|
||||
'price' => $law?->price,
|
||||
'image' => $law?->image,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatStructure($item, $modelClass)
|
||||
{
|
||||
$law = Law::find($item?->law_id ?? $item->id);
|
||||
$isLocked = ! auth()->user()->isSubscriber() && (bool) ($law?->is_locked ?? false);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
private function route($modelClass, $query)
|
||||
{
|
||||
if ((new $modelClass)->getTable() == 'art') {
|
||||
$route = array_filter([
|
||||
$query->relationLoaded('law') ? $query->law?->title : Law::find($query->law_id)?->title,
|
||||
$query->relationLoaded('volum') ? $query->volum?->title : Volum::find($query->volum_id)?->title,
|
||||
$query->relationLoaded('book') ? $query->book?->title : Book::find($query->book_id)?->title,
|
||||
Division::find($query->division_id)?->title,
|
||||
$query->relationLoaded('section') ? $query->section?->title : Section::find($query->section_id)?->title,
|
||||
$query->relationLoaded('chapter') ? $query->chapter?->title : Chapter::find($query->chapter_id)?->title,
|
||||
$query->relationLoaded('part') ? $query->part?->title : Part::find($query->part_id)?->title,
|
||||
$query->relationLoaded('gate') ? $query->gate?->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 [];
|
||||
}
|
||||
}
|
||||
@@ -3,46 +3,57 @@
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
// 1. IMPORT THE JOB WE CREATED
|
||||
use App\Jobs\CheckBazaarSubscription;
|
||||
|
||||
// These are your existing imports
|
||||
use App\Models\Law;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Category;
|
||||
use App\Models\RecentArt;
|
||||
use App\Models\SubscribePlan;
|
||||
use App\Models\UserSubscriber;
|
||||
use App\Services\AppMarketPurchaseVerifier;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function __construct(private AppMarketPurchaseVerifier $marketVerifier)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return $this->buildHomeResponse();
|
||||
}
|
||||
|
||||
public function homeV2()
|
||||
{
|
||||
return $this->buildHomeResponse();
|
||||
}
|
||||
|
||||
public function homeV3()
|
||||
{
|
||||
return $this->buildHomeResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* UNIFIED CORE LOGIC: Safely builds the home response for ALL app versions (v1, v2, v3)
|
||||
* By centralizing this, we ensure no old app version ever suffers from the math bug!
|
||||
*/
|
||||
private function buildHomeResponse()
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
// 2. DISPATCH THE JOB AT THE VERY BEGINNING
|
||||
// ===================================================================
|
||||
// Find the user's latest subscription that has a purchase token
|
||||
$latestSubscription = $user->userSubscribers()->whereNotNull('purchase_token')->latest()->first();
|
||||
// 1. Safely grant free subs (Won't overlap with paid plans)
|
||||
$this->checkAndGrantFreeSubscription($user);
|
||||
|
||||
// If such a subscription exists, create a new job and hand it to the queue
|
||||
// if ($latestSubscription) {
|
||||
// CheckBazaarSubscription::dispatch($latestSubscription);
|
||||
// }
|
||||
// ===================================================================
|
||||
// Your API now continues immediately without waiting for the check to finish.
|
||||
|
||||
|
||||
// --- ALL THE REST OF YOUR CODE REMAINS EXACTLY THE SAME ---
|
||||
$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
|
||||
'is_locked' => $q->law?->is_locked,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -50,7 +61,7 @@ class HomeController extends Controller
|
||||
return [
|
||||
'id' => $q->id,
|
||||
'name' => $q->title,
|
||||
'is_locked' => $q->is_locked
|
||||
'is_locked' => $q->is_locked,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -62,13 +73,13 @@ class HomeController extends Controller
|
||||
$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'
|
||||
'id' => $q->id,
|
||||
'title' => $q->title,
|
||||
'is_locked' => $q->is_locked,
|
||||
'category_id' => $q->category_id,
|
||||
'price' => $q->price,
|
||||
'image' => $q->image,
|
||||
'type' => 'law',
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -77,55 +88,167 @@ class HomeController extends Controller
|
||||
return [
|
||||
'id' => $q->id,
|
||||
'name' => $q->title,
|
||||
'is_locked' => $q->is_locked
|
||||
'is_locked' => $q->is_locked,
|
||||
];
|
||||
});
|
||||
|
||||
$current_plan = null;
|
||||
|
||||
$freeSubscription = $user->userSubscribers()
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
})
|
||||
// --- THE FIXED AUTHORITY LOGIC APPLIED TO ALL VERSIONS ---
|
||||
$activeSubscribers = $user->userSubscribers()
|
||||
->with('subscribe')
|
||||
->where('expired_at', '>=', now())
|
||||
->first();
|
||||
->get();
|
||||
|
||||
$expiredAt = null;
|
||||
$current_plan = null;
|
||||
|
||||
if ($freeSubscription) {
|
||||
$expiredAt = $freeSubscription->expired_at;
|
||||
if ($activeSubscribers->isNotEmpty()) {
|
||||
// Separate Paid from Free
|
||||
$paidSubscriptions = $activeSubscribers->reject(function ($subscriber) {
|
||||
return $subscriber->is_free || ($subscriber->subscribe && $subscriber->subscribe->is_free);
|
||||
});
|
||||
|
||||
// AUTHORITY RULE: Paid plans override Free plans
|
||||
if ($paidSubscriptions->isNotEmpty()) {
|
||||
$latestSubscription = $paidSubscriptions->sortByDesc('expired_at')->first();
|
||||
} else {
|
||||
$latestSubscription = $activeSubscribers->sortByDesc('expired_at')->first();
|
||||
}
|
||||
|
||||
// Fixed math (No sum)
|
||||
$expiredDays = $latestSubscription->expired_at->diffInDays(now());
|
||||
|
||||
$isFree = (bool) (
|
||||
$latestSubscription->is_free ||
|
||||
($latestSubscription->subscribe && $latestSubscription->subscribe->is_free)
|
||||
);
|
||||
|
||||
$current_plan = [
|
||||
'id' => $freeSubscription->id,
|
||||
'name' => $freeSubscription->subscribe->name,
|
||||
'price' => $freeSubscription->subscribe->price,
|
||||
'expired_day' => $freeSubscription->expired_at->diffInDays(now()),
|
||||
'is_free' => true
|
||||
'id' => $latestSubscription->id,
|
||||
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
||||
'price' => $latestSubscription->subscribe->price ?? 0,
|
||||
'expired_day' => $expiredDays,
|
||||
'is_free' => $isFree,
|
||||
];
|
||||
}
|
||||
|
||||
$latestSubscription = $user->userSubscribers()->latest()->first();
|
||||
|
||||
$expiredDays = UserSubscriber::query()->where('user_id', $user->id)->where('expired_at', '>=', now())->get()->sum(function ($subscriber) {return $subscriber->expired_at->diffInDays(now());});
|
||||
|
||||
$purchase_token = $latestSubscription?->purchase_token;
|
||||
$current_plan = [
|
||||
'id' => $latestSubscription->id,
|
||||
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
||||
'price' => $latestSubscription->subscribe->price ?? 100,
|
||||
'expired_day' => $expiredDays,
|
||||
'is_free' => false
|
||||
];
|
||||
|
||||
$unread_notifications_count = Notification::unreadForUser($user->id)->count();
|
||||
|
||||
return $this->success([
|
||||
$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),
|
||||
'keyfari_list' => $this->getCategoryList(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;
|
||||
}
|
||||
|
||||
private function checkAndGrantFreeSubscription($user): void
|
||||
{
|
||||
$hasUsedFreePlan = $user->userSubscribers()
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
})->exists();
|
||||
|
||||
$hasActivePaidPlan = $user->userSubscribers()
|
||||
->where('expired_at', '>', now())
|
||||
->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', false);
|
||||
})->exists();
|
||||
|
||||
if (!$hasUsedFreePlan && !$hasActivePaidPlan) {
|
||||
$freePlan = SubscribePlan::where('is_free', true)->where('is_active', true)->first();
|
||||
|
||||
if ($freePlan) {
|
||||
$user->userSubscribers()->create([
|
||||
'subscribe_plan_id' => $freePlan->id,
|
||||
'expired_at' => now()->addDays($freePlan->expired_day + 1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function refreshMarketSubscriptionAfterResponse($user): void
|
||||
{
|
||||
$cacheKey = "home-market-refresh-user-{$user->id}";
|
||||
$this->refreshMarketSubscription($user);
|
||||
|
||||
if (!Cache::add($cacheKey, true, now()->addHours(24))) {
|
||||
return;
|
||||
}
|
||||
|
||||
app()->terminating(function () use ($user) {
|
||||
$this->refreshMarketSubscription($user);
|
||||
});
|
||||
}
|
||||
|
||||
private function refreshMarketSubscription($user): void
|
||||
{
|
||||
$bazaarSubscription = $user->userSubscribers()
|
||||
->whereNotNull('purchase_token')
|
||||
->whereNotNull('subscription_id')
|
||||
->where(function ($query) {
|
||||
$query->where('market_provider', 'bazaar')
|
||||
->orWhereNull('market_provider');
|
||||
})
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($bazaarSubscription && $this->shouldRefreshMarketSubscription($bazaarSubscription) && $this->marketVerifier->refreshBazaarSubscriber($bazaarSubscription)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$myketSubscription = $user->userSubscribers()
|
||||
->where('market_provider', 'myket')
|
||||
->whereNotNull('purchase_token')
|
||||
->whereNotNull('product_id')
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($myketSubscription && $this->shouldRefreshMarketSubscription($myketSubscription)) {
|
||||
$this->marketVerifier->refreshMyketSubscriber($myketSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldRefreshMarketSubscription(UserSubscriber $subscriber): bool
|
||||
{
|
||||
if ($subscriber->expired_at && $subscriber->expired_at->lessThanOrEqualTo(now())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !$subscriber->last_verified_at || $subscriber->last_verified_at->lessThan(now()->subHours(12));
|
||||
}
|
||||
}
|
||||
|
||||
215
app/Http/Controllers/api/JudicialPrecedentApiController.php
Normal file
215
app/Http/Controllers/api/JudicialPrecedentApiController.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\JudicialPrecedent;
|
||||
use App\Models\JudicialPrecedentCategory;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JudicialPrecedentApiController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function categories()
|
||||
{
|
||||
$categories = JudicialPrecedentCategory::all()->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_by' => 'nullable|in:date,number',
|
||||
'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);
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'ruling_number' : 'ruling_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
// Natural numeric sorting for string numbers
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $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');
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'search' => 'nullable|string',
|
||||
'sort_by' => 'nullable|in:date,number',
|
||||
'sort' => 'nullable|in:asc,desc',
|
||||
'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}%");
|
||||
});
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'ruling_number' : 'ruling_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $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', 'Search Results Fetched');
|
||||
}
|
||||
|
||||
public function fast_search(Request $request)
|
||||
{
|
||||
$searchTerm = $request->input('search');
|
||||
$query = JudicialPrecedent::query();
|
||||
|
||||
if (!empty($searchTerm)) {
|
||||
$query->where(function ($q) use ($searchTerm) {
|
||||
$q->where('ruling_number', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('subject', 'LIKE', "%{$searchTerm}%")
|
||||
->orWhere('full_text', 'LIKE', "%{$searchTerm}%");
|
||||
});
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
$items = $query->orderBy('ruling_date', 'desc')->take(5)->get()->map(function ($item) {
|
||||
return $this->formatItem($item);
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'items' => $items,
|
||||
'count' => $count
|
||||
], 'Success', 'Fast Search Results');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Apply format and add Notes & Highlights
|
||||
$formatWithNoteAndHighlight = 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()->id())
|
||||
->where('judicial_precedent_id', $item->id)
|
||||
->get();
|
||||
|
||||
// NEW: Fetch Highlights
|
||||
$formatted['highlight'] = \App\Models\JudicialPrecedentHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')
|
||||
->where('user_id', auth()->id())
|
||||
->where('judicial_precedent_id', $item->id)
|
||||
->get();
|
||||
|
||||
return $formatted;
|
||||
};
|
||||
|
||||
$results = array_values(array_filter([
|
||||
$previousItem ? $formatWithNoteAndHighlight($previousItem) : null,
|
||||
$formatWithNoteAndHighlight($current),
|
||||
$nextItem ? $formatWithNoteAndHighlight($nextItem) : null
|
||||
]));
|
||||
|
||||
return $this->success($results, 'Success', 'Pager Window Details');
|
||||
}
|
||||
|
||||
private function isLockedForCurrentUser($item): bool
|
||||
{
|
||||
return ! auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
||||
}
|
||||
|
||||
private function formatItem($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUser($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'ruling_number' => $item->ruling_number,
|
||||
'ruling_date' => $item->ruling_date,
|
||||
'subject' => $item->subject,
|
||||
'full_text' => $isLocked ? null : $item->full_text,
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_id' => $item->category_id,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\JudicialPrecedentHighlight;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JudicialPrecedentHighlightController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'judicial_id' => 'required',
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
JudicialPrecedentHighlight::create([
|
||||
'user_id' => auth()->id(),
|
||||
'judicial_precedent_id' => $validated['judicial_id'],
|
||||
'start_index' => $validated['start_index'],
|
||||
'end_index' => $validated['end_index'],
|
||||
'selected_text' => $validated['selected_text'],
|
||||
'color_code' => $validated['color_code'],
|
||||
]);
|
||||
|
||||
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||
}
|
||||
|
||||
public function update($id, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$highlight = JudicialPrecedentHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->update($validated);
|
||||
|
||||
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$highlight = JudicialPrecedentHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->delete();
|
||||
|
||||
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/api/JudicialPrecedentNoteController.php
Normal file
56
app/Http/Controllers/api/JudicialPrecedentNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\JudicialPrecedentNote;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JudicialPrecedentNoteController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'judicial_id' => 'required', // Cleaned up!
|
||||
'note' => 'required',
|
||||
'color_code' => 'nullable',
|
||||
]);
|
||||
|
||||
JudicialPrecedentNote::query()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'judicial_precedent_id' => $validated['judicial_id'], // Maps to DB
|
||||
'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 = 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([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,9 @@ class LawController extends Controller
|
||||
{
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$page = $request->input('page', 1);
|
||||
|
||||
|
||||
$laws = Law::simplePaginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
|
||||
return $this->success($laws->items(), 'Success');
|
||||
}
|
||||
}
|
||||
|
||||
139
app/Http/Controllers/api/LeagueController.php
Normal file
139
app/Http/Controllers/api/LeagueController.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\UserQuest;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LeagueController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
/**
|
||||
* API 1: User League Stats
|
||||
*/
|
||||
public function stats(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Dynamic Level Calculation (Every 500 XP = 1 Level)
|
||||
$level = floor($user->xp / 500) + 1;
|
||||
$xpToNext = 500 - ($user->xp % 500);
|
||||
$progress = ($user->xp % 500) / 500;
|
||||
|
||||
$winRate = $user->duels_played > 0
|
||||
? round(($user->duels_won / $user->duels_played) * 100)
|
||||
: 0;
|
||||
|
||||
// Calculate Global Rank Position
|
||||
$rankPosition = User::where('xp', '>', $user->xp)->count() + 1;
|
||||
|
||||
$data = [
|
||||
'user_id' => $user->id,
|
||||
'name' => $user->name ?? 'کاربر',
|
||||
'current_level' => $level,
|
||||
'rank_name' => $this->getRankName($level),
|
||||
'total_xp' => $user->xp,
|
||||
'xp_to_next_level' => $xpToNext,
|
||||
'progress_percent' => round($progress, 2),
|
||||
'win_rate_percent' => $winRate,
|
||||
'total_duels' => $user->duels_played,
|
||||
'global_rank_position' => $rankPosition
|
||||
];
|
||||
|
||||
return $this->success($data, 'موفق', 'اطلاعات لیگ کاربر دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* API 2: Daily Quests
|
||||
*/
|
||||
public function quests(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Fetch active quests for today using the new relationship
|
||||
$userQuests = $user->activeQuests()->with('quest')->get();
|
||||
|
||||
$formattedQuests = $userQuests->map(function ($uq) {
|
||||
return [
|
||||
'id' => $uq->id,
|
||||
'title' => $uq->quest->title,
|
||||
'current_progress' => $uq->current_progress,
|
||||
'required_amount' => $uq->quest->required_amount,
|
||||
'reward_text' => '+' . $uq->quest->reward_xp . ' XP',
|
||||
'is_completed' => $uq->is_completed
|
||||
];
|
||||
});
|
||||
|
||||
$expiresIn = now()->endOfDay()->diffInSeconds(now());
|
||||
|
||||
return $this->success([
|
||||
'expires_in_seconds' => $expiresIn,
|
||||
'quests' => $formattedQuests
|
||||
], 'موفق', 'چالشهای روزانه دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* API 3: League Leaderboard
|
||||
*/
|
||||
public function leaderboard(Request $request)
|
||||
{
|
||||
$currentUser = $request->user();
|
||||
|
||||
// Get Top 50 Users
|
||||
$topUsers = User::orderByDesc('xp')->limit(50)->get(['id', 'name', 'xp']);
|
||||
|
||||
$leaderboard = $topUsers->map(function ($u, $index) use ($currentUser) {
|
||||
return [
|
||||
'rank' => $index + 1,
|
||||
'user_id' => $u->id,
|
||||
'name' => $u->name ?? 'کاربر ناشناس',
|
||||
'xp' => $u->xp,
|
||||
'is_me' => $u->id === $currentUser->id
|
||||
];
|
||||
});
|
||||
|
||||
// Current User Data
|
||||
$myRank = User::where('xp', '>', $currentUser->xp)->count() + 1;
|
||||
|
||||
$myStats = [
|
||||
'rank' => $myRank,
|
||||
'user_id' => $currentUser->id,
|
||||
'name' => $currentUser->name ?? 'شما',
|
||||
'xp' => $currentUser->xp,
|
||||
'is_me' => true
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'leaderboard' => $leaderboard,
|
||||
'current_user' => $myStats
|
||||
], 'موفق', 'جدول ردهبندی دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* API 4: PvP Matchmaking Entry
|
||||
*/
|
||||
public function matchmake(Request $request)
|
||||
{
|
||||
$ticketId = 'req_' . Str::random(12);
|
||||
|
||||
return $this->success([
|
||||
'matchmaking_ticket_id' => $ticketId,
|
||||
'status' => 'searching',
|
||||
'websocket_channel' => 'private-matchmaking.' . $ticketId,
|
||||
'estimated_wait_seconds' => 15
|
||||
], 'در حال جستجو', 'به صف انتظار دوئل اضافه شدید');
|
||||
}
|
||||
|
||||
private function getRankName($level)
|
||||
{
|
||||
if ($level >= 50) return 'الماس';
|
||||
if ($level >= 30) return 'طلا';
|
||||
if ($level >= 10) return 'نقرهای';
|
||||
return 'برنز';
|
||||
}
|
||||
}
|
||||
224
app/Http/Controllers/api/LegalOpinionApiController.php
Normal file
224
app/Http/Controllers/api/LegalOpinionApiController.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LegalOpinion;
|
||||
use App\Models\LegalOpinionCategory;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegalOpinionApiController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function categories()
|
||||
{
|
||||
$categories = LegalOpinionCategory::all()->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_by' => 'nullable|in:date,number',
|
||||
'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);
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'opinion_number' : 'opinion_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $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');
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'search' => 'nullable|string',
|
||||
'sort_by' => 'nullable|in:date,number',
|
||||
'sort' => 'nullable|in:asc,desc',
|
||||
'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}%");
|
||||
});
|
||||
}
|
||||
|
||||
$sortBy = $request->input('sort_by', 'date');
|
||||
$sortOrder = $request->input('sort', 'desc');
|
||||
$sortColumn = $sortBy === 'number' ? 'opinion_number' : 'opinion_date';
|
||||
|
||||
if ($sortBy === 'number') {
|
||||
$query->orderByRaw("LENGTH({$sortColumn}) {$sortOrder}")->orderBy($sortColumn, $sortOrder);
|
||||
} else {
|
||||
$query->orderBy($sortColumn, $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', 'Search Results Fetched');
|
||||
}
|
||||
|
||||
public function fast_search(Request $request)
|
||||
{
|
||||
$searchTerm = $request->input('search');
|
||||
$query = LegalOpinion::query();
|
||||
|
||||
if ($request->filled('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}%");
|
||||
});
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
$items = $query->orderBy('opinion_date', 'desc')->take(5)->get()->map(function ($item) {
|
||||
return $this->formatItem($item);
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'items' => $items,
|
||||
'count' => $count
|
||||
], 'Success', 'Fast Search Results');
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// Apply format and add Notes & Highlights
|
||||
$formatWithNoteAndHighlight = 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()->id())
|
||||
->where('legal_opinion_id', $item->id)
|
||||
->get();
|
||||
|
||||
// NEW: Fetch Highlights
|
||||
$formatted['highlight'] = \App\Models\LegalOpinionHighlight::select('id', 'start_index', 'end_index', 'selected_text', 'color_code')
|
||||
->where('user_id', auth()->id())
|
||||
->where('legal_opinion_id', $item->id)
|
||||
->get();
|
||||
|
||||
return $formatted;
|
||||
};
|
||||
|
||||
$results = array_values(array_filter([
|
||||
$previousItem ? $formatWithNoteAndHighlight($previousItem) : null,
|
||||
$formatWithNoteAndHighlight($current),
|
||||
$nextItem ? $formatWithNoteAndHighlight($nextItem) : null
|
||||
]));
|
||||
|
||||
return $this->success($results, 'Success', 'Pager Window Details');
|
||||
}
|
||||
|
||||
private function isLockedForCurrentUser($item): bool
|
||||
{
|
||||
return ! auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
||||
}
|
||||
|
||||
private function formatItem($item)
|
||||
{
|
||||
$isLocked = $this->isLockedForCurrentUser($item);
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'opinion_number' => $item->opinion_number,
|
||||
'opinion_date' => $item->opinion_date,
|
||||
'subject' => $item->subject,
|
||||
'full_text' => $isLocked ? null : $item->full_text,
|
||||
'issuing_authority' => $item->issuing_authority,
|
||||
'category_id' => $item->category_id,
|
||||
'category_name' => optional($item->category)->name,
|
||||
'is_locked' => $isLocked,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/api/LegalOpinionHighlightController.php
Normal file
58
app/Http/Controllers/api/LegalOpinionHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LegalOpinionHighlight;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegalOpinionHighlightController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'opinion_id' => 'required',
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
LegalOpinionHighlight::create([
|
||||
'user_id' => auth()->id(),
|
||||
'legal_opinion_id' => $validated['opinion_id'],
|
||||
'start_index' => $validated['start_index'],
|
||||
'end_index' => $validated['end_index'],
|
||||
'selected_text' => $validated['selected_text'],
|
||||
'color_code' => $validated['color_code'],
|
||||
]);
|
||||
|
||||
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||
}
|
||||
|
||||
public function update($id, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'start_index' => 'required|integer',
|
||||
'end_index' => 'required|integer',
|
||||
'selected_text' => 'nullable|string',
|
||||
'color_code' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$highlight = LegalOpinionHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->update($validated);
|
||||
|
||||
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||
}
|
||||
|
||||
public function destroy($id)
|
||||
{
|
||||
$highlight = LegalOpinionHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||
$highlight->delete();
|
||||
|
||||
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/api/LegalOpinionNoteController.php
Normal file
56
app/Http/Controllers/api/LegalOpinionNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LegalOpinionNote;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LegalOpinionNoteController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'opinion_id' => 'required', // Cleaned up!
|
||||
'note' => 'required',
|
||||
'color_code' => 'nullable',
|
||||
]);
|
||||
|
||||
LegalOpinionNote::query()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'legal_opinion_id' => $validated['opinion_id'], // Maps to DB
|
||||
'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([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class NoteController extends Controller
|
||||
$validated = $request->validate([
|
||||
'art_id' => 'required',
|
||||
'note' => 'required',
|
||||
'color_code' => 'nullable'
|
||||
'color_code' => 'nullable',
|
||||
]);
|
||||
|
||||
Note::query()->create(
|
||||
@@ -29,14 +29,14 @@ class NoteController extends Controller
|
||||
]
|
||||
);
|
||||
|
||||
return $this->success([], 'successfully created.');
|
||||
return $this->success([], 'با موفقیت اضافه شد');
|
||||
}
|
||||
|
||||
public function update($id, Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'note' => 'required',
|
||||
'color_code' => 'nullable'
|
||||
'color_code' => 'nullable',
|
||||
]);
|
||||
|
||||
$note = Note::findOrFail($id);
|
||||
@@ -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([], 'حذف شد');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,31 @@ namespace App\Http\Controllers\api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ChapterRequest;
|
||||
use App\Models\Law;
|
||||
use App\Models\Order;
|
||||
use App\Models\Part;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PartController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function index(ChapterRequest $request)
|
||||
{
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$page = $request->input('page', 1);
|
||||
|
||||
$parts = Part::where('book_id' , $request->input('book_id'))->paginate($perPage, ['*'], 'page', $page);
|
||||
$parts = Part::where('book_id', $request->input('book_id'))->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
$parts->getCollection()->transform(function ($part) {
|
||||
unset($part['book_id']);
|
||||
$gate['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $part['law_id'])->first()?->is_locked;
|
||||
$part['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $part['law_id'])->first()?->is_locked;
|
||||
|
||||
unset($gate['law_id']);
|
||||
unset($part['law_id']);
|
||||
|
||||
return $gate;
|
||||
return $part;
|
||||
});
|
||||
|
||||
|
||||
return $this->success($parts->items());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\SubscribePlan;
|
||||
use App\Models\UserSubscriber;
|
||||
@@ -29,7 +28,7 @@ class PayController extends Controller
|
||||
$invoice = (new Invoice)->amount($subscribePlan->price);
|
||||
|
||||
$callback = 'https://ghaafapp.ir';
|
||||
$payment = Payment::callbackUrl($callback . '/payment/callback')
|
||||
$payment = Payment::callbackUrl($callback.'/payment/callback')
|
||||
->purchase($invoice, function ($driver, $transaction_id) use ($subscribePlan) {
|
||||
PaymentTransaction::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
@@ -43,7 +42,8 @@ class PayController extends Controller
|
||||
|
||||
return $this->success(['url' => $payment]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Payment initiation failed: ' . $e->getMessage());
|
||||
Log::error('Payment initiation failed: '.$e->getMessage());
|
||||
|
||||
return $this->failed('Payment initialization failed', $e->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -72,26 +72,27 @@ 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' => 'پرداخت ناموفق']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function send($url, $api, $amount, $redirect, $factorId, $name, $email, $description)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "api=$api&amount=$amount&redirect=$redirect&factorId=$factorId&name=$name&email=$email&description=$description");
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -100,10 +101,11 @@ class PayController extends Controller
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "api=$api&id_get=$id_get&trans_id=$trans_id&json=1");
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
266
app/Http/Controllers/api/PvPQuizController.php
Normal file
266
app/Http/Controllers/api/PvPQuizController.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Question;
|
||||
use App\Models\PvpMatch;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PvPQuizController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
/**
|
||||
* STEP 1: Join the Matchmaking Queue
|
||||
*/
|
||||
public function findMatch(Request $request)
|
||||
{
|
||||
$userId = auth()->id();
|
||||
$ticketId = $request->input('ticket_id') ?? 'req_' . Str::random(12);
|
||||
|
||||
// 1. Check if anyone else is waiting in the cache queue
|
||||
$waitingPlayer = Cache::get('pvp_waiting_player');
|
||||
|
||||
// Ensure we don't match the user with themselves
|
||||
if ($waitingPlayer && $waitingPlayer['user_id'] != $userId) {
|
||||
|
||||
// MATCH FOUND! Remove the waiting player from the queue
|
||||
Cache::forget('pvp_waiting_player');
|
||||
|
||||
// Create the Game Room
|
||||
$match = PvpMatch::create([
|
||||
'status' => 'in_progress',
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
// Add both players to the match
|
||||
$match->players()->createMany([
|
||||
['user_id' => $waitingPlayer['user_id'], 'score' => 0],
|
||||
['user_id' => $userId, 'score' => 0],
|
||||
]);
|
||||
|
||||
// Select 5 random multiple-choice questions
|
||||
$questions = Question::where('type', 'multiple_choice')
|
||||
->inRandomOrder()
|
||||
->limit(5)
|
||||
->pluck('id');
|
||||
|
||||
// Attach questions to the match
|
||||
$match->questions()->attach($questions);
|
||||
|
||||
// Prepare the payload for both players
|
||||
$matchData = [
|
||||
'match_id' => $match->id,
|
||||
'status' => 'matched',
|
||||
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
|
||||
$q->options->each->makeHidden(['is_correct']);
|
||||
})
|
||||
];
|
||||
|
||||
// FIRE THE WEBSOCKET EVENT TO PLAYER A!
|
||||
event(new \App\Events\MatchStarted($matchData, $waitingPlayer['ticket_id']));
|
||||
|
||||
// Return the same payload to Player B via standard HTTP response
|
||||
$matchData['opponent_id'] = $waitingPlayer['user_id'];
|
||||
|
||||
return $this->success($matchData, 'موفق', 'حریف پیدا شد! بازی شروع میشود.');
|
||||
}
|
||||
|
||||
// NO MATCH FOUND YET. Put Player A in the waiting queue and save their Ticket ID
|
||||
Cache::put('pvp_waiting_player', [
|
||||
'user_id' => $userId,
|
||||
'ticket_id' => $ticketId
|
||||
], 30);
|
||||
|
||||
return $this->success([
|
||||
'status' => 'waiting',
|
||||
'ticket_id' => $ticketId
|
||||
], 'در حال جستجو', 'در حال یافتن حریف...');
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 2: Submit Answer for a single question (Fastest Finger Logic)
|
||||
*/
|
||||
public function submitAnswer(Request $request, PvpMatch $match)
|
||||
{
|
||||
$request->validate([
|
||||
'question_id' => 'required|exists:questions,id',
|
||||
'selected_option_id' => 'required|exists:question_options,id',
|
||||
'time_taken_seconds' => 'required|numeric|min:0.1|max:15', // Max 15 seconds to answer
|
||||
]);
|
||||
|
||||
// Prevent answering the same question twice!
|
||||
$alreadyAnswered = $match->answers()
|
||||
->where('user_id', auth()->id())
|
||||
->where('question_id', $request->question_id)
|
||||
->exists();
|
||||
|
||||
if ($alreadyAnswered) {
|
||||
return $this->failed(null, 'خطا', 'شما قبلا به این سوال پاسخ دادهاید.');
|
||||
}
|
||||
|
||||
$player = $match->players()->where('user_id', auth()->id())->firstOrFail();
|
||||
$question = Question::with('options')->find($request->question_id);
|
||||
|
||||
$isCorrect = false;
|
||||
$pointsEarned = 0;
|
||||
|
||||
// Check if correct
|
||||
$correctOption = $question->options->where('is_correct', true)->first();
|
||||
if ($correctOption && $correctOption->id == $request->selected_option_id) {
|
||||
$isCorrect = true;
|
||||
|
||||
// MATH: Speed Bonus Calculation
|
||||
// If base score is 10, and you answer in 2 seconds (out of 15), you get a bonus!
|
||||
$baseScore = 10;
|
||||
$maxTime = 15;
|
||||
$speedBonus = ($maxTime - $request->time_taken_seconds) * 0.5; // Half a point for every second saved
|
||||
|
||||
$pointsEarned = $baseScore + $speedBonus;
|
||||
}
|
||||
|
||||
// Save the answer
|
||||
$match->answers()->create([
|
||||
'user_id' => auth()->id(),
|
||||
'question_id' => $question->id,
|
||||
'selected_option_id' => $request->selected_option_id,
|
||||
'is_correct' => $isCorrect,
|
||||
'time_taken' => $request->time_taken_seconds,
|
||||
'points_earned' => $pointsEarned
|
||||
]);
|
||||
|
||||
// Update total score for the player
|
||||
$player->increment('score', $pointsEarned);
|
||||
|
||||
// ==========================================
|
||||
// CHECK IF THE MATCH IS OVER
|
||||
// ==========================================
|
||||
$totalQuestions = $match->questions()->count(); // Should be 5
|
||||
$totalExpectedAnswers = $totalQuestions * 2; // 5 questions * 2 players = 10 answers
|
||||
|
||||
// Count how many answers have been submitted in this match globally
|
||||
$currentTotalAnswers = $match->answers()->count();
|
||||
|
||||
$isMatchOver = false;
|
||||
|
||||
if ($currentTotalAnswers >= $totalExpectedAnswers) {
|
||||
// The final answer was just submitted! End the game.
|
||||
$this->finalizeMatch($match);
|
||||
$isMatchOver = true;
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'is_correct' => $isCorrect,
|
||||
'points_earned' => round($pointsEarned, 2),
|
||||
'current_total_score' => round($player->score, 2),
|
||||
'is_match_over' => $isMatchOver
|
||||
], 'موفق', 'پاسخ ثبت شد.');
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 3: Finalize the Match and Declare Winner (Internal Helper)
|
||||
*/
|
||||
private function finalizeMatch(PvpMatch $match)
|
||||
{
|
||||
$players = $match->players()->orderByDesc('score')->get();
|
||||
$player1 = $players->first();
|
||||
$player2 = $players->last();
|
||||
|
||||
$winnerId = null;
|
||||
if ($player1->score > $player2->score) {
|
||||
$winnerId = $player1->user_id;
|
||||
}
|
||||
|
||||
$match->update([
|
||||
'status' => 'completed',
|
||||
'ended_at' => now(),
|
||||
'winner_id' => $winnerId
|
||||
]);
|
||||
|
||||
// --- UPDATE GAMIFICATION STATS ---
|
||||
$user1 = \App\Models\User::find($player1->user_id);
|
||||
$user2 = \App\Models\User::find($player2->user_id);
|
||||
|
||||
$user1->increment('duels_played');
|
||||
$user2->increment('duels_played');
|
||||
|
||||
// Give XP, Wins, and update "Win Duel" Quests
|
||||
if ($winnerId) {
|
||||
$winner = \App\Models\User::find($winnerId);
|
||||
$winner->increment('duels_won');
|
||||
$winner->increment('xp', 50); // Winner gets 50 XP
|
||||
|
||||
$duelQuest = $winner->activeQuests()
|
||||
->whereHas('quest', function ($q) {
|
||||
$q->where('quest_type', 'duel_win');
|
||||
})->where('is_completed', false)->first();
|
||||
|
||||
if ($duelQuest) {
|
||||
$duelQuest->increment('current_progress');
|
||||
|
||||
if ($duelQuest->current_progress >= $duelQuest->quest->required_amount) {
|
||||
$duelQuest->update(['is_completed' => true]);
|
||||
$winner->increment('xp', $duelQuest->quest->reward_xp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loser/Draw gets participation XP
|
||||
$loserId = $winnerId === $user1->id ? $user2->id : $user1->id;
|
||||
\App\Models\User::find($loserId)->increment('xp', 10);
|
||||
|
||||
// Update "Answer Tests" Quest Progress for BOTH players
|
||||
foreach ([$user1, $user2] as $player) {
|
||||
$testQuest = $player->activeQuests()
|
||||
->whereHas('quest', function ($q) {
|
||||
$q->where('quest_type', 'answer_tests');
|
||||
})->where('is_completed', false)->first();
|
||||
|
||||
if ($testQuest) {
|
||||
$testQuest->increment('current_progress', 5); // They both answered 5 questions
|
||||
|
||||
if ($testQuest->current_progress >= $testQuest->quest->required_amount) {
|
||||
$testQuest->update(['is_completed' => true]);
|
||||
$player->increment('xp', $testQuest->quest->reward_xp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIRE THE WEBSOCKET EVENT TO END MATCH!
|
||||
event(new \App\Events\MatchEnded($match->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* STEP 4: Get Final Match Results
|
||||
*/
|
||||
public function matchResult(PvpMatch $match)
|
||||
{
|
||||
// Ensure user was actually in this match
|
||||
if (!$match->players()->where('user_id', auth()->id())->exists()) {
|
||||
return response()->json(['message' => 'دسترسی غیرمجاز'], 403);
|
||||
}
|
||||
|
||||
$match->load([
|
||||
'players.user:id,name', // Load player names
|
||||
'winner:id,name' // Load winner name
|
||||
]);
|
||||
|
||||
return $this->success([
|
||||
'match_id' => $match->id,
|
||||
'status' => $match->status,
|
||||
'winner_id' => $match->winner_id,
|
||||
'winner_name' => $match->winner ? $match->winner->name : 'مساوی (Draw)',
|
||||
'players' => $match->players->map(function ($p) {
|
||||
return [
|
||||
'user_id' => $p->user_id,
|
||||
'name' => optional($p->user)->name,
|
||||
'final_score' => round($p->score, 2)
|
||||
];
|
||||
})
|
||||
], 'موفق', 'نتایج نهایی مسابقه دریافت شد.');
|
||||
}
|
||||
}
|
||||
207
app/Http/Controllers/api/QuizController.php
Normal file
207
app/Http/Controllers/api/QuizController.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Quiz;
|
||||
use App\Models\QuizCategory;
|
||||
use App\Models\QuizSubject;
|
||||
use App\Models\UserQuizAttempt;
|
||||
use App\Models\UserQuizAnswer;
|
||||
use App\Models\Question;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class QuizController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
/**
|
||||
* 1. Get all active Categories
|
||||
*/
|
||||
public function categories()
|
||||
{
|
||||
$categories = QuizCategory::where('is_active', true)->get();
|
||||
return $this->success($categories, 'موفق', 'لیست دستهبندیها دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. Get all Subjects
|
||||
*/
|
||||
public function subjects()
|
||||
{
|
||||
$subjects = QuizSubject::all();
|
||||
return $this->success($subjects, 'موفق', 'لیست دروس دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. List Quizzes (Filterable by Category or Subject)
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Quiz::where('is_published', true)->with('category');
|
||||
|
||||
if ($request->filled('category_id')) {
|
||||
$query->where('category_id', $request->category_id);
|
||||
}
|
||||
|
||||
if ($request->filled('subject_id')) {
|
||||
$query->whereHas('questions', function ($q) use ($request) {
|
||||
$q->where('subject_id', $request->subject_id);
|
||||
});
|
||||
}
|
||||
|
||||
$quizzes = $query->latest()->get();
|
||||
|
||||
return $this->success($quizzes, 'موفق', 'لیست آزمونها دریافت شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. Start a Quiz & Fetch Questions (SECURELY)
|
||||
*/
|
||||
public function start(Quiz $quiz)
|
||||
{
|
||||
if (!$quiz->is_published) {
|
||||
return response()->json(['message' => 'این آزمون در دسترس نیست.'], 403);
|
||||
}
|
||||
|
||||
$attempt = UserQuizAttempt::create([
|
||||
'user_id' => auth()->id(),
|
||||
'quiz_id' => $quiz->id,
|
||||
'started_at' => now(),
|
||||
'status' => 'in_progress'
|
||||
]);
|
||||
|
||||
$quiz->load(['questions' => function($q) {
|
||||
$q->orderBy('order', 'asc');
|
||||
}, 'questions.options']);
|
||||
|
||||
// SECURITY: Hide the correct answers and explanations before sending to mobile app
|
||||
$quiz->questions->each(function ($question) {
|
||||
$question->makeHidden(['solution_explanation']);
|
||||
$question->options->each->makeHidden(['is_correct']);
|
||||
});
|
||||
|
||||
$data = [
|
||||
'attempt_id' => $attempt->id,
|
||||
'quiz' => $quiz
|
||||
];
|
||||
|
||||
return $this->success($data, 'موفق', 'آزمون با موفقیت شروع شد');
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. Submit the Quiz & Calculate Score
|
||||
*/
|
||||
public function submit(Request $request, UserQuizAttempt $attempt)
|
||||
{
|
||||
if ($attempt->user_id !== auth()->id() || $attempt->status !== 'in_progress') {
|
||||
return response()->json(['message' => 'دسترسی غیرمجاز یا آزمون قبلاً پایان یافته است.'], 403);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'answers' => 'required|array',
|
||||
'answers.*.question_id' => 'required|exists:questions,id',
|
||||
'answers.*.selected_option_id' => 'nullable|exists:question_options,id',
|
||||
'answers.*.text_answer' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$quiz = $attempt->quiz;
|
||||
|
||||
$totalScore = 0.00;
|
||||
$correctCount = 0;
|
||||
$wrongCount = 0;
|
||||
$unansweredCount = 0;
|
||||
$needsGrading = false;
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($request->answers as $ans) {
|
||||
$question = Question::with('options')->find($ans['question_id']);
|
||||
|
||||
$isCorrect = false;
|
||||
$earnedScore = 0.00;
|
||||
|
||||
// Handle Multiple Choice
|
||||
if ($question->type === 'multiple_choice') {
|
||||
if (empty($ans['selected_option_id'])) {
|
||||
$unansweredCount++;
|
||||
$isCorrect = null;
|
||||
} else {
|
||||
$correctOption = $question->options->where('is_correct', true)->first();
|
||||
|
||||
if ($correctOption && $correctOption->id == $ans['selected_option_id']) {
|
||||
$isCorrect = true;
|
||||
$earnedScore = $question->score;
|
||||
$totalScore += $earnedScore;
|
||||
$correctCount++;
|
||||
} else {
|
||||
$isCorrect = false;
|
||||
if ($quiz->has_negative_score) {
|
||||
$earnedScore = -($question->score / 3);
|
||||
$totalScore += $earnedScore;
|
||||
}
|
||||
$wrongCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle Descriptive
|
||||
else if ($question->type === 'descriptive') {
|
||||
$needsGrading = true;
|
||||
$isCorrect = null;
|
||||
}
|
||||
|
||||
UserQuizAnswer::create([
|
||||
'attempt_id' => $attempt->id,
|
||||
'question_id' => $question->id,
|
||||
'selected_option_id' => $ans['selected_option_id'] ?? null,
|
||||
'text_answer' => $ans['text_answer'] ?? null,
|
||||
'is_correct' => $isCorrect,
|
||||
'earned_score' => $earnedScore,
|
||||
]);
|
||||
}
|
||||
|
||||
$attempt->update([
|
||||
'completed_at' => now(),
|
||||
'total_score' => $totalScore,
|
||||
'correct_answers' => $correctCount,
|
||||
'wrong_answers' => $wrongCount,
|
||||
'unanswered' => $unansweredCount,
|
||||
'status' => $needsGrading ? 'pending_grading' : 'completed',
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$data = [
|
||||
'attempt_id' => $attempt->id,
|
||||
'status' => $attempt->status
|
||||
];
|
||||
|
||||
return $this->success($data, 'موفق', 'آزمون با موفقیت ثبت شد.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return response()->json(['message' => 'خطا در ثبت اطلاعات آزمون.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 6. Fetch Results
|
||||
*/
|
||||
public function result(UserQuizAttempt $attempt)
|
||||
{
|
||||
if ($attempt->user_id !== auth()->id()) {
|
||||
return response()->json(['message' => 'غیرمجاز'], 403);
|
||||
}
|
||||
|
||||
$attempt->load([
|
||||
'quiz.category',
|
||||
'answers.question.options',
|
||||
'answers.question.subject',
|
||||
'answers.selectedOption'
|
||||
]);
|
||||
|
||||
return $this->success($attempt, 'موفق', 'نتیجه آزمون با موفقیت دریافت شد');
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\SectionRequest;
|
||||
use App\Models\Law;
|
||||
use App\Models\LikeSection;
|
||||
use App\Models\Order;
|
||||
use App\Models\Section;
|
||||
use App\Traits\BaseApiResponse;
|
||||
|
||||
@@ -25,8 +24,8 @@ class SectionController extends Controller
|
||||
|
||||
$section->getCollection()->transform(function ($section) {
|
||||
unset($section['book_id']);
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked',$section['law_id'])->first()?->is_locked;
|
||||
|
||||
$section['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $section['law_id'])->first()?->is_locked;
|
||||
|
||||
unset($section['law_id']);
|
||||
|
||||
return $section;
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\PaymentTransaction;
|
||||
use App\Models\SubscribePlan;
|
||||
use App\Models\UserSubscriber;
|
||||
use App\Services\AppMarketPurchaseVerifier;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -17,6 +18,10 @@ class SubscribePlanController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function __construct(private AppMarketPurchaseVerifier $marketVerifier)
|
||||
{
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$subscribePlans = SubscribePlan::query()->where('is_active', true)->get()->map(function ($q) {
|
||||
@@ -25,6 +30,7 @@ class SubscribePlanController extends Controller
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $q->id,
|
||||
'name' => $q->name,
|
||||
@@ -32,7 +38,7 @@ class SubscribePlanController extends Controller
|
||||
'expired_day' => $q->expired_day,
|
||||
'is_free' => $q->is_free == 1 ? true : false,
|
||||
'type' => $q->type,
|
||||
'transaction' => $q->transaction
|
||||
'transaction' => $q->transaction,
|
||||
|
||||
];
|
||||
})->filter(function ($q) {
|
||||
@@ -55,8 +61,8 @@ class SubscribePlanController extends Controller
|
||||
->where('expired_at', '>', now())
|
||||
->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.']);
|
||||
if ($activeSubscription && ! $activeSubscription?->subscribe?->is_free && ! $subscribePlan->is_free) {
|
||||
return $this->failed(null, ['title' => 'اشتراک', 'message' => 'شما درحال حاظر اشتراک فعال دارید']);
|
||||
}
|
||||
|
||||
if ($subscribePlan->is_free) {
|
||||
@@ -66,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);
|
||||
@@ -76,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 {
|
||||
@@ -97,19 +103,18 @@ class SubscribePlanController extends Controller
|
||||
|
||||
return $this->success(['url' => $payment]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Payment initiation failed: ' . $e->getMessage());
|
||||
Log::error('Payment initiation failed: '.$e->getMessage());
|
||||
|
||||
return $this->failed(null, 'درگاه پرداخت در دسترس نیست');
|
||||
}
|
||||
}
|
||||
|
||||
public function subscribe_new(Request $request)
|
||||
{
|
||||
$package_name_bazzar = 'com.razzaghi.lawbook.android';
|
||||
|
||||
$request->validate([
|
||||
'subscribe_plan_id' => 'required|exists:subscribe_plans,id',
|
||||
'subscription_id' => 'nullable',
|
||||
'purchase_token' => 'nullable'
|
||||
'purchase_token' => 'nullable',
|
||||
]);
|
||||
|
||||
$subscribePlan = SubscribePlan::findOrFail($request->subscribe_plan_id);
|
||||
@@ -128,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);
|
||||
@@ -136,36 +141,26 @@ class SubscribePlanController extends Controller
|
||||
$user->userSubscribers()->create([
|
||||
'subscribe_plan_id' => $subscribePlan->id,
|
||||
'expired_at' => $expiredAt,
|
||||
'is_free' => true
|
||||
'is_free' => true,
|
||||
]);
|
||||
|
||||
return $this->success(null, 'Subscribe Plan', 'Free Subscribe Plan Successfully Activated');
|
||||
return $this->success(null, 'اشتراک', 'اشتراک رایگان فعال شد');
|
||||
}
|
||||
|
||||
$subscription_id = $request->input('subscription_id');
|
||||
$purchase_token = $request->input('purchase_token');
|
||||
|
||||
if (!$subscription_id || !$purchase_token) {
|
||||
if (! $subscription_id || ! $purchase_token) {
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid subscription details.']);
|
||||
}
|
||||
|
||||
$url = "https://pardakht.cafebazaar.ir/devapi/v2/api/applications/$package_name_bazzar/subscriptions/$subscription_id/purchases/$purchase_token";
|
||||
|
||||
$client = new \GuzzleHttp\Client();
|
||||
try {
|
||||
$response = $client->get($url, [
|
||||
'headers' => [
|
||||
'CAFEBAZAAR-PISHKHAN-API-SECRET' => 'eyJhbGciOiJIUzI1NiIsImtpZCI6ImFuY2llbnQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJuYXNoZXItcGlzaGtoYW4tYXBpIiwiaWF0IjoxNzQwMjQ3NTMzLCJleHAiOjQ4OTM4NDc1MzMsImFwaV9hZ2VudF9pZCI6MzQ2NX0.UCrr3IHxCqn77ckxfnaubrsyCfrhPm18gJgyg1qNqwA',
|
||||
]
|
||||
]);
|
||||
$bazaarExpiredAt = $this->marketVerifier->verifyBazaarSubscription($subscription_id, $purchase_token);
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
if (!isset($data['validUntilTimestampMsec']) || $data['validUntilTimestampMsec'] < now()->timestamp * 1000) {
|
||||
if (! $bazaarExpiredAt) {
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or expired subscription.']);
|
||||
}
|
||||
|
||||
$bazaarExpiredAt = Carbon::createFromTimestampMs($data['validUntilTimestampMsec']);
|
||||
|
||||
$expiredAt = $activeSubscription ? $activeSubscription->expired_at->addDays($subscribePlan->expired_day) : $bazaarExpiredAt;
|
||||
$user->userSubscribers()->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
@@ -177,16 +172,82 @@ class SubscribePlanController extends Controller
|
||||
'expired_at' => $expiredAt,
|
||||
'subscription_id' => $subscription_id,
|
||||
'purchase_token' => $purchase_token,
|
||||
'is_free' => false
|
||||
'market_provider' => 'bazaar',
|
||||
'last_verified_at' => now(),
|
||||
'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()]);
|
||||
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Failed to verify subscription.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function subscribeMyket(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'subscribe_plan_id' => 'required|exists:subscribe_plans,id',
|
||||
'sku_id' => 'required|string',
|
||||
'token' => 'required_without_all:tokenId,token_id,purchase_token|string',
|
||||
'tokenId' => 'required_without_all:token,token_id,purchase_token|string',
|
||||
'token_id' => 'required_without_all:token,tokenId,purchase_token|string',
|
||||
'purchase_token' => 'required_without_all:token,tokenId,token_id|string',
|
||||
]);
|
||||
|
||||
$subscribePlan = SubscribePlan::findOrFail($request->subscribe_plan_id);
|
||||
$user = auth()->user();
|
||||
$token = $request->input('token')
|
||||
?? $request->input('tokenId')
|
||||
?? $request->input('token_id')
|
||||
?? $request->input('purchase_token');
|
||||
|
||||
if ($subscribePlan->is_free) {
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid subscription details.']);
|
||||
}
|
||||
|
||||
try {
|
||||
$purchase = $this->marketVerifier->verifyMyketProduct($request->sku_id, $token);
|
||||
|
||||
if (! $purchase) {
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or failed Myket purchase.']);
|
||||
}
|
||||
|
||||
$expiredAt = $purchase['purchased_at']->copy()->addDays($subscribePlan->expired_day);
|
||||
|
||||
if ($expiredAt->lessThan(now())) {
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Invalid or expired subscription.']);
|
||||
}
|
||||
|
||||
$user->userSubscribers()->whereHas('subscribe', function ($query) {
|
||||
$query->where('is_free', true);
|
||||
})->update(['expired_at' => now()]);
|
||||
|
||||
$user->userSubscribers()->updateOrCreate(
|
||||
[
|
||||
'market_provider' => 'myket',
|
||||
'purchase_token' => $token,
|
||||
],
|
||||
[
|
||||
'subscribe_plan_id' => $subscribePlan->id,
|
||||
'expired_at' => $expiredAt,
|
||||
'subscription_id' => $request->sku_id,
|
||||
'product_id' => $request->sku_id,
|
||||
'purchased_at' => $purchase['purchased_at'],
|
||||
'last_verified_at' => now(),
|
||||
'is_free' => false,
|
||||
]
|
||||
);
|
||||
|
||||
return $this->success(null, 'Subscribe Plan', 'Myket subscription successfully activated.');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in Myket subscription', ['Error' => $e->getMessage()]);
|
||||
|
||||
return $this->failed(null, ['title' => 'Subscribe Plan', 'message' => 'Failed to verify Myket subscription.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function paymentCallback(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -224,22 +285,42 @@ class SubscribePlanController extends Controller
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
$subscribePlans = UserSubscriber::where('user_id', $user->id)
|
||||
->where('expired_at', '>', now())
|
||||
->orderBy('expired_at', 'desc')
|
||||
// 1. Fetch all active subscriptions
|
||||
$activeSubscribers = $user->userSubscribers()
|
||||
->with('subscribe')
|
||||
->where('expired_at', '>=', now())
|
||||
->get();
|
||||
|
||||
if ($subscribePlans->isNotEmpty()) {
|
||||
$totalExpiredDays = $subscribePlans->sum('expired_day');
|
||||
$latestPlan = $subscribePlans->first();
|
||||
if ($activeSubscribers->isNotEmpty()) {
|
||||
|
||||
// 2. Separate Paid plans from Free plans
|
||||
$paidSubscriptions = $activeSubscribers->reject(function ($subscriber) {
|
||||
return $subscriber->is_free || ($subscriber->subscribe && $subscriber->subscribe->is_free);
|
||||
});
|
||||
|
||||
// 3. AUTHORITY RULE: Paid plans absolutely override Free plans!
|
||||
if ($paidSubscriptions->isNotEmpty()) {
|
||||
$latestSubscription = $paidSubscriptions->sortByDesc('expired_at')->first();
|
||||
} else {
|
||||
$latestSubscription = $activeSubscribers->sortByDesc('expired_at')->first();
|
||||
}
|
||||
|
||||
// 4. Calculate accurate remaining days
|
||||
$expiredDays = $latestSubscription->expired_at->diffInDays(now());
|
||||
|
||||
$isFree = (bool) (
|
||||
$latestSubscription->is_free ||
|
||||
($latestSubscription->subscribe && $latestSubscription->subscribe->is_free)
|
||||
);
|
||||
|
||||
// 5. Format the exact same object expected by the frontend
|
||||
$subscribePlanData = [
|
||||
'id' => $latestPlan->id,
|
||||
'name' => $latestPlan->subscribe->name,
|
||||
'price' => $latestPlan->subscribe->price,
|
||||
'expired_day' => $totalExpiredDays,
|
||||
'is_free' => $latestPlan->subscribe->is_free == 1,
|
||||
'expired_at' => Carbon::now()->addDays($totalExpiredDays)->format('Y-m-d'),
|
||||
'id' => $latestSubscription->id,
|
||||
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
||||
'price' => $latestSubscription->subscribe->price ?? 0,
|
||||
'expired_day' => $expiredDays,
|
||||
'is_free' => $isFree,
|
||||
'expired_at' => $latestSubscription->expired_at->format('Y-m-d'),
|
||||
];
|
||||
|
||||
return $this->success($subscribePlanData, 'Subscribe Plan', 'Current Subscribe Plan');
|
||||
@@ -248,16 +329,16 @@ class SubscribePlanController extends Controller
|
||||
return $this->success(null, 'Subscribe Plan', 'No Subscribe Plan');
|
||||
}
|
||||
|
||||
|
||||
private function send($url, $api, $amount, $redirect, $factorId, $name, $email, $description)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "api=$api&amount=$amount&redirect=$redirect&factorId=$factorId&name=$name&email=$email&description=$description");
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -266,10 +347,11 @@ class SubscribePlanController extends Controller
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "api=$api&id_get=$id_get&trans_id=$trans_id&json=1");
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
||||
125
app/Http/Controllers/api/TicketApiController.php
Normal file
125
app/Http/Controllers/api/TicketApiController.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketMessage;
|
||||
use App\Traits\BaseApiResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketApiController extends Controller
|
||||
{
|
||||
use BaseApiResponse;
|
||||
|
||||
public function index()
|
||||
{
|
||||
$tickets = Ticket::where('user_id', auth()->id())
|
||||
->orderBy('updated_at', 'desc')
|
||||
->get()
|
||||
->map(function ($ticket) {
|
||||
return [
|
||||
'id' => $ticket->id,
|
||||
'subject' => $ticket->subject,
|
||||
'status' => $ticket->status,
|
||||
'priority' => $ticket->priority,
|
||||
'created_at' => $ticket->created_at,
|
||||
'updated_at' => $ticket->updated_at,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success($tickets, 'موفق', 'لیست تیکتها با موفقیت دریافت شد');
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$ticket = Ticket::with(['messages' => function ($query) {
|
||||
$query->orderBy('created_at', 'asc');
|
||||
}])->where('user_id', auth()->id())->find($id);
|
||||
|
||||
if (!$ticket) {
|
||||
return $this->failed([], 'یافت نشد', 'تیکت مورد نظر یافت نشد.', 404);
|
||||
}
|
||||
|
||||
$formattedTicket = [
|
||||
'id' => $ticket->id,
|
||||
'subject' => $ticket->subject,
|
||||
'status' => $ticket->status,
|
||||
'priority' => $ticket->priority,
|
||||
'created_at' => $ticket->created_at,
|
||||
'messages' => $ticket->messages->map(function ($msg) {
|
||||
return [
|
||||
'id' => $msg->id,
|
||||
// If user_id matches the logged in user, it's their message. Otherwise, it's admin's reply.
|
||||
'is_me' => $msg->user_id === auth()->id(),
|
||||
'message' => $msg->message,
|
||||
'attachment_url' => $msg->attachment_path ? asset('storage/' . $msg->attachment_path) : null,
|
||||
'created_at' => $msg->created_at,
|
||||
];
|
||||
}),
|
||||
];
|
||||
|
||||
return $this->success($formattedTicket, 'موفق', 'جزئیات تیکت دریافت شد');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'subject' => 'required|string|max:255',
|
||||
'priority' => 'nullable|in:low,medium,high',
|
||||
'message' => 'required|string',
|
||||
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120', // Max 5MB
|
||||
]);
|
||||
|
||||
$ticket = Ticket::create([
|
||||
'user_id' => auth()->id(),
|
||||
'subject' => $validated['subject'],
|
||||
'status' => 'open',
|
||||
'priority' => $validated['priority'] ?? 'medium',
|
||||
]);
|
||||
|
||||
$attachmentPath = null;
|
||||
if ($request->hasFile('attachment')) {
|
||||
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||
}
|
||||
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => auth()->id(),
|
||||
'message' => $validated['message'],
|
||||
'attachment_path' => $attachmentPath,
|
||||
]);
|
||||
|
||||
return $this->success(['ticket_id' => $ticket->id], 'موفق', 'تیکت شما با موفقیت ثبت شد');
|
||||
}
|
||||
|
||||
public function reply(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'message' => 'required|string',
|
||||
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120',
|
||||
]);
|
||||
|
||||
$ticket = Ticket::where('user_id', auth()->id())->find($id);
|
||||
|
||||
if (!$ticket) {
|
||||
return $this->failed([], 'خطا', 'تیکت یافت نشد', 404);
|
||||
}
|
||||
|
||||
$attachmentPath = null;
|
||||
if ($request->hasFile('attachment')) {
|
||||
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||
}
|
||||
|
||||
TicketMessage::create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => auth()->id(),
|
||||
'message' => $validated['message'],
|
||||
'attachment_path' => $attachmentPath,
|
||||
]);
|
||||
|
||||
$ticket->update(['status' => 'user_reply']);
|
||||
|
||||
return $this->success([], 'موفق', 'پاسخ شما با موفقیت ثبت شد');
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,18 @@ class VersionController extends Controller
|
||||
{
|
||||
$version = Version::query()->where('type', $versionRequest->type)->orderBy('id', 'desc')->first();
|
||||
|
||||
if (!$version) {
|
||||
if (! $version) {
|
||||
return $this->failed([], 'Version not found');
|
||||
}
|
||||
|
||||
if (intval($version->number) > $versionRequest->number) {
|
||||
return $this->success([
|
||||
'force_update' => $version->force_update == 1 ? true : false
|
||||
'force_update' => $version->force_update == 1 ? true : false,
|
||||
], 'Your app need to be updated');
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'force_update' => false
|
||||
'force_update' => false,
|
||||
], 'Version successfully');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use App\Models\Chapter;
|
||||
use App\Models\Division;
|
||||
use App\Models\Gate;
|
||||
use App\Models\Law;
|
||||
use App\Models\Order;
|
||||
use App\Models\Part;
|
||||
use App\Models\RecentArt;
|
||||
use App\Models\Section;
|
||||
@@ -35,7 +34,7 @@ class VolumController extends Controller
|
||||
|
||||
$volumes->getCollection()->transform(function ($volume) {
|
||||
$volume['has_book'] = Book::where('volum_id', $volume->id)->exists();
|
||||
$volume['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $volume['law_id'])->first()?->is_locked;
|
||||
$volume['is_locked'] = auth()->user()->isSubscriber() !== false ? false : Law::where('id', $volume['law_id'])->first()?->is_locked;
|
||||
|
||||
unset($volume['law_id']);
|
||||
|
||||
@@ -58,374 +57,111 @@ class VolumController extends Controller
|
||||
$gate_id = $request->gate_id;
|
||||
|
||||
$perPage = $request->per_page ?? 10;
|
||||
|
||||
if ($bookId) {
|
||||
$book = Book::with(['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])->find($bookId);
|
||||
if (! $book) return $this->success([], 'Book not found');
|
||||
|
||||
$book = Book::with(['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])
|
||||
->find($bookId);
|
||||
|
||||
if (!$book) {
|
||||
return $this->success([], 'Book not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'] as $relation) {
|
||||
if ($book->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $book->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
$paginationData = [
|
||||
'next_page_url' => $items->nextPageUrl(),
|
||||
];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($book, ['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success'); // Force return to prevent fall-through
|
||||
}
|
||||
|
||||
if ($division_id) {
|
||||
$division = Division::with(['sections', 'chapters', 'parts', 'gate', 'branch', 'art'])
|
||||
->find($division_id);
|
||||
$division = Division::with(['sections', 'chapters', 'parts', 'gate', 'branch', 'art'])->find($division_id);
|
||||
if (! $division) return $this->success([], 'Division not found');
|
||||
|
||||
if (!$division) {
|
||||
return $this->success([], 'Division not found');
|
||||
}
|
||||
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['sections', 'chapters', 'parts', 'gate', 'branch', 'art'] as $relation) {
|
||||
if ($division->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $division->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($division, ['sections', 'chapters', 'parts', 'gate', 'branch', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($section_id) {
|
||||
$section = Section::with(['chapters', 'parts', 'gate', 'branch', 'art'])
|
||||
->find($section_id);
|
||||
$section = Section::with(['chapters', 'parts', 'gate', 'branch', 'art'])->find($section_id);
|
||||
if (! $section) return $this->success([], 'Section not found');
|
||||
|
||||
if (!$section) {
|
||||
return $this->success([], 'Section not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['chapters', 'parts', 'gate', 'branch', 'art'] as $relation) {
|
||||
if ($section->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $section->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($section, ['chapters', 'parts', 'gate', 'branch', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($volumeId) {
|
||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])
|
||||
->find($volumeId);
|
||||
if (!$volume) {
|
||||
return $this->success([], 'Volume not found');
|
||||
}
|
||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])->find($volumeId);
|
||||
if (! $volume) return $this->success([], 'Volume not found');
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'] as $relation) {
|
||||
if ($volume->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $volume->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
}
|
||||
|
||||
if ($part_id) {
|
||||
$part = Part::with(['gate', 'branch', 'art'])
|
||||
->find($part_id);
|
||||
|
||||
if (!$part) {
|
||||
return $this->success([], 'Part not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['gate', 'branch', 'art'] as $relation) {
|
||||
if ($part->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $part->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($volume, ['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($chapter_id) {
|
||||
$chapter = Chapter::with(['parts', 'gate', 'branch', 'art'])
|
||||
->find($chapter_id);
|
||||
$chapter = Chapter::with(['parts', 'gate', 'branch', 'art'])->find($chapter_id);
|
||||
if (! $chapter) return $this->success([], 'Chapter not found');
|
||||
|
||||
if (!$chapter) {
|
||||
return $this->success([], 'Chapter not found');
|
||||
}
|
||||
$data = $this->checkRelations($chapter, ['parts', 'gate', 'branch', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['parts', 'gate', 'branch', 'art'] as $relation) {
|
||||
if ($chapter->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $chapter->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($part_id) {
|
||||
$part = Part::with(['gate', 'branch', 'art'])->find($part_id);
|
||||
if (! $part) return $this->success([], 'Part not found');
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($part, ['gate', 'branch', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($gate_id) {
|
||||
$gate = Gate::with(['branch', 'art'])->find($gate_id);
|
||||
if (! $gate) return $this->success([], 'Gate not found');
|
||||
|
||||
$data = $this->checkRelations($gate, ['branch', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($branch_id) {
|
||||
$branch = Branch::with(['art'])
|
||||
->find($branch_id);
|
||||
$branch = Branch::with(['art'])->find($branch_id);
|
||||
if (! $branch) return $this->success([], 'Branch not found');
|
||||
|
||||
if (!$branch) {
|
||||
return $this->success([], 'Branch not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['art'] as $relation) {
|
||||
if ($branch->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $branch->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($gate_id) {
|
||||
$gate = Gate::with(['branch', 'art'])
|
||||
->find($gate_id);
|
||||
|
||||
if (!$gate) {
|
||||
return $this->success([], 'Gate not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['branch', 'art'] as $relation) {
|
||||
if ($gate->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $gate->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
}
|
||||
|
||||
if ($volumeId) {
|
||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs'])
|
||||
->find($volumeId);
|
||||
|
||||
if (!$volume) {
|
||||
return $this->success([], 'Volume not found');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$foundRelation = false;
|
||||
foreach (['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs'] as $relation) {
|
||||
if ($volume->{$relation}->count() > 0) {
|
||||
$foundRelation = true;
|
||||
$items = $volume->{$relation}()->paginate($perPage);
|
||||
foreach ($items as $item) {
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $relation,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRelation) {
|
||||
return $this->success($data, 'Success');
|
||||
} else {
|
||||
return $this->success([], 'No relations found');
|
||||
}
|
||||
$data = $this->checkRelations($branch, ['art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($law_Id) {
|
||||
$law = Law::find($law_Id);
|
||||
if (! $law) return $this->success([], 'Law not found');
|
||||
|
||||
if (!$law) {
|
||||
return $this->success([], 'Law not found');
|
||||
}
|
||||
$arts = Volum::query()->where('law_id', $law->id)->get();
|
||||
|
||||
$arts = Volum::query()
|
||||
->where('law_id', $law->id)
|
||||
->get();
|
||||
|
||||
|
||||
RecentArt::query()->updateOrCreate([
|
||||
'user_id' => auth()->user()->id,
|
||||
'law_id' => $law_Id
|
||||
], [
|
||||
'user_id' => auth()->user()->id,
|
||||
'law_id' => $law_Id
|
||||
]);
|
||||
|
||||
|
||||
$data = [];
|
||||
RecentArt::query()->updateOrCreate(
|
||||
['user_id' => auth()->user()->id, 'law_id' => $law_Id],
|
||||
['user_id' => auth()->user()->id, 'law_id' => $law_Id]
|
||||
);
|
||||
|
||||
if (count($arts) > 0) {
|
||||
foreach ($arts as $item) {
|
||||
$data[] = [
|
||||
$data = $arts->map(function ($item) use ($law) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => 'volume',
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : $law->is_locked,
|
||||
];
|
||||
}
|
||||
})->toArray();
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
|
||||
$arts = Art::query()
|
||||
->where('law_id', $law->id)
|
||||
->where('section_id', null)
|
||||
->where('gate_id', null)
|
||||
->where('part_id', null)
|
||||
->where('chapter_id', null)
|
||||
->where('book_id', null)
|
||||
->where('volum_id', null)
|
||||
->whereNull('section_id')->whereNull('gate_id')->whereNull('part_id')
|
||||
->whereNull('chapter_id')->whereNull('book_id')->whereNull('volum_id')
|
||||
->get();
|
||||
|
||||
foreach ($arts as $item) {
|
||||
$data[] = [
|
||||
$data = $arts->map(function ($item) use ($law) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => 'art',
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : $law->is_locked,
|
||||
];
|
||||
}
|
||||
})->toArray();
|
||||
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
@@ -504,8 +240,8 @@ class VolumController extends Controller
|
||||
'foreign_key' => 'branch_id',
|
||||
'type' => (new Art)->getTable(),
|
||||
'check_mode' => Art::class,
|
||||
'check_key' => 'branch_id'
|
||||
]
|
||||
'check_key' => 'branch_id',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($filters as $filterKey => $filterInfo) {
|
||||
@@ -533,7 +269,7 @@ class VolumController extends Controller
|
||||
'title' => $item?->title,
|
||||
'number' => $item?->number,
|
||||
'type' => $type,
|
||||
'is_end' => $check_model::where($check_key, $item?->id)->count() !== 0 ? false : true
|
||||
'is_end' => $check_model::where($check_key, $item?->id)->count() !== 0 ? false : true,
|
||||
];
|
||||
});
|
||||
|
||||
@@ -544,7 +280,6 @@ class VolumController extends Controller
|
||||
return $this->success([], 'No law or book or volume specified');
|
||||
}
|
||||
|
||||
|
||||
public function check_filter_with_art(CheckBookFilterRequest $request)
|
||||
{
|
||||
$lawId = $request->law_id;
|
||||
@@ -559,9 +294,9 @@ class VolumController extends Controller
|
||||
$categoryId = $request->category_id;
|
||||
|
||||
$perPage = $request->per_page ?? 10;
|
||||
|
||||
if ($categoryId) {
|
||||
$categories = Category::with(['laws'])->where('type', $this->convertValueTo($categoryId))->get()->pluck('id');
|
||||
|
||||
$items = Law::query()->whereIn('category_id', $categories)->get();
|
||||
$data = [];
|
||||
foreach ($items as $item) {
|
||||
@@ -570,7 +305,7 @@ class VolumController extends Controller
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => 'laws',
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked,
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : $item->is_locked,
|
||||
'image' => $item?->image,
|
||||
'law' => $item?->title,
|
||||
'count_art' => $item->arts->count(),
|
||||
@@ -578,167 +313,111 @@ class VolumController extends Controller
|
||||
'price' => $item->price,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
if ($lawId) {
|
||||
$law = Law::with(['volums', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'arts'])->find($lawId);
|
||||
if (!$law) {
|
||||
return $this->success([], 'Law not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($law, ['volums', 'book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'arts'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the law');
|
||||
}
|
||||
|
||||
|
||||
if ($bookId) {
|
||||
$book = Book::with(['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])->find($bookId);
|
||||
if (!$book) {
|
||||
return $this->success([], 'Book not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($book, ['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the book');
|
||||
}
|
||||
|
||||
if ($volumeId) {
|
||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])->find($volumeId);
|
||||
if (!$volume) {
|
||||
return $this->success([], 'Volume not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($volume, ['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the volume');
|
||||
}
|
||||
|
||||
if ($sectionId) {
|
||||
$section = Section::with(['gates', 'parts', 'chapters', 'branchs', 'art'])->find($sectionId);
|
||||
if (!$section) {
|
||||
return $this->success([], 'Section not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($section, ['gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the section');
|
||||
}
|
||||
|
||||
if ($divisionId) {
|
||||
$division = Division::with(['gates', 'parts', 'chapters', 'branchs', 'art'])->find($divisionId);
|
||||
if (!$division) {
|
||||
return $this->success([], 'Division not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($division, ['gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the division');
|
||||
}
|
||||
|
||||
if ($partId) {
|
||||
$part = Part::with(['gates', 'art'])->find($partId);
|
||||
if (!$part) {
|
||||
return $this->success([], 'Part not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($part, ['gates', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the part');
|
||||
}
|
||||
|
||||
if ($branchId) {
|
||||
$branch = Branch::with(['gates', 'chapters', 'art'])->find($branchId);
|
||||
if (!$branch) {
|
||||
return $this->success([], 'Branch not found');
|
||||
}
|
||||
|
||||
if (! $branch) return $this->success([], 'Branch not found');
|
||||
$data = $this->checkRelations($branch, ['gates', 'chapters', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the branch');
|
||||
}
|
||||
|
||||
if ($chapterId) {
|
||||
$chapter = Chapter::with(['gate', 'parts', 'art'])->find($chapterId);
|
||||
if (!$chapter) {
|
||||
return $this->success([], 'Chapter not found');
|
||||
}
|
||||
|
||||
$data = $this->checkRelations($chapter, ['gate', 'parts', 'art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the chapter');
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($gateId) {
|
||||
$gate = Gate::with(['art'])->find($gateId);
|
||||
if (!$gate) {
|
||||
return $this->success([], 'Gate not found');
|
||||
}
|
||||
|
||||
if (! $gate) return $this->success([], 'Gate not found');
|
||||
$data = $this->checkRelations($gate, ['art'], $perPage);
|
||||
if ($data) {
|
||||
return $this->success($data, 'Success');
|
||||
}
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No related records found in the gate');
|
||||
if ($partId) {
|
||||
$part = Part::with(['gates', 'art'])->find($partId);
|
||||
if (! $part) return $this->success([], 'Part not found');
|
||||
$data = $this->checkRelations($part, ['gates', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($chapterId) {
|
||||
$chapter = Chapter::with(['gate', 'parts', 'art'])->find($chapterId);
|
||||
if (! $chapter) return $this->success([], 'Chapter not found');
|
||||
$data = $this->checkRelations($chapter, ['gate', 'parts', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($sectionId) {
|
||||
$section = Section::with(['chapters', 'parts', 'gates', 'branchs', 'art'])->find($sectionId);
|
||||
if (! $section) return $this->success([], 'Section not found');
|
||||
$data = $this->checkRelations($section, ['chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($divisionId) {
|
||||
$division = Division::with(['chapters', 'parts', 'gates', 'branchs', 'art'])->find($divisionId);
|
||||
if (! $division) return $this->success([], 'Division not found');
|
||||
$data = $this->checkRelations($division, ['chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($bookId) {
|
||||
$book = Book::with(['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])->find($bookId);
|
||||
if (! $book) return $this->success([], 'Book not found');
|
||||
$data = $this->checkRelations($book, ['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($volumeId) {
|
||||
$volume = Volum::with(['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])->find($volumeId);
|
||||
if (! $volume) return $this->success([], 'Volume not found');
|
||||
$data = $this->checkRelations($volume, ['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
if ($lawId) {
|
||||
$law = Law::with(['volums', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'arts'])->find($lawId);
|
||||
if (! $law) return $this->success([], 'Law not found');
|
||||
$data = $this->checkRelations($law, ['volums', 'book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'arts'], $perPage);
|
||||
return $this->success($data ?? [], 'Success');
|
||||
}
|
||||
|
||||
return $this->success([], 'No valid filter specified');
|
||||
}
|
||||
|
||||
|
||||
private function prepareItemsData($items, $type)
|
||||
{
|
||||
if (count($items) === 0) return [];
|
||||
|
||||
// Optimize: Calculate route only once for the first item
|
||||
$firstItem = $items[0];
|
||||
$sharedRoute = $this->route(get_class($firstItem), $firstItem);
|
||||
$law = Law::find($firstItem->law_id);
|
||||
$isLocked = auth()->user()->isSubscriber() !== false ? false : $law?->is_locked;
|
||||
|
||||
$data = [];
|
||||
foreach ($items as $item) {
|
||||
$law = Law::find($item->law_id);
|
||||
$data[] = [
|
||||
'id' => $item->id,
|
||||
'title' => $item->title,
|
||||
'number' => $item->number,
|
||||
'type' => $type,
|
||||
'route' => $this->route($item, $item),
|
||||
'is_locked' => auth()->user()->isSubscriber() !== false ? false : Law::where('is_locked', $item->law_id)->first()?->is_locked,
|
||||
// Ensure correct frontend naming
|
||||
'type' => in_array($type, ['art', 'arts']) ? 'art' : $type,
|
||||
'route' => $sharedRoute,
|
||||
'is_locked' => $isLocked,
|
||||
'law' => $law?->title,
|
||||
'image' => $law?->image,
|
||||
'count_art' => $law?->arts?->count() ?? 0,
|
||||
'count_art' => $law?->arts?->count() ?? 0,
|
||||
'count_volums' => $law?->volums?->count() ?? 0,
|
||||
'price' => $law?->price,
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function checkRelations($model, $relations, $perPage)
|
||||
{
|
||||
foreach ($relations as $relation) {
|
||||
if ($model->{$relation} && $model->{$relation}->count() > 0) {
|
||||
// Safety check: ensure relation exists on model
|
||||
if (method_exists($model, $relation) && $model->{$relation}()->count() > 0) {
|
||||
$items = $model->{$relation}()->paginate($perPage);
|
||||
return $this->prepareItemsData($items, $relation);
|
||||
}
|
||||
@@ -746,20 +425,15 @@ class VolumController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private function convertValueTo($var)
|
||||
{
|
||||
switch ($var) {
|
||||
case 1:
|
||||
return 'hagigi';
|
||||
break;
|
||||
case 2:
|
||||
return 'kifari';
|
||||
break;
|
||||
|
||||
default:
|
||||
return 'kifari';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,13 +443,13 @@ class VolumController extends Controller
|
||||
switch ((new $modelClass)->getTable()) {
|
||||
case 'laws':
|
||||
$route = array_filter([
|
||||
Law::find($query->id)?->title
|
||||
Law::find($query->id)?->title,
|
||||
]);
|
||||
break;
|
||||
case 'volums':
|
||||
$route = array_filter([
|
||||
Law::find($query->law_id)?->title,
|
||||
Volum::find($query->id)?->title
|
||||
Volum::find($query->id)?->title,
|
||||
]);
|
||||
break;
|
||||
case 'books':
|
||||
|
||||
@@ -17,7 +17,7 @@ class GetApiRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'book_id' => 'required'
|
||||
'book_id' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class SuggestionRequest extends FormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'text' => 'required'
|
||||
'text' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class CreateVersionStore extends FormRequest
|
||||
'number' => 'required',
|
||||
'log' => 'required',
|
||||
'force_update' => 'nullable',
|
||||
'type' => 'required'
|
||||
'type' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class CreateRequest extends FormRequest
|
||||
'is_active' => 'nullable',
|
||||
'is_free' => 'nullable',
|
||||
'type' => 'nullable',
|
||||
'transaction' => 'nullable'
|
||||
'transaction' => 'nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class UpdateRequest extends FormRequest
|
||||
'is_active' => 'nullable',
|
||||
'is_free' => 'nullable',
|
||||
'type' => 'nullable',
|
||||
'transaction' => 'nullable'
|
||||
'transaction' => 'nullable',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ class UpdateVersionStore extends FormRequest
|
||||
'code' => 'required',
|
||||
'number' => 'required',
|
||||
'log' => 'required',
|
||||
'force_update' => 'nullable',
|
||||
'type' => 'required'
|
||||
'force_update' => 'nullable',
|
||||
'type' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
// These are the classes we need to import for our job to work
|
||||
use App\Models\UserSubscriber;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue; // Your model for tracking user subscriptions
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\UserSubscriber; // Your model for tracking user subscriptions
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -40,8 +40,9 @@ class CheckBazaarSubscription implements ShouldQueue
|
||||
$package_name = 'com.razzaghi.lawbook.android'; // Your app's package name
|
||||
|
||||
// If there's no purchase token, we can't do anything, so we stop.
|
||||
if (!$purchase_token) {
|
||||
Log::info('Job skipped: No purchase token for UserSubscriber ID: ' . $this->userSubscriber->id);
|
||||
if (! $purchase_token) {
|
||||
Log::info('Job skipped: No purchase token for UserSubscriber ID: '.$this->userSubscriber->id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,16 +70,16 @@ class CheckBazaarSubscription implements ShouldQueue
|
||||
// If the expiration date in our database is different, update it
|
||||
if ($this->userSubscriber->expired_at->notEqualTo($bazaarExpiredAt)) {
|
||||
$this->userSubscriber->update(['expired_at' => $bazaarExpiredAt]);
|
||||
Log::info('Subscription date updated for user: ' . $this->userSubscriber->user_id);
|
||||
Log::info('Subscription date updated for user: '.$this->userSubscriber->user_id);
|
||||
}
|
||||
} else {
|
||||
// If the subscription is no longer valid, update it in our database to expire now
|
||||
$this->userSubscriber->update(['expired_at' => now()]);
|
||||
Log::warning('Subscription found expired on Bazaar for user: ' . $this->userSubscriber->user_id);
|
||||
Log::warning('Subscription found expired on Bazaar for user: '.$this->userSubscriber->user_id);
|
||||
}
|
||||
} else {
|
||||
// If the API call failed, log the error for debugging
|
||||
Log::error('Failed to fetch subscription from Bazaar', ['status' => $response->status(), 'body' => $response->body()]);
|
||||
// If the API call failed, log the error for debugging
|
||||
Log::error('Failed to fetch subscription from Bazaar', ['status' => $response->status(), 'body' => $response->body()]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -86,4 +87,4 @@ class CheckBazaarSubscription implements ShouldQueue
|
||||
Log::error('Exception while checking Bazaar subscription', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ class Art extends Model
|
||||
return $this->belongsToMany(JudicialPrecedent::class, 'art_judicial_precedent');
|
||||
}
|
||||
|
||||
public function legalOpinions()
|
||||
{
|
||||
return $this->belongsToMany(LegalOpinion::class, 'art_legal_opinion');
|
||||
}
|
||||
|
||||
public static function search($searchTerm)
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")
|
||||
|
||||
8
app/Models/ArtHighlight.php
Normal file
8
app/Models/ArtHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ArtHighlight extends Model {
|
||||
protected $fillable = ['user_id', 'art_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||
}
|
||||
@@ -48,7 +48,6 @@ class Book extends Model
|
||||
return $this->hasMany(Branch::class);
|
||||
}
|
||||
|
||||
|
||||
public static function searchLimit($searchTerm)
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")->limit(3)->get();
|
||||
|
||||
@@ -8,9 +8,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class Branch extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['title', 'number', 'chapter_id', 'part_id', 'volum_id', 'law_id', 'book_id', 'section_id', 'gate_id','division_id'];
|
||||
|
||||
protected $fillable = ['title', 'number', 'chapter_id', 'part_id', 'volum_id', 'law_id', 'book_id', 'section_id', 'gate_id', 'division_id'];
|
||||
|
||||
public function art()
|
||||
{
|
||||
@@ -21,7 +20,7 @@ class Branch extends Model
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")->get();
|
||||
}
|
||||
|
||||
|
||||
public static function searchLimit($searchTerm)
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")->limit(3)->get();
|
||||
|
||||
14
app/Models/BylawCodificationLink.php
Normal file
14
app/Models/BylawCodificationLink.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawCodificationLink extends Model
|
||||
{
|
||||
protected $table = 'bylaw_codification_links';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['source_entity_type', 'source_entity_id', 'target_entity_type', 'target_entity_id', 'link_type', 'effective_date', 'justification', 'created_at'];
|
||||
}
|
||||
34
app/Models/BylawDocument.php
Normal file
34
app/Models/BylawDocument.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawDocument extends Model
|
||||
{
|
||||
protected $table = 'bylaw_documents';
|
||||
|
||||
protected $fillable = ['title', 'category_id', 'approval_authority_id', 'approval_date', 'promulgation_authority_id', 'promulgation_date', 'promulgation_number', 'official_gazette_number', 'official_gazette_date', 'execution_date', 'bylaw_type', 'global_status', 'pdf_file_path', 'is_published', 'description'];
|
||||
|
||||
protected $casts = ['is_published' => 'boolean'];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(BylawSubjectCategory::class, 'category_id');
|
||||
}
|
||||
|
||||
public function approvalAuthority()
|
||||
{
|
||||
return $this->belongsTo(BylawLegalAuthority::class, 'approval_authority_id');
|
||||
}
|
||||
|
||||
public function promulgationAuthority()
|
||||
{
|
||||
return $this->belongsTo(BylawLegalAuthority::class, 'promulgation_authority_id');
|
||||
}
|
||||
|
||||
public function nodes()
|
||||
{
|
||||
return $this->hasMany(BylawNode::class, 'document_id');
|
||||
}
|
||||
}
|
||||
8
app/Models/BylawHighlight.php
Normal file
8
app/Models/BylawHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawHighlight extends Model {
|
||||
protected $fillable = ['user_id', 'bylaw_document_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||
}
|
||||
12
app/Models/BylawLegalAuthority.php
Normal file
12
app/Models/BylawLegalAuthority.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawLegalAuthority extends Model
|
||||
{
|
||||
protected $table = 'bylaw_legal_authorities';
|
||||
|
||||
protected $fillable = ['name', 'authority_type', 'description'];
|
||||
}
|
||||
27
app/Models/BylawNode.php
Normal file
27
app/Models/BylawNode.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawNode extends Model
|
||||
{
|
||||
protected $table = 'bylaw_nodes';
|
||||
|
||||
protected $fillable = ['document_id', 'parent_id', 'node_type_id', 'node_identifier', 'title', 'node_content', 'sort_order', 'valid_from', 'valid_to', 'node_status'];
|
||||
|
||||
public function document()
|
||||
{
|
||||
return $this->belongsTo(BylawDocument::class, 'document_id');
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(BylawNodeType::class, 'node_type_id');
|
||||
}
|
||||
}
|
||||
12
app/Models/BylawNodeType.php
Normal file
12
app/Models/BylawNodeType.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawNodeType extends Model
|
||||
{
|
||||
protected $table = 'bylaw_node_types';
|
||||
|
||||
protected $fillable = ['name', 'hierarchy_level'];
|
||||
}
|
||||
11
app/Models/BylawNote.php
Normal file
11
app/Models/BylawNote.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawNote extends Model
|
||||
{
|
||||
protected $table = 'bylaw_notes';
|
||||
protected $guarded = [];
|
||||
}
|
||||
17
app/Models/BylawSubjectCategory.php
Normal file
17
app/Models/BylawSubjectCategory.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BylawSubjectCategory extends Model
|
||||
{
|
||||
protected $table = 'bylaw_subject_categories';
|
||||
|
||||
protected $fillable = ['parent_id', 'name', 'slug', 'sort_order'];
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
}
|
||||
@@ -11,22 +11,19 @@ class Category extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'type'
|
||||
'type',
|
||||
];
|
||||
|
||||
|
||||
|
||||
public function laws()
|
||||
{
|
||||
return $this->hasMany(Law::class);
|
||||
}
|
||||
|
||||
|
||||
public static function getType()
|
||||
{
|
||||
return [
|
||||
'hagigi' => 'حقیقی',
|
||||
'kifari' => 'کیفری'
|
||||
'kifari' => 'کیفری',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class Chapter extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['title', 'number', 'section_id', 'book_id', 'volum_id', 'law_id','division_id'];
|
||||
protected $fillable = ['title', 'number', 'section_id', 'book_id', 'volum_id', 'law_id', 'division_id'];
|
||||
|
||||
protected $hidden = ['created_at', 'updated_at'];
|
||||
|
||||
@@ -28,7 +28,6 @@ class Chapter extends Model
|
||||
return $this->hasMany(Gate::class);
|
||||
}
|
||||
|
||||
|
||||
public function branch()
|
||||
{
|
||||
return $this->hasMany(Branch::class);
|
||||
|
||||
28
app/Models/DailyQuest.php
Normal file
28
app/Models/DailyQuest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DailyQuest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'required_amount',
|
||||
'reward_xp',
|
||||
'quest_type',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function userQuests()
|
||||
{
|
||||
return $this->hasMany(UserQuest::class, 'daily_quest_id');
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ class Division extends Model
|
||||
|
||||
protected $fillable = ['title', 'number', 'book_id', 'volum_id', 'law_id'];
|
||||
|
||||
|
||||
public function sections()
|
||||
{
|
||||
return $this->hasMany(Section::class);
|
||||
@@ -47,7 +46,6 @@ class Division extends Model
|
||||
return $this->hasMany(Art::class);
|
||||
}
|
||||
|
||||
|
||||
public function gates()
|
||||
{
|
||||
return $this->hasMany(Gate::class);
|
||||
@@ -67,5 +65,4 @@ class Division extends Model
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")->limit(3)->get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,11 +11,26 @@ class Folder extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name'
|
||||
'name',
|
||||
];
|
||||
|
||||
public function arts()
|
||||
{
|
||||
return $this->belongsToMany(Art::class, 'folder_art');
|
||||
return $this->belongsToMany(Art::class, 'folder_art')->withTimestamps();
|
||||
}
|
||||
|
||||
public function bylaws()
|
||||
{
|
||||
return $this->belongsToMany(BylawDocument::class, 'folder_bylaw', 'folder_id', 'bylaw_document_id')->withTimestamps();
|
||||
}
|
||||
|
||||
public function judicialPrecedents()
|
||||
{
|
||||
return $this->belongsToMany(JudicialPrecedent::class, 'folder_judicial_precedent', 'folder_id', 'judicial_precedent_id')->withTimestamps();
|
||||
}
|
||||
|
||||
public function legalOpinions()
|
||||
{
|
||||
return $this->belongsToMany(LegalOpinion::class, 'folder_legal_opinion', 'folder_id', 'legal_opinion_id')->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ class FolderArt extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'folder_id',
|
||||
'art_id'
|
||||
'art_id',
|
||||
];
|
||||
|
||||
public function arts()
|
||||
{
|
||||
return $this->belongsTo(Art::class,'art_id');
|
||||
return $this->belongsTo(Art::class, 'art_id');
|
||||
}
|
||||
}
|
||||
|
||||
13
app/Models/FolderBylaw.php
Normal file
13
app/Models/FolderBylaw.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FolderBylaw extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'folder_bylaw';
|
||||
protected $fillable = ['folder_id', 'bylaw_document_id'];
|
||||
}
|
||||
13
app/Models/FolderJudicialPrecedent.php
Normal file
13
app/Models/FolderJudicialPrecedent.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FolderJudicialPrecedent extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'folder_judicial_precedent';
|
||||
protected $fillable = ['folder_id', 'judicial_precedent_id'];
|
||||
}
|
||||
13
app/Models/FolderLegalOpinion.php
Normal file
13
app/Models/FolderLegalOpinion.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FolderLegalOpinion extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
protected $table = 'folder_legal_opinion';
|
||||
protected $fillable = ['folder_id', 'legal_opinion_id'];
|
||||
}
|
||||
@@ -9,7 +9,7 @@ class Gate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['title', 'number', 'part_id', 'chapter_id', 'section_id', 'book_id', 'volum_id', 'law_id','division_id'];
|
||||
protected $fillable = ['title', 'number', 'part_id', 'chapter_id', 'section_id', 'book_id', 'volum_id', 'law_id', 'division_id'];
|
||||
|
||||
protected $hidden = ['created_at', 'updated_at'];
|
||||
|
||||
@@ -18,7 +18,6 @@ class Gate extends Model
|
||||
return $this->hasMany(Branch::class);
|
||||
}
|
||||
|
||||
|
||||
public function art()
|
||||
{
|
||||
return $this->hasMany(Art::class);
|
||||
@@ -38,6 +37,4 @@ class Gate extends Model
|
||||
{
|
||||
return self::where('title', 'LIKE', "%{$searchTerm}%")->limit(3)->get();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
29
app/Models/GoldCurrency.php
Normal file
29
app/Models/GoldCurrency.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GoldCurrency extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'name_en',
|
||||
'price',
|
||||
'change_percent',
|
||||
'change_value',
|
||||
'symbol',
|
||||
'unit',
|
||||
'time_unix',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'integer',
|
||||
'change_percent' => 'float',
|
||||
'change_value' => 'integer',
|
||||
'time_unix' => 'integer',
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user