Compare commits

..

126 Commits

Author SHA1 Message Date
m.razaghi
2b5d218f42 Updated .env 2026-08-20 18:17:56 +03:30
m.razaghi
0771bb16f2 Updated .env 2026-08-20 17:49:45 +03:30
m.razaghi
85e1ca9d89 Updated .env 2026-08-20 17:48:14 +03:30
m.razaghi
db3f6d6553 Updated .env 2026-08-20 17:47:21 +03:30
m.razaghi
c73990b629 Updated .env 2026-08-20 17:42:52 +03:30
m.razaghi
6a204c69ac Revert "revert commit"
This reverts commit 641332e5ba.
2026-08-20 17:37:48 +03:30
m.razaghi
641332e5ba revert commit 2026-08-20 17:32:09 +03:30
m.razaghi
76d9246e0e Added quiz new features 2026-08-20 16:21:03 +03:30
m.razaghi
7cb3978775 Added quiz new features 2026-08-19 13:43:56 +03:30
m.razaghi
f31f0806ae Added quiz new features 2026-08-19 12:21:33 +03:30
m.razaghi
a6b2a1ba30 Added quiz new features 2026-08-19 12:18:35 +03:30
m.razaghi
f84ef8c067 Updated VolumController.php 2026-08-18 14:35:18 +03:30
m.razaghi
4c6eb52716 Updated VolumController.php 2026-08-18 14:04:54 +03:30
m.razaghi
b1c5345d22 Merge remote-tracking branch 'upstream-local/clean-main' into clean-main 2026-08-18 13:39:19 +03:30
m.razaghi
8b853a6f8f Updated VolumController.php 2026-08-18 13:39:05 +03:30
m.razaghi
16fd292976 fix bugs 2026-08-16 21:26:26 +03:30
m.razaghi
464ed71360 Fixed SubscribePlanController.php 2026-08-16 17:33:19 +03:30
m.razaghi
27b365c7e2 Updated user management 2026-08-16 17:15:47 +03:30
m.razaghi
452bd77f84 Updated user management 2026-08-16 16:52:42 +03:30
m.razaghi
f358c151af Updated HomeController.php 2026-08-16 15:17:57 +03:30
m.razaghi
94aab63db6 Updated HomeController.php 2026-08-16 15:11:11 +03:30
m.razaghi
5d392ff50b Added gold currency api 2026-08-15 23:47:21 +03:30
m.razaghi
4e5ef028dc Added gold currency api 2026-08-15 23:43:51 +03:30
m.razaghi
500ef019a3 Added gold currency api 2026-08-15 21:15:32 +03:30
m.razaghi
def51f6c63 fix free subscribe issue 2026-08-14 21:28:10 +03:30
m.razaghi
88813a4f86 fix free subscribe issue 2026-08-14 21:26:39 +03:30
m.razaghi
b041736a22 fix free subscribe issue 2026-08-14 21:15:46 +03:30
m.razaghi
c3f53f08f2 fix free subscribe issue 2026-08-14 21:12:19 +03:30
m.razaghi
ab0ab84a49 fix free subscribe issue 2026-08-14 21:10:12 +03:30
m.razaghi
dda5a87980 fix free subscribe issue 2026-08-14 21:06:13 +03:30
m.razaghi
fcaa51626d fix free subscribe issue 2026-08-14 21:05:21 +03:30
m.razaghi
d7f6131a33 fix free subscribe issue 2026-08-14 20:28:09 +03:30
m.razaghi
52a13dcf6e fix free subscribe issue 2026-08-14 20:22:53 +03:30
m.razaghi
ec76bc9737 fix free subscribe issue 2026-08-14 20:11:20 +03:30
m.razaghi
255889b202 fix free subscribe issue 2026-08-14 20:09:09 +03:30
m.razaghi
f801ca9eed Updated dashboard 2026-08-09 17:08:30 +03:30
m.razaghi
1be9ed98b8 Updated dashboard 2026-08-09 17:02:19 +03:30
m.razaghi
6c157e79e4 Updated dashboard 2026-08-09 16:59:21 +03:30
m.razaghi
47dfbba165 Updated dashboard 2026-08-09 16:53:42 +03:30
m.razaghi
bd4789a85c Merge remote-tracking branch 'upstream-local/clean-main' into clean-main 2026-08-08 17:44:14 +03:30
m.razaghi
01d6584e71 Added quiz features 2026-08-08 17:44:09 +03:30
m.razaghi
2d6766760f Fixed Quiz 2026-08-07 21:36:48 +03:30
m.razaghi
bb69e7f953 Fixed quiz 2026-08-07 21:30:03 +03:30
m.razaghi
d705c7c56d Fixed Quiz 2026-08-07 20:01:46 +03:30
m.razaghi
768a0aaeb0 Added quiz features 2026-08-06 14:31:46 +03:30
m.razaghi
0fffd2eb3d Added quiz features 2026-08-06 14:25:54 +03:30
m.razaghi
041a3e7bd5 Added quiz features 2026-08-06 14:20:38 +03:30
m.razaghi
af21c1c77e Updated dashboard 2026-08-05 13:56:46 +03:30
m.razaghi
5aa2447e23 Updated dashboard 2026-08-03 18:00:05 +03:30
m.razaghi
74ead9d0ed Updated dashboard 2026-08-03 17:32:04 +03:30
m.razaghi
aad799d29f Updated dashboard 2026-08-03 17:26:15 +03:30
m.razaghi
6e68ed45e4 Added ticketing feature 2026-07-31 22:20:40 +03:30
m.razaghi
067c1d2351 Added ticketing feature 2026-07-31 19:17:40 +03:30
m.razaghi
0ae3343b17 Added ticketing feature 2026-07-31 19:07:54 +03:30
m.razaghi
516bc4b26b Added ticketing feature 2026-07-31 18:54:09 +03:30
m.razaghi
d77605db75 fixed bugs 2026-07-24 18:44:02 +03:30
m.razaghi
a2799a8d1b - Added new apis 2026-07-22 10:29:01 +03:30
m.razaghi
86c68951bb Fixed api issue 2026-07-16 17:56:59 +03:30
m.razaghi
cf0f8f0c3f Fixed api issue 2026-07-16 17:51:10 +03:30
m.razaghi
209b1f2530 Fixed api issue 2026-07-16 14:58:11 +03:30
m.razaghi
a6ae5724f6 added foldering apis 2026-07-14 00:24:30 +03:30
m.razaghi
a173f17cae updated apis 2026-07-14 00:10:53 +03:30
m.razaghi
39e85df27a Fixed api issue 2026-07-13 09:12:26 +03:30
m.razaghi
2d6ba2a70d Fixed api issue 2026-07-13 09:03:47 +03:30
m.razaghi
abdfe190a7 Fixed api issue 2026-07-12 18:34:10 +03:30
m.razaghi
032ae6ebf8 Fixed api issue 2026-07-12 18:28:57 +03:30
m.razaghi
2474f268d1 Fixed api issue 2026-07-12 18:25:31 +03:30
m.razaghi
7f744beec4 Fixed api issue 2026-07-12 18:21:02 +03:30
m.razaghi
7a987a33c9 Fixed api issue 2026-07-12 18:18:02 +03:30
m.razaghi
7ca903ec3c Fixed api issue 2026-07-12 18:16:02 +03:30
m.razaghi
56e57719eb Fixed api issue 2026-07-12 18:09:33 +03:30
m.razaghi
680cf9c4fd Fixed api issue 2026-07-12 17:50:51 +03:30
m.razaghi
375e3b166c Fixed api issue 2026-07-12 17:42:34 +03:30
m.razaghi
2fab528d0a Fixed api issue 2026-07-12 17:38:35 +03:30
m.razaghi
677e101181 Fixed api issue 2026-07-12 17:33:31 +03:30
m.razaghi
e041b85e74 Merge remote-tracking branch 'upstream-local/clean-main' into clean-main 2026-07-12 17:24:03 +03:30
m.razaghi
aad67585ef Fixed api issue 2026-07-12 17:23:53 +03:30
4f94af7fc4 Merge pull request 'Fixed api issue' (#14) from new-api into clean-main
Reviewed-on: #14
2026-07-12 13:23:51 +00:00
m.razaghi
dcea6423ff Fixed api issue 2026-07-12 16:52:28 +03:30
ee8d9cdb8f Merge pull request 'Fixed api issue' (#13) from new-api into clean-main
Reviewed-on: #13
2026-07-12 13:19:50 +00:00
m.razaghi
51c4570f97 Fixed api issue 2026-07-12 16:49:12 +03:30
6062d0b3a0 Merge pull request 'Fixed api issue' (#12) from new-api into clean-main
Reviewed-on: #12
2026-07-12 13:13:17 +00:00
m.razaghi
19db0fd24d Fixed api issue 2026-07-12 16:40:27 +03:30
m.razaghi
4d15014621 Fixed api issue 2026-07-12 16:24:16 +03:30
m.razaghi
6c0b1f83f2 Fixed api issue 2026-07-12 16:19:20 +03:30
m.razaghi
cad3fa9e1f Fixed api issue 2026-07-12 16:11:38 +03:30
m.razaghi
1a3f41ca8a Merge remote-tracking branch 'upstream-local/new-api' into clean-main 2026-07-12 16:05:05 +03:30
m.razaghi
e4dad6f9a4 Fixed api issue 2026-07-12 16:02:53 +03:30
16e68c3048 Merge pull request 'Fixed api issue' (#11) from new-api into clean-main
Reviewed-on: #11
2026-07-12 12:27:18 +00:00
m.razaghi
10463e2f71 Fixed api issue 2026-07-12 15:46:34 +03:30
ed95f5b3ee Merge pull request 'Fixed api issue' (#10) from new-api into clean-main
Reviewed-on: #10
2026-07-12 11:56:37 +00:00
m.razaghi
1eedb30acc Fixed api issue 2026-07-12 15:21:01 +03:30
0962801c96 Merge pull request 'Fixed api issue' (#9) from new-api into clean-main
Reviewed-on: #9
2026-07-12 10:42:17 +00:00
m.razaghi
b2ac3d6ec8 Fixed api issue 2026-07-12 14:03:45 +03:30
ecccab9edc Merge pull request 'new-api' (#8) from new-api into clean-main
Reviewed-on: #8
2026-07-12 09:50:32 +00:00
m.razaghi
737f06f68e Fixed api issue 2026-07-12 13:16:57 +03:30
370f4c0a78 Merge pull request 'Fixed api issue' (#7) from new-api into clean-main
Reviewed-on: #7
2026-07-12 09:13:47 +00:00
m.razaghi
a95826e5e5 Fixed api issue 2026-07-12 12:43:28 +03:30
m.razaghi
398fcb4eec Fixed api issue 2026-07-12 11:58:07 +03:30
4214a4b007 Merge pull request 'new-api' (#6) from new-api into clean-main
Reviewed-on: #6
2026-07-12 06:07:23 +00:00
m.razaghi
9d124a07c1 Fixed api issue 2026-07-11 17:27:55 +03:30
m.razaghi
ffea0e3c11 Fixed api issue 2026-07-11 12:25:46 +03:30
2a6313802b Merge pull request 'updated apis' (#5) from new-api into clean-main
Reviewed-on: #5
2026-07-10 20:56:19 +00:00
m.razaghi
3b9fcd76df updated apis 2026-07-10 23:22:56 +03:30
d47f148f12 Merge pull request 'new-api' (#4) from new-api into clean-main
Reviewed-on: #4
2026-07-10 10:58:40 +00:00
m.razaghi
d19d4b8667 updated apis 2026-07-10 13:38:47 +03:30
m.razaghi
3b47d78ee8 Added new apis, fast search 2026-07-09 19:23:06 +03:30
m.razaghi
71a6f683a7 Fixed major bugs 2026-07-09 19:11:39 +03:30
m.razaghi
71dc7da8a4 Fixed major bugs 2026-07-09 17:06:07 +03:30
4ed77dd060 Merge pull request 'Fixed major bugs' (#3) from new-api into clean-main
Reviewed-on: #3
2026-07-08 07:24:33 +00:00
m.razaghi
b7f139b935 Fixed major bugs 2026-07-08 10:51:47 +03:30
0b6851ebe2 Update README.md 2026-07-08 05:44:27 +00:00
80a271d325 Merge pull request 'fixed major bugs' (#2) from new-api into clean-main
Reviewed-on: #2
2026-07-08 05:44:00 +00:00
m.razaghi
83ff22b5a4 fixed major bugs 2026-07-07 21:25:27 +03:30
d27c32c918 Merge pull request 'Added new api' (#1) from new-api into clean-main
Reviewed-on: #1
2026-07-05 10:22:22 +00:00
m.razaghi
ce5ccc00a1 Added new api 2026-07-04 17:05:50 +03:30
f242b8936a feat(BylawDocument): enhance bylaw document forms with improved validation and file upload support 2026-06-26 15:59:45 +03:30
9cf6957675 feat: add bylaw management features including documents, legal authorities, subject categories, and nodes 2026-06-26 13:49:50 +03:30
2e26bd229b refactor(testing): remove dd 2026-05-17 00:47:59 +03:30
e695a80e36 refactor(timeout): set timeout 2026-05-17 00:47:13 +03:30
ece9e6e017 refactor(Core): sent cache 2026-05-17 00:45:58 +03:30
c3d798689c fix(Core): fix bug is lock 2026-05-17 00:00:13 +03:30
2796ce7de7 feat(myket): add myket 2026-05-16 23:44:08 +03:30
c05587474f fix(syntax): fix syntax bug 2026-05-14 19:07:28 +03:30
8fda92ead5 Merge branch 'clean-main' of https://git.geekgrove.ir/soheilkhaledabadi/law-api into clean-main 2026-05-08 18:17:54 +03:30
ae522c5640 feat: add legal opinions admin management 2026-05-08 18:15:21 +03:30
219 changed files with 9723 additions and 2403 deletions

31
.env Normal file
View 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
View File

@@ -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

View File

@@ -1 +1 @@
Persian ping
## Law

View File

@@ -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
View 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';
}
}

View 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';
}
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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'));
}
}
}

View File

@@ -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'));
}
}

View 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'));
}
}

View 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'));
}
}

View 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'));
}
}

View 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'));
}
}

View File

@@ -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'));
}
}

View File

@@ -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'));
}
}

View File

@@ -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);

View 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', 'چالش روزانه حذف شد.');
}
}

View 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'
));
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'));
}
}

View File

@@ -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'));
}
}

View File

@@ -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;
}

View File

@@ -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'));
}
}

View 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'));
}
}

View File

@@ -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');
}
}

View File

@@ -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);

View 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', 'دسته‌بندی با موفقیت حذف شد.');
}
}

View 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', 'سوال حذف شد.');
}
}

View 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', 'درس با موفقیت حذف شد.');
}
}

View File

@@ -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);

View File

@@ -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');
}
}

View 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', 'تیکت با موفقیت بسته شد.');
}
}

View File

@@ -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)

View File

@@ -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'));
}
}

View File

@@ -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);

View File

@@ -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, 'موفقیت', 'پیشنهاد شما با موفقیت برای مدیر ارسال شد');
}
}

View File

@@ -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,
];
}

View 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([], 'حذف', 'هایلایت با موفقیت حذف شد');
}
}

View File

@@ -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);

View File

@@ -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']);

View 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,
];
}
}

View 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([], 'حذف', 'هایلایت با موفقیت حذف شد');
}
}

View 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([], 'حذف', 'یادداشت با موفقیت حذف شد');
}
}

View File

@@ -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());
}
}

View File

@@ -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']);

View 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());
}
}
}

View File

@@ -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(['&nbsp;', '&zwnj;', "\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,
];
}
}

View File

@@ -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());
}
}

View 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 [];
}
}

View File

@@ -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));
}
}

View 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,
];
}
}

View File

@@ -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([], 'حذف', 'هایلایت با موفقیت حذف شد');
}
}

View 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([], 'حذف', 'یادداشت با موفقیت حذف شد');
}
}

View File

@@ -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');
}
}

View 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 'برنز';
}
}

View 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,
];
}
}

View 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([], 'حذف', 'هایلایت با موفقیت حذف شد');
}
}

View 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([], 'حذف', 'یادداشت با موفقیت حذف شد');
}
}

View File

@@ -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([], 'حذف شد');
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View 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)
];
})
], 'موفق', 'نتایج نهایی مسابقه دریافت شد.');
}
}

View 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, 'موفق', 'نتیجه آزمون با موفقیت دریافت شد');
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View 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([], 'موفق', 'پاسخ شما با موفقیت ثبت شد');
}
}

View File

@@ -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');
}
}

View File

@@ -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':

View File

@@ -17,7 +17,7 @@ class GetApiRequest extends FormRequest
public function rules(): array
{
return [
'book_id' => 'required'
'book_id' => 'required',
];
}
}

View File

@@ -25,7 +25,7 @@ class SuggestionRequest extends FormRequest
public function rules(): array
{
return [
'text' => 'required'
'text' => 'required',
];
}
}

View File

@@ -18,7 +18,7 @@ class CreateVersionStore extends FormRequest
'number' => 'required',
'log' => 'required',
'force_update' => 'nullable',
'type' => 'required'
'type' => 'required',
];
}
}

View File

@@ -28,7 +28,7 @@ class CreateRequest extends FormRequest
'is_active' => 'nullable',
'is_free' => 'nullable',
'type' => 'nullable',
'transaction' => 'nullable'
'transaction' => 'nullable',
];
}
}

View File

@@ -28,7 +28,7 @@ class UpdateRequest extends FormRequest
'is_active' => 'nullable',
'is_free' => 'nullable',
'type' => 'nullable',
'transaction' => 'nullable'
'transaction' => 'nullable',
];
}
}

View File

@@ -17,8 +17,8 @@ class UpdateVersionStore extends FormRequest
'code' => 'required',
'number' => 'required',
'log' => 'required',
'force_update' => 'nullable',
'type' => 'required'
'force_update' => 'nullable',
'type' => 'required',
];
}
}

View File

@@ -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()]);
}
}
}
}

View File

@@ -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}%")

View 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'];
}

View File

@@ -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();

View File

@@ -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();

View 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'];
}

View 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');
}
}

View 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'];
}

View 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
View 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');
}
}

View 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
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BylawNote extends Model
{
protected $table = 'bylaw_notes';
protected $guarded = [];
}

View 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');
}
}

View File

@@ -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' => 'کیفری',
];
}
}

View File

@@ -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
View 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');
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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');
}
}

View 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'];
}

View 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'];
}

View 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'];
}

View File

@@ -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();
}
}

View 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