Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
34142c8
Add /api/v2/brands CRUD endpoints (Brands API for Vue migration)
edwh May 28, 2026
0bfba17
Migrate brands admin page to Vue (BrandsPage component)
edwh May 28, 2026
047bb16
Address adversarial review of brands migration
edwh May 28, 2026
9979f66
Extract reusable AdminCrudPage; BrandsPage becomes a thin wrapper
edwh May 28, 2026
7a22a1f
Migrate skills admin page to Vue (SkillsPage via AdminCrudPage)
edwh May 28, 2026
9a6ffd2
Plan: PR-first workflow (open draft early, watch CI per commit)
edwh May 28, 2026
6f6038a
Migrate global group-tags admin page to Vue + add fr/fr-BE translations
edwh May 28, 2026
4cdfc20
Remove orphan translation keys left over from deleted blade templates
edwh May 28, 2026
c2b23ed
GroupTagsPage: strip HTML via DOMParser, not regex
edwh May 28, 2026
250d7d1
Migrate categories admin page to Vue (CategoriesPage via AdminCrudPage)
edwh May 28, 2026
9ad1388
Update admin global-tags Playwright tests for the Vue SPA
edwh May 28, 2026
5614c31
Migrate roles admin page to Vue (RolesPage with permission matrix)
edwh May 28, 2026
67bc43c
Playwright spec for the new reference-data admin pages
edwh May 28, 2026
a7b827e
Reduce duplication in admin-reference-data Playwright spec
edwh May 28, 2026
982ec37
Slim admin-reference-data Playwright spec to smoke-only
edwh May 28, 2026
09f0fd9
Plan: reflect Group 2.1 / 5.2 done; iframe-widget + dead-code scope n…
edwh May 30, 2026
fa76111
Plan: Group 2.2 email-preferences tab migrated (PR #868)
edwh May 30, 2026
fb98b47
Plan: Group 2.2 calendars tab also migrated (2/5)
edwh May 30, 2026
5e5263f
Plan: Group 2.2 repair-directory tab migrated (3/5)
edwh May 30, 2026
e880824
Plan: Group 2.2 account/language form migrated
edwh May 30, 2026
59b8202
Wrap role-permission update in a transaction
edwh May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/Brands.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace App;

use DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Brands extends Model
{
use HasFactory;

protected $table = 'brands';
/**
* The attributes that are mass assignable.
Expand Down
15 changes: 15 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace App\Exceptions;

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Throwable;
use Exception;
Expand All @@ -26,7 +29,7 @@
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)

Check warning on line 32 in app/Exceptions/Handler.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 6 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=TheRestartProject_restarters.net&issues=AZ5tb9oqoIt0FrcH0OOV&open=AZ5tb9oqoIt0FrcH0OOV&pullRequest=863
{
if ($request->wantsJson()) {
if ($exception instanceof ValidationException) {
Expand All @@ -35,6 +38,18 @@
422);
}

if ($exception instanceof AuthenticationException) {
return response()->json(['message' => 'Unauthenticated.'], 401);
}

if ($exception instanceof AuthorizationException) {
return response()->json(['message' => 'Unauthorized.'], 403);
}

if ($exception instanceof ModelNotFoundException) {
return response()->json(['message' => 'Resource not found.'], 404);
}

return response()->json(
['message' => $exception->getMessage()],
method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : 500);
Expand Down
187 changes: 187 additions & 0 deletions app/Http/Controllers/API/BrandController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php

namespace App\Http\Controllers\API;

use App\Brands;
use App\Helpers\Fixometer;
use App\Http\Controllers\Controller;
use App\Http\Resources\Brand;
use App\Http\Resources\BrandCollection;
use Auth;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class BrandController extends Controller
{
/**
* @OA\Get(
* path="/api/v2/brands",
* operationId="listBrandsv2",
* tags={"Brands"},
* summary="List Brands",
* description="Returns all device brands, ordered alphabetically. Public endpoint.",
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/Brand")
* )
* )
* )
* )
*/
public function listBrandsv2()
{
$brands = Brands::orderBy('brand_name', 'asc')->get();

return BrandCollection::make($brands);
}

/**
* @OA\Get(
* path="/api/v2/brands/{id}",
* operationId="getBrandv2",
* tags={"Brands"},
* summary="Get a Brand",
* description="Returns a single brand by id.",
* @OA\Parameter(
* name="id",
* in="path",
* required=true,
* @OA\Schema(type="integer")
* ),
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(property="data", ref="#/components/schemas/Brand")
* )
* ),
* @OA\Response(response=404, description="Brand not found")
* )
*/
public function getBrandv2($id)
{
$brand = Brands::findOrFail($id);

return Brand::make($brand);
}

/**
* @OA\Post(
* path="/api/v2/brands",
* operationId="createBrandv2",
* tags={"Brands"},
* summary="Create a Brand",
* description="Create a new device brand. Administrator only.",
* security={{"apiToken":{}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"brand_name"},
* @OA\Property(property="brand_name", type="string", maxLength=255, example="Sony")
* )
* ),
* @OA\Response(
* response=201,
* description="Brand created",
* @OA\JsonContent(
* @OA\Property(property="data", ref="#/components/schemas/Brand")
* )
* ),
* @OA\Response(response=401, description="Unauthenticated"),
* @OA\Response(response=403, description="Forbidden"),
* @OA\Response(response=422, description="Validation failed")
* )
*/
public function createBrandv2(Request $request): JsonResponse
{
if (!Fixometer::hasRole(Auth::user(), 'Administrator')) {
return response()->json(['message' => 'Forbidden'], 403);
}

$validated = $request->validate([
'brand_name' => 'required|string|max:255|unique:brands,brand_name',
]);

$brand = Brands::create($validated);

return response()->json(['data' => (new Brand($brand))->toArray($request)], 201);
}

/**
* @OA\Put(
* path="/api/v2/brands/{id}",
* operationId="updateBrandv2",
* tags={"Brands"},
* summary="Update a Brand",
* description="Update a brand. Administrator only.",
* security={{"apiToken":{}}},
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"brand_name"},
* @OA\Property(property="brand_name", type="string", maxLength=255, example="Sony")
* )
* ),
* @OA\Response(
* response=200,
* description="Brand updated",
* @OA\JsonContent(
* @OA\Property(property="data", ref="#/components/schemas/Brand")
* )
* ),
* @OA\Response(response=401, description="Unauthenticated"),
* @OA\Response(response=403, description="Forbidden"),
* @OA\Response(response=404, description="Brand not found"),
* @OA\Response(response=422, description="Validation failed")
* )
*/
public function updateBrandv2(Request $request, $id)
{
if (!Fixometer::hasRole(Auth::user(), 'Administrator')) {
return response()->json(['message' => 'Forbidden'], 403);
}

$brand = Brands::findOrFail($id);

$validated = $request->validate([
'brand_name' => 'required|string|max:255|unique:brands,brand_name,' . $brand->id,
]);

$brand->update($validated);

return Brand::make($brand->fresh());
}

/**
* @OA\Delete(
* path="/api/v2/brands/{id}",
* operationId="deleteBrandv2",
* tags={"Brands"},
* summary="Delete a Brand",
* description="Delete a brand. Administrator only.",
* security={{"apiToken":{}}},
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\Response(response=204, description="Brand deleted"),
* @OA\Response(response=401, description="Unauthenticated"),
* @OA\Response(response=403, description="Forbidden"),
* @OA\Response(response=404, description="Brand not found")
* )
*/
public function deleteBrandv2($id)
{
if (!Fixometer::hasRole(Auth::user(), 'Administrator')) {
return response()->json(['message' => 'Forbidden'], 403);
}

$brand = Brands::findOrFail($id);
$brand->delete();

return response()->noContent();
}
}
Loading