diff --git a/app/Actions/Auction/CreateReportAction.php b/app/Actions/Auction/CreateReportAction.php new file mode 100644 index 0000000..d9ac748 --- /dev/null +++ b/app/Actions/Auction/CreateReportAction.php @@ -0,0 +1,22 @@ +reporter->associate($user); + $report->auction->associate($auction); + $report->save(); + + return $report; + } +} diff --git a/app/Actions/Auction/ResolveReportAction.php b/app/Actions/Auction/ResolveReportAction.php new file mode 100644 index 0000000..694c623 --- /dev/null +++ b/app/Actions/Auction/ResolveReportAction.php @@ -0,0 +1,18 @@ +resolved_at = now(); + $report->save(); + + return $report; + } +} diff --git a/app/Http/Controllers/Auction/ReportController.php b/app/Http/Controllers/Auction/ReportController.php new file mode 100644 index 0000000..2916c93 --- /dev/null +++ b/app/Http/Controllers/Auction/ReportController.php @@ -0,0 +1,65 @@ + $auction]); + } + + public function store(CreateReportRequest $request, CreateReportAction $createReportAction, Auction $auction): RedirectResponse + { + $user = $request->user(); + $this->authorize("report", $auction); + $validated = $request->validated(); + $createReportAction->execute($user, $auction, $validated); + + return redirect()->route("auctions.show", ["auction" => $auction])->with(["message" => "Aukcja została zgłoszona."]); + } + + public function index(SortHelper $sorter): Response + { + $reports = Report::query()->with(["reporter", "auction"])->where("resolved_at", "=", null); + + $perPage = (int)request()->query("per_page", 10); + + $query = $sorter->sort($reports, ["id", "created_at"], []); + $query = $sorter->search($query, "id"); + + return Inertia::render("Auction/Reports", [ + "reports" => ReportResource::collection($query->paginate($perPage)), + ]); + } + + public function show(Report $report): Response + { + $report->load(["reporter", "auction"]); + + return Inertia::render("Auction/ShowReport", [ + "report" => new ReportResource($report), + ]); + } + + public function resolve(ResolveReportAction $resolveReportAction, Report $report): RedirectResponse + { + $report = $resolveReportAction->execute($report); + + return redirect()->route("reports.show", ["report" => $report])->with(["message" => "Zgłoszenie zostało rozwiązane."]); + } +} diff --git a/app/Http/Requests/Auction/CreateReportRequest.php b/app/Http/Requests/Auction/CreateReportRequest.php new file mode 100644 index 0000000..057f6d2 --- /dev/null +++ b/app/Http/Requests/Auction/CreateReportRequest.php @@ -0,0 +1,33 @@ +user(); + $auction = $this->route("auction"); + + if ($user === null || $auction === null) { + return false; + } + + return $user->can("report", $auction); + } + + /** + * @return array|string> + */ + public function rules(): array + { + return [ + "reason" => ["sometimes", "string", "max:255"], + ]; + } +} diff --git a/app/Http/Resources/AuctionResource.php b/app/Http/Resources/AuctionResource.php index 0596cd9..5d0965f 100755 --- a/app/Http/Resources/AuctionResource.php +++ b/app/Http/Resources/AuctionResource.php @@ -28,6 +28,9 @@ public function toArray(Request $request): array "auctionState" => $this->auction_state, "createdAt" => $this->created_at, "updatedAt" => $this->updated_at, + "wasReported" => auth()->check() + ? $this->reports->contains("reporter_id", auth()->id()) + : false, ]; } } diff --git a/app/Http/Resources/ReportResource.php b/app/Http/Resources/ReportResource.php new file mode 100644 index 0000000..c8a9e7f --- /dev/null +++ b/app/Http/Resources/ReportResource.php @@ -0,0 +1,27 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "reporter" => UserResource::make($this->whenLoaded("reporter")), + "auction" => AuctionResource::make($this->whenLoaded("auction")), + "reason" => $this->reason, + "resolvedAt" => optional($this->resolved_at)?->toDateTimeString(), + "createdAt" => optional($this->created_at)?->toDateTimeString(), + "updatedAt" => optional($this->updated_at)?->toDateTimeString(), + ]; + } +} diff --git a/app/Models/Auction.php b/app/Models/Auction.php index ab0b901..f590319 100755 --- a/app/Models/Auction.php +++ b/app/Models/Auction.php @@ -74,4 +74,12 @@ public function category(): BelongsTo { return $this->belongsTo(Category::class); } + + /** + * @return HasMany + */ + public function reports() + { + return $this->hasMany(Report::class); + } } diff --git a/app/Models/Report.php b/app/Models/Report.php new file mode 100644 index 0000000..ae2d396 --- /dev/null +++ b/app/Models/Report.php @@ -0,0 +1,53 @@ + "datetime", + ]; + + /** + * @return BelongsTo + */ + public function reporter(): BelongsTo + { + return $this->belongsTo(User::class, "reporter_id"); + } + + /** + * @return BelongsTo + */ + public function auction(): BelongsTo + { + return $this->belongsTo(Auction::class, "auction_id"); + } +} diff --git a/app/Policies/AuctionPolicy.php b/app/Policies/AuctionPolicy.php index c13dab2..01f0387 100644 --- a/app/Policies/AuctionPolicy.php +++ b/app/Policies/AuctionPolicy.php @@ -16,4 +16,21 @@ public function update(User $user, Auction $auction): Response ? Response::allow() : Response::deny(); } + + public function report(User $user, Auction $auction): Response + { + $alreadyReported = $auction->reports() + ->where("reporter_id", $user->id) + ->exists(); + + if ($user->id === $auction->owner_id) { + return Response::deny("Nie można zgłosić własnej aukcji."); + } + + if ($alreadyReported) { + return Response::deny("Aukcja została już przez Ciebie zgłoszona."); + } + + return Response::allow(); + } } diff --git a/database/factories/ReportFactory.php b/database/factories/ReportFactory.php new file mode 100644 index 0000000..eaeee47 --- /dev/null +++ b/database/factories/ReportFactory.php @@ -0,0 +1,26 @@ + + */ +class ReportFactory extends Factory +{ + public function definition(): array + { + return [ + "reporter_id" => User::factory(), + "auction_id" => Auction::factory(), + "reason" => fake()->text(200), + "resolved_at" => null, + ]; + } +} diff --git a/database/migrations/2026_01_06_155512_create_reports_table.php b/database/migrations/2026_01_06_155512_create_reports_table.php new file mode 100644 index 0000000..66961d1 --- /dev/null +++ b/database/migrations/2026_01_06_155512_create_reports_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignIdFor(User::class, "reporter_id")->constrained("users")->cascadeOnDelete(); + $table->foreignIdFor(Auction::class, "auction_id")->constrained("auctions")->cascadeOnDelete(); + $table->text("reason")->nullable(); + $table->timestamp("resolved_at")->nullable(); + $table->timestamps(); + $table->unique(["reporter_id", "auction_id"]); + }); + } + + public function down(): void + { + Schema::dropIfExists("reports"); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f3462ca..adc2048 100755 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -16,6 +16,7 @@ public function run(): void AuctionSeeder::class, PermissionsSeeder::class, RolesSeeder::class, + ReportSeeder::class, ]); } } diff --git a/database/seeders/ReportSeeder.php b/database/seeders/ReportSeeder.php new file mode 100644 index 0000000..89a0a06 --- /dev/null +++ b/database/seeders/ReportSeeder.php @@ -0,0 +1,16 @@ +count(30)->create(); + } +} diff --git a/resources/js/Pages/Auction/ReportAuction.tsx b/resources/js/Pages/Auction/ReportAuction.tsx new file mode 100755 index 0000000..92db316 --- /dev/null +++ b/resources/js/Pages/Auction/ReportAuction.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {Form} from "@inertiajs/react"; +import { Auction } from "@/Types/auction"; + +type Props = { + errors: any; + auction: Auction; +} + +export function ReportAuction({errors, auction}: Props) { + return ( + <> +

Report auction

+
+ + {errors?.reason &&

{errors.reason}

} + + +
+ + ); +} diff --git a/resources/js/Pages/Auction/ShowReport.tsx b/resources/js/Pages/Auction/ShowReport.tsx new file mode 100755 index 0000000..ede2cd1 --- /dev/null +++ b/resources/js/Pages/Auction/ShowReport.tsx @@ -0,0 +1,22 @@ +import { Title } from "@/Components/Title"; +import { Report } from "@/Types/report"; + +type Props = { + report: Report, +} + +export function ShowReport({report}: Props) { + return ( +
+ Szczegóły zgłoszenia + +
ID: {report.id}
+
Reporter: {report.reporter?.email || "N/A"}
+
Auction: {report.auction?.name || "N/A"}
+
Reason: {report.reason || "Brak"}
+
Resolved At: {report.resolvedAt || "Nie rozwiązano"}
+
Created At: {report.createdAt}
+
Updated At: {report.updatedAt}
+
+ ); +} diff --git a/resources/js/Types/report.ts b/resources/js/Types/report.ts new file mode 100644 index 0000000..7fed524 --- /dev/null +++ b/resources/js/Types/report.ts @@ -0,0 +1,12 @@ +import { User } from "./user"; +import { Auction } from "./auction"; + +export type Report = { + id: number; + reporter: User; + auction: Auction; + reason: string; + resolvedAt: string | null; + createdAt: string; + updatedAt: string; +} \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index b181a1b..52892c0 100755 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use Inertia\Response; use Otoszroto\Enums\Permission; use Otoszroto\Http\Controllers\Auction\AuctionController; +use Otoszroto\Http\Controllers\Auction\ReportController; use Otoszroto\Http\Controllers\Auth\ForgotPasswordController; use Otoszroto\Http\Controllers\Auth\LoginController; use Otoszroto\Http\Controllers\Auth\LogoutController; @@ -41,6 +42,11 @@ Route::patch("/auctions/{auction}", [AuctionController::class, "update"])->name("auctions.update"); Route::patch("/auctions/{auction}/finish", [AuctionController::class, "finish"])->name("auctions.finish"); Route::patch("/auctions/{auction}/cancel", [AuctionController::class, "cancel"])->name("auctions.cancel"); + Route::get("/auctions/{auction}/report", [ReportController::class, "create"])->name("auctions.report.create"); + Route::post("/auctions/{auction}/report", [ReportController::class, "store"])->name("auctions.report.store"); + Route::get("/reports", [ReportController::class, "index"])->name("reports.index")->middleware([Authorize::using(Permission::ManageReports)]); + Route::get("/reports/{report}", [ReportController::class, "show"])->name("reports.show")->middleware([Authorize::using(Permission::ManageReports)]); + Route::get("/reports/{report}/resolve", [ReportController::class, "resolve"])->name("reports.resolve")->middleware([Authorize::using(Permission::ManageReports)]); }); Route::get("/auctions/{auction}", [AuctionController::class, "show"])->name("auctions.show");