DONE: layout letter with letterhead and footer, notification for newly...
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# SUTERA 3.0 – Guide
|
||||
# MyKOPKB 1.0 – Guide
|
||||
|
||||
Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**.
|
||||
Welcome to **MyKOPKB 1.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**.
|
||||
|
||||
---
|
||||
|
||||
@@ -13,10 +13,6 @@ Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Doc
|
||||
|
||||
---
|
||||
|
||||
## Formula Used:
|
||||
1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100
|
||||
2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100
|
||||
3. SERVISIBILITI (%) = (BDG/pegangan) * 100
|
||||
|
||||
## Laravel command to clear
|
||||
```bash
|
||||
@@ -38,58 +34,15 @@ sail artisan permission:cache-reset
|
||||
- Servisibility always higher than siapsiaga
|
||||
- Solution — put specific routes first before resource routes
|
||||
- to use soft delete, make id and deleted_at as unique
|
||||
|
||||
# Nwidart Command
|
||||
- to make new module, run:
|
||||
```bash
|
||||
sail artisan module:make Example
|
||||
```
|
||||
- There are two User model files
|
||||
|
||||
## PKJ Default Varian (DO NOT DELETE) or update. If require update, be sure to update the export name for each PKJ report:
|
||||
- BANTUAN MOBILITI
|
||||
- BEKALAN AIR
|
||||
- EOD/IED/PEMUSNAHAN
|
||||
- JURUUKUR
|
||||
- KENDERAAN JENIS A
|
||||
- KENDERAAN JENIS B
|
||||
- OPTRONIK DAN PERALATAN LATIHAN
|
||||
- PELURU DAN BAHAN LETUPAN
|
||||
- PEMUSNAHAN
|
||||
- PERALATAN KETUKANGAN
|
||||
- PERALATAN KOMUNIKASI PERTAHANAN
|
||||
- PERALATAN PD&P
|
||||
- PERALATAN PERAIRAN
|
||||
- PERALATAN PNBK
|
||||
- SENJATA PERTAHANAN
|
||||
|
||||
|
||||
```sql
|
||||
--- KJC TO UPDATE PEGANGAN IN PERJAWATAN TABLE; CHECK WILL BE UPDATED FIRST
|
||||
SELECT
|
||||
ae.id,
|
||||
ae.entitlement,
|
||||
ae.holding AS current_holding,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM kjc_asset_holdings ah
|
||||
WHERE ah.kjc_category_id = ae.kjc_category_id
|
||||
AND ah.kjc_category_id = ae.kjc_category_id
|
||||
AND ah.unit_id = ae.unit_id
|
||||
AND ah.deleted_at IS NULL
|
||||
) AS actual_holding_count
|
||||
FROM kjc_asset_entitlements ae
|
||||
WHERE ae.deleted_at IS NULL;
|
||||
|
||||
--- EXECUTE IT
|
||||
UPDATE kjc_asset_entitlements ae
|
||||
SET ae.holding = (
|
||||
SELECT COUNT(*)
|
||||
FROM kjc_asset_holdings ah
|
||||
WHERE ah.kjc_category_id = ae.kjc_category_id
|
||||
AND ah.kjc_category_id = ae.kjc_category_id
|
||||
AND ah.unit_id = ae.unit_id
|
||||
AND ah.deleted_at IS NULL
|
||||
)
|
||||
WHERE ae.deleted_at IS NULL;
|
||||
- create migration in module
|
||||
```bash
|
||||
sail artisan module:make-migration add_more_details_to_users_table User
|
||||
```
|
||||
|
||||
## Run specific laravel migration:
|
||||
@@ -117,23 +70,7 @@ SELECT last_value FROM public.permissions_id_seq;
|
||||
SELECT setval('public.permissions_id_seq', COALESCE((SELECT MAX(id) FROM permissions), 0) + 1, false);
|
||||
```
|
||||
|
||||
## Run capture data command
|
||||
```bash
|
||||
# kjc
|
||||
sail artisan kjc:historical:trigger
|
||||
|
||||
# pkj
|
||||
sail artisan pkj:historical:trigger
|
||||
```
|
||||
|
||||
## Auto-generate weekly KJC report (Jabatan Arah RAJD) - Scheduled to run every Monday at 3:00 AM.
|
||||
```bash
|
||||
# Manual trigger
|
||||
sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD"
|
||||
|
||||
# Force regenerate even if reports exist
|
||||
sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" --force
|
||||
```
|
||||
---
|
||||
|
||||
# Git
|
||||
## DO NOT RUN THIS BECAUSE IT WILL REMOVE ALL THE UNTAGGED FILES IN LOCAL
|
||||
|
||||
@@ -12,4 +12,7 @@
|
||||
[ ] wasi/penama
|
||||
[ ] pendaftaran anggota/meneruskan anggota/pencen
|
||||
[ ] daftar lembaga (backdated)
|
||||
[ ] boleh print semua borang
|
||||
[ ] boleh print semua borang
|
||||
[ ] jana surat lepas lulus anggota
|
||||
|
||||
##
|
||||
@@ -66,6 +66,14 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Browsershot (PDF generation via headless Chrome + Puppeteer in be/)
|
||||
# BROWSERSHOT_NODE_BINARY=
|
||||
# BROWSERSHOT_NPM_BINARY=
|
||||
# BROWSERSHOT_NODE_MODULE_PATH=
|
||||
# BROWSERSHOT_CHROME_PATH=
|
||||
# BROWSERSHOT_NO_SANDBOX=false
|
||||
# BROWSERSHOT_TIMEOUT=60
|
||||
|
||||
# HttpOnly Sanctum token cookie (set on api.* domain)
|
||||
AUTH_COOKIE_NAME=auth_token
|
||||
AUTH_COOKIE_LIFETIME=720
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Activity',
|
||||
];
|
||||
+5
-5
@@ -11,12 +11,12 @@ return new class extends Migration
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('countries', function (Blueprint $table) {
|
||||
Schema::create('activity_types', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('code', 2);
|
||||
$table->string('name', 100);
|
||||
$table->string('name');
|
||||
$table->string('code')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('countries');
|
||||
Schema::dropIfExists('activity_types');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activities', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('activity_type_id')->constrained('activity_types')->onDelete('cascade');
|
||||
$table->string('title');
|
||||
$table->string('reference_number')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('start_datetime')->nullable();
|
||||
$table->timestamp('end_datetime')->nullable();
|
||||
$table->string('organizer')->nullable();
|
||||
$table->string('location')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activities');
|
||||
}
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('activity_reports', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('activity_id')->constrained('activities')->onDelete('cascade');
|
||||
$table->text('report_text');
|
||||
$table->foreignUuid('prepared_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('activity_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ActivityDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Entities;
|
||||
|
||||
use App\Traits\HasDocuments;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Activity extends Model
|
||||
{
|
||||
use HasDocuments, HasUuids;
|
||||
|
||||
public const GALLERY_DOCUMENT_TYPE = 'gallery';
|
||||
|
||||
protected $table = 'activities';
|
||||
|
||||
protected $fillable = [
|
||||
'activity_type_id',
|
||||
'title',
|
||||
'reference_number',
|
||||
'description',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'organizer',
|
||||
'location',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'string',
|
||||
'reference_number' => 'string',
|
||||
'description' => 'string',
|
||||
'start_datetime' => 'datetime',
|
||||
'end_datetime' => 'datetime',
|
||||
'organizer' => 'string',
|
||||
'location' => 'string',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function activityType(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ActivityType::class);
|
||||
}
|
||||
|
||||
public function reports(): HasMany
|
||||
{
|
||||
return $this->hasMany(ActivityReport::class)->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function galleryImages(): MorphMany
|
||||
{
|
||||
return $this->documents()->where('type', self::GALLERY_DOCUMENT_TYPE);
|
||||
}
|
||||
|
||||
public function attachments(): MorphMany
|
||||
{
|
||||
return $this->documents()->where('type', '!=', self::GALLERY_DOCUMENT_TYPE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Auth\Entities\User;
|
||||
|
||||
class ActivityReport extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'activity_reports';
|
||||
|
||||
protected $fillable = [
|
||||
'activity_id',
|
||||
'report_text',
|
||||
'prepared_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'report_text' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function activity(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Activity::class);
|
||||
}
|
||||
|
||||
public function preparedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'prepared_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ActivityType extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'activity_types';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'string',
|
||||
'code' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function activities(): HasMany
|
||||
{
|
||||
return $this->hasMany(Activity::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\BaseCrudController;
|
||||
use App\Models\Document;
|
||||
use App\Services\ActivityLogger;
|
||||
use App\Services\DocumentService;
|
||||
use Exception;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
use Modules\Activity\Http\Requests\ActivityRequest;
|
||||
use Modules\Activity\Http\Requests\UploadActivityDocumentRequest;
|
||||
use Modules\Activity\Http\Requests\UploadActivityGalleryImageRequest;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityRepositoryInterface;
|
||||
use Modules\Activity\Services\ActivityService;
|
||||
use Modules\Activity\Transformers\ActivityListResource;
|
||||
use Modules\Activity\Transformers\ActivityResource;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class ActivityController extends BaseCrudController
|
||||
{
|
||||
protected $modelClass = Activity::class;
|
||||
|
||||
protected $resourceClass = ActivityResource::class;
|
||||
|
||||
protected $requestClass = ActivityRequest::class;
|
||||
|
||||
protected $resourceName = 'activity';
|
||||
|
||||
protected $resourceNamePlural = 'activities';
|
||||
|
||||
public function __construct(ActivityRepositoryInterface $repository, protected ActivityService $activityService, protected DocumentService $documentService)
|
||||
{
|
||||
parent::__construct($repository);
|
||||
}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('viewAny', $this->modelClass);
|
||||
|
||||
try {
|
||||
$perPage = $request->get('per_page', 10) ?? 10;
|
||||
$perPage = min($perPage, 1000);
|
||||
$search = (string) ($request->get('search', '') ?? '');
|
||||
$sortBy = (string) ($request->get('sort_by', 'start_datetime') ?? 'start_datetime');
|
||||
$sortOrder = (string) ($request->get('sort_order', 'desc') ?? 'desc');
|
||||
|
||||
$items = $this->getIndexData($request, $perPage, $search, $sortBy, $sortOrder);
|
||||
|
||||
if ($items instanceof LengthAwarePaginator) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ActivityListResource::collection($items->items()),
|
||||
'pagination' => [
|
||||
'current_page' => $items->currentPage(),
|
||||
'per_page' => $items->perPage(),
|
||||
'total' => $items->total(),
|
||||
'last_page' => $items->lastPage(),
|
||||
'from' => $items->firstItem(),
|
||||
'to' => $items->lastItem(),
|
||||
'has_more_pages' => $items->hasMorePages(),
|
||||
],
|
||||
'message' => $this->getSuccessMessage('index'),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => ActivityListResource::collection($items),
|
||||
'message' => $this->getSuccessMessage('index'),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error fetching {$this->resourceNamePlural}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse($this->getErrorMessage('index'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getIndexData(Request $request, int $perPage = 10, string $search = '', string $sortBy = 'start_datetime', string $sortOrder = 'desc')
|
||||
{
|
||||
if ($perPage > 0) {
|
||||
return $this->repository->getIndexPaginated($perPage, $search, $sortBy, $sortOrder);
|
||||
}
|
||||
|
||||
return $this->repository->all($search);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$this->authorize('create', $this->modelClass);
|
||||
|
||||
try {
|
||||
$validated = $this->validateRequest($request);
|
||||
$documents = collect($request->file('documents', []))
|
||||
->filter()
|
||||
->all();
|
||||
$galleryImages = collect($request->file('gallery', []))
|
||||
->filter()
|
||||
->all();
|
||||
|
||||
$activity = $this->activityService->create(
|
||||
$this->prepareStoreData(collect($validated)->except(['documents', 'gallery'])->all()),
|
||||
$documents,
|
||||
$galleryImages,
|
||||
);
|
||||
|
||||
ActivityLogger::log("Created {$this->resourceName}: {$this->getItemName($activity)}", $activity);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new $this->resourceClass($activity),
|
||||
'message' => $this->getSuccessMessage('store'),
|
||||
], 201);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error creating {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse($this->getErrorMessage('store').': '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function update(Request $request, string $id): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity = $this->repository->findById($id);
|
||||
|
||||
if (! $activity) {
|
||||
return $this->errorResponse($this->getNotFoundMessage(), 404);
|
||||
}
|
||||
|
||||
$validated = $this->validateRequest($request);
|
||||
$documents = collect($request->file('documents', []))
|
||||
->filter()
|
||||
->all();
|
||||
$galleryImages = collect($request->file('gallery', []))
|
||||
->filter()
|
||||
->all();
|
||||
|
||||
$activity = $this->activityService->update(
|
||||
$activity,
|
||||
$this->prepareUpdateData(collect($validated)->except(['documents', 'gallery'])->all(), $activity),
|
||||
$documents,
|
||||
$galleryImages,
|
||||
);
|
||||
|
||||
ActivityLogger::log("Updated {$this->resourceName}: {$this->getItemName($activity)}", $activity);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => new $this->resourceClass($activity),
|
||||
'message' => $this->getSuccessMessage('update'),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error updating {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse($this->getErrorMessage('update').': '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadShowRelations($item)
|
||||
{
|
||||
return $this->repository->findByIdWithRelations($item->id) ?? $item;
|
||||
}
|
||||
|
||||
protected function getItemName($item): string
|
||||
{
|
||||
return $item->title ?? $item->id;
|
||||
}
|
||||
|
||||
protected function checkDependencies($activity): ?JsonResponse
|
||||
{
|
||||
if ($activity->reports()->count() > 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Aktiviti tidak boleh dipadam kerana masih mempunyai laporan.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function uploadDocument(
|
||||
UploadActivityDocumentRequest $request,
|
||||
Activity $activity
|
||||
): JsonResponse {
|
||||
$this->authorize('update', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity = $this->activityService->uploadDocument(
|
||||
$activity,
|
||||
$request->file('file'),
|
||||
$request->validated('type', 'general'),
|
||||
$request->validated('description'),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Dokumen berjaya dimuat naik.',
|
||||
'data' => new ActivityResource($activity),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error uploading document for {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse('Gagal memuat naik dokumen: '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadGalleryImage(UploadActivityGalleryImageRequest $request, Activity $activity): JsonResponse {
|
||||
$this->authorize('update', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity = $this->activityService->uploadGalleryImage(
|
||||
$activity,
|
||||
$request->file('file'),
|
||||
$request->validated('description'),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Imej galeri berjaya dimuat naik.',
|
||||
'data' => new ActivityResource($activity),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error uploading gallery image for {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse('Gagal memuat naik imej galeri: '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadDocument(Activity $activity, string $documentId): StreamedResponse
|
||||
{
|
||||
$this->authorize('view', $this->modelClass);
|
||||
|
||||
$document = $activity->documents()
|
||||
->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE)
|
||||
->findOrFail($documentId);
|
||||
|
||||
return $this->documentService->downloadDocument($document->id);
|
||||
}
|
||||
|
||||
public function serveGalleryImage(Activity $activity, string $imageId): BinaryFileResponse
|
||||
{
|
||||
$this->authorize('view', $this->modelClass);
|
||||
|
||||
$document = $activity->galleryImages()->findOrFail($imageId);
|
||||
|
||||
$disk = Storage::disk(Document::STORAGE_DISK);
|
||||
|
||||
if (! $disk->exists($document->path)) {
|
||||
abort(404, 'File not found');
|
||||
}
|
||||
|
||||
return response()->file($disk->path($document->path), [
|
||||
'Content-Type' => $document->mime_type ?? 'application/octet-stream',
|
||||
'Content-Disposition' => 'inline; filename="'.$document->name.'"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteDocument(Activity $activity, string $documentId): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity->documents()
|
||||
->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE)
|
||||
->findOrFail($documentId);
|
||||
|
||||
$activity->deleteDocument($documentId);
|
||||
$activity = $this->repository->findByIdWithRelations($activity->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Dokumen berjaya dipadam.',
|
||||
'data' => new ActivityResource($activity),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error deleting document for {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse('Gagal memadam dokumen: '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteGalleryImage(Activity $activity, string $imageId): JsonResponse
|
||||
{
|
||||
$this->authorize('update', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity->galleryImages()->findOrFail($imageId);
|
||||
$activity->deleteDocument($imageId);
|
||||
$activity = $this->repository->findByIdWithRelations($activity->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Imej galeri berjaya dipadam.',
|
||||
'data' => new ActivityResource($activity),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error deleting gallery image for {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse('Gagal memadam imej galeri: '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
$this->authorize('deleteAny', $this->modelClass);
|
||||
|
||||
try {
|
||||
$activity = $this->repository->findByIdWithRelations($id);
|
||||
|
||||
if (! $activity) {
|
||||
return $this->errorResponse($this->getNotFoundMessage(), 404);
|
||||
}
|
||||
|
||||
$dependencyCheck = $this->checkDependencies($activity);
|
||||
if ($dependencyCheck !== null) {
|
||||
return $dependencyCheck;
|
||||
}
|
||||
|
||||
$activity->documents->each(
|
||||
fn ($document) => $this->documentService->deleteDocument($document->id)
|
||||
);
|
||||
|
||||
$this->repository->delete($id);
|
||||
|
||||
ActivityLogger::log("Deleted {$this->resourceName}: {$this->getItemName($activity)}", $activity);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $this->getSuccessMessage('destroy'),
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
Log::error("Error deleting {$this->resourceName}: ".$e->getMessage());
|
||||
|
||||
return $this->errorResponse($this->getErrorMessage('destroy').': '.$e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\BaseCrudController;
|
||||
use Modules\Activity\Entities\ActivityReport;
|
||||
use Modules\Activity\Http\Requests\ActivityReportRequest;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityReportRepositoryInterface;
|
||||
use Modules\Activity\Transformers\ActivityReportResource;
|
||||
|
||||
class ActivityReportController extends BaseCrudController
|
||||
{
|
||||
protected $modelClass = ActivityReport::class;
|
||||
|
||||
protected $resourceClass = ActivityReportResource::class;
|
||||
|
||||
protected $requestClass = ActivityReportRequest::class;
|
||||
|
||||
protected $resourceName = 'activity report';
|
||||
|
||||
protected $resourceNamePlural = 'activity reports';
|
||||
|
||||
public function __construct(
|
||||
ActivityReportRepositoryInterface $repository,
|
||||
) {
|
||||
parent::__construct($repository);
|
||||
}
|
||||
|
||||
protected function prepareStoreData(array $validated): array
|
||||
{
|
||||
return array_merge($validated, [
|
||||
'prepared_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getItemName($item): string
|
||||
{
|
||||
return $item->activity?->title ?? $item->id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\BaseCrudController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ActivityTypeController extends BaseCrudController
|
||||
{
|
||||
protected $modelClass = \Modules\Activity\Entities\ActivityType::class;
|
||||
|
||||
protected $resourceClass = \Modules\Activity\Transformers\ActivityTypeResource::class;
|
||||
|
||||
protected $requestClass = \Modules\Activity\Http\Requests\ActivityTypeRequest::class;
|
||||
|
||||
protected $resourceName = 'activity type';
|
||||
|
||||
protected $resourceNamePlural = 'activity types';
|
||||
|
||||
public function __construct(
|
||||
\Modules\Activity\Repositories\Contracts\ActivityTypeRepositoryInterface $repository,
|
||||
) {
|
||||
parent::__construct($repository);
|
||||
}
|
||||
|
||||
protected function getItemName($item): string
|
||||
{
|
||||
return $item->name ?? $item->id;
|
||||
}
|
||||
|
||||
protected function checkDependencies($activityType): ?JsonResponse
|
||||
{
|
||||
if ($activityType->activities()->count() > 0) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Jenis aktiviti tidak boleh dipadam kerana masih digunakan oleh aktiviti.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ActivityReportRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$isCreate = $this->isMethod('POST');
|
||||
$isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH');
|
||||
|
||||
if (! $isCreate && ! $isUpdate) {
|
||||
return [
|
||||
'activity_id' => 'nullable|uuid|exists:activities,id',
|
||||
'report_text' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'activity_id' => 'required|uuid|exists:activities,id',
|
||||
'report_text' => 'required|string',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'activity_id.required' => 'Aktiviti diperlukan.',
|
||||
'activity_id.exists' => 'Aktiviti tidak sah.',
|
||||
'report_text.required' => 'Laporan diperlukan.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ActivityRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$isCreate = $this->isMethod('POST');
|
||||
$isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH');
|
||||
|
||||
if (! $isCreate && ! $isUpdate) {
|
||||
return [
|
||||
'activity_type_id' => 'nullable|uuid|exists:activity_types,id',
|
||||
'title' => 'nullable|string|max:255',
|
||||
'reference_number' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'start_datetime' => 'nullable|date',
|
||||
'end_datetime' => 'nullable|date|after_or_equal:start_datetime',
|
||||
'organizer' => 'nullable|string|max:255',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
$referenceRule = Rule::unique('activities', 'reference_number');
|
||||
|
||||
if ($isUpdate) {
|
||||
$referenceRule = $referenceRule->ignore($this->route('activity'));
|
||||
}
|
||||
|
||||
return [
|
||||
'activity_type_id' => 'required|uuid|exists:activity_types,id',
|
||||
'title' => 'required|string|max:255',
|
||||
'reference_number' => ['nullable', 'string', 'max:255', $referenceRule],
|
||||
'description' => 'nullable|string',
|
||||
'start_datetime' => 'nullable|date',
|
||||
'end_datetime' => 'nullable|date|after_or_equal:start_datetime',
|
||||
'organizer' => 'nullable|string|max:255',
|
||||
'location' => 'nullable|string|max:255',
|
||||
'is_active' => 'required|boolean',
|
||||
'documents' => 'nullable|array',
|
||||
'documents.*' => 'file|mimes:pdf,jpg,jpeg,png,doc,docx|max:10240',
|
||||
'gallery' => 'nullable|array',
|
||||
'gallery.*' => 'file|mimes:jpg,jpeg,png,webp,gif|max:10240',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'activity_type_id.required' => 'Jenis aktiviti diperlukan.',
|
||||
'activity_type_id.exists' => 'Jenis aktiviti tidak sah.',
|
||||
'title.required' => 'Tajuk aktiviti diperlukan.',
|
||||
'title.max' => 'Tajuk aktiviti tidak boleh melebihi 255 aksara.',
|
||||
'reference_number.unique' => 'Nombor rujukan sudah wujud.',
|
||||
'end_datetime.after_or_equal' => 'Tarikh tamat mestilah selepas atau sama dengan tarikh mula.',
|
||||
'is_active.required' => 'Status aktif diperlukan.',
|
||||
'is_active.boolean' => 'Status aktif tidak sah.',
|
||||
'documents.*.file' => 'Dokumen mestilah fail yang sah.',
|
||||
'documents.*.mimes' => 'Format dokumen tidak disokong.',
|
||||
'documents.*.max' => 'Saiz dokumen melebihi had maksimum 10MB.',
|
||||
'gallery.*.file' => 'Imej galeri mestilah fail yang sah.',
|
||||
'gallery.*.mimes' => 'Format imej galeri tidak disokong. Gunakan jpg, jpeg, png, webp atau gif.',
|
||||
'gallery.*.max' => 'Saiz imej galeri melebihi had maksimum 10MB.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class ActivityTypeRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$isCreate = $this->isMethod('POST');
|
||||
$isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH');
|
||||
|
||||
if (! $isCreate && ! $isUpdate) {
|
||||
return [
|
||||
'name' => 'nullable|string|max:255',
|
||||
'code' => 'nullable|string|max:255',
|
||||
];
|
||||
}
|
||||
|
||||
$codeRule = Rule::unique('activity_types', 'code');
|
||||
|
||||
if ($isUpdate) {
|
||||
$codeRule = $codeRule->ignore($this->route('activity_type'));
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'code' => ['nullable', 'string', 'max:255', $codeRule],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'Nama jenis aktiviti diperlukan.',
|
||||
'name.max' => 'Nama jenis aktiviti tidak boleh melebihi 255 aksara.',
|
||||
'code.max' => 'Kod jenis aktiviti tidak boleh melebihi 255 aksara.',
|
||||
'code.unique' => 'Kod jenis aktiviti sudah wujud.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
|
||||
class UploadActivityDocumentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::notIn([Activity::GALLERY_DOCUMENT_TYPE]),
|
||||
],
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'file' => 'required|file|mimes:pdf,jpg,jpeg,png,doc,docx|max:10240',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'type.not_in' => 'Imej galeri mesti dimuat naik melalui endpoint galeri aktiviti.',
|
||||
'file.required' => 'Fail diperlukan.',
|
||||
'file.mimes' => 'Format fail tidak disokong.',
|
||||
'file.max' => 'Saiz fail melebihi had maksimum 10MB.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UploadActivityGalleryImageRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'file' => 'required|file|mimes:jpg,jpeg,png,webp,gif|max:10240',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'file.required' => 'Imej diperlukan.',
|
||||
'file.mimes' => 'Format imej tidak disokong. Gunakan jpg, jpeg, png, webp atau gif.',
|
||||
'file.max' => 'Saiz imej melebihi had maksimum 10MB.',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
|
||||
class ActivityPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('lihat aktiviti');
|
||||
}
|
||||
|
||||
public function view($user, ?Activity $activity = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('lihat aktiviti');
|
||||
}
|
||||
|
||||
public function create($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('tambah aktiviti');
|
||||
}
|
||||
|
||||
public function update($user, ?Activity $activity = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('kemaskini aktiviti');
|
||||
}
|
||||
|
||||
public function deleteAny($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus aktiviti');
|
||||
}
|
||||
|
||||
public function delete($user, ?Activity $activity = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus aktiviti');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Modules\Activity\Entities\ActivityReport;
|
||||
|
||||
class ActivityReportPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny($user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view($user, ?ActivityReport $activityReport = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('tambah laporan aktiviti');
|
||||
}
|
||||
|
||||
public function update($user, ?ActivityReport $activityReport = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('kemaskini laporan aktiviti');
|
||||
}
|
||||
|
||||
public function deleteAny($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus laporan aktiviti');
|
||||
}
|
||||
|
||||
public function delete($user, ?ActivityReport $activityReport = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus laporan aktiviti');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Modules\Activity\Entities\ActivityType;
|
||||
|
||||
class ActivityTypePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny($user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view($user, ?ActivityType $activityType = null): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('tambah jenis aktiviti');
|
||||
}
|
||||
|
||||
public function update($user, ?ActivityType $activityType = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('kemaskini jenis aktiviti');
|
||||
}
|
||||
|
||||
public function deleteAny($user): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus jenis aktiviti');
|
||||
}
|
||||
|
||||
public function delete($user, ?ActivityType $activityType = null): bool
|
||||
{
|
||||
return $user->hasPermissionTo('hapus jenis aktiviti');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
use Modules\Activity\Entities\ActivityReport;
|
||||
use Modules\Activity\Entities\ActivityType;
|
||||
use Modules\Activity\Policies\ActivityPolicy;
|
||||
use Modules\Activity\Policies\ActivityReportPolicy;
|
||||
use Modules\Activity\Policies\ActivityTypePolicy;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class ActivityServiceProvider extends ServiceProvider
|
||||
{
|
||||
use PathNamespace;
|
||||
|
||||
protected string $name = 'Activity';
|
||||
|
||||
protected string $nameLower = 'activity';
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerCommands();
|
||||
$this->registerCommandSchedules();
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations'));
|
||||
$this->registerPolicies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
|
||||
$this->app->bind(
|
||||
\Modules\Activity\Repositories\Contracts\ActivityTypeRepositoryInterface::class,
|
||||
\Modules\Activity\Repositories\ActivityTypeRepository::class
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
\Modules\Activity\Repositories\Contracts\ActivityRepositoryInterface::class,
|
||||
\Modules\Activity\Repositories\ActivityRepository::class
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
\Modules\Activity\Repositories\Contracts\ActivityReportRepositoryInterface::class,
|
||||
\Modules\Activity\Repositories\ActivityReportRepository::class
|
||||
);
|
||||
}
|
||||
|
||||
protected function registerPolicies(): void
|
||||
{
|
||||
Gate::policy(ActivityType::class, ActivityTypePolicy::class);
|
||||
Gate::policy(Activity::class, ActivityPolicy::class);
|
||||
Gate::policy(ActivityReport::class, ActivityReportPolicy::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register commands in the format of Command::class
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
// $this->commands([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register command Schedules.
|
||||
*/
|
||||
protected function registerCommandSchedules(): void
|
||||
{
|
||||
// $this->app->booted(function () {
|
||||
// $schedule = $this->app->make(Schedule::class);
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom($langPath);
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom(module_path($this->name, 'Lang'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||
|
||||
if (is_dir($configPath)) {
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||
|
||||
// Remove duplicated adjacent segments
|
||||
$normalized = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (end($normalized) !== $segment) {
|
||||
$normalized[] = $segment;
|
||||
}
|
||||
}
|
||||
|
||||
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||
|
||||
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||
$this->merge_config_from($file->getPathname(), $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge config from the given path recursively.
|
||||
*/
|
||||
protected function merge_config_from(string $path, string $key): void
|
||||
{
|
||||
$existing = config($key, []);
|
||||
$module_config = require $path;
|
||||
|
||||
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||
$sourcePath = module_path($this->name, 'Resources/Views');
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||
|
||||
Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (config('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
protected $listen = [];
|
||||
|
||||
/**
|
||||
* Indicates if events should be discovered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $shouldDiscoverEvents = true;
|
||||
|
||||
/**
|
||||
* Configure the proper event listeners for email verification.
|
||||
*/
|
||||
protected function configureEmailVerification(): void {}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $name = 'Activity';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path($this->name, '/Routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\ActivityReport;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityReportRepositoryInterface;
|
||||
|
||||
class ActivityReportRepository implements ActivityReportRepositoryInterface
|
||||
{
|
||||
protected array $defaultRelations = ['activity', 'preparedBy:id,name,email'];
|
||||
|
||||
protected function baseQuery()
|
||||
{
|
||||
return ActivityReport::query()
|
||||
->with($this->defaultRelations)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
protected function applySearch($query, string $search)
|
||||
{
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('report_text', 'ILIKE', "%{$search}%")
|
||||
->orWhereHas('activity', function ($activityQuery) use ($search) {
|
||||
$activityQuery->where('title', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '')
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'created_at',
|
||||
string $sortOrder = 'desc'
|
||||
) {
|
||||
$allowedSortColumns = ['id', 'report_text', 'created_at'];
|
||||
$sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'created_at';
|
||||
$sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$query = ActivityReport::query()
|
||||
->with($this->defaultRelations)
|
||||
->orderBy($sortBy, $sortOrder);
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function create(array $data): ActivityReport
|
||||
{
|
||||
return ActivityReport::create($data);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?ActivityReport
|
||||
{
|
||||
return ActivityReport::query()
|
||||
->with($this->defaultRelations)
|
||||
->find($id);
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
$report = ActivityReport::query()->find($id);
|
||||
|
||||
if ($report) {
|
||||
return $report->delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function all(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityRepositoryInterface;
|
||||
|
||||
class ActivityRepository implements ActivityRepositoryInterface
|
||||
{
|
||||
protected array $defaultRelations = ['activityType', 'documents'];
|
||||
|
||||
protected array $detailRelations = [
|
||||
'activityType',
|
||||
'documents',
|
||||
'reports.preparedBy:id,name,email',
|
||||
];
|
||||
|
||||
protected function baseQuery()
|
||||
{
|
||||
return Activity::query()
|
||||
->with($this->defaultRelations)
|
||||
->orderBy('start_datetime', 'desc');
|
||||
}
|
||||
|
||||
protected function applySearch($query, string $search)
|
||||
{
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('title', 'ILIKE', "%{$search}%")
|
||||
->orWhere('reference_number', 'ILIKE', "%{$search}%")
|
||||
->orWhere('organizer', 'ILIKE', "%{$search}%")
|
||||
->orWhere('location', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '')
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'title',
|
||||
string $sortOrder = 'asc'
|
||||
) {
|
||||
$allowedSortColumns = [
|
||||
'id',
|
||||
'title',
|
||||
'reference_number',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'organizer',
|
||||
'location',
|
||||
'is_active',
|
||||
'created_at',
|
||||
];
|
||||
$sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'start_datetime';
|
||||
$sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$query = Activity::query()
|
||||
->with($this->defaultRelations)
|
||||
->orderBy($sortBy, $sortOrder);
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getIndexPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'start_datetime',
|
||||
string $sortOrder = 'desc'
|
||||
) {
|
||||
$allowedSortColumns = [
|
||||
'id',
|
||||
'title',
|
||||
'reference_number',
|
||||
'start_datetime',
|
||||
'end_datetime',
|
||||
'organizer',
|
||||
'location',
|
||||
'is_active',
|
||||
'created_at',
|
||||
];
|
||||
$sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'start_datetime';
|
||||
$sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$query = Activity::query()
|
||||
->with([
|
||||
'activityType',
|
||||
'galleryImages' => fn ($q) => $q->orderBy('created_at')->limit(1),
|
||||
])
|
||||
->withCount(['galleryImages', 'attachments'])
|
||||
->orderBy($sortBy, $sortOrder);
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function create(array $data): Activity
|
||||
{
|
||||
return Activity::create($data);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?Activity
|
||||
{
|
||||
return Activity::query()->find($id);
|
||||
}
|
||||
|
||||
public function findByIdWithRelations(string $id): ?Activity
|
||||
{
|
||||
return Activity::query()
|
||||
->with($this->detailRelations)
|
||||
->find($id);
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
$activity = $this->findById($id);
|
||||
|
||||
if ($activity) {
|
||||
return $activity->delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function all(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\ActivityType;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityTypeRepositoryInterface;
|
||||
|
||||
class ActivityTypeRepository implements ActivityTypeRepositoryInterface
|
||||
{
|
||||
protected function baseQuery()
|
||||
{
|
||||
return ActivityType::query()->orderBy('name');
|
||||
}
|
||||
|
||||
protected function applySearch($query, string $search)
|
||||
{
|
||||
if ($search !== '') {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'ILIKE', "%{$search}%")
|
||||
->orWhere('code', 'ILIKE', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '')
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'name',
|
||||
string $sortOrder = 'asc'
|
||||
) {
|
||||
$allowedSortColumns = ['id', 'name', 'code', 'created_at'];
|
||||
$sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'name';
|
||||
$sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
$query = ActivityType::query()->orderBy($sortBy, $sortOrder);
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function create(array $data): ActivityType
|
||||
{
|
||||
return ActivityType::create($data);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?ActivityType
|
||||
{
|
||||
return ActivityType::query()->find($id);
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
$activityType = $this->findById($id);
|
||||
|
||||
if ($activityType) {
|
||||
return $activityType->delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function all(string $search = ''): Collection
|
||||
{
|
||||
$query = $this->baseQuery();
|
||||
$this->applySearch($query, $search);
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\ActivityReport;
|
||||
|
||||
interface ActivityReportRepositoryInterface
|
||||
{
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '');
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'created_at',
|
||||
string $sortOrder = 'desc'
|
||||
);
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection;
|
||||
|
||||
public function create(array $data): ActivityReport;
|
||||
|
||||
public function findById(string $id): ?ActivityReport;
|
||||
|
||||
public function delete(string $id): bool;
|
||||
|
||||
public function all(string $search = ''): Collection;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
|
||||
interface ActivityRepositoryInterface
|
||||
{
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '');
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'title',
|
||||
string $sortOrder = 'asc'
|
||||
);
|
||||
|
||||
public function getIndexPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'start_datetime',
|
||||
string $sortOrder = 'desc'
|
||||
);
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection;
|
||||
|
||||
public function create(array $data): Activity;
|
||||
|
||||
public function findById(string $id): ?Activity;
|
||||
|
||||
public function findByIdWithRelations(string $id): ?Activity;
|
||||
|
||||
public function delete(string $id): bool;
|
||||
|
||||
public function all(string $search = ''): Collection;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Repositories\Contracts;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Modules\Activity\Entities\ActivityType;
|
||||
|
||||
interface ActivityTypeRepositoryInterface
|
||||
{
|
||||
public function getAllPaginated(int $perPage = 10, string $search = '');
|
||||
|
||||
public function getAllWithRelationsPaginated(
|
||||
int $perPage = 10,
|
||||
string $search = '',
|
||||
string $sortBy = 'name',
|
||||
string $sortOrder = 'asc'
|
||||
);
|
||||
|
||||
public function getAllWithRelations(string $search = ''): Collection;
|
||||
|
||||
public function create(array $data): ActivityType;
|
||||
|
||||
public function findById(string $id): ?ActivityType;
|
||||
|
||||
public function delete(string $id): bool;
|
||||
|
||||
public function all(string $search = ''): Collection;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Activity\Http\Controllers\ActivityController;
|
||||
use Modules\Activity\Http\Controllers\ActivityReportController;
|
||||
use Modules\Activity\Http\Controllers\ActivityTypeController;
|
||||
|
||||
Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () {
|
||||
Route::apiResource('activity-types', ActivityTypeController::class)->names('activity-type');
|
||||
Route::apiResource('activities', ActivityController::class)->names('activity');
|
||||
|
||||
// gallery images
|
||||
Route::post('activities/{activity}/gallery', [ActivityController::class, 'uploadGalleryImage'])->name('activity.upload-gallery-image');
|
||||
Route::get('activities/{activity}/gallery/{imageId}', [ActivityController::class, 'serveGalleryImage'])->name('activity.serve-gallery-image');
|
||||
Route::delete('activities/{activity}/gallery/{imageId}', [ActivityController::class, 'deleteGalleryImage'])->name('activity.delete-gallery-image');
|
||||
|
||||
// documents
|
||||
Route::post('activities/{activity}/documents', [ActivityController::class, 'uploadDocument'])->name('activity.upload-document');
|
||||
Route::get('activities/{activity}/documents/{documentId}/download', [ActivityController::class, 'downloadDocument'])->name('activity.download-document');
|
||||
Route::delete('activities/{activity}/documents/{documentId}', [ActivityController::class, 'deleteDocument'])->name('activity.delete-document');
|
||||
|
||||
// reports
|
||||
Route::apiResource('activity-reports', ActivityReportController::class)->names('activity-report');
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Activity\Http\Controllers\ActivityController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::resource('activities', ActivityController::class)->names('activity');
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Services;
|
||||
|
||||
use App\Services\DocumentService;
|
||||
use Exception;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
use Modules\Activity\Repositories\Contracts\ActivityRepositoryInterface;
|
||||
|
||||
class ActivityService
|
||||
{
|
||||
public function __construct(
|
||||
protected ActivityRepositoryInterface $repository,
|
||||
protected DocumentService $documentService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<int, UploadedFile>|array<string, UploadedFile> $documents
|
||||
* @param array<int, UploadedFile> $galleryImages
|
||||
*/
|
||||
public function create(array $data, array $documents = [], array $galleryImages = []): Activity
|
||||
{
|
||||
return DB::transaction(function () use ($data, $documents, $galleryImages) {
|
||||
$activity = $this->repository->create($data);
|
||||
|
||||
$this->uploadDocuments($activity, $documents);
|
||||
$this->uploadGalleryImages($activity, $galleryImages);
|
||||
|
||||
return $this->repository->findByIdWithRelations($activity->id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<int, UploadedFile>|array<string, UploadedFile> $documents
|
||||
* @param array<int, UploadedFile> $galleryImages
|
||||
*/
|
||||
public function update(Activity $activity, array $data, array $documents = [], array $galleryImages = []): Activity
|
||||
{
|
||||
return DB::transaction(function () use ($activity, $data, $documents, $galleryImages) {
|
||||
$activity->update($data);
|
||||
|
||||
$this->uploadDocuments($activity, $documents);
|
||||
$this->uploadGalleryImages($activity, $galleryImages);
|
||||
|
||||
return $this->repository->findByIdWithRelations($activity->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function uploadDocument(
|
||||
Activity $activity,
|
||||
UploadedFile $file,
|
||||
string $type = 'general',
|
||||
?string $description = null
|
||||
): Activity {
|
||||
if ($type === Activity::GALLERY_DOCUMENT_TYPE) {
|
||||
throw new Exception('Imej galeri mesti dimuat naik melalui endpoint galeri aktiviti.');
|
||||
}
|
||||
|
||||
$this->documentService->validateFile($file);
|
||||
$this->documentService->uploadDocument($activity, $file, $type, $description);
|
||||
|
||||
return $this->repository->findByIdWithRelations($activity->id);
|
||||
}
|
||||
|
||||
public function uploadGalleryImage(
|
||||
Activity $activity,
|
||||
UploadedFile $file,
|
||||
?string $description = null
|
||||
): Activity {
|
||||
$this->validateGalleryImage($file);
|
||||
$this->documentService->uploadDocument(
|
||||
$activity,
|
||||
$file,
|
||||
Activity::GALLERY_DOCUMENT_TYPE,
|
||||
$description
|
||||
);
|
||||
|
||||
return $this->repository->findByIdWithRelations($activity->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, UploadedFile>|array<string, UploadedFile> $documents
|
||||
*/
|
||||
protected function uploadDocuments(Activity $activity, array $documents): void
|
||||
{
|
||||
foreach ($documents as $key => $file) {
|
||||
if (! $file instanceof UploadedFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$documentType = is_string($key) ? $key : 'general';
|
||||
|
||||
if ($documentType === Activity::GALLERY_DOCUMENT_TYPE) {
|
||||
$this->validateGalleryImage($file);
|
||||
$this->documentService->uploadDocument($activity, $file, Activity::GALLERY_DOCUMENT_TYPE);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->documentService->validateFile($file);
|
||||
$this->documentService->uploadDocument($activity, $file, $documentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, UploadedFile> $galleryImages
|
||||
*/
|
||||
protected function uploadGalleryImages(Activity $activity, array $galleryImages): void
|
||||
{
|
||||
foreach ($galleryImages as $file) {
|
||||
if (! $file instanceof UploadedFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->validateGalleryImage($file);
|
||||
$this->documentService->uploadDocument(
|
||||
$activity,
|
||||
$file,
|
||||
Activity::GALLERY_DOCUMENT_TYPE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateGalleryImage(UploadedFile $file): void
|
||||
{
|
||||
$validator = Validator::make(
|
||||
['file' => $file],
|
||||
['file' => 'required|file|mimes:jpg,jpeg,png,webp,gif|max:10240']
|
||||
);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new Exception($validator->errors()->first('file'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityDocumentResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$activityId = $this->documentable_id;
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'activity_id' => $activityId,
|
||||
'name' => $this->name,
|
||||
'type' => $this->type,
|
||||
'description' => $this->description,
|
||||
'mime_type' => $this->mime_type,
|
||||
'file_size' => $this->file_size,
|
||||
'uploaded_by' => $this->uploaded_by,
|
||||
'url' => $activityId
|
||||
? url(route('api.activity.download-document', [
|
||||
'activity' => $activityId,
|
||||
'documentId' => $this->id,
|
||||
]))
|
||||
: null,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityGalleryImageResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$activityId = $this->documentable_id;
|
||||
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'activity_id' => $activityId,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'mime_type' => $this->mime_type,
|
||||
'file_size' => $this->file_size,
|
||||
'uploaded_by' => $this->uploaded_by,
|
||||
'url' => $activityId
|
||||
? url(route('api.activity.serve-gallery-image', [
|
||||
'activity' => $activityId,
|
||||
'imageId' => $this->id,
|
||||
]))
|
||||
: null,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityListResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'activity_type_id' => $this->activity_type_id,
|
||||
'activity_type' => new ActivityTypeResource($this->whenLoaded('activityType')),
|
||||
'title' => $this->title,
|
||||
'reference_number' => $this->reference_number,
|
||||
'description' => $this->description,
|
||||
'start_datetime' => $this->start_datetime?->toISOString(),
|
||||
'end_datetime' => $this->end_datetime?->toISOString(),
|
||||
'organizer' => $this->organizer,
|
||||
'location' => $this->location,
|
||||
'is_active' => $this->is_active,
|
||||
'gallery_image_count' => $this->gallery_images_count ?? 0,
|
||||
'documents_count' => $this->attachments_count ?? 0,
|
||||
'cover_image' => $this->when(
|
||||
$this->relationLoaded('galleryImages') && $this->galleryImages->isNotEmpty(),
|
||||
fn () => new ActivityGalleryImageResource($this->galleryImages->first())
|
||||
),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityReportResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'activity_id' => $this->activity_id,
|
||||
'activity' => new ActivityResource($this->whenLoaded('activity')),
|
||||
'report_text' => $this->report_text,
|
||||
'prepared_by' => $this->prepared_by,
|
||||
'prepared_by_user' => $this->whenLoaded('preparedBy', fn () => [
|
||||
'id' => $this->preparedBy->id,
|
||||
'name' => $this->preparedBy->name,
|
||||
'email' => $this->preparedBy->email,
|
||||
]),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Modules\Activity\Entities\Activity;
|
||||
|
||||
class ActivityResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'activity_type_id' => $this->activity_type_id,
|
||||
'activity_type' => new ActivityTypeResource($this->whenLoaded('activityType')),
|
||||
'title' => $this->title,
|
||||
'reference_number' => $this->reference_number,
|
||||
'description' => $this->description,
|
||||
'start_datetime' => $this->start_datetime?->toISOString(),
|
||||
'end_datetime' => $this->end_datetime?->toISOString(),
|
||||
'organizer' => $this->organizer,
|
||||
'location' => $this->location,
|
||||
'is_active' => $this->is_active,
|
||||
'documents' => $this->when(
|
||||
$this->relationLoaded('documents'),
|
||||
fn () => ActivityDocumentResource::collection($this->attachmentDocuments())
|
||||
),
|
||||
'gallery_images' => $this->when(
|
||||
$this->relationLoaded('documents'),
|
||||
fn () => ActivityGalleryImageResource::collection($this->galleryDocuments())
|
||||
),
|
||||
'reports' => ActivityReportResource::collection($this->whenLoaded('reports')),
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function galleryDocuments()
|
||||
{
|
||||
return $this->documents
|
||||
->where('type', Activity::GALLERY_DOCUMENT_TYPE)
|
||||
->values();
|
||||
}
|
||||
|
||||
protected function attachmentDocuments()
|
||||
{
|
||||
return $this->documents
|
||||
->where('type', '!=', Activity::GALLERY_DOCUMENT_TYPE)
|
||||
->values();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Activity\Transformers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ActivityTypeResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'code' => $this->code,
|
||||
'created_at' => $this->created_at?->toISOString(),
|
||||
'updated_at' => $this->updated_at?->toISOString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "nwidart/activity",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Widart",
|
||||
"email": "n.widart@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Activity\\": "App",
|
||||
"Modules\\Activity\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\Activity\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Activity\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Activity",
|
||||
"alias": "activity",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Activity\\Providers\\ActivityServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
"sass": "^1.69.5",
|
||||
"postcss": "^8.3.7",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace Modules\Auth\Emails;
|
||||
|
||||
use App\Notifications\Concerns\BuildsMailMessage;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class EmailVerificationOtpEmail extends Notification
|
||||
{
|
||||
use BuildsMailMessage;
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
@@ -23,13 +25,14 @@ class EmailVerificationOtpEmail extends Notification
|
||||
{
|
||||
$minutes = (int) config('auth.email_verification.expiry_minutes', 10);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Pengesahan E-mel - Kod OTP')
|
||||
->markdown('auth::emails.verification-otp', [
|
||||
return $this->mailMessage(
|
||||
subject: 'Pengesahan E-mel - Kod OTP',
|
||||
view: 'auth::emails.verification-otp',
|
||||
data: [
|
||||
'name' => $notifiable->name,
|
||||
'otp' => $this->otp,
|
||||
'minutes' => $minutes,
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Emails;
|
||||
|
||||
use App\Notifications\Concerns\BuildsMailMessage;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class PasswordResetOtpEmail extends Notification
|
||||
{
|
||||
use BuildsMailMessage;
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected string $otp
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$minutes = (int) config('auth.password_reset.expiry_minutes', 10);
|
||||
|
||||
return $this->mailMessage(
|
||||
subject: 'Reset Kata Laluan - Kod OTP',
|
||||
view: 'auth::emails.password-reset-otp',
|
||||
data: [
|
||||
'name' => $notifiable->name,
|
||||
'otp' => $this->otp,
|
||||
'minutes' => $minutes,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PasswordResetOtp extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'password_reset_otps';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'code',
|
||||
'expires_at',
|
||||
'attempts',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'expires_at' => 'datetime',
|
||||
'attempts' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function hasExceededMaxAttempts(): bool
|
||||
{
|
||||
return $this->attempts >= (int) config('auth.password_reset.max_attempts', 5);
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,17 @@ use Spatie\Permission\PermissionRegistrar;
|
||||
use Spatie\Permission\Traits\HasPermissions;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use App\Services\ActiveRoleService;
|
||||
use App\Traits\HasVisibility;
|
||||
use Lab404\Impersonate\Models\Impersonate;
|
||||
use Modules\Role\Entities\Role;
|
||||
use Modules\User\Entities\Address;
|
||||
use Modules\User\Entities\BankDetail;
|
||||
use Modules\User\Entities\Employment;
|
||||
use Modules\User\Entities\Heir;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Modules\Auth\Database\Factories\UserFactory> */
|
||||
use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes, HasVisibility;
|
||||
use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes;
|
||||
|
||||
protected $table = 'users';
|
||||
|
||||
@@ -44,6 +47,13 @@ class User extends Authenticatable
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
'gender',
|
||||
'marriage_status',
|
||||
'member_number',
|
||||
'member_type',
|
||||
'join_date',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -70,6 +80,13 @@ class User extends Authenticatable
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'status' => 'string',
|
||||
'gender' => 'string',
|
||||
'marriage_status' => 'string',
|
||||
'member_number' => 'integer',
|
||||
'member_type' => 'string',
|
||||
'join_date' => 'date',
|
||||
'birth_date' => 'date',
|
||||
'birth_place' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -82,6 +99,26 @@ class User extends Authenticatable
|
||||
return ActiveRoleService::getActiveRole($this);
|
||||
}
|
||||
|
||||
public function addresses()
|
||||
{
|
||||
return $this->hasMany(Address::class);
|
||||
}
|
||||
|
||||
public function employments()
|
||||
{
|
||||
return $this->hasMany(Employment::class);
|
||||
}
|
||||
|
||||
public function bankDetails()
|
||||
{
|
||||
return $this->hasMany(BankDetail::class);
|
||||
}
|
||||
|
||||
public function heirs()
|
||||
{
|
||||
return $this->hasMany(Heir::class);
|
||||
}
|
||||
|
||||
public function hasPermissionAction($action): bool
|
||||
{
|
||||
$role = $this->activeRole();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\Auth\Services\PasswordResetOtpService;
|
||||
|
||||
class PasswordResetController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected PasswordResetOtpService $passwordResetOtpService
|
||||
) {}
|
||||
|
||||
public function requestOtp(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $validated['email'])->first();
|
||||
|
||||
if ($user) {
|
||||
$this->passwordResetOtpService->send($user);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Jika e-mel wujud dalam sistem, kod OTP untuk reset kata laluan telah dihantar.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function reset(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'otp' => ['required', 'string', 'digits:6'],
|
||||
'password' => ['required', 'string', 'confirmed'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $validated['email'])->first();
|
||||
|
||||
if (! $user) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Kod OTP tidak sah.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$this->passwordResetOtpService->reset(
|
||||
$user,
|
||||
$validated['otp'],
|
||||
$request->only(['password', 'password_confirmation']),
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Kata laluan anda telah berjaya ditetapkan semula. Sila log masuk.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Auth\Http\Controllers\EmailVerificationController;
|
||||
use Modules\Auth\Http\Controllers\PasswordResetController;
|
||||
use Modules\Auth\Http\Controllers\SessionController;
|
||||
use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController;
|
||||
use Laravel\Fortify\Http\Controllers\RegisteredUserController;
|
||||
@@ -15,6 +16,11 @@ Route::post('/verify-email', [EmailVerificationController::class, 'verify'])
|
||||
Route::post('/verify-email/resend', [EmailVerificationController::class, 'resend'])
|
||||
->middleware('throttle:email-verification-resend');
|
||||
|
||||
Route::post('/forgot-password', [PasswordResetController::class, 'requestOtp'])
|
||||
->middleware('throttle:password-reset-request');
|
||||
Route::post('/reset-password', [PasswordResetController::class, 'reset'])
|
||||
->middleware('throttle:password-reset');
|
||||
|
||||
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||
// get current user
|
||||
Route::get('/me', [SessionController::class, 'currentUser']);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Auth\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Modules\Auth\Actions\Fortify\PasswordValidationRules;
|
||||
use Modules\Auth\Emails\PasswordResetOtpEmail;
|
||||
use Modules\Auth\Entities\PasswordResetOtp;
|
||||
use Modules\Auth\Entities\User;
|
||||
|
||||
class PasswordResetOtpService
|
||||
{
|
||||
use PasswordValidationRules;
|
||||
|
||||
public function send(User $user): void
|
||||
{
|
||||
$otp = $this->generateOtp();
|
||||
|
||||
PasswordResetOtp::query()
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
|
||||
PasswordResetOtp::create([
|
||||
'user_id' => $user->id,
|
||||
'code' => Hash::make($otp),
|
||||
'expires_at' => now()->addMinutes($this->expiryMinutes()),
|
||||
'attempts' => 0,
|
||||
]);
|
||||
|
||||
$user->notify(new PasswordResetOtpEmail($otp));
|
||||
}
|
||||
|
||||
public function reset(User $user, string $otp, array $input): void
|
||||
{
|
||||
validator($input, [
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
$record = PasswordResetOtp::query()
|
||||
->where('user_id', $user->id)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (! $record) {
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($record->isExpired()) {
|
||||
$record->delete();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($record->hasExceededMaxAttempts()) {
|
||||
$record->delete();
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (! Hash::check($otp, $record->code)) {
|
||||
$record->increment('attempts');
|
||||
|
||||
if ($record->fresh()->hasExceededMaxAttempts()) {
|
||||
$record->delete();
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'otp' => ['Kod OTP tidak sah.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($input['password']),
|
||||
])->save();
|
||||
|
||||
$user->tokens()->delete();
|
||||
|
||||
PasswordResetOtp::query()
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
protected function generateOtp(): string
|
||||
{
|
||||
return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
protected function expiryMinutes(): int
|
||||
{
|
||||
return (int) config('auth.password_reset.expiry_minutes', 10);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ namespace Modules\Auth\Transformers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\User\Transformers\AddressResource;
|
||||
use Modules\User\Transformers\BankDetailResource;
|
||||
use Modules\User\Transformers\EmploymentResource;
|
||||
use Modules\User\Transformers\HeirResource;
|
||||
|
||||
class UserResource extends JsonResource
|
||||
{
|
||||
@@ -28,9 +32,20 @@ class UserResource extends JsonResource
|
||||
'two_factor_recovery_codes' => $this->two_factor_recovery_codes,
|
||||
'two_factor_confirmed_at' => $this->two_factor_confirmed_at,
|
||||
'email_verified_at' => $this->email_verified_at,
|
||||
'gender' => $this->gender,
|
||||
'marriage_status' => $this->marriage_status,
|
||||
'member_number' => $this->member_number,
|
||||
'member_type' => $this->member_type,
|
||||
'join_date' => $this->join_date,
|
||||
'birth_date' => $this->birth_date,
|
||||
'birth_place' => $this->birth_place,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'deleted_at' => $this->deleted_at,
|
||||
'addresses' => AddressResource::collection($this->whenLoaded('addresses')),
|
||||
'employments' => EmploymentResource::collection($this->whenLoaded('employments')),
|
||||
'bank_details' => BankDetailResource::collection($this->whenLoaded('bankDetails')),
|
||||
'heirs' => HeirResource::collection($this->whenLoaded('heirs')),
|
||||
'roles' => $this->whenLoaded('roles', function () {
|
||||
return $this->roles->map(function ($role) {
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
@component('mail::message')
|
||||
@include('emails.partials.header')
|
||||
|
||||
# Reset Kata Laluan
|
||||
|
||||
Assalamualaikum **{{ $name }}**,
|
||||
|
||||
Kami menerima permintaan untuk menetapkan semula kata laluan akaun anda. Gunakan kod OTP di bawah untuk meneruskan.
|
||||
|
||||
@component('mail::panel')
|
||||
<div style="text-align: center; font-size: 28px; font-weight: bold; letter-spacing: 6px;">
|
||||
{{ $otp }}
|
||||
</div>
|
||||
@endcomponent
|
||||
|
||||
Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**.
|
||||
|
||||
Jika anda tidak membuat permintaan ini, abaikan e-mel ini.
|
||||
|
||||
@include('emails.partials.footer')
|
||||
@endcomponent
|
||||
@@ -1,9 +1,5 @@
|
||||
@component('mail::message')
|
||||
@if (! empty($logoPath) && file_exists($logoPath))
|
||||
<div style="text-align: center; margin-bottom: 24px;">
|
||||
<img src="{{ $message->embed($logoPath) }}" alt="{{ config('app.name') }}" style="max-height: 80px; width: auto;">
|
||||
</div>
|
||||
@endif
|
||||
@include('emails.partials.header')
|
||||
|
||||
# Pengesahan E-mel
|
||||
|
||||
@@ -21,6 +17,5 @@ Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**.
|
||||
|
||||
Jika anda tidak membuat pendaftaran ini, abaikan e-mel ini.
|
||||
|
||||
Terima kasih,<br>
|
||||
{{ config('app.name') }}
|
||||
@include('emails.partials.footer')
|
||||
@endcomponent
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'MembershipApplication',
|
||||
];
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_applications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('application_number');
|
||||
$table->foreignUuid('user_id')->constrained('users')->onDelete('cascade')->nullable();
|
||||
$table->string('status');
|
||||
$table->string('board_result');
|
||||
$table->string('submitted_at');
|
||||
$table->string('completed_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_applications');
|
||||
}
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_applicants', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('email');
|
||||
$table->string('ic_number');
|
||||
$table->string('birth_date');
|
||||
$table->string('birth_place');
|
||||
$table->string('marriage_status')->comment('Belum Berkahwin, Berkahwin, Bercerai, Balu, Duda');
|
||||
|
||||
// contact information
|
||||
$table->text('address');
|
||||
$table->string('phone_number');
|
||||
$table->string('office_number')->nullable();
|
||||
$table->string('postcode');
|
||||
|
||||
// Employment
|
||||
$table->string('employer_name');
|
||||
$table->text('employer_address');
|
||||
$table->string('current_position');
|
||||
$table->date('start_work_date');
|
||||
|
||||
// contribution
|
||||
$table->decimal('stock_monthly_contribution', 10, 2);
|
||||
$table->decimal('fee_monthly_contribution', 10, 2);
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::statement('
|
||||
ALTER TABLE membership_application_applicants
|
||||
ADD COLUMN gender user_gender_enum NULL
|
||||
');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_applicants');
|
||||
}
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_heirs', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')->constrained('membership_applications')->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('ic_number');
|
||||
$table->string('relationship')->comment('Isteri, Anak, Orang Tua, Saudara, Lain-lain');
|
||||
$table->string('phone_number');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_heirs');
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_references', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')
|
||||
->constrained('membership_applications')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('reference_type')->comment('PROPOSER, SUPPORTER');
|
||||
|
||||
// Applicant may select at submit; admin may fill in later
|
||||
$table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
|
||||
// Set only when admin assigns or updates this reference
|
||||
$table->foreignUuid('assigned_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('assigned_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['membership_application_id', 'reference_type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_references');
|
||||
}
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('membership_application_reviews', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('membership_application_id')
|
||||
->constrained('membership_applications')
|
||||
->cascadeOnDelete();
|
||||
|
||||
$table->string('stage')->comment('MANAGEMENT, BOARD');
|
||||
$table->string('decision')->comment('MANAGEMENT: APPROVED, REJECTED | BOARD: PASS, FAIL');
|
||||
$table->text('remarks')->nullable();
|
||||
|
||||
$table->foreignUuid('reviewer_id')->constrained('users')->restrictOnDelete();
|
||||
$table->timestamp('reviewed_at');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['membership_application_id', 'stage']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('membership_application_reviews');
|
||||
}
|
||||
};
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class MembershipApplicationDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationFailedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing('applicant');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Keputusan Permohonan Keahlian')
|
||||
->markdown('membershipapplication::emails.failed', [
|
||||
'name' => $this->application->applicant->name,
|
||||
'applicationNumber' => $this->application->application_number,
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationPassedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing('applicant');
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Permohonan Keahlian Diluluskan')
|
||||
->markdown('membershipapplication::emails.passed', [
|
||||
'name' => $this->application->applicant->name,
|
||||
'applicationNumber' => $this->application->application_number,
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Emails;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Modules\MembershipApplication\Entities\MembershipApplication;
|
||||
|
||||
class MembershipApplicationSubmittedNotification extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
protected MembershipApplication $application
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$this->application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);
|
||||
|
||||
return (new MailMessage)
|
||||
->subject('Pengesahan Permohonan Keahlian')
|
||||
->markdown('membershipapplication::emails.submitted', [
|
||||
'application' => $this->application,
|
||||
'applicant' => $this->application->applicant,
|
||||
'heirs' => $this->application->heirs,
|
||||
'documents' => $this->application->documents,
|
||||
'documentLabels' => [
|
||||
'ic_copy' => 'Salinan Kad Pengenalan',
|
||||
'photo' => 'Gambar Passport',
|
||||
'salary_slip' => 'Slip Gaji',
|
||||
'employer_letter' => 'Surat Pengesahan Majikan',
|
||||
],
|
||||
'logoPath' => public_path('images/logo-kopkb.svg'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use App\Traits\HasDocuments;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\ApplicationStatus;
|
||||
|
||||
class MembershipApplication extends Model
|
||||
{
|
||||
use HasDocuments, HasUuids;
|
||||
|
||||
protected $table = 'membership_applications';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'application_number',
|
||||
'user_id',
|
||||
'status',
|
||||
'board_result',
|
||||
'submitted_at',
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => ApplicationStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function applicant(): HasOne
|
||||
{
|
||||
return $this->hasOne(MembershipApplicationApplicant::class);
|
||||
}
|
||||
|
||||
public function heirs(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationHeir::class);
|
||||
}
|
||||
|
||||
public function references(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationReference::class);
|
||||
}
|
||||
|
||||
public function reviews(): HasMany
|
||||
{
|
||||
return $this->hasMany(MembershipApplicationReview::class);
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MembershipApplicationApplicant extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_applicants';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'name',
|
||||
'email',
|
||||
'ic_number',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
'gender',
|
||||
'marriage_status',
|
||||
'address',
|
||||
'phone_number',
|
||||
'office_number',
|
||||
'postcode',
|
||||
'employer_name',
|
||||
'employer_address',
|
||||
'current_position',
|
||||
'start_work_date',
|
||||
'stock_monthly_contribution',
|
||||
'fee_monthly_contribution',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'start_work_date' => 'date',
|
||||
'stock_monthly_contribution' => 'decimal:2',
|
||||
'fee_monthly_contribution' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class MembershipApplicationHeir extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_heirs';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'name',
|
||||
'ic_number',
|
||||
'relationship',
|
||||
'phone_number',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'string',
|
||||
'ic_number' => 'string',
|
||||
'relationship' => 'string',
|
||||
'phone_number' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\ReferenceType;
|
||||
|
||||
class MembershipApplicationReference extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_references';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'reference_type',
|
||||
'user_id',
|
||||
'assigned_by',
|
||||
'assigned_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'reference_type' => ReferenceType::class,
|
||||
'assigned_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
|
||||
public function member(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function assignedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_by');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Entities;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Modules\Auth\Entities\User;
|
||||
use Modules\MembershipApplication\Enums\BoardDecision;
|
||||
use Modules\MembershipApplication\Enums\ManagementDecision;
|
||||
use Modules\MembershipApplication\Enums\ReviewStage;
|
||||
|
||||
class MembershipApplicationReview extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $table = 'membership_application_reviews';
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'membership_application_id',
|
||||
'stage',
|
||||
'decision',
|
||||
'remarks',
|
||||
'reviewer_id',
|
||||
'reviewed_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'stage' => ReviewStage::class,
|
||||
'reviewed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function membershipApplication(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(MembershipApplication::class);
|
||||
}
|
||||
|
||||
public function reviewer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'reviewer_id');
|
||||
}
|
||||
|
||||
public function managementDecision(): ?ManagementDecision
|
||||
{
|
||||
if ($this->stage !== ReviewStage::Management) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ManagementDecision::tryFrom($this->decision);
|
||||
}
|
||||
|
||||
public function boardDecision(): ?BoardDecision
|
||||
{
|
||||
if ($this->stage !== ReviewStage::Board) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return BoardDecision::tryFrom($this->decision);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum ApplicationStatus: string
|
||||
{
|
||||
case Submitted = 'SUBMITTED';
|
||||
case PendingBoard = 'PENDING_BOARD';
|
||||
case ManagementRejected = 'MANAGEMENT_REJECTED';
|
||||
case PendingNotification = 'PENDING_NOTIFICATION';
|
||||
case Completed = 'COMPLETED';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MembershipApplication\Enums;
|
||||
|
||||
enum BoardDecision: string
|
||||
{
|
||||
case Pass = 'PASS';
|
||||
case Fail = 'FAIL';
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user