Compare commits
67 Commits
new-api
...
b1c5345d22
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1c5345d22 | ||
|
|
8b853a6f8f | ||
|
|
16fd292976 | ||
|
|
464ed71360 | ||
|
|
27b365c7e2 | ||
|
|
452bd77f84 | ||
|
|
f358c151af | ||
|
|
94aab63db6 | ||
|
|
5d392ff50b | ||
|
|
4e5ef028dc | ||
|
|
500ef019a3 | ||
|
|
def51f6c63 | ||
|
|
88813a4f86 | ||
|
|
b041736a22 | ||
|
|
c3f53f08f2 | ||
|
|
ab0ab84a49 | ||
|
|
dda5a87980 | ||
|
|
fcaa51626d | ||
|
|
d7f6131a33 | ||
|
|
52a13dcf6e | ||
|
|
ec76bc9737 | ||
|
|
255889b202 | ||
|
|
f801ca9eed | ||
|
|
1be9ed98b8 | ||
|
|
6c157e79e4 | ||
|
|
47dfbba165 | ||
|
|
bd4789a85c | ||
|
|
01d6584e71 | ||
|
|
2d6766760f | ||
|
|
bb69e7f953 | ||
|
|
d705c7c56d | ||
|
|
768a0aaeb0 | ||
|
|
0fffd2eb3d | ||
|
|
041a3e7bd5 | ||
|
|
af21c1c77e | ||
|
|
5aa2447e23 | ||
|
|
74ead9d0ed | ||
|
|
aad799d29f | ||
|
|
6e68ed45e4 | ||
|
|
067c1d2351 | ||
|
|
0ae3343b17 | ||
|
|
516bc4b26b | ||
|
|
d77605db75 | ||
|
|
a2799a8d1b | ||
|
|
86c68951bb | ||
|
|
cf0f8f0c3f | ||
|
|
209b1f2530 | ||
|
|
a6ae5724f6 | ||
|
|
a173f17cae | ||
|
|
39e85df27a | ||
|
|
2d6ba2a70d | ||
|
|
abdfe190a7 | ||
|
|
032ae6ebf8 | ||
|
|
2474f268d1 | ||
|
|
7f744beec4 | ||
|
|
7a987a33c9 | ||
|
|
7ca903ec3c | ||
|
|
56e57719eb | ||
|
|
680cf9c4fd | ||
|
|
375e3b166c | ||
|
|
2fab528d0a | ||
|
|
677e101181 | ||
|
|
e041b85e74 | ||
|
|
aad67585ef | ||
| 4f94af7fc4 | |||
| ee8d9cdb8f | |||
| 6062d0b3a0 |
108
app/Http/Controllers/Admin/DashboardController.php
Normal file
108
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Suggestion;
|
||||||
|
use App\Models\Ticket;
|
||||||
|
use App\Models\UserSubscriber;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// 1. Calculate General Statistics
|
||||||
|
$totalUsers = User::count();
|
||||||
|
|
||||||
|
// 2. Safely Count Free Subscribers using the relationship
|
||||||
|
$freeSubscribers = UserSubscriber::where('expired_at', '>=', now())
|
||||||
|
->whereHas('subscribe', function (Builder $query) {
|
||||||
|
$query->where('is_free', true);
|
||||||
|
})
|
||||||
|
->distinct('user_id')
|
||||||
|
->count('user_id');
|
||||||
|
|
||||||
|
// 3. Get Paid Subscribers & Breakdown by Plan Type AND Market Provider
|
||||||
|
$rawBreakdown = UserSubscriber::where('user_subscribers.expired_at', '>=', now())
|
||||||
|
->join('subscribe_plans', 'user_subscribers.subscribe_plan_id', '=', 'subscribe_plans.id')
|
||||||
|
->where('subscribe_plans.is_free', false) // Replaced redundant whereHas for better performance
|
||||||
|
->selectRaw('subscribe_plans.name as plan_name, user_subscribers.market_provider, count(DISTINCT user_subscribers.user_id) as total')
|
||||||
|
->groupBy('subscribe_plans.name', 'user_subscribers.market_provider')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 4. Safely group the results in PHP to fix split numbers (merging null, '', and 'web')
|
||||||
|
$paidBreakdown = $rawBreakdown->map(function ($item) {
|
||||||
|
$provider = strtolower(trim($item->market_provider));
|
||||||
|
|
||||||
|
// Normalize the provider names
|
||||||
|
if (empty($provider) || $provider === 'web') {
|
||||||
|
$providerName = 'سایت';
|
||||||
|
} elseif ($provider === 'bazar') {
|
||||||
|
$providerName = 'بازار';
|
||||||
|
} elseif ($provider === 'myket') {
|
||||||
|
$providerName = 'مایکت';
|
||||||
|
} else {
|
||||||
|
$providerName = ucfirst($provider); // Dynamic fallback for any future gateways
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'display_name' => $item->plan_name . ' (' . $providerName . ')',
|
||||||
|
'total' => $item->total
|
||||||
|
];
|
||||||
|
})->groupBy('display_name')->map(function ($group, $key) {
|
||||||
|
// Merge any split rows (e.g. 'web' + null) into one accurate total sum
|
||||||
|
return (object) [
|
||||||
|
'display_name' => $key,
|
||||||
|
'total' => $group->sum('total')
|
||||||
|
];
|
||||||
|
})->values();
|
||||||
|
|
||||||
|
// Total paid is the exact sum of the merged breakdown
|
||||||
|
$paidSubscribers = $paidBreakdown->sum('total');
|
||||||
|
|
||||||
|
// 4. Fetch Recent Data (Limit to 5)
|
||||||
|
$recentSuggestions = Suggestion::with('user')->latest()->take(5)->get();
|
||||||
|
|
||||||
|
// Order tickets by updated_at so newly replied tickets jump to the top
|
||||||
|
$recentTickets = Ticket::with('user')->latest('updated_at')->take(5)->get();
|
||||||
|
|
||||||
|
// 5. Prepare Chart Data (Strictly the last 7 calendar days)
|
||||||
|
$chartDates = [];
|
||||||
|
$newUsersData = [];
|
||||||
|
$paidSubsData = [];
|
||||||
|
$freeSubsData = [];
|
||||||
|
|
||||||
|
for ($i = 6; $i >= 0; $i--) {
|
||||||
|
$date = now()->subDays($i);
|
||||||
|
$dbDate = $date->format('Y-m-d');
|
||||||
|
|
||||||
|
// Format X-axis to Jalali for the chart labels
|
||||||
|
$chartDates[] = \Morilog\Jalali\Jalalian::fromCarbon($date)->format('Y/m/d');
|
||||||
|
|
||||||
|
// Count records for this specific exact date
|
||||||
|
$newUsersData[] = User::whereDate('created_at', $dbDate)->count();
|
||||||
|
|
||||||
|
$paidSubsData[] = UserSubscriber::whereDate('created_at', $dbDate)
|
||||||
|
->whereHas('subscribe', fn($q) => $q->where('is_free', false))->count();
|
||||||
|
|
||||||
|
$freeSubsData[] = UserSubscriber::whereDate('created_at', $dbDate)
|
||||||
|
->whereHas('subscribe', fn($q) => $q->where('is_free', true))->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('admin.dashboard.index', compact(
|
||||||
|
'totalUsers',
|
||||||
|
'paidSubscribers',
|
||||||
|
'freeSubscribers',
|
||||||
|
'paidBreakdown',
|
||||||
|
'recentSuggestions',
|
||||||
|
'recentTickets',
|
||||||
|
'chartDates',
|
||||||
|
'newUsersData',
|
||||||
|
'paidSubsData',
|
||||||
|
'freeSubsData'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Http/Controllers/Admin/QuizCategoryController.php
Normal file
47
app/Http/Controllers/Admin/QuizCategoryController.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\QuizCategory;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class QuizCategoryController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Load categories and count how many quizzes are in each
|
||||||
|
$categories = QuizCategory::withCount('quizzes')->latest()->get();
|
||||||
|
return view('admin.quiz-categories.index', compact('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
QuizCategory::create([
|
||||||
|
'title' => $request->title,
|
||||||
|
// Automatically generate a unique slug for URLs
|
||||||
|
'slug' => Str::slug($request->title) . '-' . time(),
|
||||||
|
'description' => $request->description,
|
||||||
|
'is_active' => $request->has('is_active'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'دستهبندی با موفقیت ایجاد شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(QuizCategory $quiz_category)
|
||||||
|
{
|
||||||
|
// Check if the category has quizzes before deleting
|
||||||
|
if ($quiz_category->quizzes()->count() > 0) {
|
||||||
|
return back()->with('error', 'این دستهبندی شامل آزمون است و نمیتواند حذف شود. ابتدا آزمونها را حذف کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$quiz_category->delete();
|
||||||
|
return back()->with('success', 'دستهبندی با موفقیت حذف شد.');
|
||||||
|
}
|
||||||
|
}
|
||||||
109
app/Http/Controllers/Admin/QuizController.php
Normal file
109
app/Http/Controllers/Admin/QuizController.php
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Quiz;
|
||||||
|
use App\Models\QuizCategory;
|
||||||
|
use App\Models\QuizSubject;
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Models\QuestionOption;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class QuizController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$quizzes = Quiz::with('category')->withCount('questions')->latest()->paginate(15);
|
||||||
|
return view('admin.quizzes.index', compact('quizzes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$categories = QuizCategory::where('is_active', true)->get();
|
||||||
|
return view('admin.quizzes.create', compact('categories'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
// 1. Validate only the text and number fields
|
||||||
|
$validated = $request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'category_id' => 'required|exists:quiz_categories,id',
|
||||||
|
'year' => 'nullable|integer',
|
||||||
|
'type' => 'required|in:multiple_choice,descriptive,mixed',
|
||||||
|
'duration_minutes' => 'required|integer|min:1',
|
||||||
|
'description' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 2. Safely handle checkboxes and generate slug
|
||||||
|
$validated['slug'] = \Illuminate\Support\Str::slug($validated['title']) . '-' . time();
|
||||||
|
$validated['has_negative_score'] = $request->has('has_negative_score');
|
||||||
|
$validated['is_free'] = $request->has('is_free');
|
||||||
|
$validated['is_published'] = $request->has('is_published');
|
||||||
|
|
||||||
|
// 3. Create the quiz
|
||||||
|
$quiz = Quiz::create($validated);
|
||||||
|
|
||||||
|
return redirect()->route('quizzes.questions.index', $quiz->id)
|
||||||
|
->with('success', 'آزمون با موفقیت ایجاد شد. اکنون میتوانید سوالات را اضافه کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Quiz $quiz)
|
||||||
|
{
|
||||||
|
$quiz->delete();
|
||||||
|
return redirect()->route('quizzes.index')->with('success', 'آزمون حذف شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Questions Management ---
|
||||||
|
|
||||||
|
public function questions(Quiz $quiz)
|
||||||
|
{
|
||||||
|
$quiz->load(['questions.options', 'questions.subject']);
|
||||||
|
$subjects = QuizSubject::all();
|
||||||
|
return view('admin.quizzes.questions', compact('quiz', 'subjects'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeQuestion(Request $request, Quiz $quiz)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'question_text' => 'required|string',
|
||||||
|
'type' => 'required|in:multiple_choice,descriptive',
|
||||||
|
'subject_id' => 'nullable|exists:quiz_subjects,id',
|
||||||
|
'score' => 'required|numeric|min:0.5',
|
||||||
|
'solution_explanation' => 'nullable|string',
|
||||||
|
// Options validation for multiple-choice questions
|
||||||
|
'options' => 'required_if:type,multiple_choice|array|min:4|max:4',
|
||||||
|
'options.*' => 'required_if:type,multiple_choice|string',
|
||||||
|
'correct_option' => 'required_if:type,multiple_choice|integer|between:0,3',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$question = $quiz->questions()->create([
|
||||||
|
'subject_id' => $request->subject_id,
|
||||||
|
'question_text' => $request->question_text,
|
||||||
|
'type' => $request->type,
|
||||||
|
'score' => $request->score,
|
||||||
|
'solution_explanation' => $request->solution_explanation,
|
||||||
|
'order' => $quiz->questions()->count() + 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Save options if question is multiple choice
|
||||||
|
if ($request->type === 'multiple_choice' && $request->has('options')) {
|
||||||
|
foreach ($request->options as $index => $optionText) {
|
||||||
|
$question->options()->create([
|
||||||
|
'option_text' => $optionText,
|
||||||
|
'is_correct' => ($index == $request->correct_option),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'سوال با موفقیت ثبت شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyQuestion(Question $question)
|
||||||
|
{
|
||||||
|
$question->delete();
|
||||||
|
return back()->with('success', 'سوال حذف شد.');
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Http/Controllers/Admin/QuizSubjectController.php
Normal file
44
app/Http/Controllers/Admin/QuizSubjectController.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\QuizSubject;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class QuizSubjectController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// Load subjects and count how many questions belong to each
|
||||||
|
$subjects = QuizSubject::withCount('questions')->latest()->get();
|
||||||
|
return view('admin.quiz-subjects.index', compact('subjects'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
QuizSubject::create([
|
||||||
|
'title' => $request->title,
|
||||||
|
// Generate a unique slug for potential API/URL usage
|
||||||
|
'slug' => Str::slug($request->title) . '-' . time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', 'درس با موفقیت اضافه شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(QuizSubject $quiz_subject)
|
||||||
|
{
|
||||||
|
// Prevent deletion if questions are already assigned to this subject
|
||||||
|
if ($quiz_subject->questions()->count() > 0) {
|
||||||
|
return back()->with('error', 'این درس دارای سوال است و نمیتواند حذف شود. ابتدا سوالات مربوطه را ویرایش یا حذف کنید.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$quiz_subject->delete();
|
||||||
|
return back()->with('success', 'درس با موفقیت حذف شد.');
|
||||||
|
}
|
||||||
|
}
|
||||||
78
app/Http/Controllers/Admin/TicketController.php
Normal file
78
app/Http/Controllers/Admin/TicketController.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Ticket;
|
||||||
|
use App\Models\TicketMessage;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TicketController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
// Eager load the user who created the ticket
|
||||||
|
$query = Ticket::query()->with('user');
|
||||||
|
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$q = $request->q;
|
||||||
|
$query->where(function ($qry) use ($q) {
|
||||||
|
$qry->where('subject', 'like', "%{$q}%")
|
||||||
|
->orWhereHas('user', fn ($u) => $u->where('mobile', 'like', "%{$q}%")
|
||||||
|
->orWhere('name', 'like', "%{$q}%"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Filter by status (open, answered, user_reply, closed)
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||||
|
// Order by updated_at so tickets with new replies jump to the top
|
||||||
|
$tickets = $query->latest('updated_at')->paginate($perPage)->withQueryString();
|
||||||
|
|
||||||
|
return view('admin.tickets.index', compact('tickets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Ticket $ticket)
|
||||||
|
{
|
||||||
|
// Load messages and the user belonging to the ticket
|
||||||
|
$ticket->load(['messages.user', 'user']);
|
||||||
|
|
||||||
|
return view('admin.tickets.show', compact('ticket'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reply(Request $request, Ticket $ticket)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'message' => 'required|string',
|
||||||
|
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$attachmentPath = null;
|
||||||
|
if ($request->hasFile('attachment')) {
|
||||||
|
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketMessage::create([
|
||||||
|
'ticket_id' => $ticket->id,
|
||||||
|
'user_id' => auth()->id(), // This will be the Admin's ID
|
||||||
|
'message' => $request->message,
|
||||||
|
'attachment_path' => $attachmentPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket->update(['status' => 'answered']);
|
||||||
|
|
||||||
|
return redirect()->route('tickets.show', $ticket->id)
|
||||||
|
->with('success', 'پاسخ شما با موفقیت ارسال شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(Ticket $ticket)
|
||||||
|
{
|
||||||
|
$ticket->update(['status' => 'closed']);
|
||||||
|
|
||||||
|
return redirect()->route('tickets.index')
|
||||||
|
->with('success', 'تیکت با موفقیت بسته شد.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,58 @@ class UsersController extends Controller
|
|||||||
return view('admin.users.create');
|
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)
|
public function store(CreateUserRequest $request)
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
@@ -53,7 +105,30 @@ class UsersController extends Controller
|
|||||||
|
|
||||||
public function edit(User $user)
|
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)
|
public function update(UpdateUserRequest $request, User $user)
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ class ArtController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$art->is_like = $this->isLiked($art->id);
|
$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->category = $law?->category?->name;
|
||||||
$art->route = array_values($this->route(Art::class, $art));
|
$art->route = array_values($this->route(Art::class, $art));
|
||||||
|
|
||||||
@@ -109,7 +110,8 @@ class ArtController extends Controller
|
|||||||
if ($previousArt) {
|
if ($previousArt) {
|
||||||
$previousLaw = Law::find($previousArt->law_id);
|
$previousLaw = Law::find($previousArt->law_id);
|
||||||
$previousArt->is_like = $this->isLiked($previousArt->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->category = $previousLaw?->category?->name;
|
||||||
$previousArt->route = array_values($this->route(Art::class, $previousArt));
|
$previousArt->route = array_values($this->route(Art::class, $previousArt));
|
||||||
}
|
}
|
||||||
@@ -123,7 +125,8 @@ class ArtController extends Controller
|
|||||||
if ($nextArt) {
|
if ($nextArt) {
|
||||||
$nextLaw = Law::find($nextArt->law_id);
|
$nextLaw = Law::find($nextArt->law_id);
|
||||||
$nextArt->is_like = $this->isLiked($nextArt->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->category = $nextLaw?->category?->name;
|
||||||
$nextArt->route = array_values($this->route(Art::class, $nextArt));
|
$nextArt->route = array_values($this->route(Art::class, $nextArt));
|
||||||
}
|
}
|
||||||
|
|||||||
58
app/Http/Controllers/api/ArtHighlightController.php
Normal file
58
app/Http/Controllers/api/ArtHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\ArtHighlight;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ArtHighlightController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'art_id' => 'required',
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ArtHighlight::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'art_id' => $validated['art_id'],
|
||||||
|
'start_index' => $validated['start_index'],
|
||||||
|
'end_index' => $validated['end_index'],
|
||||||
|
'selected_text' => $validated['selected_text'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$highlight = ArtHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->update($validated);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$highlight = ArtHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use App\Http\Requests\AuthRequest;
|
use App\Http\Requests\AuthRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserCode;
|
use App\Models\UserCode;
|
||||||
|
use App\Models\SubscribePlan;
|
||||||
use App\Traits\BaseApiResponse;
|
use App\Traits\BaseApiResponse;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -91,6 +92,29 @@ class AuthController extends Controller
|
|||||||
if ($latestCode->code === $request->code) {
|
if ($latestCode->code === $request->code) {
|
||||||
$token = $user->createToken('mobile')->plainTextToken;
|
$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], 'ورود موفقیت امیز بود');
|
return $this->success(['token' => $token], 'ورود موفقیت امیز بود');
|
||||||
} else {
|
} else {
|
||||||
return $this->failed([], 'کد ورود اشتباه است', 404);
|
return $this->failed([], 'کد ورود اشتباه است', 404);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use App\Models\BylawNode;
|
|||||||
use App\Models\BylawCodificationLink;
|
use App\Models\BylawCodificationLink;
|
||||||
use App\Traits\BaseApiResponse;
|
use App\Traits\BaseApiResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class BylawApiController extends Controller
|
class BylawApiController extends Controller
|
||||||
{
|
{
|
||||||
@@ -80,8 +79,19 @@ class BylawApiController extends Controller
|
|||||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||||
$paginator = $query->paginate($perPage);
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
$items = collect($paginator->items())->map(function ($item) {
|
$docIds = collect($paginator->items())->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||||
return $this->formatBylaw($item);
|
$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 = [
|
$responseData = [
|
||||||
@@ -110,27 +120,37 @@ class BylawApiController extends Controller
|
|||||||
|
|
||||||
$formatted = $this->formatBylaw($bylaw);
|
$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']) {
|
if (!$formatted['is_locked']) {
|
||||||
$nodes = BylawNode::with('type')
|
$eloquentNodes = BylawNode::with('type')
|
||||||
->where('document_id', $bylaw->id)
|
->where('document_id', $bylaw->id)
|
||||||
->orderBy('sort_order', 'asc')
|
->orderBy('sort_order', 'asc')
|
||||||
->get()
|
->get();
|
||||||
->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,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$formatted['nodes'] = $nodes->toArray();
|
$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')
|
$links = BylawCodificationLink::where('source_entity_type', 'bylaw_doc')
|
||||||
->where('source_entity_id', $bylaw->id)
|
->where('source_entity_id', $bylaw->id)
|
||||||
@@ -175,7 +195,8 @@ class BylawApiController extends Controller
|
|||||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||||
$subQuery->select('document_id')
|
$subQuery->select('document_id')
|
||||||
->from('bylaw_nodes')
|
->from('bylaw_nodes')
|
||||||
->where('node_content', 'LIKE', "%{$searchTerm}%");
|
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,8 +214,19 @@ class BylawApiController extends Controller
|
|||||||
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
$perPage = min(max((int) $request->input('per_page', 15), 10), 100);
|
||||||
$paginator = $query->paginate($perPage);
|
$paginator = $query->paginate($perPage);
|
||||||
|
|
||||||
$items = collect($paginator->items())->map(function ($item) {
|
$docIds = collect($paginator->items())->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||||
return $this->formatBylaw($item);
|
$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 = [
|
$responseData = [
|
||||||
@@ -224,18 +256,32 @@ class BylawApiController extends Controller
|
|||||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||||
$subQuery->select('document_id')
|
$subQuery->select('document_id')
|
||||||
->from('bylaw_nodes')
|
->from('bylaw_nodes')
|
||||||
->where('node_content', 'LIKE', "%{$searchTerm}%");
|
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = $query->count();
|
$count = $query->count();
|
||||||
$items = $query->orderBy('approval_date', 'desc')->take(5)->get()->map(function ($item) {
|
$items = $query->orderBy('approval_date', 'desc')->take(5)->get();
|
||||||
return $this->formatBylaw($item);
|
|
||||||
|
$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([
|
return $this->success([
|
||||||
'items' => $items,
|
'items' => $formattedItems,
|
||||||
'count' => $count
|
'count' => $count
|
||||||
], 'Success', 'Fast Search Results');
|
], 'Success', 'Fast Search Results');
|
||||||
}
|
}
|
||||||
@@ -290,38 +336,66 @@ class BylawApiController extends Controller
|
|||||||
->orderBy('id', 'asc')
|
->orderBy('id', 'asc')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$results = array_values(array_filter([
|
$itemsToFormat = array_filter([$previousItem, $current, $nextItem]);
|
||||||
$previousItem ? $this->formatBylaw($previousItem) : null,
|
|
||||||
$this->formatBylaw($current),
|
$docIds = collect($itemsToFormat)->pluck('id')->map(fn($id) => (int)$id)->toArray();
|
||||||
$nextItem ? $this->formatBylaw($nextItem) : null
|
$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');
|
return $this->success($results, 'Success', 'Pager Window Details');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isLockedForCurrentUser($item): bool
|
private function isLockedForCurrentUser($item): bool
|
||||||
{
|
{
|
||||||
return ! auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
if (auth()->check() && auth()->user() && method_exists(auth()->user(), 'isSubscriber')) {
|
||||||
|
return !auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
||||||
|
}
|
||||||
|
return (bool) ($item->is_locked ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatBylaw($item)
|
private function formatBylaw($item, $itemNodes = null)
|
||||||
{
|
{
|
||||||
$isLocked = $this->isLockedForCurrentUser($item);
|
$isLocked = $this->isLockedForCurrentUser($item);
|
||||||
|
|
||||||
$description = $item->description;
|
if ($itemNodes === null) {
|
||||||
|
$itemNodes = BylawNode::where('document_id', $item->id)->orderBy('sort_order', 'asc')->get();
|
||||||
|
}
|
||||||
|
|
||||||
// Check if description is practically empty (null, empty string, or just empty WYSIWYG HTML like <p></p>)
|
$nodeContentAsDescription = collect($itemNodes)->pluck('node_content')
|
||||||
$cleanDesc = trim(strip_tags($description ?? ''));
|
->map(function ($text) {
|
||||||
|
$safeText = trim((string)$text);
|
||||||
|
return mb_convert_encoding($safeText, 'UTF-8', 'UTF-8');
|
||||||
|
})
|
||||||
|
->filter(function ($text) {
|
||||||
|
return $text !== '';
|
||||||
|
})
|
||||||
|
->implode("\n\n");
|
||||||
|
|
||||||
// Direct Query Fallback: Fetch node_content manually via DB facade to bypass any Model restrictions
|
if (empty($nodeContentAsDescription)) {
|
||||||
if (empty($cleanDesc)) {
|
$nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8');
|
||||||
$nodeContents = DB::table('bylaw_nodes')
|
|
||||||
->where('document_id', $item->id)
|
|
||||||
->orderBy('sort_order', 'asc')
|
|
||||||
->pluck('node_content')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
$description = implode("\n\n", array_filter($nodeContents));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -341,8 +415,9 @@ class BylawApiController extends Controller
|
|||||||
'execution_date' => $item->execution_date,
|
'execution_date' => $item->execution_date,
|
||||||
'bylaw_type' => $item->bylaw_type,
|
'bylaw_type' => $item->bylaw_type,
|
||||||
'global_status' => $item->global_status,
|
'global_status' => $item->global_status,
|
||||||
'pdf_file_url' => $isLocked ? null : ($item->pdf_file_path ? asset('storage/' . $item->pdf_file_path) : null),
|
'pdf_file_url' => $isLocked ? null : ($item->pdf_file_path ? asset(str_replace('storage/storage/', 'storage/', 'storage/' . $item->pdf_file_path)) : null),
|
||||||
'description' => $isLocked ? null : $description,
|
'description' => $isLocked ? null : $nodeContentAsDescription,
|
||||||
|
'debug_node_count' => collect($itemNodes)->count(),
|
||||||
'is_locked' => $isLocked,
|
'is_locked' => $isLocked,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
58
app/Http/Controllers/api/BylawHighlightController.php
Normal file
58
app/Http/Controllers/api/BylawHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\BylawHighlight;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BylawHighlightController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'bylaw_id' => 'required', // Clean request parameter
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
BylawHighlight::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'bylaw_document_id' => $validated['bylaw_id'], // Mapped to DB column
|
||||||
|
'start_index' => $validated['start_index'],
|
||||||
|
'end_index' => $validated['end_index'],
|
||||||
|
'selected_text' => $validated['selected_text'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$highlight = BylawHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->update($validated);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$highlight = BylawHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Http/Controllers/api/BylawNoteController.php
Normal file
56
app/Http/Controllers/api/BylawNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\BylawNote;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BylawNoteController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'bylaw_id' => 'required', // Cleaned up for the frontend!
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
BylawNote::query()->create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'bylaw_document_id' => $validated['bylaw_id'], // Maps perfectly to the DB
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'یادداشت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$note = BylawNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
|
||||||
|
$note->update([
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'یادداشت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$note = BylawNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$note->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/Http/Controllers/api/CurrencyController.php
Normal file
103
app/Http/Controllers/api/CurrencyController.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\GoldCurrency;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class CurrencyController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function goldPrices()
|
||||||
|
{
|
||||||
|
// 1. Check if we already updated the database today
|
||||||
|
$latestUpdate = GoldCurrency::max('updated_at');
|
||||||
|
$isUpdatedToday = $latestUpdate && Carbon::parse($latestUpdate)->isToday();
|
||||||
|
|
||||||
|
// 2. If not updated today, call the API
|
||||||
|
if (!$isUpdatedToday) {
|
||||||
|
$this->fetchAndCacheGoldPrices();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch data from our own database
|
||||||
|
$goldCurrencies = GoldCurrency::all();
|
||||||
|
|
||||||
|
// 4. Debug check: If it is STILL empty, the API failed and we have no backup data yet.
|
||||||
|
if ($goldCurrencies->isEmpty()) {
|
||||||
|
// FIX: Pass an array to format the 'alert' as a JSON Object for Kotlin!
|
||||||
|
return $this->failed(null, [
|
||||||
|
'title' => 'خطا',
|
||||||
|
'message' => 'ارتباط با سرور قیمتها برقرار نشد. لطفا بعدا تلاش کنید.'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Map the data
|
||||||
|
$mappedCurrencies = $goldCurrencies->map(function ($currency) {
|
||||||
|
return [
|
||||||
|
'change_percent' => $currency->change_percent,
|
||||||
|
'change_value' => $currency->change_value,
|
||||||
|
'date' => $currency->updated_at->format('Y-m-d'),
|
||||||
|
'name' => $currency->name,
|
||||||
|
'name_en' => $currency->name_en,
|
||||||
|
'price' => $currency->price,
|
||||||
|
'symbol' => $currency->symbol,
|
||||||
|
'time' => $currency->updated_at->format('H:i:s'),
|
||||||
|
'time_unix' => $currency->time_unix,
|
||||||
|
'unit' => $currency->unit,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'gold' => $mappedCurrencies
|
||||||
|
], 'موفق', 'لیست قیمت طلا و سکه دریافت شد.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper to fetch from the external API and save to DB
|
||||||
|
*/
|
||||||
|
private function fetchAndCacheGoldPrices()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$url = 'https://BrsApi.ir/Api/Market/Gold_Currency.php?key=B727Ue3kBrbtCwHR3BmSVZKRRNF1sllp';
|
||||||
|
|
||||||
|
// Added withoutVerifying() for Iranian SSL issues and a User-Agent to bypass firewalls
|
||||||
|
$response = Http::withoutVerifying()
|
||||||
|
->withHeaders([
|
||||||
|
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
])
|
||||||
|
->timeout(15)
|
||||||
|
->get($url);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
if (isset($data['gold']) && is_array($data['gold'])) {
|
||||||
|
foreach ($data['gold'] as $item) {
|
||||||
|
GoldCurrency::updateOrCreate(
|
||||||
|
['name_en' => $item['name_en']],
|
||||||
|
[
|
||||||
|
'name' => $item['name'],
|
||||||
|
'price' => (int) $item['price'],
|
||||||
|
'change_percent' => (float) $item['change_percent'],
|
||||||
|
'change_value' => (int) $item['change_value'],
|
||||||
|
'symbol' => $item['symbol'] ?? null,
|
||||||
|
'unit' => $item['unit'] ?? null,
|
||||||
|
'time_unix' => (int) $item['time_unix'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log::warning('BrsApi returned an error status: ' . $response->status() . ' - ' . $response->body());
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Failed to fetch from BrsApi: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,12 @@ namespace App\Http\Controllers\api;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Folder;
|
use App\Models\Folder;
|
||||||
use App\Models\FolderArt;
|
use App\Models\FolderArt;
|
||||||
|
use App\Models\FolderBylaw;
|
||||||
|
use App\Models\FolderJudicialPrecedent;
|
||||||
|
use App\Models\FolderLegalOpinion;
|
||||||
use App\Traits\BaseApiResponse;
|
use App\Traits\BaseApiResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class FolderController extends Controller
|
class FolderController extends Controller
|
||||||
{
|
{
|
||||||
@@ -14,12 +18,18 @@ class FolderController extends Controller
|
|||||||
|
|
||||||
public function index()
|
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) {
|
->map(function ($q) {
|
||||||
return [
|
return [
|
||||||
'id' => $q->id,
|
'id' => $q->id,
|
||||||
'name' => $q->name,
|
'name' => $q->name,
|
||||||
'count_of_arts' => $q->arts->count(),
|
'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,
|
'created_at' => $q->created_at,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -34,7 +44,7 @@ class FolderController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Folder::create([
|
Folder::create([
|
||||||
'user_id' => auth()->user()->id,
|
'user_id' => auth()->id(),
|
||||||
'name' => $validted['name'],
|
'name' => $validted['name'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -44,40 +54,72 @@ class FolderController extends Controller
|
|||||||
public function assign(Request $request)
|
public function assign(Request $request)
|
||||||
{
|
{
|
||||||
$validted = $request->validate([
|
$validted = $request->validate([
|
||||||
'folder_id' => 'required',
|
'folder_id' => 'required',
|
||||||
'art_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) {
|
// 🚨 SECURITY FIX: Check if the folder exists AND belongs to the logged-in user
|
||||||
return $this->failed([], 'پوشه مورد نظر یافت نشد', 404);
|
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) {
|
if ($request->filled('art_id')) {
|
||||||
return $this->failed([], 'ارت مورد نظر قبلا اضافه شده است', 400);
|
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([
|
return $this->success([], 'اضافه شد', 'با موفیت اضافه شد');
|
||||||
'folder_id' => $validted['folder_id'],
|
|
||||||
'art_id' => $validted['art_id'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->success([], 'اضافه شد', 'ارت با موفیت اضافه شد');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function folder($id)
|
public function folder($id)
|
||||||
{
|
{
|
||||||
$arts = FolderArt::where('folder_id', $id)
|
// Added 'arts.law.category' so we can fetch category_name without slowing down the DB
|
||||||
->with('arts.chapter', 'arts.part', 'arts.volum', 'arts.law', 'arts.book', 'arts.section', 'arts.gate')
|
$folder = Folder::with([
|
||||||
->get()
|
'arts.chapter', 'arts.part', 'arts.volum', 'arts.law.category', 'arts.book', 'arts.section', 'arts.gate',
|
||||||
->pluck('arts')
|
'bylaws.category', 'bylaws.approvalAuthority', 'bylaws.promulgationAuthority',
|
||||||
->flatten();
|
'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 ?? '';
|
$text = $art->text ?? '';
|
||||||
$shortText = '';
|
$shortText = '';
|
||||||
if (strlen($text) > 50) {
|
if (strlen($text) > 50) {
|
||||||
$shortText = substr($text, 0, 50);
|
$shortText = mb_substr($text, 0, 50);
|
||||||
$shortText = substr($shortText, 0, strrpos($shortText, ' ')).'...';
|
$shortText = mb_substr($shortText, 0, mb_strrpos($shortText, ' ')).'...';
|
||||||
} else {
|
} else {
|
||||||
$shortText = $text;
|
$shortText = $text;
|
||||||
}
|
}
|
||||||
@@ -89,6 +131,17 @@ class FolderController extends Controller
|
|||||||
'text' => $isLocked ? null : $shortText,
|
'text' => $isLocked ? null : $shortText,
|
||||||
'number' => $art->number,
|
'number' => $art->number,
|
||||||
'is_locked' => $isLocked,
|
'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 ? [
|
'chapter' => $art->chapter != null ? [
|
||||||
'id' => $art->chapter->id,
|
'id' => $art->chapter->id,
|
||||||
'title' => $art->chapter->title,
|
'title' => $art->chapter->title,
|
||||||
@@ -104,11 +157,6 @@ class FolderController extends Controller
|
|||||||
'title' => $art->volum->title,
|
'title' => $art->volum->title,
|
||||||
'number' => $art->volum->number,
|
'number' => $art->volum->number,
|
||||||
] : null,
|
] : null,
|
||||||
'law' => $art->law != null ? [
|
|
||||||
'id' => $art->law->id,
|
|
||||||
'title' => $art->law->title,
|
|
||||||
'number' => $art->law->number,
|
|
||||||
] : null,
|
|
||||||
'book' => $art->book != null ? [
|
'book' => $art->book != null ? [
|
||||||
'id' => $art->book->id,
|
'id' => $art->book->id,
|
||||||
'title' => $art->book->title,
|
'title' => $art->book->title,
|
||||||
@@ -126,27 +174,191 @@ class FolderController extends Controller
|
|||||||
] : null,
|
] : 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)
|
public function delete_folder($id)
|
||||||
{
|
{
|
||||||
if (Folder::query()->where('id', $id)->count() == 0) {
|
$folder = Folder::where('id', $id)->where('user_id', auth()->id())->first();
|
||||||
return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد', 404);
|
|
||||||
}
|
|
||||||
Folder::query()->where('id', $id)->delete();
|
|
||||||
|
|
||||||
|
if (!$folder) {
|
||||||
|
return $this->failed('پوشه یافت نشد', 'پوشه مورد نظر یافت نشد یا شما دسترسی ندارید', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$folder->delete();
|
||||||
return $this->success([], 'حذف شد', 'پوشه با موفقیت حذف شد');
|
return $this->success([], 'حذف شد', 'پوشه با موفقیت حذف شد');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkFolderOwnership($folder_id)
|
||||||
|
{
|
||||||
|
return Folder::where('id', $folder_id)->where('user_id', auth()->id())->exists();
|
||||||
|
}
|
||||||
|
|
||||||
public function delete_art($id, $art_id)
|
public function delete_art($id, $art_id)
|
||||||
{
|
{
|
||||||
if (FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->count() == 0) {
|
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||||
return $this->failed([], 'ارت مورد نظر یافت نشد', 404);
|
|
||||||
}
|
|
||||||
FolderArt::query()->where('folder_id', $id)->where('art_id', $art_id)->delete();
|
|
||||||
|
|
||||||
return $this->success([], 'حذف شد', 'ارت با موفقیت حذف شد');
|
if (FolderArt::where('folder_id', $id)->where('art_id', $art_id)->count() == 0) {
|
||||||
|
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||||
|
}
|
||||||
|
FolderArt::where('folder_id', $id)->where('art_id', $art_id)->delete();
|
||||||
|
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete_bylaw($id, $bylaw_id)
|
||||||
|
{
|
||||||
|
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||||
|
|
||||||
|
if (FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->count() == 0) {
|
||||||
|
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||||
|
}
|
||||||
|
FolderBylaw::where('folder_id', $id)->where('bylaw_document_id', $bylaw_id)->delete();
|
||||||
|
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete_judicial($id, $judicial_id)
|
||||||
|
{
|
||||||
|
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||||
|
|
||||||
|
if (FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->count() == 0) {
|
||||||
|
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||||
|
}
|
||||||
|
FolderJudicialPrecedent::where('folder_id', $id)->where('judicial_precedent_id', $judicial_id)->delete();
|
||||||
|
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete_opinion($id, $opinion_id)
|
||||||
|
{
|
||||||
|
if (!$this->checkFolderOwnership($id)) return $this->failed([], 'عدم دسترسی', 403);
|
||||||
|
|
||||||
|
if (FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->count() == 0) {
|
||||||
|
return $this->failed([], 'مورد نظر یافت نشد', 404);
|
||||||
|
}
|
||||||
|
FolderLegalOpinion::where('folder_id', $id)->where('legal_opinion_id', $opinion_id)->delete();
|
||||||
|
return $this->success([], 'حذف شد', 'با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------------------------------------------
|
||||||
|
* FORMATTING HELPERS
|
||||||
|
* ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
private function isLockedForCurrentUserGeneric($item): bool
|
||||||
|
{
|
||||||
|
if (auth()->check() && auth()->user() && method_exists(auth()->user(), 'isSubscriber')) {
|
||||||
|
return !auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
||||||
|
}
|
||||||
|
return (bool) ($item->is_locked ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBylaw($item, $itemNodes = null)
|
||||||
|
{
|
||||||
|
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||||
|
$description = $item->description;
|
||||||
|
|
||||||
|
$cleanDesc = trim(str_replace([' ', '‌', "\r", "\n", "\t", ' '], '', strip_tags($description ?? '')));
|
||||||
|
|
||||||
|
if (strlen($cleanDesc) < 5) {
|
||||||
|
if ($itemNodes === null || (is_object($itemNodes) && $itemNodes->isEmpty())) {
|
||||||
|
$dbNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->count();
|
||||||
|
if ($dbNodes > 0) {
|
||||||
|
$itemNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->orderBy('sort_order', 'asc')->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$nodeContents = [];
|
||||||
|
if ($itemNodes) {
|
||||||
|
foreach ($itemNodes as $node) {
|
||||||
|
$content = trim(strip_tags($node->node_content ?? ''));
|
||||||
|
if (!empty($content)) {
|
||||||
|
$nodeContents[] = $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$description = implode("\n\n", $nodeContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'title' => $item->title,
|
||||||
|
'category_name' => optional($item->category)->name,
|
||||||
|
'approval_authority_name' => optional($item->approvalAuthority)->name,
|
||||||
|
'approval_date' => $item->approval_date,
|
||||||
|
'promulgation_number' => $item->promulgation_number,
|
||||||
|
'description' => $isLocked ? null : mb_substr($description, 0, 100) . '...',
|
||||||
|
'is_locked' => $isLocked,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatJudicial($item)
|
||||||
|
{
|
||||||
|
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'title' => $item->subject,
|
||||||
|
'ruling_number' => $item->ruling_number,
|
||||||
|
'ruling_date' => $item->ruling_date,
|
||||||
|
'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...',
|
||||||
|
'issuing_authority' => $item->issuing_authority,
|
||||||
|
'category_name' => optional($item->category)->name,
|
||||||
|
'is_locked' => $isLocked,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatOpinion($item)
|
||||||
|
{
|
||||||
|
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $item->id,
|
||||||
|
'title' => $item->subject,
|
||||||
|
'opinion_number' => $item->opinion_number,
|
||||||
|
'opinion_date' => $item->opinion_date,
|
||||||
|
'text' => $isLocked ? null : mb_substr($item->full_text, 0, 100) . '...',
|
||||||
|
'issuing_authority' => $item->issuing_authority,
|
||||||
|
'category_name' => optional($item->category)->name,
|
||||||
|
'is_locked' => $isLocked,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class GlobalSearchApiController extends Controller
|
|||||||
$arts = Art::with(['law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
$arts = Art::with(['law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
||||||
->where(function ($q) use ($searchTerm) {
|
->where(function ($q) use ($searchTerm) {
|
||||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||||
->orWhere('text', 'LIKE', "%{$searchTerm}%");
|
->orWhere('text', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('number', 'LIKE', "%{$searchTerm}%");
|
||||||
})
|
})
|
||||||
->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
||||||
$formatted = $this->formatArt($item, $searchTerm, Art::class);
|
$formatted = $this->formatArt($item, $searchTerm, Art::class);
|
||||||
@@ -91,7 +92,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
// =========================================================
|
// =========================================================
|
||||||
// 3. Search Bylaw Documents
|
// 3. Search Bylaw Documents
|
||||||
// =========================================================
|
// =========================================================
|
||||||
$bylaws = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
$bylawsQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
||||||
->where('is_published', true)
|
->where('is_published', true)
|
||||||
->where(function ($q) use ($searchTerm) {
|
->where(function ($q) use ($searchTerm) {
|
||||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||||
@@ -101,13 +102,27 @@ class GlobalSearchApiController extends Controller
|
|||||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||||
$subQuery->select('document_id')
|
$subQuery->select('document_id')
|
||||||
->from('bylaw_nodes')
|
->from('bylaw_nodes')
|
||||||
->where('node_content', 'LIKE', "%{$searchTerm}%");
|
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||||
});
|
});
|
||||||
})->take($limitPerTable)->get()->map(function($item) {
|
})->take($limitPerTable)->get();
|
||||||
$formatted = $this->formatBylaw($item);
|
|
||||||
$formatted['type'] = 'bylaw';
|
// Safely bulk-fetch nodes using DB facade
|
||||||
return $formatted;
|
$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);
|
$sortableResults = $sortableResults->concat($bylaws);
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -193,7 +208,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
'total' => $paginator->total(),
|
'total' => $paginator->total(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->success($responseData, '', '');
|
return $this->success($responseData, 'Success', 'Global Search Results Fetched');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +228,8 @@ class GlobalSearchApiController extends Controller
|
|||||||
$artQuery = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
$artQuery = Art::with(['law', 'law.category', 'chapter', 'part', 'volum', 'book', 'section', 'gate'])
|
||||||
->where(function ($q) use ($searchTerm) {
|
->where(function ($q) use ($searchTerm) {
|
||||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||||
->orWhere('text', 'LIKE', "%{$searchTerm}%");
|
->orWhere('text', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('number', 'LIKE', "%{$searchTerm}%");
|
||||||
});
|
});
|
||||||
$totalCount += $artQuery->count();
|
$totalCount += $artQuery->count();
|
||||||
$arts = $artQuery->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
$arts = $artQuery->take($limitPerTable)->get()->map(function($item) use ($searchTerm) {
|
||||||
@@ -250,7 +266,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Bylaw Documents
|
// 3. Bylaw Documents
|
||||||
$bylawQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
$bylawsQuery = BylawDocument::with(['category', 'approvalAuthority', 'promulgationAuthority'])
|
||||||
->where('is_published', true)
|
->where('is_published', true)
|
||||||
->where(function ($q) use ($searchTerm) {
|
->where(function ($q) use ($searchTerm) {
|
||||||
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
$q->where('title', 'LIKE', "%{$searchTerm}%")
|
||||||
@@ -260,12 +276,27 @@ class GlobalSearchApiController extends Controller
|
|||||||
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
->orWhereIn('id', function($subQuery) use ($searchTerm) {
|
||||||
$subQuery->select('document_id')
|
$subQuery->select('document_id')
|
||||||
->from('bylaw_nodes')
|
->from('bylaw_nodes')
|
||||||
->where('node_content', 'LIKE', "%{$searchTerm}%");
|
->where('node_content', 'LIKE', "%{$searchTerm}%")
|
||||||
|
->orWhere('title', 'LIKE', "%{$searchTerm}%");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$totalCount += $bylawQuery->count();
|
|
||||||
$bylaws = $bylawQuery->take($limitPerTable)->get()->map(function($item) {
|
$totalCount += $bylawsQuery->count();
|
||||||
$formatted = $this->formatBylaw($item);
|
$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';
|
$formatted['type'] = 'bylaw';
|
||||||
return $formatted;
|
return $formatted;
|
||||||
});
|
});
|
||||||
@@ -306,7 +337,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
return $this->success([
|
return $this->success([
|
||||||
'items' => $results->take(15)->values(),
|
'items' => $results->take(15)->values(),
|
||||||
'count' => $totalCount
|
'count' => $totalCount
|
||||||
], '', '');
|
], 'Success', 'Global Fast Search Results');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------------------------------------------
|
/* -----------------------------------------------------------------
|
||||||
@@ -315,24 +346,35 @@ class GlobalSearchApiController extends Controller
|
|||||||
|
|
||||||
private function isLockedForCurrentUserGeneric($item): bool
|
private function isLockedForCurrentUserGeneric($item): bool
|
||||||
{
|
{
|
||||||
return ! auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
if (auth()->check() && auth()->user() && method_exists(auth()->user(), 'isSubscriber')) {
|
||||||
|
return !auth()->user()->isSubscriber() && (bool) ($item->is_locked ?? false);
|
||||||
|
}
|
||||||
|
return (bool) ($item->is_locked ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function formatBylaw($item)
|
/**
|
||||||
|
* Exact 1:1 match with BylawApiController's encoding approach
|
||||||
|
*/
|
||||||
|
private function formatBylaw($item, $itemNodes = null)
|
||||||
{
|
{
|
||||||
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
$isLocked = $this->isLockedForCurrentUserGeneric($item);
|
||||||
$description = $item->description;
|
|
||||||
|
|
||||||
$cleanDesc = trim(strip_tags($description ?? ''));
|
if ($itemNodes === null) {
|
||||||
|
$itemNodes = DB::table('bylaw_nodes')->where('document_id', $item->id)->orderBy('sort_order', 'asc')->get();
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($cleanDesc)) {
|
$nodeContentAsDescription = collect($itemNodes)->pluck('node_content')
|
||||||
$nodeContents = DB::table('bylaw_nodes')
|
->map(function ($text) {
|
||||||
->where('document_id', $item->id)
|
$safeText = trim((string)$text);
|
||||||
->orderBy('sort_order', 'asc')
|
return mb_convert_encoding($safeText, 'UTF-8', 'UTF-8');
|
||||||
->pluck('node_content')
|
})
|
||||||
->toArray();
|
->filter(function ($text) {
|
||||||
|
return $text !== '';
|
||||||
|
})
|
||||||
|
->implode("\n\n");
|
||||||
|
|
||||||
$description = implode("\n\n", array_filter($nodeContents));
|
if (empty($nodeContentAsDescription)) {
|
||||||
|
$nodeContentAsDescription = mb_convert_encoding(trim((string)$item->description), 'UTF-8', 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -342,7 +384,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
'approval_authority_name' => optional($item->approvalAuthority)->name,
|
'approval_authority_name' => optional($item->approvalAuthority)->name,
|
||||||
'approval_date' => $item->approval_date,
|
'approval_date' => $item->approval_date,
|
||||||
'promulgation_number' => $item->promulgation_number,
|
'promulgation_number' => $item->promulgation_number,
|
||||||
'description' => $isLocked ? null : $description,
|
'description' => $isLocked ? null : $nodeContentAsDescription,
|
||||||
'is_locked' => $isLocked,
|
'is_locked' => $isLocked,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -379,10 +421,6 @@ class GlobalSearchApiController extends Controller
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exact 1:1 match with ArtController::searchModel mapping
|
|
||||||
* All relations (law, chapter, etc) are strictly Strings to prevent App Crashes!
|
|
||||||
*/
|
|
||||||
private function formatArt($item, $searchTerm, $modelClass)
|
private function formatArt($item, $searchTerm, $modelClass)
|
||||||
{
|
{
|
||||||
$law = $item->law ?? Law::find($item->law_id);
|
$law = $item->law ?? Law::find($item->law_id);
|
||||||
@@ -390,6 +428,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
|
|
||||||
$text = $item->text ?? '';
|
$text = $item->text ?? '';
|
||||||
$searchQuoted = preg_quote($searchTerm, '/');
|
$searchQuoted = preg_quote($searchTerm, '/');
|
||||||
|
|
||||||
if (!empty($searchTerm) && preg_match('/\b(.{0,50})\b('.$searchQuoted.')\b(.{0,50})\b/i', $text, $matches)) {
|
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] ?? '');
|
$context = trim($matches[1] ?? '').' '.($matches[2] ?? '').' '.trim($matches[3] ?? '');
|
||||||
} else {
|
} else {
|
||||||
@@ -404,10 +443,10 @@ class GlobalSearchApiController extends Controller
|
|||||||
'is_locked' => $isLocked,
|
'is_locked' => $isLocked,
|
||||||
'route' => array_values($this->route($modelClass, $item)),
|
'route' => array_values($this->route($modelClass, $item)),
|
||||||
'category' => optional($law?->category)->name,
|
'category' => optional($law?->category)->name,
|
||||||
'law' => $law?->title ?? '', // STRICLY STRING! Prevents Android App Crash!
|
'law' => $law?->title ?? '',
|
||||||
'count_art' => $law?->arts?->count() ?? 0,
|
'count_art' => $law?->arts?->count() ?? 0,
|
||||||
'count_volums' => $law?->volums?->count() ?? 0, // WITH S
|
'count_volums' => $law?->volums?->count() ?? 0,
|
||||||
'count_volum' => $law?->volums?->count() ?? 0, // WITHOUT S (Fallback)
|
'count_volum' => $law?->volums?->count() ?? 0,
|
||||||
'price' => $law?->price,
|
'price' => $law?->price,
|
||||||
'image' => $law?->image,
|
'image' => $law?->image,
|
||||||
];
|
];
|
||||||
@@ -424,7 +463,7 @@ class GlobalSearchApiController extends Controller
|
|||||||
'number' => $item->number,
|
'number' => $item->number,
|
||||||
'is_locked' => $isLocked,
|
'is_locked' => $isLocked,
|
||||||
'route' => array_values($this->route($modelClass, $item)),
|
'route' => array_values($this->route($modelClass, $item)),
|
||||||
'law' => $law?->title ?? '', // STRICTLY STRING
|
'law' => $law?->title ?? '',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ((new $modelClass)->getTable() == 'laws') {
|
if ((new $modelClass)->getTable() == 'laws') {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Models\Law;
|
|||||||
use App\Models\Notification;
|
use App\Models\Notification;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\RecentArt;
|
use App\Models\RecentArt;
|
||||||
|
use App\Models\SubscribePlan;
|
||||||
use App\Models\UserSubscriber;
|
use App\Models\UserSubscriber;
|
||||||
use App\Services\AppMarketPurchaseVerifier;
|
use App\Services\AppMarketPurchaseVerifier;
|
||||||
use App\Traits\BaseApiResponse;
|
use App\Traits\BaseApiResponse;
|
||||||
@@ -20,9 +21,32 @@ class HomeController extends Controller
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return $this->buildHomeResponse();
|
||||||
|
}
|
||||||
|
|
||||||
public function homeV2()
|
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();
|
$user = auth()->user();
|
||||||
|
|
||||||
|
// 1. Safely grant free subs (Won't overlap with paid plans)
|
||||||
|
$this->checkAndGrantFreeSubscription($user);
|
||||||
|
|
||||||
$this->refreshMarketSubscriptionAfterResponse($user);
|
$this->refreshMarketSubscriptionAfterResponse($user);
|
||||||
|
|
||||||
$recent = RecentArt::query()->where('user_id', $user->id)->get()->map(function ($q) {
|
$recent = RecentArt::query()->where('user_id', $user->id)->get()->map(function ($q) {
|
||||||
@@ -68,41 +92,41 @@ class HomeController extends Controller
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$freeSubscription = $user->userSubscribers()
|
// --- THE FIXED AUTHORITY LOGIC APPLIED TO ALL VERSIONS ---
|
||||||
->whereHas('subscribe', function ($query) {
|
$activeSubscribers = $user->userSubscribers()
|
||||||
$query->where('is_free', true);
|
->with('subscribe')
|
||||||
})
|
|
||||||
->where('expired_at', '>=', now())
|
->where('expired_at', '>=', now())
|
||||||
->first();
|
->get();
|
||||||
|
|
||||||
$current_plan = null;
|
$current_plan = null;
|
||||||
|
|
||||||
if ($freeSubscription) {
|
if ($activeSubscribers->isNotEmpty()) {
|
||||||
$current_plan = [
|
// Separate Paid from Free
|
||||||
'id' => $freeSubscription->id,
|
$paidSubscriptions = $activeSubscribers->reject(function ($subscriber) {
|
||||||
'name' => $freeSubscription->subscribe->name,
|
return $subscriber->is_free || ($subscriber->subscribe && $subscriber->subscribe->is_free);
|
||||||
'price' => $freeSubscription->subscribe->price,
|
});
|
||||||
'expired_day' => $freeSubscription->expired_at->diffInDays(now()),
|
|
||||||
'is_free' => true,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$latestSubscription = $user->userSubscribers()
|
// AUTHORITY RULE: Paid plans override Free plans
|
||||||
->where('expired_at', '>=', now())
|
if ($paidSubscriptions->isNotEmpty()) {
|
||||||
->latest('expired_at')
|
$latestSubscription = $paidSubscriptions->sortByDesc('expired_at')->first();
|
||||||
->first();
|
} else {
|
||||||
|
$latestSubscription = $activeSubscribers->sortByDesc('expired_at')->first();
|
||||||
|
}
|
||||||
|
|
||||||
$expiredDays = UserSubscriber::query()->where('user_id', $user->id)->where('expired_at', '>=', now())->get()->sum(function ($subscriber) {
|
// Fixed math (No sum)
|
||||||
return $subscriber->expired_at->diffInDays(now());
|
$expiredDays = $latestSubscription->expired_at->diffInDays(now());
|
||||||
});
|
|
||||||
|
$isFree = (bool) (
|
||||||
|
$latestSubscription->is_free ||
|
||||||
|
($latestSubscription->subscribe && $latestSubscription->subscribe->is_free)
|
||||||
|
);
|
||||||
|
|
||||||
if ($latestSubscription) {
|
|
||||||
$current_plan = [
|
$current_plan = [
|
||||||
'id' => $latestSubscription->id,
|
'id' => $latestSubscription->id,
|
||||||
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
||||||
'price' => $latestSubscription->subscribe->price ?? 100,
|
'price' => $latestSubscription->subscribe->price ?? 0,
|
||||||
'expired_day' => $expiredDays,
|
'expired_day' => $expiredDays,
|
||||||
'is_free' => (bool)$latestSubscription->is_free,
|
'is_free' => $isFree,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +143,8 @@ class HomeController extends Controller
|
|||||||
|
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'home' => $homeData,
|
'home' => $homeData,
|
||||||
'hoqoqi_list' => $this->getCategoryList(1), // Category 1
|
'hoqoqi_list' => $this->getCategoryList(1),
|
||||||
'keyfari_list' => $this->getCategoryList(2), // Category 2
|
'keyfari_list' => $this->getCategoryList(2),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,104 +176,29 @@ class HomeController extends Controller
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkAndGrantFreeSubscription($user): void
|
||||||
public function index()
|
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$hasUsedFreePlan = $user->userSubscribers()
|
||||||
|
|
||||||
$this->refreshMarketSubscriptionAfterResponse($user);
|
|
||||||
|
|
||||||
$recent = RecentArt::query()->where('user_id', $user->id)->get()->map(function ($q) {
|
|
||||||
return [
|
|
||||||
'id' => $q->law?->id,
|
|
||||||
'name' => $q->law?->title,
|
|
||||||
'is_locked' => $q->law?->is_locked,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$laws = Law::orderBy('created_at')->get()->map(function ($q) {
|
|
||||||
return [
|
|
||||||
'id' => $q->id,
|
|
||||||
'name' => $q->title,
|
|
||||||
'is_locked' => $q->is_locked,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$categories = ['hagigi', 'kifari'];
|
|
||||||
$lawsByCategory = [];
|
|
||||||
|
|
||||||
foreach ($categories as $category) {
|
|
||||||
$lawsByCategory[$category] = Law::whereHas('category', function ($q) use ($category) {
|
|
||||||
$q->where('type', $category);
|
|
||||||
})->get()->map(function ($q) {
|
|
||||||
return [
|
|
||||||
'id' => $q->id,
|
|
||||||
'title' => $q->title,
|
|
||||||
'is_locked' => $q->is_locked,
|
|
||||||
'category_id' => $q->category_id,
|
|
||||||
'price' => $q->price,
|
|
||||||
'image' => $q->image,
|
|
||||||
'type' => 'law',
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$free_law = Law::where('is_locked', false)->orderBy('created_at')->get()->map(function ($q) {
|
|
||||||
return [
|
|
||||||
'id' => $q->id,
|
|
||||||
'name' => $q->title,
|
|
||||||
'is_locked' => $q->is_locked,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$freeSubscription = $user->userSubscribers()
|
|
||||||
->whereHas('subscribe', function ($query) {
|
->whereHas('subscribe', function ($query) {
|
||||||
$query->where('is_free', true);
|
$query->where('is_free', true);
|
||||||
})
|
})->exists();
|
||||||
->where('expired_at', '>=', now())
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$current_plan = null;
|
$hasActivePaidPlan = $user->userSubscribers()
|
||||||
|
->where('expired_at', '>', now())
|
||||||
|
->whereHas('subscribe', function ($query) {
|
||||||
|
$query->where('is_free', false);
|
||||||
|
})->exists();
|
||||||
|
|
||||||
if ($freeSubscription) {
|
if (!$hasUsedFreePlan && !$hasActivePaidPlan) {
|
||||||
$current_plan = [
|
$freePlan = SubscribePlan::where('is_free', true)->where('is_active', true)->first();
|
||||||
'id' => $freeSubscription->id,
|
|
||||||
'name' => $freeSubscription->subscribe->name,
|
if ($freePlan) {
|
||||||
'price' => $freeSubscription->subscribe->price,
|
$user->userSubscribers()->create([
|
||||||
'expired_day' => $freeSubscription->expired_at->diffInDays(now()),
|
'subscribe_plan_id' => $freePlan->id,
|
||||||
'is_free' => true,
|
'expired_at' => now()->addDays($freePlan->expired_day + 1),
|
||||||
];
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$latestSubscription = $user->userSubscribers()
|
|
||||||
->where('expired_at', '>=', now())
|
|
||||||
->latest('expired_at')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$expiredDays = UserSubscriber::query()->where('user_id', $user->id)->where('expired_at', '>=', now())->get()->sum(function ($subscriber) {
|
|
||||||
return $subscriber->expired_at->diffInDays(now());
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($latestSubscription) {
|
|
||||||
$current_plan = [
|
|
||||||
'id' => $latestSubscription->id,
|
|
||||||
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
|
||||||
'price' => $latestSubscription->subscribe->price ?? 100,
|
|
||||||
'expired_day' => $expiredDays,
|
|
||||||
'is_free' => (bool)$latestSubscription->is_free,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$unread_notifications_count = Notification::unreadForUser($user->id)->count();
|
|
||||||
|
|
||||||
return $this->success([
|
|
||||||
'recent' => $recent,
|
|
||||||
'laws' => $lawsByCategory,
|
|
||||||
'last_law' => $laws,
|
|
||||||
'free' => $free_law,
|
|
||||||
'current_plan' => $current_plan,
|
|
||||||
'unread_notifications_count' => $unread_notifications_count,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function refreshMarketSubscriptionAfterResponse($user): void
|
private function refreshMarketSubscriptionAfterResponse($user): void
|
||||||
|
|||||||
@@ -146,38 +146,46 @@ class JudicialPrecedentApiController extends Controller
|
|||||||
$current = JudicialPrecedent::find($id);
|
$current = JudicialPrecedent::find($id);
|
||||||
|
|
||||||
if (!$current) {
|
if (!$current) {
|
||||||
return $this->failed([], [
|
return $this->failed([], ['title' => 'خطا', 'message' => 'رأی وحدت رویه مورد نظر یافت نشد.'], 404);
|
||||||
'title' => 'خطا',
|
|
||||||
'message' => 'رأی وحدت رویه مورد نظر یافت نشد.'
|
|
||||||
], 404);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$previousItem = JudicialPrecedent::where(function ($query) use ($current) {
|
$previousItem = JudicialPrecedent::where(function ($query) use ($current) {
|
||||||
$query->where('ruling_date', '<', $current->ruling_date)
|
$query->where('ruling_date', '<', $current->ruling_date)
|
||||||
->orWhere(function ($subQuery) use ($current) {
|
->orWhere(function ($subQuery) use ($current) {
|
||||||
$subQuery->where('ruling_date', '=', $current->ruling_date)
|
$subQuery->where('ruling_date', '=', $current->ruling_date)->where('id', '<', $current->id);
|
||||||
->where('id', '<', $current->id);
|
|
||||||
});
|
});
|
||||||
})
|
})->orderBy('ruling_date', 'desc')->orderBy('id', 'desc')->first();
|
||||||
->orderBy('ruling_date', 'desc')
|
|
||||||
->orderBy('id', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$nextItem = JudicialPrecedent::where(function ($query) use ($current) {
|
$nextItem = JudicialPrecedent::where(function ($query) use ($current) {
|
||||||
$query->where('ruling_date', '>', $current->ruling_date)
|
$query->where('ruling_date', '>', $current->ruling_date)
|
||||||
->orWhere(function ($subQuery) use ($current) {
|
->orWhere(function ($subQuery) use ($current) {
|
||||||
$subQuery->where('ruling_date', '=', $current->ruling_date)
|
$subQuery->where('ruling_date', '=', $current->ruling_date)->where('id', '>', $current->id);
|
||||||
->where('id', '>', $current->id);
|
|
||||||
});
|
});
|
||||||
})
|
})->orderBy('ruling_date', 'asc')->orderBy('id', 'asc')->first();
|
||||||
->orderBy('ruling_date', 'asc')
|
|
||||||
->orderBy('id', 'asc')
|
// Apply format and add Notes & Highlights
|
||||||
->first();
|
$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([
|
$results = array_values(array_filter([
|
||||||
$previousItem ? $this->formatItem($previousItem) : null,
|
$previousItem ? $formatWithNoteAndHighlight($previousItem) : null,
|
||||||
$this->formatItem($current),
|
$formatWithNoteAndHighlight($current),
|
||||||
$nextItem ? $this->formatItem($nextItem) : null
|
$nextItem ? $formatWithNoteAndHighlight($nextItem) : null
|
||||||
]));
|
]));
|
||||||
|
|
||||||
return $this->success($results, 'Success', 'Pager Window Details');
|
return $this->success($results, 'Success', 'Pager Window Details');
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\JudicialPrecedentHighlight;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class JudicialPrecedentHighlightController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'judicial_id' => 'required',
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
JudicialPrecedentHighlight::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'judicial_precedent_id' => $validated['judicial_id'],
|
||||||
|
'start_index' => $validated['start_index'],
|
||||||
|
'end_index' => $validated['end_index'],
|
||||||
|
'selected_text' => $validated['selected_text'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$highlight = JudicialPrecedentHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->update($validated);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$highlight = JudicialPrecedentHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Http/Controllers/api/JudicialPrecedentNoteController.php
Normal file
56
app/Http/Controllers/api/JudicialPrecedentNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\JudicialPrecedentNote;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class JudicialPrecedentNoteController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'judicial_id' => 'required', // Cleaned up!
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
JudicialPrecedentNote::query()->create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'judicial_precedent_id' => $validated['judicial_id'], // Maps to DB
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'یادداشت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$note = JudicialPrecedentNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
|
||||||
|
$note->update([
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'یادداشت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$note = JudicialPrecedentNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$note->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,49 +147,54 @@ class LegalOpinionApiController extends Controller
|
|||||||
$current = LegalOpinion::find($id);
|
$current = LegalOpinion::find($id);
|
||||||
|
|
||||||
if (!$current) {
|
if (!$current) {
|
||||||
return $this->failed([], [
|
return $this->failed([], ['title' => 'خطا', 'message' => 'نظریه قانونی مورد نظر یافت نشد.'], 404);
|
||||||
'title' => 'خطا',
|
|
||||||
'message' => 'نظریه قانونی مورد نظر یافت نشد.'
|
|
||||||
], 404);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$previousItem = LegalOpinion::where(function ($query) use ($current) {
|
$previousItem = LegalOpinion::where(function ($query) use ($current) {
|
||||||
if ($current->opinion_date) {
|
if ($current->opinion_date) {
|
||||||
$query->where('opinion_date', '<', $current->opinion_date)
|
$query->where('opinion_date', '<', $current->opinion_date)->orWhere(function ($subQuery) use ($current) {
|
||||||
->orWhere(function ($subQuery) use ($current) {
|
$subQuery->where('opinion_date', '=', $current->opinion_date)->where('id', '<', $current->id);
|
||||||
$subQuery->where('opinion_date', '=', $current->opinion_date)
|
});
|
||||||
->where('id', '<', $current->id);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
$query->whereNull('opinion_date')->where('id', '<', $current->id);
|
$query->whereNull('opinion_date')->where('id', '<', $current->id);
|
||||||
}
|
}
|
||||||
})
|
})->orderBy('opinion_date', 'desc')->orderBy('id', 'desc')->first();
|
||||||
->orderBy('opinion_date', 'desc')
|
|
||||||
->orderBy('id', 'desc')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$nextItem = LegalOpinion::where(function ($query) use ($current) {
|
$nextItem = LegalOpinion::where(function ($query) use ($current) {
|
||||||
if ($current->opinion_date) {
|
if ($current->opinion_date) {
|
||||||
$query->where('opinion_date', '>', $current->opinion_date)
|
$query->where('opinion_date', '>', $current->opinion_date)->orWhere(function ($subQuery) use ($current) {
|
||||||
->orWhere(function ($subQuery) use ($current) {
|
$subQuery->where('opinion_date', '=', $current->opinion_date)->where('id', '>', $current->id);
|
||||||
$subQuery->where('opinion_date', '=', $current->opinion_date)
|
});
|
||||||
->where('id', '>', $current->id);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
$query->whereNotNull('opinion_date')
|
$query->whereNotNull('opinion_date')->orWhere(function ($subQuery) use ($current) {
|
||||||
->orWhere(function ($subQuery) use ($current) {
|
$subQuery->whereNull('opinion_date')->where('id', '>', $current->id);
|
||||||
$subQuery->whereNull('opinion_date')->where('id', '>', $current->id);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})->orderBy('opinion_date', 'asc')->orderBy('id', 'asc')->first();
|
||||||
->orderBy('opinion_date', 'asc')
|
|
||||||
->orderBy('id', 'asc')
|
// Apply format and add Notes & Highlights
|
||||||
->first();
|
$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([
|
$results = array_values(array_filter([
|
||||||
$previousItem ? $this->formatItem($previousItem) : null,
|
$previousItem ? $formatWithNoteAndHighlight($previousItem) : null,
|
||||||
$this->formatItem($current),
|
$formatWithNoteAndHighlight($current),
|
||||||
$nextItem ? $this->formatItem($nextItem) : null
|
$nextItem ? $formatWithNoteAndHighlight($nextItem) : null
|
||||||
]));
|
]));
|
||||||
|
|
||||||
return $this->success($results, 'Success', 'Pager Window Details');
|
return $this->success($results, 'Success', 'Pager Window Details');
|
||||||
|
|||||||
58
app/Http/Controllers/api/LegalOpinionHighlightController.php
Normal file
58
app/Http/Controllers/api/LegalOpinionHighlightController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\LegalOpinionHighlight;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LegalOpinionHighlightController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'opinion_id' => 'required',
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
LegalOpinionHighlight::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'legal_opinion_id' => $validated['opinion_id'],
|
||||||
|
'start_index' => $validated['start_index'],
|
||||||
|
'end_index' => $validated['end_index'],
|
||||||
|
'selected_text' => $validated['selected_text'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'هایلایت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'start_index' => 'required|integer',
|
||||||
|
'end_index' => 'required|integer',
|
||||||
|
'selected_text' => 'nullable|string',
|
||||||
|
'color_code' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$highlight = LegalOpinionHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->update($validated);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'هایلایت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$highlight = LegalOpinionHighlight::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$highlight->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'هایلایت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Http/Controllers/api/LegalOpinionNoteController.php
Normal file
56
app/Http/Controllers/api/LegalOpinionNoteController.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\LegalOpinionNote;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LegalOpinionNoteController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'opinion_id' => 'required', // Cleaned up!
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
LegalOpinionNote::query()->create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'legal_opinion_id' => $validated['opinion_id'], // Maps to DB
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'یادداشت با موفقیت اضافه شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'note' => 'required',
|
||||||
|
'color_code' => 'nullable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$note = LegalOpinionNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
|
||||||
|
$note->update([
|
||||||
|
'note' => $validated['note'],
|
||||||
|
'color_code' => $validated['color_code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([], 'بروزرسانی', 'یادداشت با موفقیت بروزرسانی شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
$note = LegalOpinionNote::where('user_id', auth()->id())->findOrFail($id);
|
||||||
|
$note->delete();
|
||||||
|
|
||||||
|
return $this->success([], 'حذف', 'یادداشت با موفقیت حذف شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
200
app/Http/Controllers/api/PvPQuizController.php
Normal file
200
app/Http/Controllers/api/PvPQuizController.php
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
class PvPQuizController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 1: Join the Matchmaking Queue
|
||||||
|
*/
|
||||||
|
public function findMatch(Request $request)
|
||||||
|
{
|
||||||
|
$userId = auth()->id();
|
||||||
|
|
||||||
|
// 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 != $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, '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);
|
||||||
|
|
||||||
|
// TODO (Next Step): Fire a WebSocket Event here to tell Player 1 the match started!
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'match_id' => $match->id,
|
||||||
|
'status' => 'matched',
|
||||||
|
'opponent_id' => $waitingPlayer,
|
||||||
|
'questions' => $match->questions()->with('options')->get()->makeHidden(['solution_explanation'])->each(function($q) {
|
||||||
|
$q->options->each->makeHidden(['is_correct']);
|
||||||
|
})
|
||||||
|
], 'موفق', 'حریف پیدا شد! بازی شروع میشود.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO MATCH FOUND YET. Put this user in the waiting queue for 30 seconds.
|
||||||
|
Cache::put('pvp_waiting_player', $userId, 30);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'status' => 'waiting'
|
||||||
|
], 'در حال جستجو', 'در حال یافتن حریف...');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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)
|
||||||
|
{
|
||||||
|
// Get both players, ordered by the highest score first
|
||||||
|
$players = $match->players()->orderByDesc('score')->get();
|
||||||
|
|
||||||
|
$player1 = $players->first(); // The one with the higher score
|
||||||
|
$player2 = $players->last(); // The one with the lower score
|
||||||
|
|
||||||
|
$winnerId = null;
|
||||||
|
|
||||||
|
// Check if it's a draw, or if someone actually won
|
||||||
|
if ($player1->score > $player2->score) {
|
||||||
|
$winnerId = $player1->user_id;
|
||||||
|
}
|
||||||
|
// If scores are exactly equal, winnerId stays null (Draw/Tie)
|
||||||
|
|
||||||
|
// Close the room
|
||||||
|
$match->update([
|
||||||
|
'status' => 'completed',
|
||||||
|
'ended_at' => now(),
|
||||||
|
'winner_id' => $winnerId
|
||||||
|
]);
|
||||||
|
|
||||||
|
// TODO (Next Step): Fire a WebSocket Event here to push the "Game Over" screen!
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* STEP 4: Get Final Match Results
|
||||||
|
*/
|
||||||
|
public function matchResult(PvpMatch $match)
|
||||||
|
{
|
||||||
|
// Ensure user was actually in this match
|
||||||
|
if (!$match->players()->where('user_id', auth()->id())->exists()) {
|
||||||
|
return response()->json(['message' => 'دسترسی غیرمجاز'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$match->load([
|
||||||
|
'players.user:id,name', // Load player names
|
||||||
|
'winner:id,name' // Load winner name
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'match_id' => $match->id,
|
||||||
|
'status' => $match->status,
|
||||||
|
'winner_id' => $match->winner_id,
|
||||||
|
'winner_name' => $match->winner ? $match->winner->name : 'مساوی (Draw)',
|
||||||
|
'players' => $match->players->map(function ($p) {
|
||||||
|
return [
|
||||||
|
'user_id' => $p->user_id,
|
||||||
|
'name' => optional($p->user)->name,
|
||||||
|
'final_score' => round($p->score, 2)
|
||||||
|
];
|
||||||
|
})
|
||||||
|
], 'موفق', 'نتایج نهایی مسابقه دریافت شد.');
|
||||||
|
}
|
||||||
|
}
|
||||||
207
app/Http/Controllers/api/QuizController.php
Normal file
207
app/Http/Controllers/api/QuizController.php
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Quiz;
|
||||||
|
use App\Models\QuizCategory;
|
||||||
|
use App\Models\QuizSubject;
|
||||||
|
use App\Models\UserQuizAttempt;
|
||||||
|
use App\Models\UserQuizAnswer;
|
||||||
|
use App\Models\Question;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class QuizController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Get all active Categories
|
||||||
|
*/
|
||||||
|
public function categories()
|
||||||
|
{
|
||||||
|
$categories = QuizCategory::where('is_active', true)->get();
|
||||||
|
return $this->success($categories, 'موفق', 'لیست دستهبندیها دریافت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2. Get all Subjects
|
||||||
|
*/
|
||||||
|
public function subjects()
|
||||||
|
{
|
||||||
|
$subjects = QuizSubject::all();
|
||||||
|
return $this->success($subjects, 'موفق', 'لیست دروس دریافت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3. List Quizzes (Filterable by Category or Subject)
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Quiz::where('is_published', true)->with('category');
|
||||||
|
|
||||||
|
if ($request->filled('category_id')) {
|
||||||
|
$query->where('category_id', $request->category_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('subject_id')) {
|
||||||
|
$query->whereHas('questions', function ($q) use ($request) {
|
||||||
|
$q->where('subject_id', $request->subject_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$quizzes = $query->latest()->get();
|
||||||
|
|
||||||
|
return $this->success($quizzes, 'موفق', 'لیست آزمونها دریافت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4. Start a Quiz & Fetch Questions (SECURELY)
|
||||||
|
*/
|
||||||
|
public function start(Quiz $quiz)
|
||||||
|
{
|
||||||
|
if (!$quiz->is_published) {
|
||||||
|
return response()->json(['message' => 'این آزمون در دسترس نیست.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt = UserQuizAttempt::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'quiz_id' => $quiz->id,
|
||||||
|
'started_at' => now(),
|
||||||
|
'status' => 'in_progress'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$quiz->load(['questions' => function($q) {
|
||||||
|
$q->orderBy('order', 'asc');
|
||||||
|
}, 'questions.options']);
|
||||||
|
|
||||||
|
// SECURITY: Hide the correct answers and explanations before sending to mobile app
|
||||||
|
$quiz->questions->each(function ($question) {
|
||||||
|
$question->makeHidden(['solution_explanation']);
|
||||||
|
$question->options->each->makeHidden(['is_correct']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'attempt_id' => $attempt->id,
|
||||||
|
'quiz' => $quiz
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->success($data, 'موفق', 'آزمون با موفقیت شروع شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 5. Submit the Quiz & Calculate Score
|
||||||
|
*/
|
||||||
|
public function submit(Request $request, UserQuizAttempt $attempt)
|
||||||
|
{
|
||||||
|
if ($attempt->user_id !== auth()->id() || $attempt->status !== 'in_progress') {
|
||||||
|
return response()->json(['message' => 'دسترسی غیرمجاز یا آزمون قبلاً پایان یافته است.'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'answers' => 'required|array',
|
||||||
|
'answers.*.question_id' => 'required|exists:questions,id',
|
||||||
|
'answers.*.selected_option_id' => 'nullable|exists:question_options,id',
|
||||||
|
'answers.*.text_answer' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$quiz = $attempt->quiz;
|
||||||
|
|
||||||
|
$totalScore = 0.00;
|
||||||
|
$correctCount = 0;
|
||||||
|
$wrongCount = 0;
|
||||||
|
$unansweredCount = 0;
|
||||||
|
$needsGrading = false;
|
||||||
|
|
||||||
|
DB::beginTransaction();
|
||||||
|
try {
|
||||||
|
foreach ($request->answers as $ans) {
|
||||||
|
$question = Question::with('options')->find($ans['question_id']);
|
||||||
|
|
||||||
|
$isCorrect = false;
|
||||||
|
$earnedScore = 0.00;
|
||||||
|
|
||||||
|
// Handle Multiple Choice
|
||||||
|
if ($question->type === 'multiple_choice') {
|
||||||
|
if (empty($ans['selected_option_id'])) {
|
||||||
|
$unansweredCount++;
|
||||||
|
$isCorrect = null;
|
||||||
|
} else {
|
||||||
|
$correctOption = $question->options->where('is_correct', true)->first();
|
||||||
|
|
||||||
|
if ($correctOption && $correctOption->id == $ans['selected_option_id']) {
|
||||||
|
$isCorrect = true;
|
||||||
|
$earnedScore = $question->score;
|
||||||
|
$totalScore += $earnedScore;
|
||||||
|
$correctCount++;
|
||||||
|
} else {
|
||||||
|
$isCorrect = false;
|
||||||
|
if ($quiz->has_negative_score) {
|
||||||
|
$earnedScore = -($question->score / 3);
|
||||||
|
$totalScore += $earnedScore;
|
||||||
|
}
|
||||||
|
$wrongCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle Descriptive
|
||||||
|
else if ($question->type === 'descriptive') {
|
||||||
|
$needsGrading = true;
|
||||||
|
$isCorrect = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserQuizAnswer::create([
|
||||||
|
'attempt_id' => $attempt->id,
|
||||||
|
'question_id' => $question->id,
|
||||||
|
'selected_option_id' => $ans['selected_option_id'] ?? null,
|
||||||
|
'text_answer' => $ans['text_answer'] ?? null,
|
||||||
|
'is_correct' => $isCorrect,
|
||||||
|
'earned_score' => $earnedScore,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt->update([
|
||||||
|
'completed_at' => now(),
|
||||||
|
'total_score' => $totalScore,
|
||||||
|
'correct_answers' => $correctCount,
|
||||||
|
'wrong_answers' => $wrongCount,
|
||||||
|
'unanswered' => $unansweredCount,
|
||||||
|
'status' => $needsGrading ? 'pending_grading' : 'completed',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::commit();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'attempt_id' => $attempt->id,
|
||||||
|
'status' => $attempt->status
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->success($data, 'موفق', 'آزمون با موفقیت ثبت شد.');
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
DB::rollBack();
|
||||||
|
return response()->json(['message' => 'خطا در ثبت اطلاعات آزمون.'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 6. Fetch Results
|
||||||
|
*/
|
||||||
|
public function result(UserQuizAttempt $attempt)
|
||||||
|
{
|
||||||
|
if ($attempt->user_id !== auth()->id()) {
|
||||||
|
return response()->json(['message' => 'غیرمجاز'], 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt->load([
|
||||||
|
'quiz.category',
|
||||||
|
'answers.question.options',
|
||||||
|
'answers.question.subject',
|
||||||
|
'answers.selectedOption'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success($attempt, 'موفق', 'نتیجه آزمون با موفقیت دریافت شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -285,22 +285,42 @@ class SubscribePlanController extends Controller
|
|||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
$subscribePlans = UserSubscriber::where('user_id', $user->id)
|
// 1. Fetch all active subscriptions
|
||||||
->where('expired_at', '>', now())
|
$activeSubscribers = $user->userSubscribers()
|
||||||
->orderBy('expired_at', 'desc')
|
->with('subscribe')
|
||||||
|
->where('expired_at', '>=', now())
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($subscribePlans->isNotEmpty()) {
|
if ($activeSubscribers->isNotEmpty()) {
|
||||||
$totalExpiredDays = $subscribePlans->sum('expired_day');
|
|
||||||
$latestPlan = $subscribePlans->first();
|
|
||||||
|
|
||||||
|
// 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 = [
|
$subscribePlanData = [
|
||||||
'id' => $latestPlan->id,
|
'id' => $latestSubscription->id,
|
||||||
'name' => $latestPlan->subscribe->name,
|
'name' => $latestSubscription->subscribe->name ?? 'Subscription',
|
||||||
'price' => $latestPlan->subscribe->price,
|
'price' => $latestSubscription->subscribe->price ?? 0,
|
||||||
'expired_day' => $totalExpiredDays,
|
'expired_day' => $expiredDays,
|
||||||
'is_free' => $latestPlan->subscribe->is_free == 1,
|
'is_free' => $isFree,
|
||||||
'expired_at' => Carbon::now()->addDays($totalExpiredDays)->format('Y-m-d'),
|
'expired_at' => $latestSubscription->expired_at->format('Y-m-d'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->success($subscribePlanData, 'Subscribe Plan', 'Current Subscribe Plan');
|
return $this->success($subscribePlanData, 'Subscribe Plan', 'Current Subscribe Plan');
|
||||||
|
|||||||
125
app/Http/Controllers/api/TicketApiController.php
Normal file
125
app/Http/Controllers/api/TicketApiController.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Ticket;
|
||||||
|
use App\Models\TicketMessage;
|
||||||
|
use App\Traits\BaseApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TicketApiController extends Controller
|
||||||
|
{
|
||||||
|
use BaseApiResponse;
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$tickets = Ticket::where('user_id', auth()->id())
|
||||||
|
->orderBy('updated_at', 'desc')
|
||||||
|
->get()
|
||||||
|
->map(function ($ticket) {
|
||||||
|
return [
|
||||||
|
'id' => $ticket->id,
|
||||||
|
'subject' => $ticket->subject,
|
||||||
|
'status' => $ticket->status,
|
||||||
|
'priority' => $ticket->priority,
|
||||||
|
'created_at' => $ticket->created_at,
|
||||||
|
'updated_at' => $ticket->updated_at,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->success($tickets, 'موفق', 'لیست تیکتها با موفقیت دریافت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show($id)
|
||||||
|
{
|
||||||
|
$ticket = Ticket::with(['messages' => function ($query) {
|
||||||
|
$query->orderBy('created_at', 'asc');
|
||||||
|
}])->where('user_id', auth()->id())->find($id);
|
||||||
|
|
||||||
|
if (!$ticket) {
|
||||||
|
return $this->failed([], 'یافت نشد', 'تیکت مورد نظر یافت نشد.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedTicket = [
|
||||||
|
'id' => $ticket->id,
|
||||||
|
'subject' => $ticket->subject,
|
||||||
|
'status' => $ticket->status,
|
||||||
|
'priority' => $ticket->priority,
|
||||||
|
'created_at' => $ticket->created_at,
|
||||||
|
'messages' => $ticket->messages->map(function ($msg) {
|
||||||
|
return [
|
||||||
|
'id' => $msg->id,
|
||||||
|
// If user_id matches the logged in user, it's their message. Otherwise, it's admin's reply.
|
||||||
|
'is_me' => $msg->user_id === auth()->id(),
|
||||||
|
'message' => $msg->message,
|
||||||
|
'attachment_url' => $msg->attachment_path ? asset('storage/' . $msg->attachment_path) : null,
|
||||||
|
'created_at' => $msg->created_at,
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->success($formattedTicket, 'موفق', 'جزئیات تیکت دریافت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'subject' => 'required|string|max:255',
|
||||||
|
'priority' => 'nullable|in:low,medium,high',
|
||||||
|
'message' => 'required|string',
|
||||||
|
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120', // Max 5MB
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket = Ticket::create([
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'subject' => $validated['subject'],
|
||||||
|
'status' => 'open',
|
||||||
|
'priority' => $validated['priority'] ?? 'medium',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$attachmentPath = null;
|
||||||
|
if ($request->hasFile('attachment')) {
|
||||||
|
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketMessage::create([
|
||||||
|
'ticket_id' => $ticket->id,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'message' => $validated['message'],
|
||||||
|
'attachment_path' => $attachmentPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success(['ticket_id' => $ticket->id], 'موفق', 'تیکت شما با موفقیت ثبت شد');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reply(Request $request, $id)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'message' => 'required|string',
|
||||||
|
'attachment' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:5120',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket = Ticket::where('user_id', auth()->id())->find($id);
|
||||||
|
|
||||||
|
if (!$ticket) {
|
||||||
|
return $this->failed([], 'خطا', 'تیکت یافت نشد', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachmentPath = null;
|
||||||
|
if ($request->hasFile('attachment')) {
|
||||||
|
$attachmentPath = $request->file('attachment')->store('tickets', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
TicketMessage::create([
|
||||||
|
'ticket_id' => $ticket->id,
|
||||||
|
'user_id' => auth()->id(),
|
||||||
|
'message' => $validated['message'],
|
||||||
|
'attachment_path' => $attachmentPath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket->update(['status' => 'user_reply']);
|
||||||
|
|
||||||
|
return $this->success([], 'موفق', 'پاسخ شما با موفقیت ثبت شد');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,8 +58,8 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
$perPage = $request->per_page ?? 10;
|
$perPage = $request->per_page ?? 10;
|
||||||
if ($bookId) {
|
if ($bookId) {
|
||||||
|
// ترتیب درست اعمال شد
|
||||||
$book = Book::with(['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])
|
$book = Book::with(['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])
|
||||||
->find($bookId);
|
->find($bookId);
|
||||||
|
|
||||||
if (! $book) {
|
if (! $book) {
|
||||||
@@ -68,7 +68,7 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$foundRelation = false;
|
$foundRelation = false;
|
||||||
foreach (['divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'] as $relation) {
|
foreach (['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'] as $relation) {
|
||||||
if ($book->{$relation}->count() > 0) {
|
if ($book->{$relation}->count() > 0) {
|
||||||
$foundRelation = true;
|
$foundRelation = true;
|
||||||
$items = $book->{$relation}()->paginate($perPage);
|
$items = $book->{$relation}()->paginate($perPage);
|
||||||
@@ -96,7 +96,7 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($division_id) {
|
if ($division_id) {
|
||||||
$division = Division::with(['sections', 'chapters', 'parts', 'gate', 'branch', 'art'])
|
$division = Division::with(['sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])
|
||||||
->find($division_id);
|
->find($division_id);
|
||||||
|
|
||||||
if (! $division) {
|
if (! $division) {
|
||||||
@@ -105,7 +105,7 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$foundRelation = false;
|
$foundRelation = false;
|
||||||
foreach (['sections', 'chapters', 'parts', 'gate', 'branch', 'art'] as $relation) {
|
foreach (['sections', 'chapters', 'parts', 'gates', 'branchs', 'art'] as $relation) {
|
||||||
if ($division->{$relation}->count() > 0) {
|
if ($division->{$relation}->count() > 0) {
|
||||||
$foundRelation = true;
|
$foundRelation = true;
|
||||||
$items = $division->{$relation}()->paginate($perPage);
|
$items = $division->{$relation}()->paginate($perPage);
|
||||||
@@ -130,7 +130,7 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($section_id) {
|
if ($section_id) {
|
||||||
$section = Section::with(['chapters', 'parts', 'gate', 'branch', 'art'])
|
$section = Section::with(['chapters', 'parts', 'gates', 'branchs', 'art'])
|
||||||
->find($section_id);
|
->find($section_id);
|
||||||
|
|
||||||
if (! $section) {
|
if (! $section) {
|
||||||
@@ -139,7 +139,7 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$foundRelation = false;
|
$foundRelation = false;
|
||||||
foreach (['chapters', 'parts', 'gate', 'branch', 'art'] as $relation) {
|
foreach (['chapters', 'parts', 'gates', 'branchs', 'art'] as $relation) {
|
||||||
if ($section->{$relation}->count() > 0) {
|
if ($section->{$relation}->count() > 0) {
|
||||||
$foundRelation = true;
|
$foundRelation = true;
|
||||||
$items = $section->{$relation}()->paginate($perPage);
|
$items = $section->{$relation}()->paginate($perPage);
|
||||||
@@ -164,7 +164,7 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($volumeId) {
|
if ($volumeId) {
|
||||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])
|
$volume = Volum::with(['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])
|
||||||
->find($volumeId);
|
->find($volumeId);
|
||||||
if (! $volume) {
|
if (! $volume) {
|
||||||
return $this->success([], 'Volume not found');
|
return $this->success([], 'Volume not found');
|
||||||
@@ -196,8 +196,42 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($chapter_id) {
|
||||||
|
$chapter = Chapter::with(['parts', 'gates', 'branchs', 'art'])
|
||||||
|
->find($chapter_id);
|
||||||
|
|
||||||
|
if (! $chapter) {
|
||||||
|
return $this->success([], 'Chapter not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
$foundRelation = false;
|
||||||
|
foreach (['parts', 'gates', 'branchs', '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('id', $item->law_id)->first()?->is_locked,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($foundRelation) {
|
||||||
|
return $this->success($data, 'Success');
|
||||||
|
} else {
|
||||||
|
return $this->success([], 'No relations found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($part_id) {
|
if ($part_id) {
|
||||||
$part = Part::with(['gate', 'branch', 'art'])
|
$part = Part::with(['gates', 'branchs', 'art'])
|
||||||
->find($part_id);
|
->find($part_id);
|
||||||
|
|
||||||
if (! $part) {
|
if (! $part) {
|
||||||
@@ -206,7 +240,7 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$foundRelation = false;
|
$foundRelation = false;
|
||||||
foreach (['gate', 'branch', 'art'] as $relation) {
|
foreach (['gates', 'branchs', 'art'] as $relation) {
|
||||||
if ($part->{$relation}->count() > 0) {
|
if ($part->{$relation}->count() > 0) {
|
||||||
$foundRelation = true;
|
$foundRelation = true;
|
||||||
$items = $part->{$relation}()->paginate($perPage);
|
$items = $part->{$relation}()->paginate($perPage);
|
||||||
@@ -230,20 +264,20 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($chapter_id) {
|
if ($gate_id) {
|
||||||
$chapter = Chapter::with(['parts', 'gate', 'branch', 'art'])
|
$gate = Gate::with(['branchs', 'art'])
|
||||||
->find($chapter_id);
|
->find($gate_id);
|
||||||
|
|
||||||
if (! $chapter) {
|
if (! $gate) {
|
||||||
return $this->success([], 'Chapter not found');
|
return $this->success([], 'Gate not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [];
|
$data = [];
|
||||||
$foundRelation = false;
|
$foundRelation = false;
|
||||||
foreach (['parts', 'gate', 'branch', 'art'] as $relation) {
|
foreach (['branchs', 'art'] as $relation) {
|
||||||
if ($chapter->{$relation}->count() > 0) {
|
if ($gate->{$relation}->count() > 0) {
|
||||||
$foundRelation = true;
|
$foundRelation = true;
|
||||||
$items = $chapter->{$relation}()->paginate($perPage);
|
$items = $gate->{$relation}()->paginate($perPage);
|
||||||
foreach ($items as $item) {
|
foreach ($items as $item) {
|
||||||
$data[] = [
|
$data[] = [
|
||||||
'id' => $item->id,
|
'id' => $item->id,
|
||||||
@@ -298,74 +332,6 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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('id', $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('id', $item->law_id)->first()?->is_locked,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($foundRelation) {
|
|
||||||
return $this->success($data, 'Success');
|
|
||||||
} else {
|
|
||||||
return $this->success([], 'No relations found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($law_Id) {
|
if ($law_Id) {
|
||||||
$law = Law::find($law_Id);
|
$law = Law::find($law_Id);
|
||||||
|
|
||||||
@@ -579,12 +545,12 @@ class VolumController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($lawId) {
|
if ($lawId) {
|
||||||
$law = Law::with(['volums', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'arts'])->find($lawId);
|
$law = Law::with(['volums', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'arts'])->find($lawId);
|
||||||
if (! $law) {
|
if (! $law) {
|
||||||
return $this->success([], 'Law not found');
|
return $this->success([], 'Law not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($law, ['volums', 'book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'arts'], $perPage);
|
$data = $this->checkRelations($law, ['volums', 'book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'arts'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
@@ -592,27 +558,13 @@ class VolumController extends Controller
|
|||||||
return $this->success([], 'No related records found in the law');
|
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) {
|
if ($volumeId) {
|
||||||
$volume = Volum::with(['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'])->find($volumeId);
|
$volume = Volum::with(['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])->find($volumeId);
|
||||||
if (! $volume) {
|
if (! $volume) {
|
||||||
return $this->success([], 'Volume not found');
|
return $this->success([], 'Volume not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($volume, ['book', 'divisions', 'sections', 'gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
$data = $this->checkRelations($volume, ['book', 'divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
@@ -620,27 +572,27 @@ class VolumController extends Controller
|
|||||||
return $this->success([], 'No related records found in the volume');
|
return $this->success([], 'No related records found in the volume');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sectionId) {
|
if ($bookId) {
|
||||||
$section = Section::with(['gates', 'parts', 'chapters', 'branchs', 'art'])->find($sectionId);
|
$book = Book::with(['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])->find($bookId);
|
||||||
if (! $section) {
|
if (! $book) {
|
||||||
return $this->success([], 'Section not found');
|
return $this->success([], 'Book not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($section, ['gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
$data = $this->checkRelations($book, ['divisions', 'sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success([], 'No related records found in the section');
|
return $this->success([], 'No related records found in the book');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($divisionId) {
|
if ($divisionId) {
|
||||||
$division = Division::with(['gates', 'parts', 'chapters', 'branchs', 'art'])->find($divisionId);
|
$division = Division::with(['sections', 'chapters', 'parts', 'gates', 'branchs', 'art'])->find($divisionId);
|
||||||
if (! $division) {
|
if (! $division) {
|
||||||
return $this->success([], 'Division not found');
|
return $this->success([], 'Division not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($division, ['gates', 'parts', 'chapters', 'branchs', 'art'], $perPage);
|
$data = $this->checkRelations($division, ['sections', 'chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
@@ -648,41 +600,27 @@ class VolumController extends Controller
|
|||||||
return $this->success([], 'No related records found in the division');
|
return $this->success([], 'No related records found in the division');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($partId) {
|
if ($sectionId) {
|
||||||
$part = Part::with(['gates', 'art'])->find($partId);
|
$section = Section::with(['chapters', 'parts', 'gates', 'branchs', 'art'])->find($sectionId);
|
||||||
if (! $part) {
|
if (! $section) {
|
||||||
return $this->success([], 'Part not found');
|
return $this->success([], 'Section not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($part, ['gates', 'art'], $perPage);
|
$data = $this->checkRelations($section, ['chapters', 'parts', 'gates', 'branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success([], 'No related records found in the part');
|
return $this->success([], 'No related records found in the section');
|
||||||
}
|
|
||||||
|
|
||||||
if ($branchId) {
|
|
||||||
$branch = Branch::with(['gates', 'chapters', 'art'])->find($branchId);
|
|
||||||
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) {
|
if ($chapterId) {
|
||||||
$chapter = Chapter::with(['gate', 'parts', 'art'])->find($chapterId);
|
$chapter = Chapter::with(['parts', 'gates', 'branchs', 'art'])->find($chapterId);
|
||||||
if (! $chapter) {
|
if (! $chapter) {
|
||||||
return $this->success([], 'Chapter not found');
|
return $this->success([], 'Chapter not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($chapter, ['gate', 'parts', 'art'], $perPage);
|
$data = $this->checkRelations($chapter, ['parts', 'gates', 'branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
@@ -690,13 +628,27 @@ class VolumController extends Controller
|
|||||||
return $this->success([], 'No related records found in the chapter');
|
return $this->success([], 'No related records found in the chapter');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($partId) {
|
||||||
|
$part = Part::with(['gates', 'branchs', 'art'])->find($partId);
|
||||||
|
if (! $part) {
|
||||||
|
return $this->success([], 'Part not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->checkRelations($part, ['gates', 'branchs', 'art'], $perPage);
|
||||||
|
if ($data) {
|
||||||
|
return $this->success($data, 'Success');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([], 'No related records found in the part');
|
||||||
|
}
|
||||||
|
|
||||||
if ($gateId) {
|
if ($gateId) {
|
||||||
$gate = Gate::with(['art'])->find($gateId);
|
$gate = Gate::with(['branchs', 'art'])->find($gateId);
|
||||||
if (! $gate) {
|
if (! $gate) {
|
||||||
return $this->success([], 'Gate not found');
|
return $this->success([], 'Gate not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $this->checkRelations($gate, ['art'], $perPage);
|
$data = $this->checkRelations($gate, ['branchs', 'art'], $perPage);
|
||||||
if ($data) {
|
if ($data) {
|
||||||
return $this->success($data, 'Success');
|
return $this->success($data, 'Success');
|
||||||
}
|
}
|
||||||
@@ -704,6 +656,20 @@ class VolumController extends Controller
|
|||||||
return $this->success([], 'No related records found in the gate');
|
return $this->success([], 'No related records found in the gate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($branchId) {
|
||||||
|
$branch = Branch::with(['art'])->find($branchId);
|
||||||
|
if (! $branch) {
|
||||||
|
return $this->success([], 'Branch not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->checkRelations($branch, ['art'], $perPage);
|
||||||
|
if ($data) {
|
||||||
|
return $this->success($data, 'Success');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success([], 'No related records found in the branch');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success([], 'No valid filter specified');
|
return $this->success([], 'No valid filter specified');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,4 +823,5 @@ class VolumController extends Controller
|
|||||||
|
|
||||||
return array_values($route);
|
return array_values($route);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
app/Models/ArtHighlight.php
Normal file
8
app/Models/ArtHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ArtHighlight extends Model {
|
||||||
|
protected $fillable = ['user_id', 'art_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||||
|
}
|
||||||
8
app/Models/BylawHighlight.php
Normal file
8
app/Models/BylawHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BylawHighlight extends Model {
|
||||||
|
protected $fillable = ['user_id', 'bylaw_document_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||||
|
}
|
||||||
11
app/Models/BylawNote.php
Normal file
11
app/Models/BylawNote.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class BylawNote extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'bylaw_notes';
|
||||||
|
protected $guarded = [];
|
||||||
|
}
|
||||||
@@ -16,6 +16,21 @@ class Folder extends Model
|
|||||||
|
|
||||||
public function arts()
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
app/Models/FolderBylaw.php
Normal file
13
app/Models/FolderBylaw.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class FolderBylaw extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $table = 'folder_bylaw';
|
||||||
|
protected $fillable = ['folder_id', 'bylaw_document_id'];
|
||||||
|
}
|
||||||
13
app/Models/FolderJudicialPrecedent.php
Normal file
13
app/Models/FolderJudicialPrecedent.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class FolderJudicialPrecedent extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $table = 'folder_judicial_precedent';
|
||||||
|
protected $fillable = ['folder_id', 'judicial_precedent_id'];
|
||||||
|
}
|
||||||
13
app/Models/FolderLegalOpinion.php
Normal file
13
app/Models/FolderLegalOpinion.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class FolderLegalOpinion extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
protected $table = 'folder_legal_opinion';
|
||||||
|
protected $fillable = ['folder_id', 'legal_opinion_id'];
|
||||||
|
}
|
||||||
29
app/Models/GoldCurrency.php
Normal file
29
app/Models/GoldCurrency.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class GoldCurrency extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'name_en',
|
||||||
|
'price',
|
||||||
|
'change_percent',
|
||||||
|
'change_value',
|
||||||
|
'symbol',
|
||||||
|
'unit',
|
||||||
|
'time_unix',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'price' => 'integer',
|
||||||
|
'change_percent' => 'float',
|
||||||
|
'change_value' => 'integer',
|
||||||
|
'time_unix' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
8
app/Models/JudicialPrecedentHighlight.php
Normal file
8
app/Models/JudicialPrecedentHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class JudicialPrecedentHighlight extends Model {
|
||||||
|
protected $fillable = ['user_id', 'judicial_precedent_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||||
|
}
|
||||||
11
app/Models/JudicialPrecedentNote.php
Normal file
11
app/Models/JudicialPrecedentNote.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class JudicialPrecedentNote extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'judicial_precedent_notes';
|
||||||
|
protected $guarded = [];
|
||||||
|
}
|
||||||
8
app/Models/LegalOpinionHighlight.php
Normal file
8
app/Models/LegalOpinionHighlight.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class LegalOpinionHighlight extends Model {
|
||||||
|
protected $fillable = ['user_id', 'legal_opinion_id', 'start_index', 'end_index', 'selected_text', 'color_code'];
|
||||||
|
}
|
||||||
11
app/Models/LegalOpinionNote.php
Normal file
11
app/Models/LegalOpinionNote.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class LegalOpinionNote extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'legal_opinion_notes';
|
||||||
|
protected $guarded = [];
|
||||||
|
}
|
||||||
47
app/Models/PvpMatch.php
Normal file
47
app/Models/PvpMatch.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PvpMatch extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'status',
|
||||||
|
'started_at',
|
||||||
|
'ended_at',
|
||||||
|
'winner_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'ended_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// The two players in this match
|
||||||
|
public function players()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PvpMatchPlayer::class, 'pvp_match_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// All answers submitted in this match
|
||||||
|
public function answers()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PvpMatchAnswer::class, 'pvp_match_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 5 random questions assigned to this match via pivot table
|
||||||
|
public function questions()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Question::class, 'pvp_match_questions', 'pvp_match_id', 'question_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user who won
|
||||||
|
public function winner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'winner_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Models/PvpMatchAnswer.php
Normal file
47
app/Models/PvpMatchAnswer.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PvpMatchAnswer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pvp_match_id',
|
||||||
|
'user_id',
|
||||||
|
'question_id',
|
||||||
|
'selected_option_id',
|
||||||
|
'is_correct',
|
||||||
|
'time_taken',
|
||||||
|
'points_earned',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_correct' => 'boolean',
|
||||||
|
'time_taken' => 'float',
|
||||||
|
'points_earned' => 'float',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function match()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PvpMatch::class, 'pvp_match_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function question()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Question::class, 'question_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectedOption()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(QuestionOption::class, 'selected_option_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Models/PvpMatchPlayer.php
Normal file
31
app/Models/PvpMatchPlayer.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PvpMatchPlayer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'pvp_match_id',
|
||||||
|
'user_id',
|
||||||
|
'score',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'score' => 'float',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function match()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PvpMatch::class, 'pvp_match_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Models/Question.php
Normal file
46
app/Models/Question.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Question extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'quiz_id',
|
||||||
|
'subject_id',
|
||||||
|
'question_text',
|
||||||
|
'type',
|
||||||
|
'score',
|
||||||
|
'solution_explanation',
|
||||||
|
'order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'score' => 'float',
|
||||||
|
'order' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function quiz()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Quiz::class, 'quiz_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subject()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(QuizSubject::class, 'subject_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function options()
|
||||||
|
{
|
||||||
|
return $this->hasMany(QuestionOption::class, 'question_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userAnswers()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserQuizAnswer::class, 'question_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Models/QuestionOption.php
Normal file
26
app/Models/QuestionOption.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class QuestionOption extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'question_id',
|
||||||
|
'option_text',
|
||||||
|
'is_correct',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_correct' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function question()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Question::class, 'question_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Models/Quiz.php
Normal file
47
app/Models/Quiz.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Quiz extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'category_id',
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'year',
|
||||||
|
'type',
|
||||||
|
'duration_minutes',
|
||||||
|
'has_negative_score',
|
||||||
|
'is_free',
|
||||||
|
'is_published',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'year' => 'integer',
|
||||||
|
'duration_minutes' => 'integer',
|
||||||
|
'has_negative_score' => 'boolean',
|
||||||
|
'is_free' => 'boolean',
|
||||||
|
'is_published' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function category()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(QuizCategory::class, 'category_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function questions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Question::class, 'quiz_id')->orderBy('order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attempts()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserQuizAttempt::class, 'quiz_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Models/QuizCategory.php
Normal file
27
app/Models/QuizCategory.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class QuizCategory extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function quizzes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Quiz::class, 'category_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Models/QuizSubject.php
Normal file
21
app/Models/QuizSubject.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class QuizSubject extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'title',
|
||||||
|
'slug',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function questions()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Question::class, 'subject_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/Ticket.php
Normal file
25
app/Models/Ticket.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Ticket extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'subject',
|
||||||
|
'status', // 'open', 'answered', 'user_reply', 'closed'
|
||||||
|
'priority', // 'low', 'medium', 'high'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return $this->hasMany(TicketMessage::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Models/TicketMessage.php
Normal file
26
app/Models/TicketMessage.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class TicketMessage extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'ticket_id',
|
||||||
|
'user_id',
|
||||||
|
'message',
|
||||||
|
'attachment_path',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function ticket()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ticket::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Models/UserQuizAnswer.php
Normal file
41
app/Models/UserQuizAnswer.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserQuizAnswer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'attempt_id',
|
||||||
|
'question_id',
|
||||||
|
'selected_option_id',
|
||||||
|
'text_answer',
|
||||||
|
'is_correct',
|
||||||
|
'earned_score',
|
||||||
|
'reviewer_feedback',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_correct' => 'boolean',
|
||||||
|
'earned_score' => 'float',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function attempt()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(UserQuizAttempt::class, 'attempt_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function question()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Question::class, 'question_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectedOption()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(QuestionOption::class, 'selected_option_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Models/UserQuizAttempt.php
Normal file
47
app/Models/UserQuizAttempt.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class UserQuizAttempt extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'quiz_id',
|
||||||
|
'started_at',
|
||||||
|
'completed_at',
|
||||||
|
'total_score',
|
||||||
|
'correct_answers',
|
||||||
|
'wrong_answers',
|
||||||
|
'unanswered',
|
||||||
|
'status',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'started_at' => 'datetime',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
'total_score' => 'float',
|
||||||
|
'correct_answers' => 'integer',
|
||||||
|
'wrong_answers' => 'integer',
|
||||||
|
'unanswered' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function quiz()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Quiz::class, 'quiz_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function answers()
|
||||||
|
{
|
||||||
|
return $this->hasMany(UserQuizAnswer::class, 'attempt_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
234
resources/views/admin/dashboard/index.blade.php
Normal file
234
resources/views/admin/dashboard/index.blade.php
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<!-- 1. Statistics Cards -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mini-stats-wid text-white bg-primary">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-white-50 font-weight-medium mb-2">کل کاربران</p>
|
||||||
|
<h4 class="text-white mb-0">{{ number_format($totalUsers) }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="mini-stat-icon avatar-sm rounded-circle bg-white align-self-center">
|
||||||
|
<span class="avatar-title bg-white text-primary"><i class="mdi mdi-account-group font-size-24"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mini-stats-wid text-white bg-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-white-50 font-weight-medium mb-2">اشتراکهای فعال (پولی)</p>
|
||||||
|
<h4 class="text-white mb-2">{{ number_format($paidSubscribers) }}</h4>
|
||||||
|
|
||||||
|
<!-- Breakdown of Paid Plans -->
|
||||||
|
<div>
|
||||||
|
@foreach($paidBreakdown as $plan)
|
||||||
|
<span class="badge bg-light text-success font-size-12 me-1 mb-1 shadow-sm">
|
||||||
|
{{ $plan->display_name }}: {{ $plan->total }}
|
||||||
|
</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="mini-stat-icon avatar-sm rounded-circle bg-white align-self-center">
|
||||||
|
<span class="avatar-title bg-white text-success"><i class="mdi mdi-cash-multiple font-size-24"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card mini-stats-wid text-white bg-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<p class="text-white-50 font-weight-medium mb-2">اشتراکهای فعال (رایگان)</p>
|
||||||
|
<h4 class="text-white mb-0">{{ number_format($freeSubscribers) }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="mini-stat-icon avatar-sm rounded-circle bg-white align-self-center">
|
||||||
|
<span class="avatar-title bg-white text-info"><i class="mdi mdi-gift font-size-24"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Charts Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<!-- Line Chart: New Users -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">ثبتنام کاربران جدید (۷ روز گذشته)</h4>
|
||||||
|
<div id="users-line-chart" style="height: 350px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column Chart: Subscriptions -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">اشتراکهای ثبت شده (۷ روز گذشته)</h4>
|
||||||
|
<div id="subs-column-chart" style="height: 350px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. Recent Tables Section -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Tickets -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">آخرین تیکتها</h4>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-centered table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>کاربر</th>
|
||||||
|
<th>موضوع</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>مشاهده</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($recentTickets as $ticket)
|
||||||
|
<tr>
|
||||||
|
<td>{{ optional($ticket->user)->mobile ?? 'ناشناس' }}</td>
|
||||||
|
<td>{{ Str::limit($ticket->subject, 30) }}</td>
|
||||||
|
<td>
|
||||||
|
@if($ticket->status === 'open' || $ticket->status === 'user_reply')
|
||||||
|
<span class="badge bg-danger">نیاز به پاسخ</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-success">پاسخ داده شده</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td><a href="{{ route('tickets.show', $ticket->id) }}" class="btn btn-sm btn-primary">مشاهده</a></td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="4" class="text-center">تیکتی وجود ندارد</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Suggestions -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">آخرین پیشنهادات</h4>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-centered table-nowrap mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>کاربر</th>
|
||||||
|
<th>پیشنهاد</th>
|
||||||
|
<th>تاریخ ثبت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($recentSuggestions as $suggestion)
|
||||||
|
<tr>
|
||||||
|
<td>{{ optional($suggestion->user)->mobile ?? 'ناشناس' }}</td>
|
||||||
|
<td title="{{ $suggestion->text }}">{{ Str::limit($suggestion->text, 40) }}</td>
|
||||||
|
<td dir="ltr">{{ \Morilog\Jalali\Jalalian::fromCarbon($suggestion->created_at)->format('Y/m/d H:i') }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="3" class="text-center">پیشنهادی وجود ندارد</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// --- 1. Line Chart: New Users ---
|
||||||
|
var userOptions = {
|
||||||
|
series: [{
|
||||||
|
name: "کاربران جدید",
|
||||||
|
data: {!! json_encode($newUsersData) !!}
|
||||||
|
}],
|
||||||
|
chart: {
|
||||||
|
height: 350,
|
||||||
|
type: 'line',
|
||||||
|
fontFamily: 'Vazirmatn, Tahoma, sans-serif',
|
||||||
|
toolbar: { show: false }
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
width: 3,
|
||||||
|
curve: 'smooth' // Makes the line wavy and modern
|
||||||
|
},
|
||||||
|
colors: ['#007bff'],
|
||||||
|
xaxis: {
|
||||||
|
categories: {!! json_encode($chartDates) !!}
|
||||||
|
},
|
||||||
|
dataLabels: { enabled: false }
|
||||||
|
};
|
||||||
|
var userChart = new ApexCharts(document.querySelector("#users-line-chart"), userOptions);
|
||||||
|
userChart.render();
|
||||||
|
|
||||||
|
|
||||||
|
// --- 2. Column Chart: Subscriptions ---
|
||||||
|
var subsOptions = {
|
||||||
|
series: [{
|
||||||
|
name: 'اشتراک پولی',
|
||||||
|
data: {!! json_encode($paidSubsData) !!}
|
||||||
|
}, {
|
||||||
|
name: 'اشتراک رایگان',
|
||||||
|
data: {!! json_encode($freeSubsData) !!}
|
||||||
|
}],
|
||||||
|
chart: {
|
||||||
|
type: 'bar', // 'bar' is used for column charts in ApexCharts
|
||||||
|
height: 350,
|
||||||
|
fontFamily: 'Vazirmatn, Tahoma, sans-serif',
|
||||||
|
toolbar: { show: false }
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
horizontal: false,
|
||||||
|
columnWidth: '50%',
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataLabels: { enabled: false },
|
||||||
|
stroke: { show: true, width: 2, colors: ['transparent'] },
|
||||||
|
colors: ['#28a745', '#17a2b8'], // Green for paid, Info/Blue for free
|
||||||
|
xaxis: {
|
||||||
|
categories: {!! json_encode($chartDates) !!}
|
||||||
|
},
|
||||||
|
fill: { opacity: 1 },
|
||||||
|
tooltip: {
|
||||||
|
y: {
|
||||||
|
formatter: function (val) {
|
||||||
|
return val + " عدد"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var subsChart = new ApexCharts(document.querySelector("#subs-column-chart"), subsOptions);
|
||||||
|
subsChart.render();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<div class="rightbar-overlay"></div>
|
<div class="rightbar-overlay"></div>
|
||||||
|
|
||||||
@include('admin.partial.scripts')
|
@include('admin.partial.scripts')
|
||||||
|
@yield('scripts')
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -23,6 +23,51 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('tickets.index') }}" class="waves-effect">
|
||||||
|
<i class="mdi mdi-headset"></i>
|
||||||
|
<span>تیکتهای پشتیبانی</span>
|
||||||
|
@php
|
||||||
|
$openTicketsCount = \App\Models\Ticket::whereIn('status', ['open', 'user_reply'])->count();
|
||||||
|
@endphp
|
||||||
|
@if($openTicketsCount > 0)
|
||||||
|
<span class="badge badge-pill badge-danger float-right">{{ $openTicketsCount }}</span>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="javascript:%20void(0);" class="has-arrow waves-effect">
|
||||||
|
<i class="mdi mdi-account-circle-outline"></i>
|
||||||
|
<span>پیشنهادات</span>
|
||||||
|
</a>
|
||||||
|
<ul class="sub-menu" aria-expanded="false">
|
||||||
|
<li><a href="{{route('suggestions.index')}}">پیشنهادات</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('quizzes.index') }}" class="waves-effect">
|
||||||
|
<i class="mdi mdi-format-list-checks"></i>
|
||||||
|
<span>مدیریت آزمونها</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('quiz-categories.index') }}" class="waves-effect">
|
||||||
|
<i class="mdi mdi-shape-plus"></i>
|
||||||
|
<span>دستهبندی آزمونها</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('quiz-subjects.index') }}" class="waves-effect">
|
||||||
|
<i class="mdi mdi-book-open-page-variant"></i>
|
||||||
|
<span>مدیریت دروس</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="javascript:%20void(0);" class="has-arrow waves-effect">
|
<a href="javascript:%20void(0);" class="has-arrow waves-effect">
|
||||||
<i class="mdi mdi-account-circle-outline"></i>
|
<i class="mdi mdi-account-circle-outline"></i>
|
||||||
@@ -256,15 +301,6 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
|
||||||
<a href="javascript:%20void(0);" class="has-arrow waves-effect">
|
|
||||||
<i class="mdi mdi-account-circle-outline"></i>
|
|
||||||
<span>پیشنهادات</span>
|
|
||||||
</a>
|
|
||||||
<ul class="sub-menu" aria-expanded="false">
|
|
||||||
<li><a href="{{route('suggestions.index')}}">پیشنهادات</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
82
resources/views/admin/quiz-categories/index.blade.php
Normal file
82
resources/views/admin/quiz-categories/index.blade.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<!-- Form to Add Category -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="mb-0 text-white">افزودن دستهبندی جدید</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="{{ route('quiz-categories.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">عنوان دستهبندی <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control" placeholder="مثال: کانون وکلا" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">توضیحات (اختیاری)</label>
|
||||||
|
<textarea name="description" class="form-control" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_active" id="isActive" checked>
|
||||||
|
<label class="form-check-label" for="isActive">فعال باشد</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success w-100">ثبت دستهبندی</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of Categories -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-4">دستهبندیهای موجود</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>عنوان</th>
|
||||||
|
<th>تعداد آزمونها</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($categories as $category)
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ $category->title }}</strong></td>
|
||||||
|
<td>{{ $category->quizzes_count }} آزمون</td>
|
||||||
|
<td>
|
||||||
|
@if($category->is_active)
|
||||||
|
<span class="badge bg-success">فعال</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-danger">غیرفعال</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{ route('quiz-categories.destroy', $category->id) }}" method="POST" class="d-inline" onsubmit="return confirm('آیا از حذف این دستهبندی اطمینان دارید؟')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button class="btn btn-sm btn-outline-danger">حذف</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center py-3">هیچ دستهبندی یافت نشد.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
77
resources/views/admin/quiz-subjects/index.blade.php
Normal file
77
resources/views/admin/quiz-subjects/index.blade.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<!-- Form to Add Subject -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="mb-0 text-white">افزودن درس جدید</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('quiz-subjects.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">عنوان درس <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control" value="{{ old('title') }}" placeholder="مثال: حقوق مدنی" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success w-100">ثبت درس</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of Subjects -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-4">دروس ثبت شده</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>عنوان درس</th>
|
||||||
|
<th>تعداد سوالات مرتبط</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($subjects as $subject)
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ $subject->title }}</strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-soft-info text-info">{{ $subject->questions_count }} سوال</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form action="{{ route('quiz-subjects.destroy', $subject->id) }}" method="POST" class="d-inline" onsubmit="return confirm('آیا از حذف این درس اطمینان دارید؟')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button class="btn btn-sm btn-outline-danger">حذف</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="text-center py-3">هیچ درسی یافت نشد.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
94
resources/views/admin/quizzes/create.blade.php
Normal file
94
resources/views/admin/quizzes/create.blade.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="mb-0 text-white">تعریف آزمون جدید</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- Display Validation Errors if any -->
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('quizzes.store') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">عنوان آزمون <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" name="title" class="form-control" value="{{ old('title') }}" placeholder="مثلاً: آزمون وکالت کانون وکلا سال ۱۴۰۲" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">دستهبندی <span class="text-danger">*</span></label>
|
||||||
|
<select name="category_id" class="form-select" required>
|
||||||
|
<option value="">انتخاب کنید...</option>
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<option value="{{ $category->id }}" {{ old('category_id') == $category->id ? 'selected' : '' }}>
|
||||||
|
{{ $category->title }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">سال برگزاری</label>
|
||||||
|
<input type="number" name="year" class="form-control" value="{{ old('year') }}" placeholder="1402">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">نوع سوالات <span class="text-danger">*</span></label>
|
||||||
|
<select name="type" class="form-select" required>
|
||||||
|
<option value="multiple_choice" {{ old('type') == 'multiple_choice' ? 'selected' : '' }}>چهار گزینهای (تستی)</option>
|
||||||
|
<option value="descriptive" {{ old('type') == 'descriptive' ? 'selected' : '' }}>تشریحی</option>
|
||||||
|
<option value="mixed" {{ old('type') == 'mixed' ? 'selected' : '' }}>ترکیبی</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">مدت زمان (دقیقه)</label>
|
||||||
|
<input type="number" name="duration_minutes" class="form-control" value="{{ old('duration_minutes', 60) }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="form-label">توضیحات آزمون</label>
|
||||||
|
<textarea name="description" class="form-control" rows="3" placeholder="توضیحات و راهنمای شرکت در آزمون...">{{ old('description') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="has_negative_score" id="negScore" {{ old('has_negative_score', true) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="negScore">محاسبه نمره منفی (برای تستی)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_free" id="isFree" {{ old('is_free') ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="isFree">آزمون رایگان است (بدون نیاز به اشتراک)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_published" id="isPub" {{ old('is_published', true) ? 'checked' : '' }}>
|
||||||
|
<label class="form-check-label" for="isPub">انتشار فوری روی اپلیکیشن</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 mt-4 text-end">
|
||||||
|
<a href="{{ route('quizzes.index') }}" class="btn btn-light me-2">انصراف</a>
|
||||||
|
<button type="submit" class="btn btn-success">ثبت و ورود به مرحله ثبت سوالات</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
71
resources/views/admin/quizzes/index.blade.php
Normal file
71
resources/views/admin/quizzes/index.blade.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h4 class="mb-0">مدیریت آزمونها</h4>
|
||||||
|
<a href="{{ route('quizzes.create') }}" class="btn btn-primary">+ تعریف آزمون جدید</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>عنوان آزمون</th>
|
||||||
|
<th>دستهبندی</th>
|
||||||
|
<th>سال</th>
|
||||||
|
<th>نوع</th>
|
||||||
|
<th>تعداد سوالات</th>
|
||||||
|
<th>مدت (دقیقه)</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($quizzes as $quiz)
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ $quiz->title }}</strong></td>
|
||||||
|
<td><span class="badge bg-soft-info text-info">{{ optional($quiz->category)->title }}</span></td>
|
||||||
|
<td>{{ $quiz->year ?? '-' }}</td>
|
||||||
|
<td>
|
||||||
|
@if($quiz->type === 'multiple_choice')
|
||||||
|
<span class="badge bg-primary">چهار گزینهای</span>
|
||||||
|
@elseif($quiz->type === 'descriptive')
|
||||||
|
<span class="badge bg-warning">تشریحی</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">ترکیبی</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>{{ $quiz->questions_count }} سوال</td>
|
||||||
|
<td>{{ $quiz->duration_minutes }} دقیقه</td>
|
||||||
|
<td>
|
||||||
|
@if($quiz->is_published)
|
||||||
|
<span class="badge bg-success">منتشر شده</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">پیشنویس</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('quizzes.questions.index', $quiz->id) }}" class="btn btn-sm btn-info me-1">مدیریت سوالات</a>
|
||||||
|
<form action="{{ route('quizzes.destroy', $quiz->id) }}" method="POST" class="d-inline" onsubmit="return confirm('آیا از حذف این آزمون اطمینان دارید؟')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button class="btn btn-sm btn-danger">حذف</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="text-center py-4">هیچ آزمونی ثبت نشده است.</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
{{ $quizzes->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
165
resources/views/admin/quizzes/questions.blade.php
Normal file
165
resources/views/admin/quizzes/questions.blade.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4>مدیریت سوالات: {{ $quiz->title }}</h4>
|
||||||
|
<a href="{{ route('quizzes.index') }}" class="btn btn-outline-secondary">بازگشت به لیست آزمونها</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Form to Add Question -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="mb-0 text-white">افزودن سوال جدید</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- 🚨 Display Validation Errors Here 🚨 -->
|
||||||
|
@if ($errors->any())
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<ul class="mb-0">
|
||||||
|
@foreach ($errors->all() as $error)
|
||||||
|
<li>{{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form action="{{ route('quizzes.questions.store', $quiz->id) }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">نوع سوال</label>
|
||||||
|
<select name="type" id="questionTypeSelect" class="form-select" onchange="toggleOptionFields(this.value)">
|
||||||
|
<option value="multiple_choice" {{ old('type') == 'multiple_choice' ? 'selected' : '' }}>چهار گزینهای (تستی)</option>
|
||||||
|
<option value="descriptive" {{ old('type') == 'descriptive' ? 'selected' : '' }}>تشریحی</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">درس مربوطه</label>
|
||||||
|
<select name="subject_id" class="form-select">
|
||||||
|
<option value="">انتخاب درس (مثلاً: حقوق مدنی)</option>
|
||||||
|
@foreach($subjects as $subject)
|
||||||
|
<option value="{{ $subject->id }}" {{ old('subject_id') == $subject->id ? 'selected' : '' }}>
|
||||||
|
{{ $subject->title }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">متن سوال / مسئله حقوقی <span class="text-danger">*</span></label>
|
||||||
|
<textarea name="question_text" class="form-control" rows="4" required placeholder="صورت سوال یا مسئله حقوقی را وارد کنید...">{{ old('question_text') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">بارم / ضریب سوال</label>
|
||||||
|
<input type="number" step="0.25" name="score" class="form-control" value="{{ old('score', '1.00') }}" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiple Choice Options Box -->
|
||||||
|
<div id="optionsContainer">
|
||||||
|
<h6 class="border-bottom pb-2 mb-3">گزینهها و انتخاب پاسخ صحیح:</h6>
|
||||||
|
@for($i = 0; $i < 4; $i++)
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<div class="input-group-text">
|
||||||
|
<!-- Keep the correct radio button checked -->
|
||||||
|
<input class="form-check-input mt-0" type="radio" name="correct_option" value="{{ $i }}" {{ old('correct_option', 0) == $i ? 'checked' : '' }}>
|
||||||
|
</div>
|
||||||
|
<!-- Keep the typed option text -->
|
||||||
|
<input type="text" name="options[]" class="form-control" value="{{ old('options.'.$i) }}" placeholder="متن گزینه {{ $i + 1 }}">
|
||||||
|
</div>
|
||||||
|
@endfor
|
||||||
|
<small class="text-muted d-block mb-3">دکمه رادیویی کنار هر گزینه نشاندهنده گزینه صحیح است.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">پاسخ تشریحی و مستندات مواد قانونی</label>
|
||||||
|
<textarea name="solution_explanation" class="form-control" rows="3" placeholder="توضیح پاسخ، استدلال و مواد قانونی مربوطه...">{{ old('solution_explanation') }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success w-100">افزودن سوال</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Questions List -->
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-dark text-white">
|
||||||
|
<h6 class="mb-0 text-white">سوالات ثبت شده ({{ $quiz->questions->count() }})</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
@forelse($quiz->questions as $index => $question)
|
||||||
|
<div class="border rounded p-3 mb-3 bg-light">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<span class="badge bg-secondary">سوال شماره {{ $index + 1 }}</span>
|
||||||
|
<div>
|
||||||
|
@if($question->subject)
|
||||||
|
<span class="badge bg-info me-1">{{ $question->subject->title }}</span>
|
||||||
|
@endif
|
||||||
|
<span class="badge bg-{{ $question->type === 'multiple_choice' ? 'primary' : 'warning' }}">
|
||||||
|
{{ $question->type === 'multiple_choice' ? 'تستی' : 'تشریحی' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="fw-bold mb-2">{{ $question->question_text }}</p>
|
||||||
|
|
||||||
|
@if($question->type === 'multiple_choice')
|
||||||
|
<div class="row g-2 mb-2">
|
||||||
|
@foreach($question->options as $optIndex => $option)
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="p-2 border rounded {{ $option->is_correct ? 'bg-success text-white fw-bold' : 'bg-white' }}">
|
||||||
|
{{ $optIndex + 1 }}) {{ $option->option_text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($question->solution_explanation)
|
||||||
|
<div class="alert alert-secondary py-2 px-3 mt-2 mb-0 font-size-13">
|
||||||
|
<strong>پاسخ تشریحی / مستند قانون:</strong> {{ $question->solution_explanation }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="text-end mt-2">
|
||||||
|
<form action="{{ route('questions.destroy', $question->id) }}" method="POST" class="d-inline" onsubmit="return confirm('آیا این سوال حذف شود؟')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button class="btn btn-sm btn-outline-danger">حذف سوال</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-center text-muted py-4">هنوز سوالی برای این آزمون ثبت نشده است.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('scripts')
|
||||||
|
<script>
|
||||||
|
// Run this immediately on page load to check if we should hide options
|
||||||
|
// in case the page reloads with old('type') set to 'descriptive'
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
var typeSelect = document.getElementById('questionTypeSelect');
|
||||||
|
toggleOptionFields(typeSelect.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleOptionFields(type) {
|
||||||
|
const container = document.getElementById('optionsContainer');
|
||||||
|
if (type === 'descriptive') {
|
||||||
|
container.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@@ -1,48 +1,58 @@
|
|||||||
@extends('admin.layouts.app')
|
@extends('admin.layouts.app')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@include('admin.partial.list-filter', ['filterRoute' => 'subscribe-plans.index'])
|
@include('admin.partial.list-filter', ['filterRoute' => 'subscribe-plans.index'])
|
||||||
<div id="datatable_wrapper" class="dataTables_wrapper dt-bootstrap4 no-footer">
|
<div id="datatable_wrapper" class="dataTables_wrapper dt-bootstrap4 no-footer">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<table id="datatable" class="table table-bordered dt-responsive nowrap dataTable no-footer dtr-inline" style="border-collapse: collapse; border-spacing: 0px; width: 100%;" role="grid" aria-describedby="datatable_info">
|
<table id="datatable" class="table table-bordered dt-responsive nowrap dataTable no-footer dtr-inline" style="border-collapse: collapse; border-spacing: 0px; width: 100%;" role="grid" aria-describedby="datatable_info">
|
||||||
<thead>
|
<thead>
|
||||||
<tr role="row">
|
<tr role="row">
|
||||||
<th>نام پلن</th>
|
<th>نام پلن</th>
|
||||||
|
<th>نوع (Type)</th>
|
||||||
<th>قیمت</th>
|
<th>قیمت</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
<th>عملیات</th>
|
<th>عملیات</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach ($subscribePlans as $item)
|
@foreach ($subscribePlans as $item)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $item->name }}</td>
|
<td>{{ $item->name }}</td>
|
||||||
<td>{{ $item->price }}</td>
|
<td><span class="badge badge-info">{{ $item->type ?? 'ندارد' }}</span></td>
|
||||||
<td>
|
<td>{{ number_format($item->price) }} تومان</td>
|
||||||
<form action="{{ route('subscribe-plans.destroy', $item->id) }}" method="POST" style="display: inline;">
|
<td>
|
||||||
@csrf
|
@if($item->is_active)
|
||||||
@method('DELETE')
|
<span class="badge badge-success">فعال</span>
|
||||||
<button type="submit" class="btn btn-danger waves-effect waves-light" onclick="return confirm('آیا مطمئنید که میخواهید این کتاب را حذف کنید؟')">حذف</button>
|
@else
|
||||||
</form>
|
<span class="badge badge-danger">غیرفعال</span>
|
||||||
<a href="{{route('subscribe-plans.edit',$item->id)}}" type="button" class="btn btn-primary waves-effect waves-light">ویرایش</a>
|
@endif
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td>
|
||||||
|
<form action="{{ route('subscribe-plans.destroy', $item->id) }}" method="POST" style="display: inline;">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-danger waves-effect waves-light" onclick="return confirm('آیا مطمئنید که میخواهید این پلن را حذف کنید؟')">حذف</button>
|
||||||
|
</form>
|
||||||
|
<a href="{{route('subscribe-plans.edit',$item->id)}}" type="button" class="btn btn-primary waves-effect waves-light">ویرایش</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center mt-3">
|
||||||
|
{{ $subscribePlans->withQueryString()->links() }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-center mt-3">
|
|
||||||
{{ $subscribePlans->withQueryString()->links() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endsection
|
||||||
@endsection
|
|
||||||
|
|||||||
76
resources/views/admin/tickets/index.blade.php
Normal file
76
resources/views/admin/tickets/index.blade.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Reuses your existing search component -->
|
||||||
|
@include('admin.partial.list-filter', ['filterRoute' => 'tickets.index'])
|
||||||
|
|
||||||
|
<div id="datatable_wrapper" class="dataTables_wrapper dt-bootstrap4 no-footer">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<table id="datatable" class="table table-bordered dt-responsive nowrap dataTable no-footer dtr-inline" style="border-collapse: collapse; border-spacing: 0px; width: 100%;" role="grid">
|
||||||
|
<thead>
|
||||||
|
<tr role="row">
|
||||||
|
<th>شناسه</th>
|
||||||
|
<th>کاربر (موبایل)</th>
|
||||||
|
<th>موضوع</th>
|
||||||
|
<th>وضعیت</th>
|
||||||
|
<th>اولویت</th>
|
||||||
|
<th>تاریخ بروزرسانی</th>
|
||||||
|
<th>عملیات</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($tickets as $ticket)
|
||||||
|
<tr>
|
||||||
|
<td>#{{ $ticket->id }}</td>
|
||||||
|
<td>{{ optional($ticket->user)->mobile ?? 'کاربر ناشناس' }}</td>
|
||||||
|
<td>{{ Str::limit($ticket->subject, 50) }}</td>
|
||||||
|
<td>
|
||||||
|
@if($ticket->status === 'open')
|
||||||
|
<span class="badge bg-primary">باز (جدید)</span>
|
||||||
|
@elseif($ticket->status === 'user_reply')
|
||||||
|
<span class="badge bg-warning text-dark">پاسخ کاربر</span>
|
||||||
|
@elseif($ticket->status === 'answered')
|
||||||
|
<span class="badge bg-success">پاسخ داده شده</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-secondary">بسته شده</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if($ticket->priority === 'high')
|
||||||
|
<span class="badge bg-danger">مهم</span>
|
||||||
|
@elseif($ticket->priority === 'medium')
|
||||||
|
<span class="badge bg-info">متوسط</span>
|
||||||
|
@else
|
||||||
|
<span class="badge bg-light text-dark">کم</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td dir="ltr">{{ $ticket->updated_at->format('Y/m/d H:i') }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ route('tickets.show', $ticket->id) }}" class="btn btn-primary waves-effect waves-light btn-sm">مشاهده و پاسخ</a>
|
||||||
|
|
||||||
|
@if($ticket->status !== 'closed')
|
||||||
|
<form action="{{ route('tickets.close', $ticket->id) }}" method="POST" style="display: inline;">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-secondary waves-effect waves-light btn-sm" onclick="return confirm('آیا از بستن این تیکت اطمینان دارید؟')">بستن</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-center mt-3">
|
||||||
|
{{ $tickets->withQueryString()->links() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
69
resources/views/admin/tickets/show.blade.php
Normal file
69
resources/views/admin/tickets/show.blade.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@extends('admin.layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
@include('admin.layouts.errors')
|
||||||
|
<div class="col-lg-8 offset-lg-2">
|
||||||
|
<!-- Ticket Info Header -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body bg-light">
|
||||||
|
<h4 class="card-title">موضوع: {{ $ticket->subject }}</h4>
|
||||||
|
<p class="mb-0 text-muted">تیکت #{{ $ticket->id }} | ایجاد کننده: {{ optional($ticket->user)->mobile }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat History -->
|
||||||
|
@foreach($ticket->messages as $msg)
|
||||||
|
<div class="card mb-3 shadow-sm {{ $msg->user_id === $ticket->user_id ? 'border-primary' : 'border-success' }}">
|
||||||
|
<div class="card-header {{ $msg->user_id === $ticket->user_id ? 'bg-primary text-white' : 'bg-success text-white' }}">
|
||||||
|
<strong>{{ $msg->user_id === $ticket->user_id ? 'کاربر' : 'پشتیبانی (شما)' }}</strong>
|
||||||
|
<span class="float-end" dir="ltr" style="font-size: 0.85rem;">{{ $msg->created_at->format('Y/m/d H:i') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p style="white-space: pre-wrap;">{{ $msg->message }}</p>
|
||||||
|
|
||||||
|
@if($msg->attachment_path)
|
||||||
|
<hr>
|
||||||
|
<a href="{{ asset('storage/' . $msg->attachment_path) }}" target="_blank" class="btn btn-sm btn-info waves-effect waves-light">
|
||||||
|
<i class="fas fa-paperclip"></i> مشاهده فایل ضمیمه
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<!-- Reply Form -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-body">
|
||||||
|
@if($ticket->status !== 'closed')
|
||||||
|
<h4 class="card-title mb-3">ارسال پاسخ</h4>
|
||||||
|
<!-- Notice the enctype="multipart/form-data" which is required for attachments -->
|
||||||
|
<form action="{{ route('tickets.reply', $ticket->id) }}" method="post" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="message">متن پاسخ</label>
|
||||||
|
<textarea class="form-control" name="message" id="message" rows="5" placeholder="پاسخ خود را بنویسید..." required></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="attachment">فایل ضمیمه (اختیاری)</label>
|
||||||
|
<input class="form-control" name="attachment" id="attachment" type="file" accept=".jpg,.jpeg,.png,.pdf">
|
||||||
|
<small class="text-muted">فرمتهای مجاز: jpg, png, pdf (حداکثر 5 مگابایت)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<button type="submit" class="btn btn-success waves-effect waves-light">ارسال پاسخ</button>
|
||||||
|
<a href="{{ route('tickets.index') }}" class="btn btn-outline-secondary">بازگشت به لیست</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<div class="alert alert-secondary text-center mb-0">
|
||||||
|
این تیکت بسته شده است و امکان ارسال پاسخ جدید وجود ندارد.
|
||||||
|
<br>
|
||||||
|
<a href="{{ route('tickets.index') }}" class="btn btn-secondary btn-sm mt-2">بازگشت به لیست</a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -27,7 +27,11 @@
|
|||||||
<td>{{ $user->mobile }}</td>
|
<td>{{ $user->mobile }}</td>
|
||||||
<td>{{ $user->is_admin ? 'ادمین' : 'مشتری' }}</td>
|
<td>{{ $user->is_admin ? 'ادمین' : 'مشتری' }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ $user->userSubscribers->sum(function ($subscriber) { return $subscriber->expired_at ? $subscriber->expired_at->diffInDays(now()) : 0; }) }}
|
@php
|
||||||
|
$activeSub = $user->userSubscribers->sortByDesc('expired_at')->first();
|
||||||
|
$days = $activeSub && $activeSub->expired_at ? $activeSub->expired_at->diffInDays(now()) : 0;
|
||||||
|
@endphp
|
||||||
|
{{ $days }} روز
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form action="{{ route('users.destroy', $user->id) }}" id="user-{{$user->id}}-delete" method="POST" style="display: inline;">
|
<form action="{{ route('users.destroy', $user->id) }}" id="user-{{$user->id}}-delete" method="POST" style="display: inline;">
|
||||||
|
|||||||
@@ -1,34 +1,86 @@
|
|||||||
@extends('admin.layouts.app')
|
@extends('admin.layouts.app')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@include('admin.layouts.errors')
|
@include('admin.layouts.errors')
|
||||||
|
|
||||||
<div class="col-lg-6">
|
<!-- Existing User Edit Box -->
|
||||||
<div class="card">
|
<div class="col-lg-6">
|
||||||
<div class="card-body">
|
<div class="card">
|
||||||
<h4 class="card-title">ویرایش کاربر</h4>
|
<div class="card-body">
|
||||||
<div>
|
<h4 class="card-title">ویرایش کاربر</h4>
|
||||||
<form action="{{ route('users.update',$user->id) }}" method="post">
|
<div>
|
||||||
@csrf
|
<form action="{{ route('users.update',$user->id) }}" method="post">
|
||||||
@method('put')
|
@csrf
|
||||||
<div class="mb-4">
|
@method('put')
|
||||||
<input class="form-control" name="name" type="text" placeholder="نام" value="{{$user->name}}">
|
<div class="mb-4">
|
||||||
</div>
|
<input class="form-control" name="name" type="text" placeholder="نام" value="{{$user->name}}">
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<input class="form-control" name="email" type="email" placeholder="ایمیل" value="{{$user->email}}">
|
<div class="mb-4">
|
||||||
</div>
|
<input class="form-control" name="email" type="email" placeholder="ایمیل" value="{{$user->email}}">
|
||||||
<div class="mb-4">
|
</div>
|
||||||
<input class="form-control" name="mobile" type="text" placeholder="موبایل" value="{{$user->mobile}}">
|
<div class="mb-4">
|
||||||
</div>
|
<input class="form-control" name="mobile" type="text" placeholder="موبایل" value="{{$user->mobile}}">
|
||||||
<div class="custom-control custom-switch mb-2" dir="ltr">
|
</div>
|
||||||
<input type="checkbox" class="custom-control-input" id="customSwitch1" name="is_admin" {{ $user->is_admin ? 'checked="true"' :'' }}">
|
<div class="custom-control custom-switch mb-2" dir="ltr">
|
||||||
<label class="custom-control-label" for="customSwitch1">ادمین</label>
|
<input type="checkbox" class="custom-control-input" id="customSwitch1" name="is_admin" {{ $user->is_admin ? 'checked="true"' :'' }}>
|
||||||
</div>
|
<label class="custom-control-label" for="customSwitch1">ادمین</label>
|
||||||
<button class="btn btn-primary" type="submit">ثبت</button>
|
</div>
|
||||||
</form>
|
<button class="btn btn-primary" type="submit">ثبت</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Subscription & Revoke Box -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title mb-4">مدیریت اشتراک</h4>
|
||||||
|
|
||||||
|
@if(session('success'))
|
||||||
|
<div class="alert alert-success">{{ session('success') }}</div>
|
||||||
|
@endif
|
||||||
|
@if(session('error'))
|
||||||
|
<div class="alert alert-danger">{{ session('error') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- ADD SUBSCRIPTION FORM -->
|
||||||
|
<form action="{{ route('admin.subscriptions.manual') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="user_id" value="{{ $user->id }}">
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="subscribe_plan_id" class="form-label">انتخاب پلن اشتراک</label>
|
||||||
|
<select name="subscribe_plan_id" id="subscribe_plan_id" class="form-control" required>
|
||||||
|
<option value="">یک پلن انتخاب کنید...</option>
|
||||||
|
@foreach($paidPlans as $plan)
|
||||||
|
<option value="{{ $plan->id }}">
|
||||||
|
{{ $plan->name }} | نوع: {{ $plan->type ?? 'ندارد' }} | ({{ number_format($plan->price) }} تومان)
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-success w-100">ثبت اشتراک دستی</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- REVOKE (0 DAYS) FORM -->
|
||||||
|
<h5 class="text-danger mb-2">لغو اشتراک</h5>
|
||||||
|
<p class="text-muted font-size-13">با این کار، تمامی روزهای باقیمانده از اشتراک فعلی کاربر صفر خواهد شد.</p>
|
||||||
|
<form action="{{ route('admin.subscriptions.revoke') }}" method="POST">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="user_id" value="{{ $user->id }}">
|
||||||
|
<button type="submit" class="btn btn-outline-danger w-100" onclick="return confirm('آیا مطمئنید که میخواهید اشتراک این کاربر را صفر کنید؟')">
|
||||||
|
صفر کردن اشتراک (۰ روز)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@endsection
|
||||||
@endsection
|
|
||||||
|
|||||||
@@ -11,14 +11,18 @@ use App\Http\Controllers\api\HomeController;
|
|||||||
use App\Http\Controllers\api\LawController;
|
use App\Http\Controllers\api\LawController;
|
||||||
use App\Http\Controllers\api\NoteController;
|
use App\Http\Controllers\api\NoteController;
|
||||||
use App\Http\Controllers\api\NotificationController;
|
use App\Http\Controllers\api\NotificationController;
|
||||||
|
use App\Http\Controllers\api\TicketApiController;
|
||||||
use App\Http\Controllers\api\PartController;
|
use App\Http\Controllers\api\PartController;
|
||||||
use App\Http\Controllers\api\PayController;
|
use App\Http\Controllers\api\PayController;
|
||||||
use App\Http\Controllers\api\SectionController;
|
use App\Http\Controllers\api\SectionController;
|
||||||
use App\Http\Controllers\api\SubscribePlanController;
|
use App\Http\Controllers\api\SubscribePlanController;
|
||||||
use App\Http\Controllers\api\VersionController;
|
use App\Http\Controllers\api\VersionController;
|
||||||
use App\Http\Controllers\api\VolumController;
|
use App\Http\Controllers\api\VolumController;
|
||||||
|
use App\Http\Controllers\api\CurrencyController;
|
||||||
use App\Http\Controllers\SuggestionController;
|
use App\Http\Controllers\SuggestionController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\api\QuizController;
|
||||||
|
use App\Http\Controllers\api\PvPQuizController;
|
||||||
|
|
||||||
Route::prefix('v1')->group(function () {
|
Route::prefix('v1')->group(function () {
|
||||||
Route::post('login', [AuthController::class, 'login']);
|
Route::post('login', [AuthController::class, 'login']);
|
||||||
@@ -30,6 +34,7 @@ Route::prefix('v1')->group(function () {
|
|||||||
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
|
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
|
||||||
Route::get('home', [HomeController::class, 'index']);
|
Route::get('home', [HomeController::class, 'index']);
|
||||||
Route::get('home-v2', [HomeController::class, 'homeV2']);
|
Route::get('home-v2', [HomeController::class, 'homeV2']);
|
||||||
|
Route::get('home-v3', [HomeController::class, 'homeV3']); // <-- NEW V3 ENDPOINT
|
||||||
Route::get('notifications', [NotificationController::class, 'index']);
|
Route::get('notifications', [NotificationController::class, 'index']);
|
||||||
Route::get('art', [ArtController::class, 'index']);
|
Route::get('art', [ArtController::class, 'index']);
|
||||||
Route::get('categories', [CategoriesController::class, 'index']);
|
Route::get('categories', [CategoriesController::class, 'index']);
|
||||||
@@ -95,4 +100,70 @@ Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
|
|||||||
// Global Search
|
// Global Search
|
||||||
Route::post('global-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'search']);
|
Route::post('global-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'search']);
|
||||||
Route::post('global-search/fast-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'fast_search']); // NEW
|
Route::post('global-search/fast-search', [\App\Http\Controllers\api\GlobalSearchApiController::class, 'fast_search']); // NEW
|
||||||
|
|
||||||
|
// Bylaw Notes
|
||||||
|
Route::post('/bylaw-notes', [\App\Http\Controllers\api\BylawNoteController::class, 'create']);
|
||||||
|
Route::put('/bylaw-notes/{id}', [\App\Http\Controllers\api\BylawNoteController::class, 'update']);
|
||||||
|
Route::delete('/bylaw-notes/{id}', [\App\Http\Controllers\api\BylawNoteController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Judicial Precedent Notes
|
||||||
|
Route::post('/judicial-notes', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'create']);
|
||||||
|
Route::put('/judicial-notes/{id}', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'update']);
|
||||||
|
Route::delete('/judicial-notes/{id}', [\App\Http\Controllers\api\JudicialPrecedentNoteController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Legal Opinion Notes
|
||||||
|
Route::post('/opinion-notes', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'create']);
|
||||||
|
Route::put('/opinion-notes/{id}', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'update']);
|
||||||
|
Route::delete('/opinion-notes/{id}', [\App\Http\Controllers\api\LegalOpinionNoteController::class, 'destroy']);
|
||||||
|
|
||||||
|
|
||||||
|
Route::delete('/folders/{id}/bylaw/{bylaw_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_bylaw']);
|
||||||
|
Route::delete('/folders/{id}/judicial/{judicial_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_judicial']);
|
||||||
|
Route::delete('/folders/{id}/opinion/{opinion_id}', [\App\Http\Controllers\api\FolderController::class, 'delete_opinion']);
|
||||||
|
|
||||||
|
// --- HIGHLIGHTS ---
|
||||||
|
Route::prefix('highlights')->middleware('auth:sanctum')->group(function () {
|
||||||
|
// Arts
|
||||||
|
Route::post('/art', [\App\Http\Controllers\api\ArtHighlightController::class, 'create']);
|
||||||
|
Route::put('/art/{id}', [\App\Http\Controllers\api\ArtHighlightController::class, 'update']);
|
||||||
|
Route::delete('/art/{id}', [\App\Http\Controllers\api\ArtHighlightController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Bylaws
|
||||||
|
Route::post('/bylaw', [\App\Http\Controllers\api\BylawHighlightController::class, 'create']);
|
||||||
|
Route::put('/bylaw/{id}', [\App\Http\Controllers\api\BylawHighlightController::class, 'update']);
|
||||||
|
Route::delete('/bylaw/{id}', [\App\Http\Controllers\api\BylawHighlightController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Judicial Precedents
|
||||||
|
Route::post('/judicial', [\App\Http\Controllers\api\JudicialPrecedentHighlightController::class, 'create']);
|
||||||
|
Route::put('/judicial/{id}', [\App\Http\Controllers\api\JudicialPrecedentHighlightController::class, 'update']);
|
||||||
|
Route::delete('/judicial/{id}', [\App\Http\Controllers\api\JudicialPrecedentHighlightController::class, 'destroy']);
|
||||||
|
|
||||||
|
// Legal Opinions
|
||||||
|
Route::post('/opinion', [\App\Http\Controllers\api\LegalOpinionHighlightController::class, 'create']);
|
||||||
|
Route::put('/opinion/{id}', [\App\Http\Controllers\api\LegalOpinionHighlightController::class, 'update']);
|
||||||
|
Route::delete('/opinion/{id}', [\App\Http\Controllers\api\LegalOpinionHighlightController::class, 'destroy']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- TICKETING SYSTEM ---
|
||||||
|
Route::prefix('tickets')->group(function () {
|
||||||
|
Route::get('/', [\App\Http\Controllers\api\TicketApiController::class, 'index']);
|
||||||
|
Route::get('/{id}', [\App\Http\Controllers\api\TicketApiController::class, 'show']);
|
||||||
|
Route::post('/', [\App\Http\Controllers\api\TicketApiController::class, 'store']);
|
||||||
|
Route::post('/{id}/reply', [\App\Http\Controllers\api\TicketApiController::class, 'reply']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('/quiz/categories', [QuizController::class, 'categories']);
|
||||||
|
Route::get('/quiz/subjects', [QuizController::class, 'subjects']);
|
||||||
|
Route::get('/quizzes', [QuizController::class, 'index']);
|
||||||
|
|
||||||
|
// Start, Submit, Result flow
|
||||||
|
Route::get('/quizzes/{quiz}/start', [QuizController::class, 'start']);
|
||||||
|
Route::post('/quiz-attempts/{attempt}/submit', [QuizController::class, 'submit']);
|
||||||
|
Route::get('/quiz-attempts/{attempt}/result', [QuizController::class, 'result']);
|
||||||
|
|
||||||
|
Route::get('/pvp-matches/{match}/result', [PvPQuizController::class, 'matchResult']);
|
||||||
|
|
||||||
|
Route::get('market/gold', [CurrencyController::class, 'goldPrices']);
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,10 +29,17 @@ use App\Http\Controllers\Admin\VersionController;
|
|||||||
use App\Http\Controllers\Admin\VolumController;
|
use App\Http\Controllers\Admin\VolumController;
|
||||||
use App\Http\Controllers\api\PayController;
|
use App\Http\Controllers\api\PayController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Admin\TicketController;
|
||||||
|
use App\Http\Controllers\Admin\DashboardController;
|
||||||
|
use App\Http\Controllers\Admin\QuizController;
|
||||||
|
use App\Http\Controllers\Admin\QuizCategoryController;
|
||||||
|
use App\Http\Controllers\Admin\QuizSubjectController;
|
||||||
|
|
||||||
Route::get('payment/callback', [PayController::class, 'callback']);
|
Route::get('payment/callback', [PayController::class, 'callback']);
|
||||||
|
|
||||||
Route::middleware(['auth', config('jetstream.auth_session')])->group(function () {
|
Route::middleware(['auth', config('jetstream.auth_session')])->group(function () {
|
||||||
Route::get('dashboard', [HomeController::class, 'index'])->name('dashboard');
|
|
||||||
|
|
||||||
Route::resource('categories', CategoryController::class);
|
Route::resource('categories', CategoryController::class);
|
||||||
Route::resource('versions', VersionController::class);
|
Route::resource('versions', VersionController::class);
|
||||||
Route::resource('users', UsersController::class);
|
Route::resource('users', UsersController::class);
|
||||||
@@ -58,5 +65,29 @@ Route::middleware(['auth', config('jetstream.auth_session')])->group(function ()
|
|||||||
Route::resource('bylaw-node-type', BylawNodeTypeController::class);
|
Route::resource('bylaw-node-type', BylawNodeTypeController::class);
|
||||||
Route::resource('bylaw-node', BylawNodeController::class);
|
Route::resource('bylaw-node', BylawNodeController::class);
|
||||||
Route::resource('bylaw-codification-link', BylawCodificationLinkController::class);
|
Route::resource('bylaw-codification-link', BylawCodificationLinkController::class);
|
||||||
|
|
||||||
Route::get('suggestions', [SuggestionController::class, 'index'])->name('suggestions.index');
|
Route::get('suggestions', [SuggestionController::class, 'index'])->name('suggestions.index');
|
||||||
|
|
||||||
|
Route::get('tickets', [TicketController::class, 'index'])->name('tickets.index');
|
||||||
|
Route::get('tickets/{ticket}', [TicketController::class, 'show'])->name('tickets.show');
|
||||||
|
Route::post('tickets/{ticket}/reply', [TicketController::class, 'reply'])->name('tickets.reply');
|
||||||
|
Route::post('tickets/{ticket}/close', [TicketController::class, 'close'])->name('tickets.close');
|
||||||
|
|
||||||
|
Route::resource('quizzes', QuizController::class);
|
||||||
|
Route::get('quizzes/{quiz}/questions', [QuizController::class, 'questions'])->name('quizzes.questions.index');
|
||||||
|
Route::post('quizzes/{quiz}/questions', [QuizController::class, 'storeQuestion'])->name('quizzes.questions.store');
|
||||||
|
Route::delete('questions/{question}', [QuizController::class, 'destroyQuestion'])->name('questions.destroy');
|
||||||
|
Route::resource('quiz-categories', QuizCategoryController::class)->except(['create', 'show', 'edit', 'update']);
|
||||||
|
|
||||||
|
Route::resource('quiz-subjects', QuizSubjectController::class)->except(['create', 'show', 'edit', 'update']);
|
||||||
|
|
||||||
|
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
|
Route::post('/users/manual-subscription', [\App\Http\Controllers\Admin\UsersController::class, 'addManualSubscription'])
|
||||||
|
->name('admin.subscriptions.manual');
|
||||||
|
|
||||||
|
Route::post('/users/revoke-subscription', [\App\Http\Controllers\Admin\UsersController::class, 'revokeSubscription'])
|
||||||
|
->name('admin.subscriptions.revoke');
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user