add all project files

مخزن کامیت contained در
مسعود نیک زاد 2026-07-11 12:34:20 +03:30
والد 0e34de6f12
کامیت 53ab7244e4
306فایلهای تغییر یافته به همراه37848 افزوده شده و 0 حذف شده

18
backend/.editorconfig normal فایل
مشاهده پرونده

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[compose.yaml]
indent_size = 4

65
backend/.env.example normal فایل
مشاهده پرونده

@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
backend/.gitattributes فروخته شده normal فایل
مشاهده پرونده

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

24
backend/.gitignore فروخته شده normal فایل
مشاهده پرونده

@ -0,0 +1,24 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db

59
backend/README.md normal فایل
مشاهده پرونده

@ -0,0 +1,59 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

مشاهده پرونده

@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\ActivityLogResource;
use App\Models\ActivityLog;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ActivityLogController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = ActivityLog::with('user');
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('task_id')) {
$query->where('task_id', $request->task_id);
}
if ($request->filled('user_id')) {
$query->where('user_id', $request->user_id);
}
if ($request->filled('action')) {
$query->where('action', $request->action);
}
if ($request->filled('subject_type')) {
$query->where('subject_type', $request->subject_type);
}
if ($request->filled('subject_id')) {
$query->where('subject_id', $request->subject_id);
}
$perPage = $request->input('per_page', 15);
$sortBy = $request->input('sort_by', 'created_at');
$sortDir = $request->input('sort_dir', 'desc');
$logs = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => ActivityLogResource::collection($logs),
'meta' => [
'current_page' => $logs->currentPage(),
'last_page' => $logs->lastPage(),
'per_page' => $logs->perPage(),
'total' => $logs->total(),
],
'message' => 'لاگ فعالیت‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت لاگ فعالیت‌ها',
], 500);
}
}
public function show(ActivityLog $activityLog): JsonResponse
{
try {
$activityLog->load('user');
return response()->json([
'success' => true,
'data' => new ActivityLogResource($activityLog),
'message' => 'جزئیات لاگ',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت جزئیات لاگ',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,240 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\UpdateProfileRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Services\ActivityLogService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
class AuthController extends Controller
{
public function __construct(protected ActivityLogService $activityLogService) {}
public function login(LoginRequest $request): JsonResponse
{
try {
$credentials = $request->validated();
$identifier = strtolower(trim($credentials['identifier']));
$user = $this->findUserForLogin($identifier);
if (!$user || !Hash::check($credentials['password'], $user->password)) {
$this->logAuthEvent(null, 'failed_login', 'تلاش ناموفق ورود', $request, $identifier);
return response()->json([
'success' => false,
'message' => 'نام کاربری یا رمز عبور اشتباه است',
], 401);
}
if ($user->status !== null && $user->status !== 'active') {
$this->logAuthEvent($user->id, 'blocked_login', 'تلاش ورود کاربر غیرفعال', $request, $identifier);
return response()->json([
'success' => false,
'message' => 'امکان ورود با این اطلاعات وجود ندارد',
], 403);
}
$token = $user->createToken('api-token')->plainTextToken;
$user->load(['roles.permissions']);
$this->logAuthEvent($user->id, 'login', 'ورود موفق به سیستم', $request, $identifier);
return response()->json([
'success' => true,
'data' => [
'user' => new UserResource($user),
'token' => $token,
'permissions' => $user->roles->flatMap(function ($role) {
return $role->permissions->pluck('name');
})->unique()->values(),
],
'message' => 'ورود با موفقیت انجام شد',
]);
} catch (\Exception $e) {
Log::error('Login failed unexpectedly', [
'identifier_hash' => isset($identifier) ? hash('sha256', $identifier) : null,
'exception' => $e,
]);
return response()->json([
'success' => false,
'message' => 'خطا در ورود به سیستم',
], 500);
}
}
private function findUserForLogin(string $identifier): ?User
{
$query = User::query()
->whereRaw('LOWER(email) = ?', [$identifier])
->orWhereRaw('LOWER(name) = ?', [$identifier]);
if (Schema::hasColumn('users', 'username')) {
$query->orWhereRaw('LOWER(username) = ?', [$identifier]);
}
$user = $query->orderBy('id')->first();
if ($user || str_contains($identifier, '@')) {
return $user;
}
$emailPrefixMatches = User::query()
->whereRaw('LOWER(email) LIKE ?', [$identifier.'@%'])
->limit(2)
->get();
return $emailPrefixMatches->count() === 1 ? $emailPrefixMatches->first() : null;
}
public function logout(Request $request): JsonResponse
{
try {
$this->logAuthEvent($request->user()?->id, 'logout', 'خروج از سیستم', $request);
$request->user()->currentAccessToken()?->delete();
return response()->json([
'success' => true,
'message' => 'خروج با موفقیت انجام شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در خروج از سیستم',
], 500);
}
}
public function user(Request $request): JsonResponse
{
try {
$user = $request->user()->load(['roles.permissions']);
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'اطلاعات کاربر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات کاربر',
], 500);
}
}
public function updateProfile(UpdateProfileRequest $request): JsonResponse
{
try {
$user = $request->user();
$user->update($request->validated());
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'پروفایل با موفقیت به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی پروفایل',
], 500);
}
}
public function updateAvatar(Request $request): JsonResponse
{
try {
$request->validate([
'avatar' => 'required|image|mimes:jpg,jpeg,png,webp|max:2048',
]);
$user = $request->user();
if ($user->avatar && $user->avatar !== 'avatars/default.png' && Storage::disk('public')->exists($user->avatar)) {
Storage::disk('public')->delete($user->avatar);
}
$path = $request->file('avatar')->store('avatars', 'public');
$user->update(['avatar' => $path]);
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'عکس پروفایل با موفقیت به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی عکس پروفایل',
], 500);
}
}
public function changePassword(ChangePasswordRequest $request): JsonResponse
{
try {
$user = $request->user();
if (!Hash::check($request->current_password, $user->password)) {
return response()->json([
'success' => false,
'message' => 'رمز عبور فعلی اشتباه است',
], 400);
}
$user->update(['password' => Hash::make($request->new_password)]);
return response()->json([
'success' => true,
'message' => 'رمز عبور با موفقیت تغییر یافت',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در تغییر رمز عبور',
], 500);
}
}
public function forgotPassword(Request $request): JsonResponse
{
return response()->json([
'success' => true,
'message' => 'لینک بازیابی رمز عبور به ایمیل شما ارسال شد',
]);
}
private function logAuthEvent(?int $userId, string $action, string $description, Request $request, ?string $identifier = null): void
{
try {
$properties = [
'ip' => $request->ip(),
'user_agent' => substr((string) $request->userAgent(), 0, 255),
];
if ($identifier) {
$properties['identifier_hash'] = hash('sha256', strtolower($identifier));
}
$this->activityLogService->log($userId, $action, $description, 'auth', $userId, null, null, $properties);
} catch (\Throwable $e) {
Log::warning('Auth activity log failed', [
'user_id' => $userId,
'action' => $action,
'exception' => $e,
]);
}
}
}

مشاهده پرونده

@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreBacklogItemRequest;
use App\Http\Resources\BacklogItemResource;
use App\Http\Resources\TaskResource;
use App\Models\BacklogItem;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BacklogController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = BacklogItem::with(['project', 'assignedSprint', 'creator']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('type')) {
$query->where('type', $request->type);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('priority')) {
$query->where('priority', $request->priority);
}
$perPage = $request->input('per_page', 15);
$sortBy = $request->input('sort_by', 'created_at');
$sortDir = $request->input('sort_dir', 'desc');
$items = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => BacklogItemResource::collection($items),
'meta' => [
'current_page' => $items->currentPage(),
'last_page' => $items->lastPage(),
'per_page' => $items->perPage(),
'total' => $items->total(),
],
'message' => 'لیست بکلاگ',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت بکلاگ',
], 500);
}
}
public function show(BacklogItem $backlogItem): JsonResponse
{
try {
$backlogItem->load(['project', 'assignedSprint', 'creator']);
return response()->json([
'success' => true,
'data' => new BacklogItemResource($backlogItem),
'message' => 'اطلاعات آیتم بکلاگ',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات آیتم بکلاگ',
], 500);
}
}
public function store(StoreBacklogItemRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['created_by'] = $request->user()->id;
$item = BacklogItem::create($data);
$item->load(['project', 'assignedSprint', 'creator']);
return response()->json([
'success' => true,
'data' => new BacklogItemResource($item),
'message' => 'آیتم بکلاگ ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد آیتم بکلاگ',
], 500);
}
}
public function update(Request $request, BacklogItem $backlogItem): JsonResponse
{
try {
$request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'type' => 'sometimes|required|string|max:50',
'priority' => 'nullable|string|max:50',
'estimated_effort' => 'nullable|numeric',
'status' => 'nullable|string|max:50',
'assigned_sprint_id' => 'nullable|exists:sprints,id',
]);
$backlogItem->update($request->only([
'title', 'description', 'type', 'priority',
'estimated_effort', 'status', 'assigned_sprint_id'
]));
$backlogItem->load(['project', 'assignedSprint', 'creator']);
return response()->json([
'success' => true,
'data' => new BacklogItemResource($backlogItem->fresh()),
'message' => 'آیتم بکلاگ به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی آیتم بکلاگ',
], 500);
}
}
public function destroy(BacklogItem $backlogItem): JsonResponse
{
try {
$backlogItem->delete();
return response()->json([
'success' => true,
'message' => 'آیتم بکلاگ حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف آیتم بکلاگ',
], 500);
}
}
public function convertToTask(Request $request, BacklogItem $backlogItem): JsonResponse
{
try {
$request->validate([
'assignee_id' => 'nullable|exists:users,id',
'priority' => 'nullable|string',
'due_date' => 'nullable|date',
]);
$task = Task::create([
'title' => $backlogItem->title,
'description' => $backlogItem->description,
'project_id' => $backlogItem->project_id,
'assignee_id' => $request->assignee_id,
'reporter_id' => $request->user()->id,
'created_by' => $request->user()->id,
'priority' => $request->priority ?? $backlogItem->priority,
'status' => 'todo',
]);
$backlogItem->update(['status' => 'converted']);
$task->load(['project', 'assignee', 'reporter']);
return response()->json([
'success' => true,
'data' => new TaskResource($task),
'message' => 'آیتم بکلاگ به وظیفه تبدیل شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در تبدیل آیتم بکلاگ به وظیفه',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\ChecklistResource;
use App\Models\Checklist;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ChecklistController extends Controller
{
public function index(Request $request, Task $task): JsonResponse
{
try {
$checklists = $task->checklists()->orderBy('position')->get();
return response()->json([
'success' => true,
'data' => ChecklistResource::collection($checklists),
'message' => 'لیست چک‌لیست‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت چک‌لیست‌ها',
], 500);
}
}
public function store(Request $request, Task $task): JsonResponse
{
try {
$request->validate([
'title' => 'required|string|max:255',
'position' => 'nullable|integer',
]);
$data = $request->only(['title', 'position']);
$data['task_id'] = $task->id;
$data['is_completed'] = false;
$checklist = Checklist::create($data);
return response()->json([
'success' => true,
'data' => new ChecklistResource($checklist),
'message' => 'آیتم چک‌لیست ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد چک‌لیست',
], 500);
}
}
public function update(Request $request, Checklist $checklist): JsonResponse
{
try {
$request->validate([
'title' => 'sometimes|required|string|max:255',
'position' => 'nullable|integer',
]);
$checklist->update($request->only(['title', 'position']));
return response()->json([
'success' => true,
'data' => new ChecklistResource($checklist->fresh()),
'message' => 'چک‌لیست به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی چک‌لیست',
], 500);
}
}
public function destroy(Checklist $checklist): JsonResponse
{
try {
$checklist->delete();
return response()->json([
'success' => true,
'message' => 'چک‌لیست حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف چک‌لیست',
], 500);
}
}
public function toggleComplete(Checklist $checklist): JsonResponse
{
try {
$checklist->update(['is_completed' => !$checklist->is_completed]);
return response()->json([
'success' => true,
'data' => new ChecklistResource($checklist->fresh()),
'message' => $checklist->is_completed ? 'تکمیل شد' : 'ناتمام',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در تغییر وضعیت چک‌لیست',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreCommentRequest;
use App\Http\Resources\CommentResource;
use App\Models\Comment;
use App\Models\User;
use App\Services\ActivityLogService;
use App\Services\NotificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function __construct(
protected ActivityLogService $activityLogService,
protected NotificationService $notificationService,
) {}
public function index(Request $request): JsonResponse
{
try {
$query = Comment::with(['user', 'files']);
if ($request->filled('commentable_type')) {
$query->where('commentable_type', $request->commentable_type);
}
if ($request->filled('commentable_id')) {
$query->where('commentable_id', $request->commentable_id);
}
$perPage = $request->input('per_page', 15);
$comments = $query->orderBy('created_at', 'desc')->paginate($perPage);
return response()->json([
'success' => true,
'data' => CommentResource::collection($comments),
'meta' => [
'current_page' => $comments->currentPage(),
'last_page' => $comments->lastPage(),
'per_page' => $comments->perPage(),
'total' => $comments->total(),
],
'message' => 'لیست نظرات',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت نظرات',
], 500);
}
}
public function store(StoreCommentRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['user_id'] = $request->user()->id;
$comment = Comment::create($data);
$comment->load('user');
if (!empty($data['mentioned_user_id'])) {
$mentionedUser = User::find($data['mentioned_user_id']);
if ($mentionedUser && $mentionedUser->id !== $request->user()->id) {
$this->notificationService->create(
$mentionedUser->id,
'mention',
[
'comment_id' => $comment->id,
'task_id' => $comment->commentable_type === 'App\\Models\\Task' ? $comment->commentable_id : null,
'notifiable_type' => $comment->commentable_type,
'notifiable_id' => $comment->commentable_id,
'url' => $comment->commentable_type === 'App\\Models\\Task' ? '/kanban' : null,
],
"شما توسط {$request->user()->name} در یک نوت منشن شدید"
);
}
}
preg_match_all('/@(\w+)/', $comment->body, $matches);
if (!empty($matches[1])) {
$mentionedUsers = User::whereIn('name', $matches[1])->get();
foreach ($mentionedUsers as $mentionedUser) {
$mentionedUser->notifications()->create([
'type' => 'mention',
'title' => 'منشن جدید',
'body' => "شما در کامنت توسط {$request->user()->name} منشن شدید",
'data' => [
'comment_id' => $comment->id,
'task_id' => $comment->commentable_type === 'App\\Models\\Task' ? $comment->commentable_id : null,
'url' => $comment->commentable_type === 'App\\Models\\Task' ? '/kanban' : null,
],
'is_read' => false,
]);
}
}
$this->activityLogService->log(
$request->user()->id,
'create_comment',
'نظر جدید اضافه شد',
$comment->commentable_type,
$comment->commentable_id,
null,
null,
['comment_id' => $comment->id]
);
return response()->json([
'success' => true,
'data' => new CommentResource($comment),
'message' => 'نظر با موفقیت ثبت شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ثبت نظر',
], 500);
}
}
public function update(Request $request, Comment $comment): JsonResponse
{
try {
if ($comment->user_id !== $request->user()->id) {
return response()->json([
'success' => false,
'message' => 'شما اجازه ویرایش این نظر را ندارید',
], 403);
}
$request->validate(['body' => 'required|string']);
$comment->update(['body' => $request->body]);
return response()->json([
'success' => true,
'data' => new CommentResource($comment->fresh()->load('user')),
'message' => 'نظر به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی نظر',
], 500);
}
}
public function destroy(Request $request, Comment $comment): JsonResponse
{
try {
if ($comment->user_id !== $request->user()->id) {
return response()->json([
'success' => false,
'message' => 'شما اجازه حذف این نظر را ندارید',
], 403);
}
$comment->delete();
return response()->json([
'success' => true,
'message' => 'نظر حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف نظر',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\DashboardService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function __construct(protected DashboardService $dashboardService) {}
public function summary(Request $request): JsonResponse
{
try {
$data = $this->dashboardService->getSummary($request->user());
return response()->json([
'success' => true,
'data' => $data,
'message' => 'خلاصه داشبورد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات داشبورد',
], 500);
}
}
public function charts(): JsonResponse
{
try {
$data = $this->dashboardService->getChartData();
return response()->json([
'success' => true,
'data' => $data,
'message' => 'نمودارهای داشبورد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت نمودارها',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,370 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Department;
use App\Models\User;
use App\Services\ActivityLogService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class DepartmentController extends Controller
{
public function __construct(protected ActivityLogService $activityLogService) {}
public function index(): JsonResponse
{
try {
$departments = Department::with(['manager', 'children', 'members'])->orderBy('sort_order')->get();
$departmentIds = $departments->pluck('id');
$tree = $departments
->filter(fn($department) => $department->parent_id === null || !$departmentIds->contains($department->parent_id))
->values()
->map(fn($d) => $this->formatNode($d, $departments));
return response()->json([
'success' => true,
'data' => $tree,
'flat' => $departments->values()->map(fn($d) => [
'id' => $d->id,
'name' => $d->name,
'type' => $d->type ?? 'department',
'parent_id' => $d->parent_id,
'parent_name' => $departments->firstWhere('id', $d->parent_id)?->name,
'children_count' => $departments->where('parent_id', $d->id)->count(),
]),
'message' => 'ساختار سازمانی',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت ساختار سازمانی',
], 500);
}
}
private function formatNode($dept, $all): array
{
$children = $all->where('parent_id', $dept->id)->values();
return [
'id' => $dept->id,
'name' => $dept->name,
'description' => $dept->description,
'type' => $dept->type ?? 'department',
'parent_id' => $dept->parent_id,
'manager' => $dept->manager ? ['id' => $dept->manager->id, 'name' => $dept->manager->name] : null,
'manager_id' => $dept->manager_id,
'manager_changed_at' => $dept->manager_changed_at,
'is_active' => $dept->is_active,
'sort_order' => $dept->sort_order,
'users_count' => $dept->members()->count() ?: $dept->users()->count(),
'children' => $children->map(fn($c) => $this->formatNode($c, $all)),
];
}
public function store(Request $request): JsonResponse
{
try {
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'type' => 'nullable|string|in:organization,department,unit,team',
'parent_id' => 'nullable|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
'is_active' => 'nullable|boolean',
'sort_order' => 'nullable|integer',
]);
$data = $request->only(['name', 'description', 'type', 'parent_id', 'manager_id', 'is_active', 'sort_order']);
$data['type'] = $data['type'] ?? 'department';
$data['is_active'] = $data['is_active'] ?? true;
if (!empty($data['manager_id'])) {
$data['manager_changed_at'] = now();
}
$dept = Department::create($data);
$this->activityLogService->log(
$request->user()?->id,
'create_department',
"بخش {$dept->name} ایجاد شد",
'department',
$dept->id,
null,
null,
['new' => $dept->only(['name', 'type', 'parent_id', 'manager_id', 'is_active'])]
);
return response()->json([
'success' => true,
'data' => $dept->load('manager'),
'message' => 'دپارتمان با موفقیت ایجاد شد',
], 201);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد دپارتمان',
], 500);
}
}
public function show(Department $department): JsonResponse
{
try {
$department->load(['manager', 'parent', 'children']);
return response()->json([
'success' => true,
'data' => $department,
'message' => 'اطلاعات دپارتمان',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات دپارتمان',
], 500);
}
}
public function update(Request $request, Department $department): JsonResponse
{
try {
$request->validate([
'name' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'type' => 'nullable|string|in:organization,department,unit,team',
'parent_id' => 'nullable|exists:departments,id',
'manager_id' => 'nullable|exists:users,id',
'is_active' => 'nullable|boolean',
'sort_order' => 'nullable|integer',
]);
$old = $department->only(['name', 'description', 'type', 'parent_id', 'manager_id', 'is_active', 'sort_order']);
$payload = $request->only(['name', 'description', 'type', 'parent_id', 'manager_id', 'is_active', 'sort_order']);
if (array_key_exists('manager_id', $payload) && (int) $payload['manager_id'] !== (int) $department->manager_id) {
$payload['manager_changed_at'] = now();
}
$department->update($payload);
$changed = collect($payload)
->except('manager_changed_at')
->filter(fn($value, $key) => array_key_exists($key, $old) && $old[$key] != $department->{$key})
->map(fn($value, $key) => ['old' => $old[$key], 'new' => $department->{$key}])
->all();
if (!empty($changed)) {
$this->activityLogService->log(
$request->user()?->id,
array_key_exists('manager_id', $changed) ? 'change_department_manager' : 'update_department',
array_key_exists('manager_id', $changed)
? "مدیر بخش {$department->name} تغییر کرد"
: "بخش {$department->name} ویرایش شد",
'department',
$department->id,
null,
null,
['changes' => $changed]
);
}
return response()->json([
'success' => true,
'data' => $department->fresh()->load('manager'),
'message' => 'دپارتمان به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی دپارتمان',
], 500);
}
}
public function destroy(Request $request, Department $department): JsonResponse
{
try {
if ($department->children()->exists()) {
return response()->json([
'success' => false,
'message' => 'این دپارتمان دارای زیرمجموعه است. ابتدا زیرمجموعه‌ها را حذف کنید.',
], 400);
}
if ($department->members()->exists() || $department->users()->exists()) {
return response()->json([
'success' => false,
'message' => 'این بخش عضو دارد. ابتدا اعضا را منتقل کنید یا بخش را غیرفعال کنید.',
], 400);
}
$departmentName = $department->name;
$departmentId = $department->id;
$department->delete();
$this->activityLogService->log(
$request->user()?->id,
'delete_department',
"بخش {$departmentName} حذف شد",
'department',
$departmentId
);
return response()->json([
'success' => true,
'message' => 'دپارتمان حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف دپارتمان',
], 500);
}
}
public function addMember(Request $request, Department $department): JsonResponse
{
try {
$request->validate([
'user_id' => 'required|exists:users,id',
'role_in_team' => 'nullable|string|max:255',
'is_primary' => 'nullable|boolean',
]);
$user = User::findOrFail($request->user_id);
$isPrimary = $request->boolean('is_primary');
$this->syncMember($department, $user, $request->role_in_team, $isPrimary);
$this->activityLogService->log(
$request->user()?->id,
'add_department_member',
"{$user->name} به {$department->name} اضافه شد",
'department',
$department->id,
null,
null,
['user_id' => $user->id, 'role_in_team' => $request->role_in_team, 'is_primary' => $isPrimary]
);
return response()->json(['success' => true, 'message' => 'عضو اضافه شد']);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در افزودن عضو'], 500);
}
}
public function updateMember(Request $request, Department $department, User $user): JsonResponse
{
try {
$request->validate([
'role_in_team' => 'nullable|string|max:255',
'is_primary' => 'nullable|boolean',
]);
$isPrimary = $request->boolean('is_primary');
$this->syncMember($department, $user, $request->role_in_team, $isPrimary);
$this->activityLogService->log(
$request->user()?->id,
'update_department_member',
"عضویت {$user->name} در {$department->name} ویرایش شد",
'department',
$department->id,
null,
null,
['user_id' => $user->id, 'role_in_team' => $request->role_in_team, 'is_primary' => $isPrimary]
);
return response()->json(['success' => true, 'message' => 'عضویت به‌روزرسانی شد']);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در به‌روزرسانی عضویت'], 500);
}
}
public function removeMember(Request $request, Department $department, User $user): JsonResponse
{
try {
$department->members()->detach($user->id);
if ((int) $user->department_id === (int) $department->id) {
$primary = $user->departments()->wherePivot('is_primary', true)->first();
$user->update([
'department_id' => $primary?->id,
'department' => $primary?->name,
]);
}
$this->activityLogService->log(
$request->user()?->id,
'remove_department_member',
"{$user->name} از {$department->name} حذف شد",
'department',
$department->id,
null,
null,
['user_id' => $user->id]
);
return response()->json(['success' => true, 'message' => 'عضو حذف شد']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در حذف عضو'], 500);
}
}
public function transferMember(Request $request, Department $department, User $user): JsonResponse
{
try {
$request->validate([
'target_department_id' => 'required|exists:departments,id',
'role_in_team' => 'nullable|string|max:255',
'is_primary' => 'nullable|boolean',
]);
$target = Department::findOrFail($request->target_department_id);
$department->members()->detach($user->id);
$this->syncMember($target, $user, $request->role_in_team, $request->boolean('is_primary', true));
$this->activityLogService->log(
$request->user()?->id,
'transfer_department_member',
"{$user->name} از {$department->name} به {$target->name} منتقل شد",
'department',
$department->id,
null,
null,
['user_id' => $user->id, 'from' => $department->id, 'to' => $target->id]
);
return response()->json(['success' => true, 'message' => 'عضو منتقل شد']);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در انتقال عضو'], 500);
}
}
private function syncMember(Department $department, User $user, ?string $roleInTeam, bool $isPrimary): void
{
if ($isPrimary) {
DB::table('department_user')->where('user_id', $user->id)->update(['is_primary' => false]);
$user->update([
'department_id' => $department->id,
'department' => $department->name,
]);
}
$department->members()->syncWithoutDetaching([
$user->id => [
'role_in_team' => $roleInTeam,
'is_primary' => $isPrimary,
'joined_at' => now(),
],
]);
}
}

مشاهده پرونده

@ -0,0 +1,155 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\FileResource;
use App\Models\File;
use App\Models\Project;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class FileController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = File::with('user');
if ($request->filled('fileable_type')) {
$query->where('fileable_type', $request->fileable_type);
}
if ($request->filled('fileable_id')) {
$query->where('fileable_id', $request->fileable_id);
}
$perPage = $request->input('per_page', 15);
$files = $query->orderBy('created_at', 'desc')->paginate($perPage);
return response()->json([
'success' => true,
'data' => FileResource::collection($files),
'meta' => [
'current_page' => $files->currentPage(),
'last_page' => $files->lastPage(),
'per_page' => $files->perPage(),
'total' => $files->total(),
],
'message' => 'لیست فایل‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت فایل‌ها',
], 500);
}
}
public function store(Request $request): JsonResponse
{
try {
$request->validate([
'file' => 'required|file|max:10240|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,jpg,jpeg,png,webp,zip',
'fileable_type' => 'nullable|string|in:App\\Models\\Project,App\\Models\\Task,App\\Models\\Comment,App\\Models\\Meeting',
'fileable_id' => 'nullable|integer',
'project_id' => 'nullable|exists:projects,id',
]);
$fileableType = $request->fileable_type;
$fileableId = $request->fileable_id;
if ($request->filled('project_id')) {
$fileableType = Project::class;
$fileableId = $request->project_id;
}
if (!$fileableType || !$fileableId) {
return response()->json([
'success' => false,
'message' => 'انتخاب محل آپلود فایل الزامی است',
], 422);
}
$uploadedFile = $request->file('file');
$originalName = $uploadedFile->getClientOriginalName();
$extension = strtolower($uploadedFile->getClientOriginalExtension());
$name = Str::uuid()->toString() . ($extension ? ".{$extension}" : '');
$path = $uploadedFile->storeAs('files', $name, 'local');
$file = File::create([
'name' => $name,
'original_name' => $originalName,
'path' => $path,
'mime_type' => $uploadedFile->getMimeType(),
'size' => $uploadedFile->getSize(),
'fileable_id' => $fileableId,
'fileable_type' => $fileableType,
'user_id' => $request->user()->id,
]);
$file->load('user');
return response()->json([
'success' => true,
'data' => new FileResource($file),
'message' => 'فایل با موفقیت آپلود شد',
], 201);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در آپلود فایل',
], 500);
}
}
public function show(File $file): JsonResponse
{
try {
$disk = Storage::disk('local')->exists($file->path) ? 'local' : 'public';
if (!Storage::disk($disk)->exists($file->path)) {
return response()->json([
'success' => false,
'message' => 'فایل یافت نشد',
], 404);
}
$filePath = Storage::disk($disk)->path($file->path);
return response()->download($filePath, $file->original_name);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت فایل',
], 500);
}
}
public function destroy(File $file): JsonResponse
{
try {
foreach (['local', 'public'] as $disk) {
if (Storage::disk($disk)->exists($file->path)) {
Storage::disk($disk)->delete($file->path);
break;
}
}
$file->delete();
return response()->json([
'success' => true,
'message' => 'فایل حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف فایل',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,306 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreMeetingRequest;
use App\Http\Resources\MeetingResource;
use App\Http\Resources\MeetingActionItemResource;
use App\Http\Resources\TaskResource;
use App\Models\Meeting;
use App\Models\MeetingActionItem;
use App\Models\Task;
use App\Models\User;
use App\Services\NotificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MeetingController extends Controller
{
public function __construct(protected NotificationService $notificationService) {}
public function index(Request $request): JsonResponse
{
try {
$query = Meeting::with(['project', 'creator', 'participants']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('date_from')) {
$query->whereDate('date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('date', '<=', $request->date_to);
}
if ($request->filled('meeting_type')) {
$query->where('meeting_type', $request->meeting_type);
}
$perPage = $request->input('per_page', 15);
$sortBy = $request->input('sort_by', 'date');
$sortDir = $request->input('sort_dir', 'desc');
$meetings = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => MeetingResource::collection($meetings),
'meta' => [
'current_page' => $meetings->currentPage(),
'last_page' => $meetings->lastPage(),
'per_page' => $meetings->perPage(),
'total' => $meetings->total(),
],
'message' => 'لیست جلسات',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت جلسات',
], 500);
}
}
public function show(Meeting $meeting): JsonResponse
{
try {
$meeting->load(['project', 'creator', 'participants', 'actionItems.assignee']);
return response()->json([
'success' => true,
'data' => new MeetingResource($meeting),
'message' => 'اطلاعات جلسه',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات جلسه',
], 500);
}
}
public function store(StoreMeetingRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['created_by'] = $request->user()->id;
$meeting = Meeting::create($data);
$meeting->load(['project', 'creator']);
return response()->json([
'success' => true,
'data' => new MeetingResource($meeting),
'message' => 'جلسه با موفقیت ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد جلسه',
], 500);
}
}
public function update(Request $request, Meeting $meeting): JsonResponse
{
try {
$request->validate([
'title' => 'sometimes|required|string|max:255',
'date' => 'sometimes|required|date',
'start_time' => 'nullable',
'end_time' => 'nullable',
'meeting_type' => 'sometimes|required|string|max:50',
'location' => 'nullable|string',
'meeting_link' => 'nullable|string',
'agenda' => 'nullable|string',
'notes' => 'nullable|string',
'decisions' => 'nullable|string',
]);
$meeting->update($request->only([
'title', 'date', 'start_time', 'end_time', 'meeting_type',
'location', 'meeting_link', 'agenda', 'notes', 'decisions'
]));
$meeting->load(['project', 'creator']);
return response()->json([
'success' => true,
'data' => new MeetingResource($meeting->fresh()),
'message' => 'جلسه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی جلسه',
], 500);
}
}
public function destroy(Meeting $meeting): JsonResponse
{
try {
$meeting->delete();
return response()->json([
'success' => true,
'message' => 'جلسه حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف جلسه',
], 500);
}
}
public function addParticipant(Request $request, Meeting $meeting): JsonResponse
{
try {
$request->validate(['user_id' => 'required|exists:users,id']);
$meeting->participants()->syncWithoutDetaching([$request->user_id]);
$this->notificationService->create(
$request->user_id,
'meeting_assigned',
[
'meeting_id' => $meeting->id,
'project_id' => $meeting->project_id,
'url' => '/meetings',
'notifiable_type' => 'App\\Models\\Meeting',
'notifiable_id' => $meeting->id,
],
"شما به جلسه {$meeting->title} اضافه شدید"
);
return response()->json([
'success' => true,
'message' => 'شرکت‌کننده اضافه شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در اضافه کردن شرکت‌کننده',
], 500);
}
}
public function removeParticipant(Meeting $meeting, User $user): JsonResponse
{
try {
$meeting->participants()->detach($user->id);
return response()->json([
'success' => true,
'message' => 'شرکت‌کننده حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف شرکت‌کننده',
], 500);
}
}
public function addActionItem(Request $request, Meeting $meeting): JsonResponse
{
try {
$request->validate([
'title' => 'required|string|max:255',
'assigned_to' => 'nullable|exists:users,id',
'due_date' => 'nullable|date',
]);
$actionItem = $meeting->actionItems()->create([
'title' => $request->title,
'assigned_to' => $request->assigned_to,
'due_date' => $request->due_date,
'is_completed' => false,
]);
$actionItem->load('assignee');
if ($actionItem->assigned_to) {
$this->notificationService->create(
$actionItem->assigned_to,
'meeting_action_assigned',
[
'meeting_id' => $meeting->id,
'action_item_id' => $actionItem->id,
'url' => '/meetings',
'notifiable_type' => 'App\\Models\\MeetingActionItem',
'notifiable_id' => $actionItem->id,
],
"اقدام جلسه {$actionItem->title} به شما اختصاص داده شد"
);
}
return response()->json([
'success' => true,
'data' => new MeetingActionItemResource($actionItem),
'message' => 'ایتم اقدام ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد ایتم اقدام',
], 500);
}
}
public function updateActionItem(Request $request, Meeting $meeting, MeetingActionItem $actionItem): JsonResponse
{
try {
$request->validate([
'title' => 'sometimes|required|string|max:255',
'assigned_to' => 'nullable|exists:users,id',
'due_date' => 'nullable|date',
'is_completed' => 'nullable|boolean',
]);
$actionItem->update($request->only(['title', 'assigned_to', 'due_date', 'is_completed']));
$actionItem->load('assignee');
return response()->json([
'success' => true,
'data' => new MeetingActionItemResource($actionItem->fresh()),
'message' => 'ایتم اقدام به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی ایتم اقدام',
], 500);
}
}
public function convertActionItemToTask(Request $request, Meeting $meeting, MeetingActionItem $actionItem): JsonResponse
{
try {
$request->validate([
'project_id' => 'required|exists:projects,id',
]);
$task = Task::create([
'title' => $actionItem->title,
'project_id' => $request->project_id,
'assignee_id' => $actionItem->assigned_to,
'reporter_id' => $request->user()->id,
'created_by' => $request->user()->id,
'due_date' => $actionItem->due_date,
'status' => 'todo',
]);
$actionItem->update(['converted_to_task_id' => $task->id]);
$task->load(['project', 'assignee']);
return response()->json([
'success' => true,
'data' => new TaskResource($task),
'message' => 'ایتم اقدام به وظیفه تبدیل شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در تبدیل ایتم اقدام به وظیفه',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\NotificationResource;
use App\Models\Notification;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class NotificationController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = $request->user()->notifications();
$perPage = $request->input('per_page', 15);
$notifications = $query->orderBy('created_at', 'desc')->paginate($perPage);
return response()->json([
'success' => true,
'data' => NotificationResource::collection($notifications),
'meta' => [
'current_page' => $notifications->currentPage(),
'last_page' => $notifications->lastPage(),
'per_page' => $notifications->perPage(),
'total' => $notifications->total(),
'unread_count' => $request->user()->notifications()->whereNull('read_at')->count(),
],
'message' => 'لیست اعلان‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اعلان‌ها',
], 500);
}
}
public function markAsRead(Notification $notification): JsonResponse
{
try {
if ($notification->user_id !== request()->user()->id) {
return response()->json([
'success' => false,
'message' => 'شما اجازه دسترسی به این اعلان را ندارید',
], 403);
}
$notification->update(['read_at' => Carbon::now(), 'is_read' => true]);
return response()->json([
'success' => true,
'data' => new NotificationResource($notification->fresh()),
'message' => 'اعلان به عنوان خوانده شده علامت خورد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی اعلان',
], 500);
}
}
public function markAllAsRead(Request $request): JsonResponse
{
try {
$request->user()->notifications()->whereNull('read_at')->update([
'read_at' => Carbon::now(),
'is_read' => true,
]);
return response()->json([
'success' => true,
'message' => 'همه اعلان‌ها به عنوان خوانده شده علامت خوردند',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی اعلان‌ها',
], 500);
}
}
public function destroy(Request $request, Notification $notification): JsonResponse
{
try {
if ($notification->user_id !== $request->user()->id) {
return response()->json([
'success' => false,
'message' => 'شما اجازه حذف این اعلان را ندارید',
], 403);
}
$notification->delete();
return response()->json([
'success' => true,
'message' => 'اعلان حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف اعلان',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PermissionResource;
use App\Models\Permission;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class PermissionController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$permissions = Permission::all()->groupBy('module');
return response()->json([
'success' => true,
'data' => $permissions->map(function ($items, $module) {
return [
'module' => $module,
'permissions' => PermissionResource::collection($items),
];
})->values(),
'message' => 'لیست دسترسی‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت دسترسی‌ها',
], 500);
}
}
public function show(Permission $permission): JsonResponse
{
try {
return response()->json([
'success' => true,
'data' => new PermissionResource($permission),
'message' => 'اطلاعات دسترسی',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات دسترسی',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,301 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
use App\Http\Resources\ProjectResource;
use App\Models\Project;
use App\Models\User;
use App\Services\ActivityLogService;
use App\Services\NotificationService;
use App\Services\ProjectProgressService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ProjectController extends Controller
{
public function __construct(
protected ActivityLogService $activityLogService,
protected NotificationService $notificationService,
protected ProjectProgressService $projectProgressService,
) {}
public function index(Request $request): JsonResponse
{
try {
$query = Project::with(['projectManager', 'creator', 'department']);
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('priority')) {
$query->where('priority', $request->priority);
}
if ($request->filled('project_manager_id')) {
$query->where('project_manager_id', $request->project_manager_id);
}
if ($request->filled('risk_level')) {
$query->where('risk_level', $request->risk_level);
}
if ($request->filled('search')) {
$search = $request->search;
$query->where('title', 'like', "%{$search}%");
}
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'title', 'status', 'priority', 'created_at', 'updated_at', 'start_date', 'end_date'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$projects = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => ProjectResource::collection($projects),
'meta' => [
'current_page' => $projects->currentPage(),
'last_page' => $projects->lastPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
'message' => 'لیست پروژه‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت لیست پروژه‌ها',
], 500);
}
}
public function show(Project $project): JsonResponse
{
try {
$project->load(['projectManager', 'creator', 'department', 'members', 'tasks', 'sprints', 'meetings']);
return response()->json([
'success' => true,
'data' => new ProjectResource($project),
'message' => 'اطلاعات پروژه',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات پروژه',
], 500);
}
}
public function store(StoreProjectRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['created_by'] = $request->user()->id;
$project = Project::create($data);
$project->load(['projectManager', 'creator', 'department']);
$this->activityLogService->log(
$request->user()->id,
'create_project',
"پروژه {$project->title} ایجاد شد",
'project',
$project->id,
$project->id,
null,
['project_title' => $project->title]
);
return response()->json([
'success' => true,
'data' => new ProjectResource($project),
'message' => 'پروژه با موفقیت ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد پروژه',
], 500);
}
}
public function update(UpdateProjectRequest $request, Project $project): JsonResponse
{
try {
$project->update($request->validated());
$project->load(['projectManager', 'creator', 'department']);
$this->activityLogService->log(
$request->user()->id,
'update_project',
"پروژه {$project->title} به‌روزرسانی شد",
'project',
$project->id,
$project->id,
null,
['project_title' => $project->title]
);
return response()->json([
'success' => true,
'data' => new ProjectResource($project->fresh()),
'message' => 'پروژه با موفقیت به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی پروژه',
], 500);
}
}
public function destroy(Request $request, Project $project): JsonResponse
{
try {
$projectTitle = $project->title;
$project->delete();
$this->activityLogService->log(
$request->user()->id,
'delete_project',
"پروژه {$projectTitle} حذف شد",
'project',
$project->id,
$project->id,
null,
['project_title' => $projectTitle]
);
return response()->json([
'success' => true,
'message' => 'پروژه با موفقیت حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف پروژه',
], 500);
}
}
public function archive(Request $request, Project $project): JsonResponse
{
try {
$project->update(['is_archived' => true]);
return response()->json([
'success' => true,
'data' => new ProjectResource($project->fresh()),
'message' => 'پروژه بایگانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در بایگانی پروژه',
], 500);
}
}
public function restore(Request $request, Project $project): JsonResponse
{
try {
$project->update(['is_archived' => false]);
return response()->json([
'success' => true,
'data' => new ProjectResource($project->fresh()),
'message' => 'پروژه از بایگانی خارج شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در بازیابی پروژه',
], 500);
}
}
public function addMember(Request $request, Project $project): JsonResponse
{
try {
$request->validate([
'user_id' => 'required|exists:users,id',
'role_in_project' => 'nullable|string',
]);
$project->members()->syncWithoutDetaching([
$request->user_id => [
'role_in_project' => $request->role_in_project ?? 'member',
],
]);
$this->activityLogService->log(
$request->user()->id,
'add_member',
"عضو جدید به پروژه {$project->title} اضافه شد",
'project',
$project->id,
$project->id,
null,
['user_id' => $request->user_id]
);
return response()->json([
'success' => true,
'data' => new ProjectResource($project->fresh()->load(['projectManager', 'creator', 'department', 'members'])),
'message' => 'عضو با موفقیت اضافه شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در اضافه کردن عضو',
], 500);
}
}
public function removeMember(Request $request, Project $project, User $user): JsonResponse
{
try {
$project->members()->detach($user->id);
$this->activityLogService->log(
$request->user()->id,
'remove_member',
"عضو از پروژه {$project->title} حذف شد",
'project',
$project->id,
$project->id,
null,
['user_id' => $user->id]
);
return response()->json([
'success' => true,
'message' => 'عضو با موفقیت حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف عضو',
], 500);
}
}
public function updateProgress(Request $request, Project $project): JsonResponse
{
try {
$progress = $this->projectProgressService->calculateProgress($project->id);
return response()->json([
'success' => true,
'data' => ['progress' => $progress],
'message' => 'پیشرفت پروژه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی پیشرفت',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,988 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\CommentResource;
use App\Http\Resources\FileResource;
use App\Models\Notification;
use App\Models\ActivityLog;
use App\Models\Comment;
use App\Models\File;
use App\Models\Project;
use App\Models\Sprint;
use App\Models\Task;
use App\Models\User;
use App\Services\ProjectProgressService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PwaController extends Controller
{
public function __construct(protected ProjectProgressService $projectProgressService) {}
public function profile(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions', 'dept', 'departments']);
$permissions = $user->roles
->flatMap(fn($role) => $role->permissions->pluck('name'))
->unique()
->values();
return response()->json([
'success' => true,
'data' => [
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'phone' => $user->phone,
'job_title' => $user->job_title,
'department' => $user->dept?->name ?: $user->department,
'status' => $user->status ?: 'active',
'avatar' => $user->avatar,
'avatar_url' => $user->avatar ? asset('storage/' . $user->avatar) : null,
'roles' => $user->roles->map(fn($role) => [
'id' => $role->id,
'name' => $role->name,
'display_name' => $role->display_name,
])->values(),
'primary_role' => $user->roles->first()?->display_name ?: $user->roles->first()?->name,
'departments' => $user->departments->map(fn($department) => [
'id' => $department->id,
'name' => $department->name,
'role_in_team' => $department->pivot?->role_in_team,
'is_primary' => (bool) $department->pivot?->is_primary,
])->values(),
],
'access_summary' => $this->profileAccessSummary($permissions, $user),
'capabilities' => [
'can_change_password' => true,
'can_update_profile' => true,
'can_update_avatar' => true,
'push_backend_ready' => false,
],
],
'message' => 'پروفایل موبایل',
]);
}
public function projects(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions']);
$query = $this->visibleProjectsQuery($user)
->with(['projectManager:id,name,job_title,avatar', 'members:id,name,job_title,avatar'])
->withCount([
'tasks',
'tasks as done_tasks_count' => fn($taskQuery) => $taskQuery->where('status', 'done'),
'tasks as overdue_tasks_count' => fn($taskQuery) => $taskQuery
->whereDate('due_date', '<', now()->toDateString())
->whereNotIn('status', ['done', 'canceled', 'cancelled']),
]);
$filter = $request->input('filter', 'all');
match ($filter) {
'active' => $query->where('status', 'active'),
'waiting' => $query->whereIn('status', ['waiting', 'pending']),
'completed' => $query->whereIn('status', ['completed', 'done']),
'delayed' => $query->whereHas('tasks', fn($taskQuery) => $taskQuery
->whereDate('due_date', '<', now()->toDateString())
->whereNotIn('status', ['done', 'canceled', 'cancelled'])),
'mine' => $query->where(function ($scopeQuery) use ($user) {
$scopeQuery->where('project_manager_id', $user->id)
->orWhereHas('members', fn($memberQuery) => $memberQuery->where('users.id', $user->id));
}),
default => null,
};
if ($request->filled('search')) {
$search = trim($request->input('search'));
$query->where('title', 'like', "%{$search}%");
}
$projects = $query
->where(function ($scopeQuery) {
$scopeQuery->where('is_archived', false)->orWhereNull('is_archived');
})
->orderByRaw('case when end_date is null then 1 else 0 end')
->orderBy('end_date')
->latest()
->paginate(min((int) $request->input('per_page', 20), 50));
return response()->json([
'success' => true,
'data' => collect($projects->items())->map(fn(Project $project) => $this->projectCard($project))->values(),
'meta' => [
'current_page' => $projects->currentPage(),
'last_page' => $projects->lastPage(),
'per_page' => $projects->perPage(),
'total' => $projects->total(),
],
'message' => 'پروژه‌های موبایل',
]);
}
public function projectDetail(Request $request, Project $project): JsonResponse
{
$this->abortUnlessProjectVisible($request, $project);
$today = now()->toDateString();
$project->load([
'projectManager:id,name,job_title,avatar',
'members:id,name,job_title,avatar',
'tasks' => fn($taskQuery) => $taskQuery
->with(['assignee:id,name,job_title'])
->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end")
->orderByRaw('case when due_date is null then 1 else 0 end')
->orderBy('due_date')
->limit(8),
])->loadCount([
'tasks',
'tasks as done_tasks_count' => fn($taskQuery) => $taskQuery->where('status', 'done'),
'tasks as overdue_tasks_count' => fn($taskQuery) => $taskQuery
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled', 'cancelled']),
'tasks as today_tasks_count' => fn($taskQuery) => $taskQuery->whereDate('due_date', $today),
'members',
]);
$activities = ActivityLog::query()
->where('project_id', $project->id)
->latest()
->limit(6)
->get(['id', 'action', 'description', 'created_at'])
->map(fn(ActivityLog $activity) => [
'id' => $activity->id,
'action' => $activity->action,
'description' => $activity->description,
'created_at' => $activity->created_at,
]);
return response()->json([
'success' => true,
'data' => array_merge($this->projectCard($project), [
'description' => $project->description,
'members_count' => $project->members_count,
'today_tasks_count' => $project->today_tasks_count,
'tasks' => $project->tasks->map(fn(Task $task) => $this->taskCard($task))->values(),
'members' => $project->members->map(fn(User $member) => [
'id' => $member->id,
'name' => $member->name,
'job_title' => $member->job_title,
'avatar_url' => $member->avatar ? asset('storage/' . $member->avatar) : null,
'role_in_project' => $member->pivot?->role_in_project,
])->values(),
'activities' => $activities,
]),
'message' => 'جزئیات پروژه',
]);
}
public function updateProjectStatus(Request $request, Project $project): JsonResponse
{
$this->abortUnlessProjectVisible($request, $project);
$data = $request->validate([
'status' => ['required', Rule::in(['active', 'waiting', 'pending', 'paused', 'completed', 'done'])],
], [
'status.required' => 'انتخاب وضعیت پروژه الزامی است.',
'status.in' => 'وضعیت انتخاب‌شده معتبر نیست.',
]);
$status = $data['status'] === 'done' ? 'completed' : $data['status'];
$status = $status === 'pending' ? 'waiting' : $status;
$project->update(['status' => $status]);
$project->load(['projectManager:id,name,job_title,avatar', 'members:id,name,job_title,avatar'])
->loadCount([
'tasks',
'tasks as done_tasks_count' => fn($taskQuery) => $taskQuery->where('status', 'done'),
'tasks as overdue_tasks_count' => fn($taskQuery) => $taskQuery
->whereDate('due_date', '<', now()->toDateString())
->whereNotIn('status', ['done', 'canceled', 'cancelled']),
]);
return response()->json([
'success' => true,
'data' => $this->projectCard($project),
'message' => 'وضعیت پروژه تغییر کرد.',
]);
}
public function notifications(Request $request): JsonResponse
{
$query = $request->user()->notifications()
->whereNull('dismissed_at')
->where(function ($innerQuery) {
$innerQuery->whereNull('remind_at')->orWhere('remind_at', '<=', now());
});
$filter = $request->input('filter', 'all');
match ($filter) {
'unread' => $query->where(function ($innerQuery) {
$innerQuery->whereNull('read_at')->orWhere('is_read', false);
}),
'tasks' => $query->whereIn('type', ['task_assigned', 'task_new', 'task_created', 'task_status_changed', 'task_overdue']),
'comments' => $query->whereIn('type', ['comment_created', 'comment_new']),
'mentions' => $query->where('type', 'mention'),
'deadlines' => $query->whereIn('type', ['deadline_soon', 'task_overdue']),
'system' => $query->where('type', 'system'),
default => null,
};
$notifications = $query
->latest()
->paginate(min((int) $request->input('per_page', 20), 50));
return response()->json([
'success' => true,
'data' => collect($notifications->items())->map(fn(Notification $notification) => $this->notificationPayload($notification))->values(),
'meta' => [
'current_page' => $notifications->currentPage(),
'last_page' => $notifications->lastPage(),
'per_page' => $notifications->perPage(),
'total' => $notifications->total(),
'unread_count' => $this->unreadNotificationsCount($request),
],
'message' => 'اعلان‌های موبایل',
]);
}
public function notificationRead(Request $request, Notification $notification): JsonResponse
{
abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.');
$notification->update([
'read_at' => now(),
'is_read' => true,
]);
return response()->json([
'success' => true,
'data' => $this->notificationPayload($notification->fresh()),
'meta' => ['unread_count' => $this->unreadNotificationsCount($request)],
'message' => 'اعلان خوانده شد.',
]);
}
public function notificationUnread(Request $request, Notification $notification): JsonResponse
{
abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.');
$notification->update([
'read_at' => null,
'is_read' => false,
]);
return response()->json([
'success' => true,
'data' => $this->notificationPayload($notification->fresh()),
'meta' => ['unread_count' => $this->unreadNotificationsCount($request)],
'message' => 'اعلان به حالت خوانده‌نشده برگشت.',
]);
}
public function notificationRemind(Request $request, Notification $notification): JsonResponse
{
abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.');
$notification->update([
'remind_at' => now()->addMinutes(30),
'dismissed_at' => null,
'read_at' => null,
'is_read' => false,
]);
return response()->json([
'success' => true,
'data' => $this->notificationPayload($notification->fresh()),
'meta' => ['unread_count' => $this->unreadNotificationsCount($request)],
'message' => '۳۰ دقیقه دیگر یادآوری می‌شود.',
]);
}
public function notificationDismiss(Request $request, Notification $notification): JsonResponse
{
abort_unless((int) $notification->user_id === (int) $request->user()->id, 403, 'شما به این اعلان دسترسی ندارید.');
$notification->update([
'dismissed_at' => now(),
]);
return response()->json([
'success' => true,
'meta' => ['unread_count' => $this->unreadNotificationsCount($request)],
'message' => 'اعلان از لیست خارج شد.',
]);
}
public function notificationsReadAll(Request $request): JsonResponse
{
$request->user()->notifications()
->where(function ($query) {
$query->whereNull('read_at')->orWhere('is_read', false);
})
->update([
'read_at' => now(),
'is_read' => true,
]);
return response()->json([
'success' => true,
'meta' => ['unread_count' => 0],
'message' => 'همه اعلان‌ها خوانده شدند.',
]);
}
public function tasks(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions']);
$query = $this->visibleTasksQuery($user)
->with(['project:id,title,status', 'assignee:id,name,job_title'])
->withCount(['comments', 'files']);
$filter = $request->input('filter', 'all');
$today = now()->toDateString();
match ($filter) {
'today' => $query->whereDate('due_date', $today),
'overdue' => $query->whereDate('due_date', '<', $today)->whereNotIn('status', ['done', 'canceled', 'cancelled']),
'active' => $query->whereIn('status', ['waiting', 'todo', 'in_progress', 'review']),
'done' => $query->where('status', 'done'),
'urgent' => $query->where('priority', 'urgent'),
default => null,
};
if ($request->filled('search')) {
$search = trim($request->input('search'));
$query->where(function ($innerQuery) use ($search) {
$innerQuery->where('title', 'like', "%{$search}%")
->orWhereHas('project', fn($projectQuery) => $projectQuery->where('title', 'like', "%{$search}%"));
});
}
if ($request->filled('project_id')) {
$query->where('project_id', $request->input('project_id'));
}
$tasks = $query
->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end")
->orderByRaw('case when due_date is null then 1 else 0 end')
->orderBy('due_date')
->latest()
->paginate(min((int) $request->input('per_page', 20), 50));
return response()->json([
'success' => true,
'data' => collect($tasks->items())->map(fn(Task $task) => $this->taskCard($task))->values(),
'meta' => [
'current_page' => $tasks->currentPage(),
'last_page' => $tasks->lastPage(),
'per_page' => $tasks->perPage(),
'total' => $tasks->total(),
],
'message' => 'تسک‌های موبایل',
]);
}
public function taskDetail(Request $request, Task $task): JsonResponse
{
$this->abortUnlessTaskVisible($request, $task);
$task->load([
'project:id,title,status',
'assignee:id,name,job_title',
'reporter:id,name,job_title',
'comments.user:id,name,job_title',
'files.user:id,name,job_title',
])->loadCount(['comments', 'files']);
return response()->json([
'success' => true,
'data' => $this->taskDetailPayload($task),
'message' => 'جزئیات تسک',
]);
}
public function updateTaskStatus(Request $request, Task $task): JsonResponse
{
$this->abortUnlessTaskVisible($request, $task);
$data = $request->validate([
'status' => ['required', Rule::in(['waiting', 'todo', 'in_progress', 'done'])],
], [
'status.required' => 'انتخاب وضعیت الزامی است.',
'status.in' => 'وضعیت انتخاب‌شده معتبر نیست.',
]);
$task->update(['status' => $data['status']]);
$this->projectProgressService->calculateProgress($task->project_id);
$task->load(['project:id,title,status', 'assignee:id,name,job_title'])->loadCount(['comments', 'files']);
return response()->json([
'success' => true,
'data' => $this->taskCard($task->fresh(['project', 'assignee'])->loadCount(['comments', 'files'])),
'message' => 'وضعیت تسک تغییر کرد.',
]);
}
public function addTaskComment(Request $request, Task $task): JsonResponse
{
$this->abortUnlessTaskVisible($request, $task);
$data = $request->validate([
'body' => 'required|string|max:3000',
], [
'body.required' => 'متن کامنت الزامی است.',
'body.max' => 'متن کامنت بیش از حد طولانی است.',
]);
$comment = Comment::create([
'body' => $data['body'],
'user_id' => $request->user()->id,
'commentable_id' => $task->id,
'commentable_type' => Task::class,
])->load('user');
return response()->json([
'success' => true,
'data' => new CommentResource($comment),
'message' => 'کامنت ثبت شد.',
], 201);
}
public function addTaskAttachment(Request $request, Task $task): JsonResponse
{
$this->abortUnlessTaskVisible($request, $task);
$request->validate([
'file' => 'required|file|max:10240|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,txt,csv,jpg,jpeg,png,webp,zip',
], [
'file.required' => 'انتخاب فایل الزامی است.',
'file.max' => 'حجم فایل نباید بیشتر از ۱۰ مگابایت باشد.',
'file.mimes' => 'نوع فایل انتخاب‌شده مجاز نیست.',
]);
$uploadedFile = $request->file('file');
$extension = strtolower($uploadedFile->getClientOriginalExtension());
$name = Str::uuid()->toString() . ($extension ? ".{$extension}" : '');
$path = $uploadedFile->storeAs('files', $name, 'local');
$file = File::create([
'name' => $name,
'original_name' => $uploadedFile->getClientOriginalName(),
'path' => $path,
'mime_type' => $uploadedFile->getMimeType(),
'size' => $uploadedFile->getSize(),
'fileable_id' => $task->id,
'fileable_type' => Task::class,
'user_id' => $request->user()->id,
])->load('user');
return response()->json([
'success' => true,
'data' => new FileResource($file),
'message' => 'فایل با موفقیت آپلود شد.',
], 201);
}
public function downloadTaskAttachment(Request $request, Task $task, File $file)
{
$this->abortUnlessTaskVisible($request, $task);
if ($file->fileable_type !== Task::class || (int) $file->fileable_id !== (int) $task->id) {
abort(404);
}
$disk = Storage::disk('local')->exists($file->path) ? 'local' : 'public';
abort_unless(Storage::disk($disk)->exists($file->path), 404);
return response()->download(Storage::disk($disk)->path($file->path), $file->original_name);
}
public function taskOptions(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions']);
if (!$user->hasPermission('tasks.create')) {
return response()->json([
'success' => false,
'message' => 'شما مجوز ایجاد تسک را ندارید.',
], 403);
}
$projectQuery = Project::query()
->with(['members:id,name,job_title,avatar', 'projectManager:id,name,job_title,avatar'])
->when(!$user->hasPermission('reports.view'), function ($query) use ($user) {
$query->where(function ($scopeQuery) use ($user) {
$scopeQuery->where('project_manager_id', $user->id)
->orWhere('created_by', $user->id)
->orWhereHas('members', fn($memberQuery) => $memberQuery->where('users.id', $user->id));
});
})
->where(function ($query) {
$query->where('is_archived', false)->orWhereNull('is_archived');
})
->orderBy('title');
$projects = $projectQuery->limit(100)->get();
$projectUserIds = $projects
->flatMap(fn(Project $project) => $project->members->pluck('id')->push($project->project_manager_id))
->filter()
->unique()
->values();
$users = User::query()
->when(!$user->hasPermission('reports.view'), function ($query) use ($user, $projectUserIds) {
$query->where('id', $user->id)
->when($projectUserIds->isNotEmpty(), fn($innerQuery) => $innerQuery->orWhereIn('id', $projectUserIds));
})
->where(function ($query) {
$query->where('status', 'active')->orWhereNull('status');
})
->orderBy('name')
->limit(200)
->get(['id', 'name', 'job_title', 'avatar']);
return response()->json([
'success' => true,
'data' => [
'projects' => $projects->map(fn(Project $project) => [
'id' => $project->id,
'title' => $project->title,
'members' => $project->members
->push($project->projectManager)
->filter()
->unique('id')
->values()
->map(fn(User $member) => [
'id' => $member->id,
'name' => $member->name,
'job_title' => $member->job_title,
]),
])->values(),
'users' => $users->map(fn(User $optionUser) => [
'id' => $optionUser->id,
'name' => $optionUser->name,
'job_title' => $optionUser->job_title,
])->values(),
'priorities' => [
['value' => 'low', 'label' => 'کم'],
['value' => 'medium', 'label' => 'متوسط'],
['value' => 'high', 'label' => 'زیاد'],
['value' => 'urgent', 'label' => 'فوری'],
],
],
'message' => 'گزینه‌های ایجاد تسک',
]);
}
public function sprints(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions']);
$query = Sprint::query()->with('project:id,title,status');
if (!$user->hasPermission('reports.view')) {
$visibleProjectIds = $this->visibleProjectsQuery($user)->pluck('id');
$query->where(function ($scopeQuery) use ($user, $visibleProjectIds) {
$scopeQuery
->when($visibleProjectIds->isNotEmpty(), fn($innerQuery) => $innerQuery->whereIn('project_id', $visibleProjectIds))
->orWhereHas('members', fn($memberQuery) => $memberQuery->where('users.id', $user->id))
->orWhereHas('tasks', function ($taskQuery) use ($user) {
$taskQuery
->where('assignee_id', $user->id)
->orWhere('reporter_id', $user->id)
->orWhere('created_by', $user->id);
});
});
}
$sprints = $query
->orderByRaw("case when status = 'active' then 0 when status in ('planning','planned') then 1 else 2 end")
->orderByRaw('case when end_date is null then 1 else 0 end')
->orderByDesc('end_date')
->orderByDesc('start_date')
->limit(8)
->get();
return response()->json([
'success' => true,
'data' => $sprints->map(fn(Sprint $sprint) => $this->pwaSprintPayload($sprint, $user))->values(),
'message' => 'اسپرینت‌های موبایل',
]);
}
public function home(Request $request): JsonResponse
{
$user = $request->user()->loadMissing(['roles.permissions']);
$today = now()->toDateString();
$openStatuses = ['waiting', 'todo', 'in_progress', 'review'];
$myTasks = Task::query()
->with(['project:id,title,status'])
->where('assignee_id', $user->id);
$todayTasks = (clone $myTasks)
->whereDate('due_date', $today)
->whereIn('status', $openStatuses);
$overdueTasks = (clone $myTasks)
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled', 'cancelled']);
$activeTasks = (clone $myTasks)
->whereIn('status', ['todo', 'in_progress', 'review', 'waiting']);
$importantTasks = (clone $myTasks)
->whereIn('status', $openStatuses)
->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end")
->orderByRaw('case when due_date is null then 1 else 0 end')
->orderBy('due_date')
->limit(5)
->get()
->map(fn(Task $task) => $this->taskPreview($task))
->values();
$notifications = Notification::query()
->where('user_id', $user->id)
->latest()
->limit(5)
->get()
->map(fn(Notification $notification) => [
'id' => $notification->id,
'type' => $notification->type,
'title' => $notification->title,
'body' => $notification->body,
'data' => $notification->data,
'notifiable_type' => $notification->notifiable_type,
'notifiable_id' => $notification->notifiable_id,
'is_read' => (bool) $notification->is_read,
'created_at' => $notification->created_at,
])
->values();
$permissions = $user->roles
->flatMap(fn($role) => $role->permissions->pluck('name'))
->unique()
->values();
$canViewTeamScope = $user->hasAnyPermission(['reports.view', 'team.view', 'tasks.edit']);
$managerSummary = null;
if ($canViewTeamScope) {
$manageableProjectIds = $this->manageableProjectIds($user);
$teamTaskQuery = Task::query()
->when($manageableProjectIds->isNotEmpty(), fn($query) => $query->whereIn('project_id', $manageableProjectIds))
->when($manageableProjectIds->isEmpty() && !$user->hasPermission('reports.view'), fn($query) => $query->whereRaw('1 = 0'));
$managerSummary = [
'overdue_team_tasks' => (clone $teamTaskQuery)
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled', 'cancelled'])
->count(),
'review_tasks' => (clone $teamTaskQuery)->where('status', 'review')->count(),
'active_sprint' => Sprint::query()
->with('project:id,title')
->where('status', 'active')
->when($manageableProjectIds->isNotEmpty(), fn($query) => $query->whereIn('project_id', $manageableProjectIds))
->latest()
->first()?->only(['id', 'title', 'project_id', 'start_date', 'end_date']),
'delayed_members' => (clone $teamTaskQuery)
->whereDate('due_date', '<', $today)
->whereNotIn('status', ['done', 'canceled', 'cancelled'])
->whereNotNull('assignee_id')
->distinct('assignee_id')
->count('assignee_id'),
];
}
return response()->json([
'success' => true,
'data' => [
'user' => [
'id' => $user->id,
'name' => $user->name,
'job_title' => $user->job_title,
'role_label' => $user->roles->first()?->display_name,
],
'permissions' => $permissions,
'stats' => [
'today_tasks' => $todayTasks->count(),
'overdue_tasks' => $overdueTasks->count(),
'active_tasks' => $activeTasks->count(),
'unread_notifications' => Notification::where('user_id', $user->id)->where('is_read', false)->count(),
],
'tasks' => $importantTasks,
'notifications' => $notifications,
'manager_summary' => $managerSummary,
],
'message' => 'خانه موبایل',
]);
}
private function taskPreview(Task $task): array
{
return [
'id' => $task->id,
'title' => $task->title,
'status' => $task->status,
'priority' => $task->priority,
'due_date' => $task->due_date?->format('Y-m-d'),
'project' => $task->project ? [
'id' => $task->project->id,
'title' => $task->project->title,
] : null,
];
}
private function profileAccessSummary($permissions, User $user): array
{
$items = [
['permissions' => ['projects.view'], 'label' => 'مشاهده پروژه‌ها'],
['permissions' => ['projects.edit', 'projects.create'], 'label' => 'مدیریت پروژه‌ها'],
['permissions' => ['tasks.view'], 'label' => 'مشاهده تسک‌ها'],
['permissions' => ['tasks.create'], 'label' => 'ایجاد تسک'],
['permissions' => ['tasks.edit'], 'label' => 'تغییر وضعیت تسک'],
['permissions' => ['comments.create'], 'label' => 'ثبت کامنت'],
['permissions' => ['files.upload'], 'label' => 'آپلود فایل'],
['permissions' => ['notifications.view'], 'label' => 'مشاهده اعلان‌ها'],
['permissions' => ['reports.view'], 'label' => 'مشاهده گزارش‌ها'],
];
$permissionSet = $permissions->flip();
$summary = collect($items)
->filter(fn($item) => collect($item['permissions'])->contains(fn($permission) => $permissionSet->has($permission)))
->map(fn($item) => ['label' => $item['label']])
->values();
$isAdmin = $user->roles->contains(fn($role) => in_array($role->name, ['admin', 'super_admin', 'system_admin'], true))
|| $permissionSet->has('roles.edit')
|| $permissionSet->has('settings.edit');
return [
'is_admin' => $isAdmin,
'items' => $summary,
];
}
private function manageableProjectIds($user)
{
if ($user->hasPermission('reports.view')) {
return collect();
}
return $user->managedProjects()->pluck('id')
->merge($user->projects()->pluck('projects.id'))
->unique()
->values();
}
private function visibleTasksQuery(User $user)
{
if ($user->hasPermission('reports.view')) {
return Task::query();
}
$manageableProjectIds = $this->manageableProjectIds($user);
return Task::query()->where(function ($query) use ($user, $manageableProjectIds) {
$query->where('assignee_id', $user->id)
->orWhere('reporter_id', $user->id)
->orWhere('created_by', $user->id)
->when($manageableProjectIds->isNotEmpty(), fn($innerQuery) => $innerQuery->orWhereIn('project_id', $manageableProjectIds));
});
}
private function abortUnlessTaskVisible(Request $request, Task $task): void
{
$user = $request->user()->loadMissing(['roles.permissions']);
$allowed = $this->visibleTasksQuery($user)->where('id', $task->id)->exists();
abort_unless($allowed, 403, 'شما به این تسک دسترسی ندارید.');
}
private function visibleProjectsQuery(User $user)
{
if ($user->hasPermission('reports.view')) {
return Project::query();
}
return Project::query()->where(function ($query) use ($user) {
$query->where('project_manager_id', $user->id)
->orWhere('created_by', $user->id)
->orWhereHas('members', fn($memberQuery) => $memberQuery->where('users.id', $user->id));
});
}
private function abortUnlessProjectVisible(Request $request, Project $project): void
{
$user = $request->user()->loadMissing(['roles.permissions']);
$allowed = $this->visibleProjectsQuery($user)->where('id', $project->id)->exists();
abort_unless($allowed, 403, 'شما به این پروژه دسترسی ندارید.');
}
private function projectCard(Project $project): array
{
return [
'id' => $project->id,
'title' => $project->title,
'description' => $project->description,
'status' => $project->status,
'progress' => (int) ($project->progress ?? 0),
'due_date' => $project->end_date?->format('Y-m-d'),
'manager' => $project->projectManager ? [
'id' => $project->projectManager->id,
'name' => $project->projectManager->name,
] : null,
'task_counts' => [
'total' => $project->tasks_count ?? 0,
'done' => $project->done_tasks_count ?? 0,
'overdue' => $project->overdue_tasks_count ?? 0,
],
'members_preview' => $project->relationLoaded('members')
? $project->members->take(4)->map(fn(User $member) => [
'id' => $member->id,
'name' => $member->name,
'avatar_url' => $member->avatar ? asset('storage/' . $member->avatar) : null,
])->values()
: [],
];
}
private function notificationPayload(Notification $notification): array
{
$data = $notification->data ?? [];
return [
'id' => $notification->id,
'type' => $notification->type,
'title' => $notification->title,
'body' => $notification->body,
'message' => $notification->body,
'data' => [
'task_id' => $data['task_id'] ?? null,
'project_id' => $data['project_id'] ?? null,
'comment_id' => $data['comment_id'] ?? null,
'target_type' => $data['target_type'] ?? $data['targetType'] ?? null,
'target_id' => $data['target_id'] ?? $data['targetId'] ?? null,
],
'notifiable_type' => $notification->notifiable_type,
'notifiable_id' => $notification->notifiable_id,
'is_read' => (bool) $notification->is_read || $notification->read_at !== null,
'read_at' => $notification->read_at,
'remind_at' => $notification->remind_at,
'dismissed_at' => $notification->dismissed_at,
'created_at' => $notification->created_at,
];
}
private function unreadNotificationsCount(Request $request): int
{
return $request->user()->notifications()
->whereNull('dismissed_at')
->where(function ($outerQuery) {
$outerQuery->whereNull('remind_at')->orWhere('remind_at', '<=', now());
})
->where(function ($query) {
$query->whereNull('read_at')->orWhere('is_read', false);
})
->count();
}
private function taskCard(Task $task): array
{
$latestComment = $task->relationLoaded('comments')
? $task->comments->sortByDesc('created_at')->first()
: $task->comments()->latest()->first();
return [
'id' => $task->id,
'title' => $task->title,
'status' => $task->status,
'priority' => $task->priority,
'due_date' => $task->due_date?->format('Y-m-d'),
'created_at' => $task->created_at,
'project' => $task->project ? [
'id' => $task->project->id,
'title' => $task->project->title,
] : null,
'assignee' => $task->assignee ? [
'id' => $task->assignee->id,
'name' => $task->assignee->name,
] : null,
'comments_count' => $task->comments_count,
'files_count' => $task->files_count,
'latest_comment' => $latestComment ? [
'id' => $latestComment->id,
'body' => Str::limit($latestComment->body, 90),
'created_at' => $latestComment->created_at,
] : null,
];
}
private function pwaSprintPayload(Sprint $sprint, User $user): array
{
$today = now()->toDateString();
$tasks = $this->visibleTasksQuery($user)
->whereHas('sprints', fn($sprintQuery) => $sprintQuery->where('sprints.id', $sprint->id))
->with(['project:id,title,status', 'assignee:id,name,job_title'])
->withCount(['comments', 'files'])
->orderByRaw("case priority when 'urgent' then 0 when 'high' then 1 when 'medium' then 2 else 3 end")
->orderByRaw('case when due_date is null then 1 else 0 end')
->orderBy('due_date')
->get();
$total = $tasks->count();
$done = $tasks->where('status', 'done')->count();
$inProgress = $tasks->whereIn('status', ['in_progress', 'review'])->count();
$overdue = $tasks
->filter(fn(Task $task) => $task->due_date && $task->due_date->format('Y-m-d') < $today && !in_array($task->status, ['done', 'canceled', 'cancelled'], true))
->count();
$progress = $total > 0 ? (int) round(($done / $total) * 100) : 0;
$remainingDays = $sprint->end_date ? now()->startOfDay()->diffInDays($sprint->end_date->copy()->startOfDay(), false) : null;
return [
'id' => $sprint->id,
'title' => $sprint->title,
'status' => $sprint->status,
'goal' => $sprint->goal,
'start_date' => $sprint->start_date?->format('Y-m-d'),
'end_date' => $sprint->end_date?->format('Y-m-d'),
'remaining_days' => $remainingDays,
'progress' => $progress,
'project' => $sprint->project ? [
'id' => $sprint->project->id,
'title' => $sprint->project->title,
] : null,
'task_counts' => [
'total' => $total,
'done' => $done,
'in_progress' => $inProgress,
'overdue' => $overdue,
],
'tasks' => $tasks->map(fn(Task $task) => $this->taskCard($task))->values(),
'capabilities' => [
'can_create_task' => $user->hasPermission('tasks.create'),
'can_update_task_status' => $user->hasPermission('tasks.edit'),
'can_comment' => $user->hasPermission('comments.create'),
],
];
}
private function taskDetailPayload(Task $task): array
{
return array_merge($this->taskCard($task), [
'description' => $task->description,
'reporter' => $task->reporter ? [
'id' => $task->reporter->id,
'name' => $task->reporter->name,
] : null,
'comments' => CommentResource::collection($task->comments->sortByDesc('created_at')->values()),
'files' => FileResource::collection($task->files->sortByDesc('created_at')->values()),
]);
}
}

مشاهده پرونده

@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\ReportService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ReportController extends Controller
{
public function __construct(protected ReportService $reportService) {}
public function projectStatus(Request $request): JsonResponse
{
try {
$data = $this->reportService->projectStatus($request->only(['status', 'project_id']));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش وضعیت پروژه‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function delayedTasks(Request $request): JsonResponse
{
try {
$data = $this->reportService->delayedTasks($request->only([
'date_from', 'date_to', 'project_id', 'user_id', 'status'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش وظایف عقب افتاده',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function teamPerformance(Request $request): JsonResponse
{
try {
$data = $this->reportService->teamPerformance($request->only([
'date_from', 'date_to', 'user_id'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش عملکرد تیم',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function workload(Request $request): JsonResponse
{
try {
$data = $this->reportService->workload($request->only(['user_id']));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش بار کاری',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function sprintProgress(Request $request): JsonResponse
{
try {
$data = $this->reportService->sprintProgress($request->only([
'project_id', 'status'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش پیشرفت اسپرینت',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function timeEstimate(Request $request): JsonResponse
{
try {
$data = $this->reportService->timeEstimate($request->only([
'date_from', 'date_to', 'project_id', 'user_id', 'status'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش برآورد زمانی',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function recentActivities(Request $request): JsonResponse
{
try {
$data = $this->reportService->recentActivities($request->only([
'date_from', 'date_to', 'project_id', 'user_id', 'action'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش فعالیت‌های اخیر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
public function riskyProjects(Request $request): JsonResponse
{
try {
$data = $this->reportService->riskyProjects($request->only([
'status', 'project_id'
]));
return response()->json([
'success' => true,
'data' => $data,
'message' => 'گزارش پروژه‌های پرخطر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت گزارش',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,179 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreRoleRequest;
use App\Http\Requests\UpdateRoleRequest;
use App\Http\Resources\RoleResource;
use App\Http\Resources\PermissionResource;
use App\Models\Role;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class RoleController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = Role::with('permissions')->withCount('users');
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'name', 'display_name', 'created_at', 'updated_at'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$roles = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => RoleResource::collection($roles),
'meta' => [
'current_page' => $roles->currentPage(),
'last_page' => $roles->lastPage(),
'per_page' => $roles->perPage(),
'total' => $roles->total(),
],
'message' => 'لیست نقش‌ها',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت نقش‌ها',
], 500);
}
}
public function show(Role $role): JsonResponse
{
try {
$role->load('permissions');
return response()->json([
'success' => true,
'data' => new RoleResource($role),
'message' => 'اطلاعات نقش',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات نقش',
], 500);
}
}
public function store(StoreRoleRequest $request): JsonResponse
{
try {
$data = $request->validated();
$permissionIds = $data['permissions'] ?? [];
unset($data['permissions']);
$data['guard_name'] = $data['guard_name'] ?? 'web';
$role = Role::create($data);
$role->permissions()->sync($permissionIds);
$role->load('permissions')->loadCount('users');
return response()->json([
'success' => true,
'data' => new RoleResource($role),
'message' => 'نقش با موفقیت ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد نقش',
], 500);
}
}
public function update(UpdateRoleRequest $request, Role $role): JsonResponse
{
try {
$data = $request->validated();
$permissionIds = $data['permissions'] ?? null;
unset($data['permissions']);
if (array_key_exists('guard_name', $data) && !$data['guard_name']) {
$data['guard_name'] = 'web';
}
$role->update($data);
if ($permissionIds !== null) {
$role->permissions()->sync($permissionIds);
}
$role->load('permissions')->loadCount('users');
return response()->json([
'success' => true,
'data' => new RoleResource($role),
'message' => 'نقش به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی نقش',
], 500);
}
}
public function destroy(Role $role): JsonResponse
{
try {
$role->delete();
return response()->json([
'success' => true,
'message' => 'نقش حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف نقش',
], 500);
}
}
public function assignPermissions(Request $request, Role $role): JsonResponse
{
try {
$request->validate([
'permissions' => 'present|array',
'permissions.*' => 'exists:permissions,id',
]);
$role->permissions()->sync($request->permissions);
$role->load('permissions')->loadCount('users');
return response()->json([
'success' => true,
'data' => new RoleResource($role),
'message' => 'دسترسی‌های نقش به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی دسترسی‌های نقش',
], 500);
}
}
public function getPermissions(Role $role): JsonResponse
{
try {
$role->load('permissions');
return response()->json([
'success' => true,
'data' => PermissionResource::collection($role->permissions),
'message' => 'دسترسی‌های نقش',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت دسترسی‌های نقش',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\ProjectResource;
use App\Http\Resources\TaskResource;
use App\Http\Resources\UserResource;
use App\Http\Resources\MeetingResource;
use App\Http\Resources\BacklogItemResource;
use App\Models\Project;
use App\Models\Task;
use App\Models\User;
use App\Models\Meeting;
use App\Models\BacklogItem;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function search(Request $request): JsonResponse
{
try {
$request->validate(['q' => 'required|string|min:2']);
$keyword = $request->q;
$limit = $request->input('limit', 5);
$projects = Project::where('title', 'like', "%{$keyword}%")->limit($limit)->get();
$tasks = Task::where('title', 'like', "%{$keyword}%")->limit($limit)->get();
$users = User::where('name', 'like', "%{$keyword}%")->orWhere('email', 'like', "%{$keyword}%")->limit($limit)->get();
$meetings = Meeting::where('title', 'like', "%{$keyword}%")->limit($limit)->get();
$backlogItems = BacklogItem::where('title', 'like', "%{$keyword}%")->limit($limit)->get();
return response()->json([
'success' => true,
'data' => [
'projects' => ProjectResource::collection($projects),
'tasks' => TaskResource::collection($tasks),
'users' => UserResource::collection($users),
'meetings' => MeetingResource::collection($meetings),
'backlog_items' => BacklogItemResource::collection($backlogItems),
],
'message' => 'نتایج جستجو',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در جستجو',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class SettingController extends Controller
{
public function index(Request $request): JsonResponse
{
try {
$query = Setting::query();
if ($request->filled('group')) {
$query->where('group', $request->group);
}
$settings = $query->orderBy('group')->orderBy('key')->get();
return response()->json([
'success' => true,
'data' => $settings,
'message' => 'لیست تنظیمات',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت تنظیمات',
], 500);
}
}
public function store(Request $request): JsonResponse
{
try {
$request->validate([
'key' => 'required|string|max:255|unique:settings,key',
'value' => 'present',
'group' => 'nullable|string|max:255',
'type' => 'nullable|string|max:50',
]);
$setting = Setting::create([
'key' => $request->input('key'),
'value' => $request->input('value'),
'group' => $request->input('group', 'general'),
'type' => $request->input('type'),
]);
return response()->json([
'success' => true,
'data' => $setting,
'message' => 'تنظیم با موفقیت ایجاد شد',
], 201);
} catch (ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'اطلاعات تنظیم معتبر نیست',
'errors' => $e->errors(),
], 422);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد تنظیم',
], 500);
}
}
public function update(Request $request, Setting $setting): JsonResponse
{
try {
$request->validate([
'value' => 'present',
'group' => 'nullable|string|max:255',
'type' => 'nullable|string|max:50',
]);
$setting->update([
'value' => $request->input('value'),
'group' => $request->input('group', $setting->group),
'type' => $request->input('type', $setting->type),
]);
return response()->json([
'success' => true,
'data' => $setting->fresh(),
'message' => 'تنظیم به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
return response()->json([
'success' => false,
'message' => 'اطلاعات تنظیم معتبر نیست',
'errors' => $e->errors(),
], 422);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی تنظیم',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,425 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\SprintResource;
use App\Http\Resources\TaskResource;
use App\Models\Sprint;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class SprintController extends Controller
{
private array $statuses = ['planning', 'active', 'completed', 'cancelled'];
public function index(Request $request): JsonResponse
{
try {
$query = Sprint::with(['project', 'tasks', 'members']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->boolean('mine')) {
$query->whereHas('members', fn($q) => $q->where('users.id', $request->user()->id));
}
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'title', 'status', 'created_at', 'updated_at', 'start_date', 'end_date'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$sprints = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => SprintResource::collection($sprints),
'meta' => [
'current_page' => $sprints->currentPage(),
'last_page' => $sprints->lastPage(),
'per_page' => $sprints->perPage(),
'total' => $sprints->total(),
],
'message' => 'لیست Sprintها',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت Sprintها'], 500);
}
}
public function show(Sprint $sprint): JsonResponse
{
try {
$sprint->load([
'project',
'tasks.project',
'tasks.assignee',
'tasks.reporter',
'members',
'creator',
'retrospective',
]);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint),
'message' => 'اطلاعات Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت اطلاعات Sprint'], 500);
}
}
public function store(Request $request): JsonResponse
{
try {
$data = $this->validatedSprintData($request);
$memberIds = $data['member_ids'] ?? [];
unset($data['member_ids']);
$data['created_by'] = $request->user()->id;
$data['status'] = 'planning';
$sprint = DB::transaction(function () use ($data, $memberIds) {
$sprint = Sprint::create($data);
$sprint->members()->sync($memberIds);
return $sprint;
});
$sprint->load(['project', 'tasks', 'members']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint),
'message' => 'Sprint با موفقیت ایجاد شد',
], 201);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در ایجاد Sprint'], 500);
}
}
public function update(Request $request, Sprint $sprint): JsonResponse
{
try {
$data = $this->validatedSprintData($request, true);
$memberIds = $data['member_ids'] ?? null;
unset($data['member_ids']);
DB::transaction(function () use ($sprint, $data, $memberIds) {
$sprint->update($data);
if (is_array($memberIds)) {
$sprint->members()->sync($memberIds);
}
});
$sprint->load(['project', 'tasks', 'members', 'retrospective']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'Sprint به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در به‌روزرسانی Sprint'], 500);
}
}
public function destroy(Sprint $sprint): JsonResponse
{
try {
$sprint->delete();
return response()->json(['success' => true, 'message' => 'Sprint حذف شد']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در حذف Sprint'], 500);
}
}
public function availableTasks(Sprint $sprint): JsonResponse
{
try {
$taskIdsInSprint = $sprint->tasks()->pluck('tasks.id');
$tasks = Task::with(['project', 'assignee'])
->withCount(['comments', 'files'])
->where('project_id', $sprint->project_id)
->whereNotIn('id', $taskIdsInSprint)
->whereNotIn('status', ['done', 'canceled'])
->orderBy('created_at', 'desc')
->get();
return response()->json([
'success' => true,
'data' => TaskResource::collection($tasks),
'message' => 'تسک‌های قابل افزودن به Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت تسک‌های قابل افزودن'], 500);
}
}
public function addTask(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate(['task_id' => 'required|exists:tasks,id']);
$task = Task::findOrFail($request->task_id);
if ($task->project_id !== $sprint->project_id) {
return response()->json(['success' => false, 'message' => 'این تسک متعلق به پروژه Sprint نیست'], 422);
}
$sprint->tasks()->syncWithoutDetaching([$task->id]);
return response()->json(['success' => true, 'message' => 'تسک به Sprint اضافه شد']);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در اضافه کردن تسک به Sprint'], 500);
}
}
public function removeTask(Sprint $sprint, Task $task): JsonResponse
{
try {
$sprint->tasks()->detach($task->id);
return response()->json(['success' => true, 'message' => 'تسک از Sprint حذف شد']);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در حذف تسک از Sprint'], 500);
}
}
public function updateTaskStatus(Request $request, Sprint $sprint, Task $task): JsonResponse
{
try {
$request->validate([
'status' => 'required|string|in:waiting,todo,in_progress,review,done',
]);
if (!$sprint->tasks()->where('tasks.id', $task->id)->exists()) {
return response()->json(['success' => false, 'message' => 'این تسک در Sprint انتخاب‌شده نیست'], 404);
}
$task->update(['status' => $request->status]);
return response()->json([
'success' => true,
'data' => new TaskResource($task->fresh()->load(['project', 'assignee'])),
'message' => 'وضعیت تسک به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در تغییر وضعیت تسک'], 500);
}
}
public function updateStatus(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate(['status' => 'required|string|in:' . implode(',', $this->statuses)]);
$sprint->update(['status' => $request->status]);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'وضعیت Sprint به‌روزرسانی شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در به‌روزرسانی وضعیت Sprint'], 500);
}
}
public function start(Sprint $sprint): JsonResponse
{
try {
if ($sprint->status !== 'planning') {
return response()->json(['success' => false, 'message' => 'فقط Sprint برنامه‌ریزی شده قابل شروع است'], 422);
}
$activeSprint = Sprint::where('project_id', $sprint->project_id)
->where('status', 'active')
->where('id', '!=', $sprint->id)
->first();
if ($activeSprint) {
return response()->json([
'success' => false,
'message' => "در این پروژه Sprint فعال دیگری وجود دارد: {$activeSprint->title}",
], 422);
}
$sprint->update(['status' => 'active']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'Sprint شروع شد',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در شروع Sprint'], 500);
}
}
public function end(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate([
'incomplete_action' => 'required|string|in:move_to_next,backlog,keep',
'target_sprint_id' => 'nullable|required_if:incomplete_action,move_to_next|exists:sprints,id',
]);
if ($sprint->status !== 'active') {
return response()->json(['success' => false, 'message' => 'فقط Sprint فعال قابل پایان دادن است'], 422);
}
$incompleteTasks = $sprint->tasks()->where('status', '!=', 'done')->get();
$completedTasks = $sprint->tasks()->where('status', 'done')->count();
$totalTasks = $sprint->tasks()->count();
DB::transaction(function () use ($request, $sprint, $incompleteTasks, $totalTasks, $completedTasks) {
if ($request->incomplete_action === 'move_to_next') {
$targetSprint = Sprint::where('id', $request->target_sprint_id)
->where('project_id', $sprint->project_id)
->where('status', 'planning')
->firstOrFail();
$targetSprint->tasks()->syncWithoutDetaching($incompleteTasks->pluck('id')->all());
$sprint->tasks()->detach($incompleteTasks->pluck('id')->all());
}
if (in_array($request->incomplete_action, ['backlog', 'keep'], true)) {
$sprint->tasks()->detach($incompleteTasks->pluck('id')->all());
}
$sprint->update([
'status' => 'completed',
'completed_at' => now(),
'completion_summary' => [
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'incomplete_tasks' => $incompleteTasks->count(),
'incomplete_action' => $request->incomplete_action,
],
]);
});
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'Sprint پایان یافت',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در پایان Sprint'], 500);
}
}
public function cancel(Sprint $sprint): JsonResponse
{
try {
if (!in_array($sprint->status, ['planning', 'active'], true)) {
return response()->json(['success' => false, 'message' => 'این Sprint قابل لغو نیست'], 422);
}
$sprint->update(['status' => 'cancelled']);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members'])),
'message' => 'Sprint لغو شد',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در لغو Sprint'], 500);
}
}
public function report(Sprint $sprint): JsonResponse
{
try {
$sprint->load(['tasks.assignee', 'members']);
$tasks = $sprint->tasks;
$totalTasks = $tasks->count();
$completedTasks = $tasks->where('status', 'done')->count();
$incompleteTasks = $totalTasks - $completedTasks;
return response()->json([
'success' => true,
'data' => [
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'incomplete_tasks' => $incompleteTasks,
'completion_percentage' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
'completed_by_member' => $tasks
->where('status', 'done')
->groupBy(fn($task) => $task->assignee?->name ?? 'بدون مسئول')
->map(fn($items, $name) => ['name' => $name, 'completed' => $items->count()])
->values(),
'unfinished_tasks' => TaskResource::collection($tasks->where('status', '!=', 'done')->values()),
],
'message' => 'گزارش Sprint',
]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در دریافت گزارش Sprint'], 500);
}
}
public function updateRetrospective(Request $request, Sprint $sprint): JsonResponse
{
try {
$request->validate([
'went_well' => 'nullable|string',
'problems' => 'nullable|string',
'improvements' => 'nullable|string',
]);
$sprint->retrospective()->updateOrCreate(
['sprint_id' => $sprint->id],
[
'went_well' => $request->went_well,
'problems' => $request->problems,
'improvements' => $request->improvements,
'updated_by' => $request->user()->id,
]
);
return response()->json([
'success' => true,
'data' => new SprintResource($sprint->fresh()->load(['project', 'tasks', 'members', 'retrospective'])),
'message' => 'یادداشت پایان Sprint ذخیره شد',
]);
} catch (ValidationException $e) {
throw $e;
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'خطا در ذخیره یادداشت پایان Sprint'], 500);
}
}
private function validatedSprintData(Request $request, bool $partial = false): array
{
$required = $partial ? 'sometimes|required' : 'required';
return $request->validate([
'title' => "{$required}|string|max:255",
'project_id' => "{$required}|exists:projects,id",
'goal' => 'nullable|string|max:1000',
'capacity_hours' => 'nullable|numeric|min:0',
'start_date' => "{$required}|date",
'end_date' => "{$required}|date|after_or_equal:start_date",
'member_ids' => 'nullable|array',
'member_ids.*' => 'exists:users,id',
]);
}
}

مشاهده پرونده

@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\SubtaskResource;
use App\Models\Subtask;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SubtaskController extends Controller
{
public function index(Request $request, Task $task): JsonResponse
{
try {
$subtasks = $task->subtasks()->with('assignee')->orderBy('position')->get();
return response()->json([
'success' => true,
'data' => SubtaskResource::collection($subtasks),
'message' => 'لیست زیروظایف',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت زیروظایف',
], 500);
}
}
public function store(Request $request, Task $task): JsonResponse
{
try {
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'assignee_id' => 'nullable|exists:users,id',
'due_date' => 'nullable|date',
'position' => 'nullable|integer',
]);
$data = $request->only(['title', 'description', 'assignee_id', 'due_date', 'position']);
$data['task_id'] = $task->id;
$data['status'] = 'todo';
$subtask = Subtask::create($data);
$subtask->load('assignee');
return response()->json([
'success' => true,
'data' => new SubtaskResource($subtask),
'message' => 'زیروظیفه ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد زیروظیفه',
], 500);
}
}
public function update(Request $request, Subtask $subtask): JsonResponse
{
try {
$request->validate([
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'assignee_id' => 'nullable|exists:users,id',
'due_date' => 'nullable|date',
'position' => 'nullable|integer',
]);
$subtask->update($request->only(['title', 'description', 'assignee_id', 'due_date', 'position']));
$subtask->load('assignee');
return response()->json([
'success' => true,
'data' => new SubtaskResource($subtask->fresh()),
'message' => 'زیروظیفه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی زیروظیفه',
], 500);
}
}
public function destroy(Subtask $subtask): JsonResponse
{
try {
$subtask->delete();
return response()->json([
'success' => true,
'message' => 'زیروظیفه حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف زیروظیفه',
], 500);
}
}
public function updateStatus(Request $request, Subtask $subtask): JsonResponse
{
try {
$request->validate(['status' => 'required|string']);
$subtask->update(['status' => $request->status]);
$subtask->load('assignee');
return response()->json([
'success' => true,
'data' => new SubtaskResource($subtask->fresh()),
'message' => 'وضعیت زیروظیفه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی وضعیت',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,368 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreTaskRequest;
use App\Http\Requests\UpdateTaskRequest;
use App\Http\Resources\TaskResource;
use App\Models\Task;
use App\Services\ActivityLogService;
use App\Services\NotificationService;
use App\Services\ProjectProgressService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TaskController extends Controller
{
public function __construct(
protected ActivityLogService $activityLogService,
protected NotificationService $notificationService,
protected ProjectProgressService $projectProgressService,
) {}
public function index(Request $request): JsonResponse
{
try {
$query = Task::with(['project', 'assignee', 'reporter'])->withCount(['comments', 'files']);
if ($request->filled('project_id')) {
$query->where('project_id', $request->project_id);
}
if ($request->filled('assignee_id')) {
$query->where('assignee_id', $request->assignee_id);
}
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('priority')) {
$query->where('priority', $request->priority);
}
if ($request->filled('search')) {
$search = $request->search;
$query->where('title', 'like', "%{$search}%");
}
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'title', 'status', 'priority', 'created_at', 'updated_at', 'due_date', 'sort_order'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$tasks = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => TaskResource::collection($tasks),
'meta' => [
'current_page' => $tasks->currentPage(),
'last_page' => $tasks->lastPage(),
'per_page' => $tasks->perPage(),
'total' => $tasks->total(),
],
'message' => 'لیست وظایف',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت لیست وظایف',
], 500);
}
}
public function show(Task $task): JsonResponse
{
try {
$task->load(['project', 'assignee', 'reporter', 'checklists', 'subtasks', 'comments.user', 'files']);
return response()->json([
'success' => true,
'data' => new TaskResource($task),
'message' => 'اطلاعات وظیفه',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات وظیفه',
], 500);
}
}
public function store(StoreTaskRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['reporter_id'] = $request->user()->id;
$data['created_by'] = $request->user()->id;
$task = Task::create($data);
$this->projectProgressService->calculateProgress($task->project_id);
$this->activityLogService->log(
$request->user()->id,
'create_task',
"وظیفه {$task->title} ایجاد شد",
'task',
$task->id,
$task->project_id,
$task->id,
['task_title' => $task->title]
);
if ($task->assignee_id) {
$this->notificationService->create(
$task->assignee_id,
'task_assigned',
[
'task_id' => $task->id,
'project_id' => $task->project_id,
'url' => '/kanban',
'notifiable_type' => 'App\\Models\\Task',
'notifiable_id' => $task->id,
],
"وظیفه جدید {$task->title} به شما اختصاص داده شد"
);
}
$task->load(['project', 'assignee', 'reporter']);
return response()->json([
'success' => true,
'data' => new TaskResource($task),
'message' => 'وظیفه با موفقیت ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد وظیفه',
], 500);
}
}
public function update(UpdateTaskRequest $request, Task $task): JsonResponse
{
try {
$oldStatus = $task->status;
$task->update($request->validated());
if ($task->wasChanged('status')) {
$this->activityLogService->log(
$request->user()->id,
'update_task_status',
"وضعیت وظیفه {$task->title} از {$oldStatus} به {$task->status} تغییر یافت",
'task',
$task->id,
$task->project_id,
$task->id,
['old_status' => $oldStatus, 'new_status' => $task->status]
);
}
$this->projectProgressService->calculateProgress($task->project_id);
$task->load(['project', 'assignee', 'reporter']);
return response()->json([
'success' => true,
'data' => new TaskResource($task->fresh()),
'message' => 'وظیفه با موفقیت به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی وظیفه',
], 500);
}
}
public function destroy(Request $request, Task $task): JsonResponse
{
try {
$projectId = $task->project_id;
$taskId = $task->id;
$taskTitle = $task->title;
$this->activityLogService->log(
$request->user()->id,
'delete_task',
"وظیفه {$taskTitle} حذف شد",
'task',
$taskId,
$projectId,
null,
['task_title' => $taskTitle]
);
$task->delete();
$this->projectProgressService->calculateProgress($projectId);
return response()->json([
'success' => true,
'message' => 'وظیفه با موفقیت حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف وظیفه',
], 500);
}
}
public function updateStatus(Request $request, Task $task): JsonResponse
{
try {
$request->validate(['status' => 'required|string']);
$oldStatus = $task->status;
$task->update(['status' => $request->status]);
$this->projectProgressService->calculateProgress($task->project_id);
$this->activityLogService->log(
$request->user()->id,
'update_task_status',
"وضعیت وظیفه {$task->title} از {$oldStatus} به {$task->status} تغییر یافت",
'task',
$task->id,
$task->project_id,
$task->id,
['old_status' => $oldStatus, 'new_status' => $task->status]
);
if ($task->assignee_id) {
$this->notificationService->create(
$task->assignee_id,
'task_status_changed',
['task_id' => $task->id, 'status' => $task->status],
"وضعیت وظیفه {$task->title} به {$task->status} تغییر یافت"
);
}
return response()->json([
'success' => true,
'data' => new TaskResource($task->fresh()->load(['project', 'assignee'])),
'message' => 'وضعیت وظیفه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی وضعیت',
], 500);
}
}
public function updateAssignee(Request $request, Task $task): JsonResponse
{
try {
$request->validate(['assignee_id' => 'nullable|exists:users,id']);
$oldAssigneeId = $task->assignee_id;
$task->update(['assignee_id' => $request->assignee_id]);
if ($request->assignee_id && $request->assignee_id != $oldAssigneeId) {
$this->notificationService->create(
$request->assignee_id,
'task_assigned',
[
'task_id' => $task->id,
'project_id' => $task->project_id,
'url' => '/kanban',
'notifiable_type' => 'App\\Models\\Task',
'notifiable_id' => $task->id,
],
"وظیفه {$task->title} به شما اختصاص داده شد"
);
}
$this->activityLogService->log(
$request->user()->id,
'update_task_assignee',
"مسئول وظیفه {$task->title} تغییر یافت",
'task',
$task->id,
$task->project_id,
$task->id,
['old_assignee' => $oldAssigneeId, 'new_assignee' => $request->assignee_id]
);
return response()->json([
'success' => true,
'data' => new TaskResource($task->fresh()->load(['project', 'assignee'])),
'message' => 'مسئول وظیفه به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی مسئول',
], 500);
}
}
public function reorder(Request $request): JsonResponse
{
try {
$request->validate([
'tasks' => 'required|array',
'tasks.*.id' => 'required|exists:tasks,id',
'tasks.*.sort_order' => 'required|integer',
]);
foreach ($request->tasks as $item) {
Task::where('id', $item['id'])->update(['sort_order' => $item['sort_order']]);
}
return response()->json([
'success' => true,
'message' => 'ترتیب وظایف به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در مرتب‌سازی وظایف',
], 500);
}
}
public function myTasks(Request $request): JsonResponse
{
try {
$tasks = Task::with(['project', 'assignee'])
->where('assignee_id', $request->user()->id)
->where('status', '!=', 'done')
->orderBy('due_date')
->get();
return response()->json([
'success' => true,
'data' => TaskResource::collection($tasks),
'message' => 'وظایف من',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت وظایف من',
], 500);
}
}
public function delayedTasks(): JsonResponse
{
try {
$tasks = Task::with(['project', 'assignee'])
->where('due_date', '<', Carbon::now())
->where('status', '!=', 'done')
->orderBy('due_date')
->get();
return response()->json([
'success' => true,
'data' => TaskResource::collection($tasks),
'message' => 'وظایف عقب افتاده',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت وظایف عقب افتاده',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,237 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Models\Task;
use App\Services\WorkloadService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
public function __construct(protected WorkloadService $workloadService) {}
public function index(Request $request): JsonResponse
{
try {
$query = User::with(['roles.permissions', 'dept', 'departments']);
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
$perPage = min((int) $request->input('per_page', 15), 100);
$sortBy = in_array($request->input('sort_by'), ['id', 'name', 'email', 'status', 'created_at', 'updated_at'], true)
? $request->input('sort_by')
: 'created_at';
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
$users = $query->orderBy($sortBy, $sortDir)->paginate($perPage);
return response()->json([
'success' => true,
'data' => UserResource::collection($users),
'meta' => [
'current_page' => $users->currentPage(),
'last_page' => $users->lastPage(),
'per_page' => $users->perPage(),
'total' => $users->total(),
],
'message' => 'لیست کاربران',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت لیست کاربران',
], 500);
}
}
public function show(User $user): JsonResponse
{
try {
$user->load(['roles.permissions', 'projects', 'dept', 'departments']);
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'اطلاعات کاربر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات کاربر',
], 500);
}
}
public function store(StoreUserRequest $request): JsonResponse
{
try {
$data = $request->validated();
$data['password'] = Hash::make($data['password']);
$data['status'] = $data['status'] ?? 'active';
$user = User::create($data);
if (array_key_exists('role_id', $data)) {
$data['role_id'] ? $user->roles()->sync([$data['role_id']]) : $user->roles()->detach();
}
$user->load(['role', 'roles.permissions', 'dept']);
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'کاربر با موفقیت ایجاد شد',
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در ایجاد کاربر',
], 500);
}
}
public function update(UpdateUserRequest $request, User $user): JsonResponse
{
try {
$data = $request->validated();
if (!empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
$user->update($data);
if (!empty($data['role_id'])) {
$user->roles()->sync([$data['role_id']]);
}
$user->load(['role', 'roles.permissions']);
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'کاربر با موفقیت به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی کاربر',
], 500);
}
}
public function destroy(User $user): JsonResponse
{
try {
if ($user->id === request()->user()->id) {
return response()->json([
'success' => false,
'message' => 'نمی‌توانید خودتان را حذف کنید',
], 400);
}
$user->delete();
return response()->json([
'success' => true,
'message' => 'کاربر با موفقیت حذف شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در حذف کاربر',
], 500);
}
}
public function updateStatus(Request $request, User $user): JsonResponse
{
try {
$request->validate(['status' => 'required|string|in:active,inactive']);
$user->update(['status' => $request->status]);
return response()->json([
'success' => true,
'data' => new UserResource($user->fresh()),
'message' => 'وضعیت کاربر به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی وضعیت',
], 500);
}
}
public function syncRoles(Request $request, User $user): JsonResponse
{
try {
$request->validate([
'roles' => 'present|array',
'roles.*' => 'integer|exists:roles,id',
]);
$user->roles()->sync($request->roles);
$user->load(['roles.permissions', 'dept', 'departments']);
return response()->json([
'success' => true,
'data' => new UserResource($user),
'message' => 'نقش‌های کاربر به‌روزرسانی شد',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در به‌روزرسانی نقش‌های کاربر',
], 500);
}
}
public function updateWorkload(User $user): JsonResponse
{
try {
$workload = collect($this->workloadService->getWorkload())
->firstWhere('user_id', $user->id);
return response()->json([
'success' => true,
'data' => $workload,
'message' => 'بار کاری کاربر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت بار کاری',
], 500);
}
}
public function taskStats(User $user): JsonResponse
{
try {
$tasks = Task::where('assignee_id', $user->id);
return response()->json([
'success' => true,
'data' => [
'assigned_tasks_count' => (clone $tasks)->count(),
'in_progress_count' => (clone $tasks)->whereNotIn('status', ['done', 'canceled'])->count(),
'delayed_tasks_count' => (clone $tasks)->whereDate('due_date', '<', now()->toDateString())->where('status', '!=', 'done')->count(),
],
'message' => 'آمار تسک‌های کاربر',
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'خطا در دریافت آمار کاربر',
], 500);
}
}
}

مشاهده پرونده

@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

مشاهده پرونده

@ -0,0 +1,35 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserHasPermission
{
public function handle(Request $request, Closure $next, string $permission): Response
{
$user = $request->user();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated.',
], 401);
}
$user->loadMissing('roles.permissions');
$isAdmin = $user->roles->contains('name', 'admin');
if (!$isAdmin && !$user->hasPermission($permission)) {
return response()->json([
'success' => false,
'message' => 'شما دسترسی لازم برای انجام این عملیات را ندارید',
], 403);
}
return $next($request);
}
}

مشاهده پرونده

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
/** @var Response $response */
$response = $next($request);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
if (!$response->headers->has('Content-Security-Policy')) {
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"
);
}
return $response;
}
}

مشاهده پرونده

@ -0,0 +1,21 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ChangePasswordRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'current_password' => 'required',
'new_password' => 'required|min:8|confirmed',
];
}
}

مشاهده پرونده

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
protected function prepareForValidation(): void
{
$identifier = $this->input('identifier', $this->input('email'));
if ($identifier !== null) {
$this->merge([
'identifier' => strtolower(trim((string) $identifier)),
]);
}
}
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'identifier' => 'required|string|max:255',
'email' => 'sometimes|nullable|string|max:255',
'password' => 'required|string',
];
}
}

مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreBacklogItemRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'type' => 'required|string|max:50',
'project_id' => 'nullable|exists:projects,id',
'priority' => 'nullable|string|max:50',
'estimated_effort' => 'nullable|numeric',
'status' => 'nullable|string|max:50',
];
}
}

مشاهده پرونده

@ -0,0 +1,23 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreCommentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'body' => 'required|string',
'commentable_id' => 'required|integer',
'commentable_type' => 'required|string',
'mentioned_user_id' => 'nullable|exists:users,id',
];
}
}

مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreMeetingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'project_id' => 'nullable|exists:projects,id',
'date' => 'required|date',
'start_time' => 'nullable',
'end_time' => 'nullable',
'meeting_type' => 'required|string|max:50',
];
}
}

مشاهده پرونده

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'client' => 'nullable|string|max:255',
'project_manager_id' => 'nullable|exists:users,id',
'department_id' => 'nullable|exists:departments,id',
'start_date' => 'nullable|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'priority' => 'nullable|string|max:50',
'status' => 'nullable|string|max:50',
'risk_level' => 'nullable|string|max:50',
'budget' => 'nullable|numeric',
'estimated_hours' => 'nullable|numeric',
'actual_hours' => 'nullable|numeric',
];
}
}

مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreRoleRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255|unique:roles,name',
'display_name' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
'guard_name' => 'nullable|string|max:50',
'permissions' => 'nullable|array',
'permissions.*' => 'integer|exists:permissions,id',
];
}
}

مشاهده پرونده

@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreSprintRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'project_id' => 'required|exists:projects,id',
'goal' => 'nullable|string',
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
];
}
}

مشاهده پرونده

@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StoreTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'project_id' => 'required|exists:projects,id',
'assignee_id' => 'nullable|exists:users,id',
'priority' => ['nullable', Rule::in(['low', 'medium', 'high', 'urgent'])],
'status' => ['nullable', Rule::in(['waiting', 'todo', 'in_progress', 'review', 'done', 'canceled', 'cancelled'])],
'blocker_type' => 'nullable|string|max:80',
'blocker_note' => 'nullable|string',
'start_date' => 'nullable|date',
'due_date' => 'nullable|date|after_or_equal:today',
'estimated_time' => 'nullable|numeric',
'actual_time' => 'nullable|numeric',
];
}
public function messages(): array
{
return [
'title.required' => 'عنوان تسک الزامی است.',
'title.max' => 'عنوان تسک نباید بیشتر از ۲۵۵ کاراکتر باشد.',
'project_id.required' => 'انتخاب پروژه الزامی است.',
'project_id.exists' => 'پروژه انتخاب‌شده معتبر نیست.',
'assignee_id.exists' => 'مسئول انتخاب‌شده معتبر نیست.',
'priority.in' => 'اولویت انتخاب‌شده معتبر نیست.',
'status.in' => 'وضعیت انتخاب‌شده معتبر نیست.',
'due_date.date' => 'مهلت انجام معتبر نیست.',
'due_date.after_or_equal' => 'مهلت انجام نمی‌تواند قبل از امروز باشد.',
];
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
$user = $this->user()?->loadMissing('roles.permissions');
$project = Project::with('members:id')->find($this->input('project_id'));
if (!$user || !$project) {
return;
}
$canUseProject = $user->hasPermission('reports.view')
|| (int) $project->project_manager_id === (int) $user->id
|| (int) $project->created_by === (int) $user->id
|| $project->members->contains('id', $user->id);
if (!$canUseProject) {
$validator->errors()->add('project_id', 'شما به این پروژه دسترسی ندارید.');
}
if ($this->filled('assignee_id')) {
$assignee = User::find($this->input('assignee_id'));
$assigneeAllowed = $assignee && (
$user->hasPermission('reports.view')
|| (int) $assignee->id === (int) $user->id
|| $project->members->contains('id', $assignee->id)
|| (int) $project->project_manager_id === (int) $assignee->id
);
if (!$assigneeAllowed) {
$validator->errors()->add('assignee_id', 'مسئول انتخاب‌شده برای این پروژه مجاز نیست.');
}
}
});
}
}

مشاهده پرونده

@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
protected function prepareForValidation(): void
{
foreach (['role_id', 'department_id', 'status'] as $field) {
if ($this->input($field) === '') {
$this->merge([$field => null]);
}
}
if ($this->has('email')) {
$this->merge([
'email' => strtolower(trim((string) $this->input('email'))),
]);
}
}
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'phone' => 'nullable|string|max:50',
'job_title' => 'nullable|string|max:255',
'department' => 'nullable|string|max:255',
'department_id' => 'nullable|exists:departments,id',
'role_id' => 'nullable|exists:roles,id',
'status' => 'nullable|string|in:active,inactive',
'password' => 'required|min:8',
];
}
}

مشاهده پرونده

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProfileRequest extends FormRequest
{
protected function prepareForValidation(): void
{
if ($this->has('email')) {
$this->merge([
'email' => strtolower(trim((string) $this->input('email'))),
]);
}
}
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $this->user()->id,
'phone' => 'nullable|string|max:50',
'job_title' => 'nullable|string|max:255',
'department' => 'nullable|string|max:255',
];
}
}

مشاهده پرونده

@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateProjectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'client' => 'nullable|string|max:255',
'project_manager_id' => 'nullable|exists:users,id',
'department_id' => 'nullable|exists:departments,id',
'start_date' => 'nullable|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'priority' => 'nullable|string|max:50',
'status' => 'nullable|string|max:50',
'risk_level' => 'nullable|string|max:50',
'budget' => 'nullable|numeric',
'estimated_hours' => 'nullable|numeric',
'actual_hours' => 'nullable|numeric',
];
}
}

مشاهده پرونده

@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateRoleRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$roleId = $this->route('role');
return [
'name' => 'sometimes|required|string|max:255|unique:roles,name,' . $roleId,
'display_name' => 'sometimes|required|string|max:255',
'description' => 'nullable|string|max:1000',
'guard_name' => 'nullable|string|max:50',
'permissions' => 'nullable|array',
'permissions.*' => 'integer|exists:permissions,id',
];
}
}

مشاهده پرونده

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => 'sometimes|required|string|max:255',
'description' => 'nullable|string',
'project_id' => 'sometimes|required|exists:projects,id',
'assignee_id' => 'nullable|exists:users,id',
'priority' => 'nullable|string|max:50',
'status' => 'nullable|string|max:50',
'blocker_type' => 'nullable|string|max:80',
'blocker_note' => 'nullable|string',
'start_date' => 'nullable|date',
'due_date' => 'nullable|date',
'estimated_time' => 'nullable|numeric',
'actual_time' => 'nullable|numeric',
];
}
}

مشاهده پرونده

@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateUserRequest extends FormRequest
{
protected function prepareForValidation(): void
{
foreach (['role_id', 'department_id', 'status'] as $field) {
if ($this->input($field) === '') {
$this->merge([$field => null]);
}
}
if ($this->has('email')) {
$this->merge([
'email' => strtolower(trim((string) $this->input('email'))),
]);
}
}
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$userId = $this->route('user');
$userId = is_object($userId) ? $userId->id : $userId;
return [
'name' => 'sometimes|required|string|max:255',
'email' => 'sometimes|required|email|unique:users,email,' . $userId,
'phone' => 'nullable|string|max:50',
'job_title' => 'nullable|string|max:255',
'department' => 'nullable|string|max:255',
'department_id' => 'nullable|exists:departments,id',
'role_id' => 'nullable|exists:roles,id',
'status' => 'nullable|string|in:active,inactive',
'password' => 'nullable|min:8',
];
}
}

مشاهده پرونده

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ActivityLogResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'user_id' => $this->user_id,
'user' => new UserResource($this->whenLoaded('user')),
'action' => $this->action,
'description' => $this->description,
'subject_type' => $this->subject_type,
'subject_id' => $this->subject_id,
'project_id' => $this->project_id,
'task_id' => $this->task_id,
'properties' => $this->properties,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BacklogItemResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'type' => $this->type,
'project_id' => $this->project_id,
'project' => new ProjectResource($this->whenLoaded('project')),
'priority' => $this->priority,
'estimated_effort' => $this->estimated_effort,
'status' => $this->status,
'assigned_sprint_id' => $this->assigned_sprint_id,
'assigned_sprint' => new SprintResource($this->whenLoaded('assignedSprint')),
'created_by' => $this->created_by,
'creator' => new UserResource($this->whenLoaded('creator')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ChecklistResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'task_id' => $this->task_id,
'title' => $this->title,
'is_completed' => $this->is_completed,
'position' => $this->position,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CommentResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'body' => $this->body,
'user_id' => $this->user_id,
'user' => new UserResource($this->whenLoaded('user')),
'commentable_id' => $this->commentable_id,
'commentable_type' => $this->commentable_type,
'files' => FileResource::collection($this->whenLoaded('files')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FileResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'original_name' => $this->original_name,
'download_url' => url("/api/files/{$this->id}"),
'mime_type' => $this->mime_type,
'size' => $this->size,
'fileable_id' => $this->fileable_id,
'fileable_type' => $this->fileable_type,
'user_id' => $this->user_id,
'user' => new UserResource($this->whenLoaded('user')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MeetingActionItemResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'meeting_id' => $this->meeting_id,
'title' => $this->title,
'assigned_to' => $this->assigned_to,
'assignee' => new UserResource($this->whenLoaded('assignee')),
'due_date' => $this->due_date?->format('Y-m-d'),
'is_completed' => $this->is_completed,
'converted_to_task_id' => $this->converted_to_task_id,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MeetingResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'project_id' => $this->project_id,
'project' => new ProjectResource($this->whenLoaded('project')),
'date' => $this->date?->format('Y-m-d'),
'start_time' => $this->start_time,
'end_time' => $this->end_time,
'location' => $this->location,
'meeting_link' => $this->meeting_link,
'meeting_type' => $this->meeting_type,
'agenda' => $this->agenda,
'notes' => $this->notes,
'decisions' => $this->decisions,
'participants' => UserResource::collection($this->whenLoaded('participants')),
'action_items' => MeetingActionItemResource::collection($this->whenLoaded('actionItems')),
'created_by' => $this->created_by,
'creator' => new UserResource($this->whenLoaded('creator')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,27 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class NotificationResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'type' => $this->type,
'title' => $this->title,
'body' => $this->body,
'data' => $this->data,
'message' => $this->title,
'notifiable_type' => $this->notifiable_type,
'notifiable_id' => $this->notifiable_id,
'is_read' => $this->is_read,
'read_at' => $this->read_at,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PermissionResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'display_name' => $this->display_name,
'module' => $this->module,
'guard_name' => $this->guard_name,
'created_at' => $this->created_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,46 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'client' => $this->client,
'project_manager_id' => $this->project_manager_id,
'project_manager' => new UserResource($this->whenLoaded('projectManager')),
'department_id' => $this->department_id,
'department' => $this->whenLoaded('department', fn() => [
'id' => $this->department?->id,
'name' => $this->department?->name,
'type' => $this->department?->type,
]),
'members' => UserResource::collection($this->whenLoaded('members')),
'start_date' => $this->start_date?->format('Y-m-d'),
'end_date' => $this->end_date?->format('Y-m-d'),
'priority' => $this->priority,
'status' => $this->status,
'progress' => $this->progress,
'risk_level' => $this->risk_level,
'budget' => $this->budget,
'estimated_hours' => $this->estimated_hours,
'actual_hours' => $this->actual_hours,
'tags' => $this->tags,
'notes' => $this->notes,
'is_archived' => $this->is_archived,
'created_by' => $this->created_by,
'creator' => new UserResource($this->whenLoaded('creator')),
'tasks_count' => $this->when($this->tasks_count !== null, $this->tasks_count),
'meetings_count' => $this->when($this->meetings_count !== null, $this->meetings_count),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class RoleResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'display_name' => $this->display_name,
'description' => $this->description,
'guard_name' => $this->guard_name,
'permissions' => PermissionResource::collection($this->whenLoaded('permissions')),
'users_count' => $this->when($this->users_count !== null, $this->users_count),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,44 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class SprintResource extends JsonResource
{
public function toArray(Request $request): array
{
$totalTasks = $this->whenLoaded('tasks', fn() => $this->tasks->count(), 0);
$completedTasks = $this->whenLoaded('tasks', fn() => $this->tasks->where('status', 'done')->count(), 0);
$remainingTasks = max($totalTasks - $completedTasks, 0);
return [
'id' => $this->id,
'title' => $this->title,
'project_id' => $this->project_id,
'project' => new ProjectResource($this->whenLoaded('project')),
'goal' => $this->goal,
'capacity_hours' => $this->capacity_hours,
'start_date' => $this->start_date?->format('Y-m-d'),
'end_date' => $this->end_date?->format('Y-m-d'),
'status' => $this->status,
'tasks' => TaskResource::collection($this->whenLoaded('tasks')),
'members' => UserResource::collection($this->whenLoaded('members')),
'retrospective' => $this->whenLoaded('retrospective', fn() => [
'went_well' => $this->retrospective?->went_well,
'problems' => $this->retrospective?->problems,
'improvements' => $this->retrospective?->improvements,
]),
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'remaining_tasks' => $remainingTasks,
'progress' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
'completed_at' => $this->completed_at?->toDateTimeString(),
'completion_summary' => $this->completion_summary,
'created_by' => $this->created_by,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,26 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class SubtaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'task_id' => $this->task_id,
'title' => $this->title,
'description' => $this->description,
'assignee_id' => $this->assignee_id,
'assignee' => new UserResource($this->whenLoaded('assignee')),
'status' => $this->status,
'due_date' => $this->due_date,
'position' => $this->position,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,41 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'description' => $this->description,
'project_id' => $this->project_id,
'project' => new ProjectResource($this->whenLoaded('project')),
'assignee_id' => $this->assignee_id,
'assignee' => new UserResource($this->whenLoaded('assignee')),
'reporter_id' => $this->reporter_id,
'reporter' => new UserResource($this->whenLoaded('reporter')),
'priority' => $this->priority,
'status' => $this->status,
'blocker_type' => $this->blocker_type,
'blocker_note' => $this->blocker_note,
'start_date' => $this->start_date?->format('Y-m-d'),
'due_date' => $this->due_date?->format('Y-m-d'),
'estimated_time' => $this->estimated_time,
'actual_time' => $this->actual_time,
'tags' => $this->tags,
'sort_order' => $this->sort_order,
'checklists' => ChecklistResource::collection($this->whenLoaded('checklists')),
'subtasks' => SubtaskResource::collection($this->whenLoaded('subtasks')),
'comments_count' => $this->when($this->comments_count !== null, $this->comments_count),
'files_count' => $this->when($this->files_count !== null, $this->files_count),
'created_by' => $this->created_by,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,41 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'phone' => $this->phone,
'job_title' => $this->job_title,
'department' => $this->department,
'department_id' => $this->department_id,
'department_name' => $this->when($this->relationLoaded('dept'), fn() => $this->dept?->name),
'departments' => $this->when($this->relationLoaded('departments'), fn() => $this->departments->map(fn($department) => [
'id' => $department->id,
'name' => $department->name,
'type' => $department->type,
'role_in_team' => $department->pivot?->role_in_team,
'is_primary' => (bool) $department->pivot?->is_primary,
'joined_at' => $department->pivot?->joined_at,
])->values()),
'status' => $this->status,
'skills' => $this->skills,
'avatar' => $this->avatar,
'avatar_url' => $this->avatar ? asset('storage/' . $this->avatar) : null,
'role_id' => $this->role_id,
'role' => new RoleResource($this->whenLoaded('role')),
'roles' => RoleResource::collection($this->whenLoaded('roles')),
'projects_count' => $this->when($this->projects_count !== null, $this->projects_count),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}

مشاهده پرونده

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivityLog extends Model
{
protected $fillable = [
'user_id', 'action', 'description', 'subject_type', 'subject_id',
'project_id', 'task_id', 'properties'
];
protected $casts = [
'properties' => 'array',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

مشاهده پرونده

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class BacklogItem extends Model
{
protected $fillable = [
'title', 'description', 'type', 'project_id', 'priority',
'estimated_effort', 'status', 'assigned_sprint_id', 'created_by'
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function assignedSprint(): BelongsTo
{
return $this->belongsTo(Sprint::class, 'assigned_sprint_id');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

مشاهده پرونده

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Checklist extends Model
{
protected $fillable = ['task_id', 'title', 'is_completed', 'position'];
protected $casts = [
'is_completed' => 'boolean',
];
public function task(): BelongsTo
{
return $this->belongsTo(Task::class);
}
}

28
backend/app/Models/Comment.php normal فایل
مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = ['body', 'user_id', 'commentable_id', 'commentable_type'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function commentable(): MorphTo
{
return $this->morphTo();
}
public function files(): MorphMany
{
return $this->morphMany(File::class, 'fileable');
}
}

مشاهده پرونده

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Department extends Model
{
protected $fillable = ['name', 'description', 'type', 'parent_id', 'manager_id', 'manager_changed_at', 'is_active', 'sort_order'];
protected $casts = [
'is_active' => 'boolean',
'manager_changed_at' => 'datetime',
];
public function parent(): BelongsTo
{
return $this->belongsTo(Department::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(Department::class, 'parent_id')->orderBy('sort_order');
}
public function manager(): BelongsTo
{
return $this->belongsTo(User::class, 'manager_id');
}
public function users(): HasMany
{
return $this->hasMany(User::class, 'department_id');
}
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class, 'department_user')
->withPivot(['role_in_team', 'is_primary', 'joined_at'])
->withTimestamps();
}
}

22
backend/app/Models/File.php normal فایل
مشاهده پرونده

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class File extends Model
{
protected $fillable = ['name', 'original_name', 'path', 'mime_type', 'size', 'fileable_id', 'fileable_type', 'user_id'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function fileable(): MorphTo
{
return $this->morphTo();
}
}

51
backend/app/Models/Meeting.php normal فایل
مشاهده پرونده

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Meeting extends Model
{
protected $fillable = [
'title', 'project_id', 'date', 'start_time', 'end_time', 'location',
'meeting_link', 'meeting_type', 'agenda', 'notes', 'decisions', 'created_by'
];
protected $casts = [
'date' => 'date',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function participants(): BelongsToMany
{
return $this->belongsToMany(User::class, 'meeting_user')->withTimestamps();
}
public function actionItems(): HasMany
{
return $this->hasMany(MeetingActionItem::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
public function files(): MorphMany
{
return $this->morphMany(File::class, 'fileable');
}
}

مشاهده پرونده

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MeetingActionItem extends Model
{
protected $fillable = [
'meeting_id', 'title', 'assigned_to', 'due_date', 'is_completed', 'converted_to_task_id'
];
protected $casts = [
'due_date' => 'date',
'is_completed' => 'boolean',
];
public function meeting(): BelongsTo
{
return $this->belongsTo(Meeting::class);
}
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to');
}
public function convertedTask(): BelongsTo
{
return $this->belongsTo(Task::class, 'converted_to_task_id');
}
}

مشاهده پرونده

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
protected $fillable = [
'user_id', 'title', 'body', 'type', 'notifiable_type', 'notifiable_id',
'data', 'is_read', 'read_at', 'remind_at', 'dismissed_at'
];
protected $casts = [
'data' => 'array',
'is_read' => 'boolean',
'read_at' => 'datetime',
'remind_at' => 'datetime',
'dismissed_at' => 'datetime',
];
}

مشاهده پرونده

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Permission extends Model
{
protected $fillable = ['name', 'display_name', 'guard_name', 'module'];
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
}

67
backend/app/Models/Project.php normal فایل
مشاهده پرونده

@ -0,0 +1,67 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Project extends Model
{
protected $fillable = [
'title', 'description', 'client', 'project_manager_id', 'department_id', 'start_date', 'end_date',
'priority', 'status', 'progress', 'risk_level', 'budget', 'estimated_hours',
'actual_hours', 'tags', 'notes', 'is_archived', 'created_by'
];
protected $casts = [
'tags' => 'array',
'start_date' => 'date',
'end_date' => 'date',
'is_archived' => 'boolean',
'budget' => 'decimal:2',
'estimated_hours' => 'decimal:2',
'actual_hours' => 'decimal:2',
];
public function projectManager(): BelongsTo
{
return $this->belongsTo(User::class, 'project_manager_id');
}
public function department(): BelongsTo
{
return $this->belongsTo(Department::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class, 'project_user')->withPivot('role_in_project')->withTimestamps();
}
public function tasks(): HasMany
{
return $this->hasMany(Task::class);
}
public function sprints(): HasMany
{
return $this->hasMany(Sprint::class);
}
public function meetings(): HasMany
{
return $this->hasMany(Meeting::class);
}
public function backlogItems(): HasMany
{
return $this->hasMany(BacklogItem::class);
}
}

21
backend/app/Models/Role.php normal فایل
مشاهده پرونده

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $fillable = ['name', 'display_name', 'description', 'guard_name'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class)->withTimestamps();
}
}

29
backend/app/Models/Setting.php normal فایل
مشاهده پرونده

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
class Setting extends Model
{
protected $fillable = ['key', 'value', 'group', 'type'];
protected function value(): Attribute
{
return Attribute::make(
get: function ($value) {
if ($value === null || $value === '') {
return $value;
}
$decoded = json_decode($value, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
},
set: fn ($value) => is_array($value) || is_object($value) || is_bool($value)
? json_encode($value, JSON_UNESCAPED_UNICODE)
: $value,
);
}
}

55
backend/app/Models/Sprint.php normal فایل
مشاهده پرونده

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Sprint extends Model
{
protected $fillable = [
'title', 'project_id', 'goal', 'capacity_hours', 'start_date', 'end_date',
'status', 'completed_at', 'completion_summary', 'created_by',
];
protected $casts = [
'start_date' => 'date',
'end_date' => 'date',
'capacity_hours' => 'decimal:2',
'completed_at' => 'datetime',
'completion_summary' => 'array',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function tasks(): BelongsToMany
{
return $this->belongsToMany(Task::class, 'sprint_task')->withTimestamps();
}
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class, 'sprint_members')->withTimestamps();
}
public function retrospective(): HasOne
{
return $this->hasOne(SprintRetrospective::class);
}
public function backlogItems(): HasMany
{
return $this->hasMany(BacklogItem::class, 'assigned_sprint_id');
}
}

مشاهده پرونده

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SprintRetrospective extends Model
{
protected $fillable = [
'sprint_id', 'went_well', 'problems', 'improvements', 'updated_by',
];
public function sprint(): BelongsTo
{
return $this->belongsTo(Sprint::class);
}
public function updater(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
}

25
backend/app/Models/Subtask.php normal فایل
مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Subtask extends Model
{
protected $fillable = ['task_id', 'title', 'description', 'assignee_id', 'status', 'due_date', 'position'];
protected $casts = [
'due_date' => 'date',
];
public function task(): BelongsTo
{
return $this->belongsTo(Task::class);
}
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assignee_id');
}
}

70
backend/app/Models/Task.php normal فایل
مشاهده پرونده

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Task extends Model
{
protected $fillable = [
'title', 'description', 'project_id', 'assignee_id', 'reporter_id',
'priority', 'status', 'blocker_type', 'blocker_note', 'start_date', 'due_date', 'estimated_time',
'actual_time', 'tags', 'sort_order', 'created_by'
];
protected $casts = [
'tags' => 'array',
'start_date' => 'date',
'due_date' => 'date',
'estimated_time' => 'decimal:2',
'actual_time' => 'decimal:2',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assignee_id');
}
public function reporter(): BelongsTo
{
return $this->belongsTo(User::class, 'reporter_id');
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function checklists(): HasMany
{
return $this->hasMany(Checklist::class);
}
public function subtasks(): HasMany
{
return $this->hasMany(Subtask::class);
}
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
public function files(): MorphMany
{
return $this->morphMany(File::class, 'fileable');
}
public function sprints()
{
return $this->belongsToMany(Sprint::class, 'sprint_task')->withTimestamps();
}
}

113
backend/app/Models/User.php normal فایل
مشاهده پرونده

@ -0,0 +1,113 @@
<?php
namespace App\Models;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name', 'email', 'password', 'role_id', 'phone', 'job_title',
'department', 'department_id', 'status', 'skills', 'avatar',
];
protected $hidden = [
'password', 'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'skills' => 'array',
];
}
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
public function dept(): BelongsTo
{
return $this->belongsTo(Department::class, 'department_id');
}
public function departments(): BelongsToMany
{
return $this->belongsToMany(Department::class, 'department_user')
->withPivot(['role_in_team', 'is_primary', 'joined_at'])
->withTimestamps();
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_user')->withTimestamps();
}
public function hasPermission(string $permission): bool
{
foreach ($this->roles as $role) {
if ($role->permissions->contains('name', $permission)) {
return true;
}
}
return false;
}
public function hasAnyPermission(array $permissions): bool
{
foreach ($permissions as $permission) {
if ($this->hasPermission($permission)) {
return true;
}
}
return false;
}
public function managedProjects(): HasMany
{
return $this->hasMany(Project::class, 'project_manager_id');
}
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class, 'project_user')->withPivot('role_in_project')->withTimestamps();
}
public function tasks(): HasMany
{
return $this->hasMany(Task::class, 'assignee_id');
}
public function reportedTasks(): HasMany
{
return $this->hasMany(Task::class, 'reporter_id');
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function notifications(): HasMany
{
return $this->hasMany(Notification::class);
}
public function activityLogs(): HasMany
{
return $this->hasMany(ActivityLog::class);
}
}

مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Policies;
use App\Models\User;
class MeetingPolicy
{
public function view(User $user): bool
{
return $user->hasPermission('view_meetings');
}
public function create(User $user): bool
{
return $user->hasPermission('create_meetings');
}
public function update(User $user): bool
{
return $user->hasPermission('edit_meetings');
}
public function delete(User $user): bool
{
return $user->hasPermission('delete_meetings');
}
}

مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Policies;
use App\Models\User;
class ProjectPolicy
{
public function view(User $user): bool
{
return $user->hasPermission('view_projects');
}
public function create(User $user): bool
{
return $user->hasPermission('create_projects');
}
public function update(User $user): bool
{
return $user->hasPermission('edit_projects');
}
public function delete(User $user): bool
{
return $user->hasPermission('delete_projects');
}
}

مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Policies;
use App\Models\User;
class SprintPolicy
{
public function view(User $user): bool
{
return $user->hasPermission('view_sprints');
}
public function create(User $user): bool
{
return $user->hasPermission('create_sprints');
}
public function update(User $user): bool
{
return $user->hasPermission('edit_sprints');
}
public function delete(User $user): bool
{
return $user->hasPermission('delete_sprints');
}
}

مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Policies;
use App\Models\User;
class TaskPolicy
{
public function view(User $user): bool
{
return $user->hasPermission('view_tasks');
}
public function create(User $user): bool
{
return $user->hasPermission('create_tasks');
}
public function update(User $user): bool
{
return $user->hasPermission('edit_tasks');
}
public function delete(User $user): bool
{
return $user->hasPermission('delete_tasks');
}
}

مشاهده پرونده

@ -0,0 +1,28 @@
<?php
namespace App\Policies;
use App\Models\User;
class UserPolicy
{
public function view(User $user): bool
{
return $user->hasPermission('view_team');
}
public function create(User $user): bool
{
return $user->hasPermission('create_team');
}
public function update(User $user): bool
{
return $user->hasPermission('edit_team');
}
public function delete(User $user): bool
{
return $user->hasPermission('delete_team');
}
}

مشاهده پرونده

@ -0,0 +1,44 @@
<?php
namespace App\Providers;
use App\Services\ActivityLogService;
use App\Services\DashboardService;
use App\Services\NotificationService;
use App\Services\ProjectProgressService;
use App\Services\ReportService;
use App\Services\WorkloadService;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(ActivityLogService::class);
$this->app->singleton(NotificationService::class);
$this->app->singleton(DashboardService::class);
$this->app->singleton(ReportService::class);
$this->app->singleton(ProjectProgressService::class);
$this->app->singleton(WorkloadService::class);
}
public function boot(): void
{
RateLimiter::for('login', function (Request $request) {
$identifier = Str::lower((string) $request->input('identifier', $request->input('email')));
return [
Limit::perMinute(5)->by($identifier.'|'.$request->ip()),
Limit::perMinute(20)->by($request->ip()),
];
});
RateLimiter::for('sensitive', function (Request $request) {
return Limit::perMinute(30)->by(optional($request->user())->id ?: $request->ip());
});
}
}

مشاهده پرونده

@ -0,0 +1,22 @@
<?php
namespace App\Services;
use App\Models\ActivityLog;
class ActivityLogService
{
public function log($userId, $action, $description, $subjectType = null, $subjectId = null, $projectId = null, $taskId = null, $properties = [])
{
return ActivityLog::create([
'user_id' => $userId,
'action' => $action,
'description' => $description,
'subject_type' => $subjectType,
'subject_id' => $subjectId,
'project_id' => $projectId,
'task_id' => $taskId,
'properties' => $properties,
]);
}
}

مشاهده پرونده

@ -0,0 +1,162 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Models\Task;
use App\Models\Meeting;
use App\Models\ActivityLog;
use App\Models\Sprint;
use Carbon\Carbon;
class DashboardService
{
public function getSummary($user = null)
{
$activeProjects = Project::where('status', '!=', 'done')->where('is_archived', false)->count();
$completedProjects = Project::where('status', 'done')->count();
$today = Carbon::today();
$delayedProjects = Project::whereDate('end_date', '<', $today)->where('status', '!=', 'done')->where('is_archived', false)->count();
$openTasks = Task::whereNotIn('status', ['done', 'canceled'])->count();
$completedTasks = Task::where('status', 'done')->count();
$delayedTasks = Task::whereDate('due_date', '<', $today)->where('status', '!=', 'done')->count();
$teamPerformance = [];
$workload = [];
$riskyProjects = Project::whereIn('risk_level', ['high', 'critical'])->where('is_archived', false)->limit(5)->get();
$upcomingDeadlines = Task::with('project')
->whereDate('due_date', '>=', $today)
->whereDate('due_date', '<=', $today->copy()->addDays(7))
->where('status', '!=', 'done')
->orderBy('due_date')
->limit(10)
->get()
->map(fn($t) => [
'id' => $t->id,
'title' => $t->title,
'project' => $t->project?->title,
'due_date' => $t->due_date?->format('Y-m-d'),
'days_left' => $today->diffInDays($t->due_date, false),
]);
$recentActivities = ActivityLog::with('user')
->orderBy('created_at', 'desc')
->limit(10)
->get()
->map(fn($log) => [
'id' => $log->id,
'description' => $log->description,
'user' => $log->user?->name ?? '?',
'created_at' => $log->created_at?->toDateTimeString(),
'time_ago' => $log->created_at?->diffForHumans(),
]);
$myTasks = collect();
if ($user) {
$myTasks = Task::where('assignee_id', $user->id)
->where('status', '!=', 'done')
->orderBy('due_date')
->limit(10)
->get();
}
$activeSprint = null;
if ($user) {
$sprint = Sprint::with(['project', 'tasks.assignee'])
->where('status', 'active')
->whereHas('members', fn($query) => $query->where('users.id', $user->id))
->orderBy('end_date')
->first();
if ($sprint) {
$totalTasks = $sprint->tasks->count();
$completedTasks = $sprint->tasks->where('status', 'done')->count();
$activeSprint = [
'id' => $sprint->id,
'title' => $sprint->title,
'project' => $sprint->project?->title,
'end_date' => $sprint->end_date?->format('Y-m-d'),
'remaining_days' => max(Carbon::today()->diffInDays($sprint->end_date, false), 0),
'progress' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
'my_tasks' => $sprint->tasks
->where('assignee_id', $user->id)
->values()
->map(fn($task) => [
'id' => $task->id,
'title' => $task->title,
'status' => $task->status,
]),
];
}
}
$todayMeetings = Meeting::whereDate('date', Carbon::today())->count();
$teamMembers = \App\Models\User::count();
$projectProgresses = Project::where('is_archived', false)
->select(['id', 'title', 'progress', 'status'])
->limit(10)
->get();
return [
'activeProjects' => $activeProjects,
'completedProjects' => $completedProjects,
'delayedProjects' => $delayedProjects,
'openTasks' => $openTasks,
'completedTasks' => $completedTasks,
'delayedTasks' => $delayedTasks,
'teamPerformance' => $teamPerformance,
'workload' => $workload,
'riskyProjects' => $riskyProjects,
'upcomingDeadlines' => $upcomingDeadlines,
'recentActivities' => $recentActivities,
'myTasks' => $myTasks,
'todayMeetings' => $todayMeetings,
'teamMembers' => $teamMembers,
'projectProgresses' => $projectProgresses,
'activeSprint' => $activeSprint,
];
}
public function getChartData()
{
$months = collect();
$taskCreationData = [];
$taskCompletionData = [];
for ($i = 5; $i >= 0; $i--) {
$month = Carbon::now()->subMonths($i);
$monthName = $month->format('Y-m');
$months->push($monthName);
$taskCreationData[] = Task::whereYear('created_at', $month->year)
->whereMonth('created_at', $month->month)
->count();
$taskCompletionData[] = Task::where('status', 'done')
->whereYear('updated_at', $month->year)
->whereMonth('updated_at', $month->month)
->count();
}
$monthlyTasks = [];
foreach ($months as $i => $month) {
$monthlyTasks[] = [
'month' => $month,
'created' => $taskCreationData[$i],
'completed' => $taskCompletionData[$i],
];
}
return [
'labels' => $months->values(),
'taskCreation' => $taskCreationData,
'taskCompletion' => $taskCompletionData,
'monthlyTasks' => $monthlyTasks,
'monthly_tasks' => $monthlyTasks,
];
}
}

مشاهده پرونده

@ -0,0 +1,25 @@
<?php
namespace App\Services;
use App\Models\Notification;
class NotificationService
{
public function create($userId, $type, $data, $message)
{
$payload = is_array($data) ? $data : null;
return Notification::create([
'user_id' => $userId,
'type' => $type,
'title' => $message,
'body' => is_array($data) ? null : $data,
'data' => $payload,
'notifiable_type' => $payload['notifiable_type'] ?? null,
'notifiable_id' => $payload['notifiable_id'] ?? null,
'is_read' => false,
'read_at' => null,
]);
}
}

مشاهده پرونده

@ -0,0 +1,27 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Models\Task;
class ProjectProgressService
{
public function calculateProgress($projectId)
{
$project = Project::findOrFail($projectId);
$totalTasks = Task::where('project_id', $projectId)->count();
if ($totalTasks === 0) {
$project->update(['progress' => 0]);
return 0;
}
$completedTasks = Task::where('project_id', $projectId)->where('status', 'done')->count();
$progress = round(($completedTasks / $totalTasks) * 100, 2);
$project->update(['progress' => $progress]);
return $progress;
}
}

مشاهده پرونده

@ -0,0 +1,247 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Models\Task;
use App\Models\Sprint;
use App\Models\ActivityLog;
use App\Models\User;
use Carbon\Carbon;
class ReportService
{
protected function applyDateFilters($query, $filters, $dateField = 'created_at')
{
if (!empty($filters['date_from'])) {
$query->where($dateField, '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$query->where($dateField, '<=', $filters['date_to']);
}
return $query;
}
public function projectStatus($filters = [])
{
$query = Project::query();
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['project_id'])) {
$query->where('id', $filters['project_id']);
}
$projects = $query->get();
$grouped = $projects->groupBy('status');
return $grouped->map(fn($items, $status) => [
'status' => $status === 'planning' ? 'برنامه‌ریزی' : ($status === 'in_progress' ? 'در حال انجام' : ($status === 'done' ? 'تکمیل شده' : ($status === 'on_hold' ? 'متوقف' : $status))),
'count' => $items->count(),
'label' => $status,
'value' => $items->count(),
])->values();
}
public function delayedTasks($filters = [])
{
$query = Task::with(['project:id,title', 'assignee:id,name'])
->whereDate('due_date', '<', Carbon::today())
->where('status', '!=', 'done');
$query = $this->applyDateFilters($query, $filters, 'due_date');
if (!empty($filters['project_id'])) {
$query->where('project_id', $filters['project_id']);
}
if (!empty($filters['user_id'])) {
$query->where('assignee_id', $filters['user_id']);
}
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
return $query->orderBy('due_date')->get()->map(fn($t) => [
'id' => $t->id,
'title' => $t->title,
'project' => ['title' => $t->project?->title],
'project_name' => $t->project?->title,
'assignee' => ['name' => $t->assignee?->name],
'assignee_name' => $t->assignee?->name,
'due_date' => $t->due_date?->format('Y-m-d'),
'delay_days' => abs(Carbon::today()->diffInDays($t->due_date, false)),
'days_delayed' => abs(Carbon::today()->diffInDays($t->due_date, false)),
'priority' => $t->priority,
]);
}
public function teamPerformance($filters = [])
{
$query = User::with(['tasks' => function ($q) use ($filters) {
if (!empty($filters['date_from'])) {
$q->where('created_at', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$q->where('created_at', '<=', $filters['date_to']);
}
}]);
if (!empty($filters['user_id'])) {
$query->where('id', $filters['user_id']);
}
$users = $query->get();
return $users->map(function ($user) {
$totalTasks = $user->tasks->count();
$completedTasks = $user->tasks->where('status', 'done')->count();
$delayedTasks = $user->tasks->where('due_date', '<', Carbon::now())->where('status', '!=', 'done')->count();
$inProgress = $user->tasks->whereNotIn('status', ['done', 'canceled'])->count();
return [
'name' => $user->name,
'completed' => $completedTasks,
'in_progress' => $inProgress,
'delayed' => $delayedTasks,
'total' => $totalTasks,
];
});
}
public function workload($filters = [])
{
$query = User::query();
if (!empty($filters['user_id'])) {
$query->where('id', $filters['user_id']);
}
$users = $query->get();
$allActiveTasks = Task::where('status', '!=', 'done')->count();
$maxTasks = max($allActiveTasks, 1);
return $users->map(function ($user) use ($maxTasks) {
$activeTasks = Task::where('assignee_id', $user->id)->where('status', '!=', 'done')->count();
$loadPercent = min(round(($activeTasks / $maxTasks) * 100), 100);
return [
'user_id' => $user->id,
'name' => $user->name,
'tasks_count' => $activeTasks,
'total' => $activeTasks,
'load_percent' => $loadPercent,
'load' => $loadPercent,
];
});
}
public function sprintProgress($filters = [])
{
$query = Sprint::with(['project:id,title', 'tasks']);
if (!empty($filters['project_id'])) {
$query->where('project_id', $filters['project_id']);
}
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
$sprints = $query->get();
return $sprints->map(function ($sprint) {
$totalTasks = $sprint->tasks->count();
$completedTasks = $sprint->tasks->where('status', 'done')->count();
return [
'id' => $sprint->id,
'title' => $sprint->title,
'project' => $sprint->project?->title,
'start_date' => $sprint->start_date,
'end_date' => $sprint->end_date,
'status' => $sprint->status,
'total_tasks' => $totalTasks,
'completed_tasks' => $completedTasks,
'progress' => $totalTasks > 0 ? round(($completedTasks / $totalTasks) * 100, 2) : 0,
];
});
}
public function timeEstimate($filters = [])
{
$query = Task::with('project');
$query = $this->applyDateFilters($query, $filters);
if (!empty($filters['project_id'])) {
$query->where('project_id', $filters['project_id']);
}
if (!empty($filters['user_id'])) {
$query->where('assignee_id', $filters['user_id']);
}
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
$tasks = $query->get();
return $tasks->map(function ($task) {
$diff = ($task->estimated_time ?? 0) - ($task->actual_time ?? 0);
return [
'id' => $task->id,
'title' => $task->title,
'project' => ['title' => $task->project?->title ?? '—'],
'estimated' => $task->estimated_time,
'estimated_time' => $task->estimated_time,
'actual' => $task->actual_time,
'actual_time' => $task->actual_time,
'diff' => $diff,
'difference' => $diff,
'status' => $task->status,
];
});
}
public function recentActivities($filters = [])
{
$query = ActivityLog::with('user');
$query = $this->applyDateFilters($query, $filters);
if (!empty($filters['project_id'])) {
$query->where('project_id', $filters['project_id']);
}
if (!empty($filters['user_id'])) {
$query->where('user_id', $filters['user_id']);
}
if (!empty($filters['action'])) {
$query->where('action', $filters['action']);
}
return $query->orderBy('created_at', 'desc')->limit(50)->get()->map(fn($log) => [
'id' => $log->id,
'description' => $log->description,
'user' => $log->user?->name ?? '?',
'user_name' => $log->user?->name ?? '?',
'created_at' => $log->created_at?->toDateTimeString(),
'time_ago' => $log->created_at?->diffForHumans(),
]);
}
public function riskyProjects($filters = [])
{
$query = Project::whereIn('risk_level', ['high', 'critical'])->where('is_archived', false);
if (!empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (!empty($filters['project_id'])) {
$query->where('id', $filters['project_id']);
}
return $query->with('projectManager:id,name')->get()->map(fn($p) => [
'id' => $p->id,
'title' => $p->title,
'risk_level' => $p->risk_level,
'progress' => $p->progress,
'delayed_tasks' => Task::where('project_id', $p->id)->where('due_date', '<', Carbon::now())->where('status', '!=', 'done')->count(),
]);
}
}

مشاهده پرونده

@ -0,0 +1,35 @@
<?php
namespace App\Services;
use App\Models\User;
use App\Models\Task;
use Carbon\Carbon;
class WorkloadService
{
public function getWorkload()
{
$users = User::all();
return $users->map(function ($user) {
$activeTasks = Task::where('assignee_id', $user->id)->where('status', '!=', 'done')->count();
$delayedTasks = Task::where('assignee_id', $user->id)
->where('due_date', '<', Carbon::now())
->where('status', '!=', 'done')
->count();
$estimatedHours = Task::where('assignee_id', $user->id)->sum('estimated_time');
$actualHours = Task::where('assignee_id', $user->id)->sum('actual_time');
return [
'user_id' => $user->id,
'user_name' => $user->name,
'active_tasks' => $activeTasks,
'delayed_tasks' => $delayedTasks,
'estimated_hours' => (float) $estimatedHours,
'actual_hours' => (float) $actualHours,
'workload_percentage' => $estimatedHours > 0 ? round(($actualHours / $estimatedHours) * 100, 2) : 0,
];
});
}
}

18
backend/artisan normal فایل
مشاهده پرونده

@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

35
backend/bootstrap/app.php normal فایل
مشاهده پرونده

@ -0,0 +1,35 @@
<?php
use Illuminate\Auth\AuthenticationException;
use App\Http\Middleware\EnsureUserHasPermission;
use App\Http\Middleware\SecurityHeaders;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->append(SecurityHeaders::class);
$middleware->alias([
'permission' => EnsureUserHasPermission::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->render(function (AuthenticationException $e, Request $request) {
if ($request->is('api/*') || $request->expectsJson()) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated.',
], 401);
}
return null;
});
})->create();

2
backend/bootstrap/cache/.gitignore فروخته شده normal فایل
مشاهده پرونده

@ -0,0 +1,2 @@
*
!.gitignore

مشاهده پرونده

@ -0,0 +1,7 @@
<?php
use App\Providers\AppServiceProvider;
return [
AppServiceProvider::class,
];

87
backend/composer.json normal فایل
مشاهده پرونده

@ -0,0 +1,87 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.50"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve --host=127.0.0.1 --port=8000\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

8475
backend/composer.lock تولید شده normal فایل

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است Diff را بارگزاری کن

126
backend/config/app.php normal فایل
مشاهده پرونده

@ -0,0 +1,126 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];

117
backend/config/auth.php normal فایل
مشاهده پرونده

@ -0,0 +1,117 @@
<?php
use App\Models\User;
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

117
backend/config/cache.php normal فایل
مشاهده پرونده

@ -0,0 +1,117 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_STORE', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
];

18
backend/config/cors.php normal فایل
مشاهده پرونده

@ -0,0 +1,18 @@
<?php
$frontendUrl = env('FRONTEND_URL');
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => array_values(array_filter([
$frontendUrl,
env('APP_ENV') === 'local' ? 'http://localhost:4004' : null,
env('APP_ENV') === 'local' ? 'http://127.0.0.1:4004' : null,
])),
'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'X-Requested-With', 'Authorization', 'Accept', 'Origin'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];

184
backend/config/database.php normal فایل
مشاهده پرونده

@ -0,0 +1,184 @@
<?php
use Illuminate\Support\Str;
use Pdo\Mysql;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];

برخی از فایل ها نشان داده نشدند زیرا تعداد زیادی فایل در این تفاوت تغییر کرده اند نمایش بیشتر