DONE: layout letter with letterhead and footer, notification for newly...

This commit is contained in:
ISMAIL MASSERAN
2026-06-28 02:16:49 +00:00
parent 94ecbe5887
commit 034ecf947f
400 changed files with 29802 additions and 5446 deletions
View File
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'name' => 'Activity',
];
@@ -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_types', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('name');
$table->string('code')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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');
}
};
@@ -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([]);
}
}
View File
+65
View File
@@ -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.',
];
}
}
View File
@@ -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;
}
View File
+24
View File
@@ -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');
});
+8
View File
@@ -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(),
];
}
}
+30
View File
@@ -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/"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Activity",
"alias": "activity",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Activity\\Providers\\ActivityServiceProvider"
],
"files": []
}
+15
View File
@@ -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"
}
}