From 40b7efac2f4ea467d2a0a03b463fda240c81e196 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Wed, 24 Jun 2026 17:52:59 +0330 Subject: [PATCH] feat: announcement --- .../Controllers/AnnouncementController.php | 97 +++++++++++++++++++ app/Models/Announcement.php | 33 +++++++ ...6_24_100000_create_announcements_table.php | 27 ++++++ routes/api.php | 10 ++ 4 files changed, 167 insertions(+) create mode 100644 app/Http/Controllers/AnnouncementController.php create mode 100644 app/Models/Announcement.php create mode 100644 database/migrations/2026_06_24_100000_create_announcements_table.php diff --git a/app/Http/Controllers/AnnouncementController.php b/app/Http/Controllers/AnnouncementController.php new file mode 100644 index 0000000..6035e8f --- /dev/null +++ b/app/Http/Controllers/AnnouncementController.php @@ -0,0 +1,97 @@ +json([ + 'data' => Announcement::with('image')->latest()->get(), + ]); + } + + // App side: only the single latest currently-active announcement (or null). + public function latest() + { + $announcement = Announcement::with('image') + ->active() + ->orderByDesc('start_date') + ->latest() + ->first(); + + return response()->json(['data' => $announcement]); + } + + public function store(Request $request) + { + $data = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'link' => 'nullable|string|max:500', + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'image_id' => 'nullable|exists:images,id', + 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', + ]); + + // An uploaded image file takes precedence over a provided image_id. + $data['image_id'] = $this->uploadedImageId($request) ?? ($data['image_id'] ?? null); + + $announcement = Announcement::create($data); + + return response()->json([ + 'message' => 'Announcement created successfully', + 'data' => $announcement->load('image'), + ], 201); + } + + public function show($id) + { + return response()->json( + Announcement::with('image')->findOrFail($id) + ); + } + + public function update(Request $request, $id) + { + $announcement = Announcement::findOrFail($id); + + $data = $request->validate([ + 'title' => 'sometimes|string|max:255', + 'description' => 'nullable|string', + 'link' => 'nullable|string|max:500', + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'image_id' => 'nullable|exists:images,id', + 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', + ]); + + // An uploaded image file takes precedence over a provided image_id. + if (($uploadedImageId = $this->uploadedImageId($request)) !== null) { + $data['image_id'] = $uploadedImageId; + } + + $announcement->update($data); + + return response()->json([ + 'message' => 'Announcement updated successfully', + 'data' => $announcement->load('image'), + ]); + } + + public function destroy($id) + { + $announcement = Announcement::findOrFail($id); + $announcement->delete(); + + return response()->json(['message' => 'Announcement deleted successfully']); + } +} diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php new file mode 100644 index 0000000..0816895 --- /dev/null +++ b/app/Models/Announcement.php @@ -0,0 +1,33 @@ + 'datetime', + 'end_date' => 'datetime', + ]; + + public function image(): BelongsTo + { + return $this->belongsTo(Image::class); + } + + // Currently within its [start_date, end_date] window (null bounds = open-ended). + public function scopeActive($query) + { + $now = now(); + + return $query + ->where(fn ($q) => $q->whereNull('start_date')->orWhere('start_date', '<=', $now)) + ->where(fn ($q) => $q->whereNull('end_date')->orWhere('end_date', '>=', $now)); + } +} diff --git a/database/migrations/2026_06_24_100000_create_announcements_table.php b/database/migrations/2026_06_24_100000_create_announcements_table.php new file mode 100644 index 0000000..dfdaf4c --- /dev/null +++ b/database/migrations/2026_06_24_100000_create_announcements_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('image_id')->nullable()->constrained('images')->nullOnDelete(); + $table->string('title'); + $table->text('description')->nullable(); + $table->string('link')->nullable(); + $table->dateTime('start_date')->nullable(); + $table->dateTime('end_date')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('announcements'); + } +}; diff --git a/routes/api.php b/routes/api.php index a9e3437..aa7f129 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,6 +18,7 @@ use App\Http\Controllers\FaqController; use App\Http\Controllers\FaqCategoryController; use App\Http\Controllers\SliderController; +use App\Http\Controllers\AnnouncementController; use App\Http\Controllers\SceneController; use App\Http\Controllers\ThemeController; use App\Http\Controllers\BellSoundController; @@ -198,6 +199,15 @@ Route::post('/slider/{id}', [SliderController::class, 'update']); // update slider (multipart: image upload) Route::delete('/slider/{id}', [SliderController::class, 'destroy']); // delete slider + /// announcements + Route::get('/announcements/latest', [AnnouncementController::class, 'latest']); // single latest active (app) + Route::get('/announcements', [AnnouncementController::class, 'index']); // list all (admin) + Route::post('/announcements', [AnnouncementController::class, 'store']); // create + Route::get('/announcements/{id}', [AnnouncementController::class, 'show']); // single + Route::put('/announcements/{id}', [AnnouncementController::class, 'update']); // update (json) + Route::post('/announcements/{id}', [AnnouncementController::class, 'update']); // update (multipart: image) + Route::delete('/announcements/{id}', [AnnouncementController::class, 'destroy']); // delete + /// themes (color themes a scene can use) Route::get('/themes', [ThemeController::class, 'index']);