feat: add feedback api

This commit is contained in:
2026-06-07 15:38:35 +03:30
parent 11a1565323
commit 49dc7824c3
4 changed files with 150 additions and 0 deletions
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\AppFeedback;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class AppFeedbackController extends Controller
{
// USER: get the current user's feedback (null if none yet).
public function mine()
{
$feedback = AppFeedback::where('user_id', auth()->id())->first();
return response()->json($feedback);
}
// USER: submit or update feedback (one editable row per user).
public function store(Request $request)
{
$data = $request->validate([
'stars' => 'nullable|integer|min:1|max:5',
'content' => 'nullable|string|max:2000',
]);
if (!$request->filled('stars') && !$request->filled('content')) {
throw ValidationException::withMessages([
'content' => ['Either a rating or a comment is required.'],
]);
}
$feedback = AppFeedback::firstOrNew(['user_id' => auth()->id()]);
if ($request->has('stars')) {
$feedback->stars = $data['stars'] ?? null;
}
if ($request->has('content')) {
$feedback->content = $data['content'] ?? null;
}
$feedback->save();
return response()->json([
'message' => 'Feedback submitted successfully',
'feedback' => $feedback,
]);
}
// ADMIN: monitor all feedback, with a summary header.
public function adminIndex(Request $request)
{
$query = AppFeedback::with('user');
if ($request->filled('has_comment')) {
$query->whereNotNull('content')->where('content', '!=', '');
}
if ($request->filled('stars')) {
$query->where('stars', $request->integer('stars'));
}
$feedback = $query->latest()->paginate($request->integer('per_page', 20));
$base = AppFeedback::query();
return response()->json([
'summary' => [
'total' => (clone $base)->count(),
'rated' => (clone $base)->whereNotNull('stars')->count(),
'with_comment' => (clone $base)->whereNotNull('content')->where('content', '!=', '')->count(),
'average_stars' => round((float) (clone $base)->whereNotNull('stars')->avg('stars'), 2),
],
'feedback' => $feedback,
]);
}
// ADMIN: remove a feedback entry (moderation).
public function adminDestroy($id)
{
$feedback = AppFeedback::findOrFail($id);
$feedback->delete();
return response()->json(['message' => 'Feedback deleted successfully']);
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AppFeedback extends Model
{
protected $table = 'app_feedback';
protected $fillable = ['user_id', 'stars', 'content'];
protected $casts = [
'stars' => 'integer',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
@@ -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
{
/**
* App-level feedback (نظرات و ایده‌ها): one editable entry per user holding an
* overall star rating and/or an idea/comment about the application.
*/
public function up(): void
{
Schema::create('app_feedback', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->unsignedTinyInteger('stars')->nullable(); // 1-5 overall rating
$table->text('content')->nullable(); // idea / comment
$table->timestamps();
$table->unique('user_id'); // one feedback row per user (edited in place)
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('app_feedback');
}
};
+10
View File
@@ -13,6 +13,7 @@
use App\Http\Controllers\QuestionController;
use App\Http\Controllers\SurveyQuestionController;
use App\Http\Controllers\ChatTopicController;
use App\Http\Controllers\AppFeedbackController;
use App\Http\Controllers\SliderController;
use App\Http\Controllers\SceneController;
use App\Http\Controllers\BellSoundController;
@@ -129,6 +130,15 @@
Route::apiResource('chat-topics', ChatTopicController::class);
/// app feedback — ideas & reviews (نظرات و ایده‌ها)
Route::get('/app-feedback', [AppFeedbackController::class, 'mine']);
Route::post('/app-feedback', [AppFeedbackController::class, 'store']);
Route::middleware('abilities:admin')->group(function () {
Route::get('/admin/app-feedback', [AppFeedbackController::class, 'adminIndex']);
Route::delete('/admin/app-feedback/{id}', [AppFeedbackController::class, 'adminDestroy']);
});
/// survey questions feature (question + description + single/multi options, answered by users)
// Admin: see all users' answers (must be registered before the resource so it isn't caught by {survey_question}).
Route::middleware('abilities:admin')->group(function () {