diff --git a/backend/app/Actions/CreateProductAction.php b/backend/app/Actions/CreateProductAction.php new file mode 100644 index 0000000..e9ec199 --- /dev/null +++ b/backend/app/Actions/CreateProductAction.php @@ -0,0 +1,16 @@ +image->store('public/images'); + $uploadImageDTO->product->images()->create([ + 'path' => $path, + ]); + } +} diff --git a/backend/app/Actions/GetAllProductCategory.php b/backend/app/Actions/GetAllProductCategory.php new file mode 100644 index 0000000..24d12c3 --- /dev/null +++ b/backend/app/Actions/GetAllProductCategory.php @@ -0,0 +1,19 @@ +map(fn ($category) => ProductCategoryDTO::fromModel($category)) + ->toArray(); + } +} diff --git a/backend/app/Actions/UploadImageAction.php b/backend/app/Actions/UploadImageAction.php new file mode 100644 index 0000000..cd11381 --- /dev/null +++ b/backend/app/Actions/UploadImageAction.php @@ -0,0 +1,16 @@ +image->store('public/images'); + $uploadImageDTO->product->images()->create([ + 'path' => $path, + ]); + } +} diff --git a/backend/app/Console/Commands/MakeActionCommand.php b/backend/app/Console/Commands/MakeActionCommand.php new file mode 100644 index 0000000..88aa3a4 --- /dev/null +++ b/backend/app/Console/Commands/MakeActionCommand.php @@ -0,0 +1,32 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + ]; + } + + public static function fromModel(ProductCategory $category): self + { + return new self( + id: $category->id, + name: $category->name, + slug: $category->slug, + ); + } +} diff --git a/backend/app/Data/ProductDTO.php b/backend/app/Data/ProductDTO.php new file mode 100644 index 0000000..ea34b90 --- /dev/null +++ b/backend/app/Data/ProductDTO.php @@ -0,0 +1,53 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'actualPrice' => $this->actualPrice, + 'listPrice' => $this->listPrice, + 'category' => $this->category->toArray(), + 'productImage' => array_map(fn (ProductImageDTO $productImage) => $productImage->toArray(), + $this->productImages), + ]; + } + + public static function fromModel(Product $product): self + { + return new self( + id: $product->id, + title: $product->title, + description: $product->description, + actualPrice: $product->actual_price, + listPrice: $product->list_price, + category: ProductCategoryDTO::fromModel($product->category), + productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(), + ); + } +} diff --git a/backend/app/Data/ProductImageDTO.php b/backend/app/Data/ProductImageDTO.php new file mode 100644 index 0000000..0736274 --- /dev/null +++ b/backend/app/Data/ProductImageDTO.php @@ -0,0 +1,36 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'path' => $this->path, + 'productId' => $this->productId, + ]; + } + + public static function fromModel(ProductImage $productImage): self + { + return new self( + $productImage->id, + $productImage->path, + $productImage->product_id + ); + } +} diff --git a/backend/app/Data/UploadImageDTO.php b/backend/app/Data/UploadImageDTO.php new file mode 100644 index 0000000..7a17477 --- /dev/null +++ b/backend/app/Data/UploadImageDTO.php @@ -0,0 +1,22 @@ + $this->email, 'mobileNumber' => $this->mobileNumber, 'city' => $this->city, + 'role' => $this->role, ]; } } diff --git a/backend/app/Enums/UserRoles.php b/backend/app/Enums/UserRoles.php new file mode 100644 index 0000000..561885e --- /dev/null +++ b/backend/app/Enums/UserRoles.php @@ -0,0 +1,10 @@ +name, email: $user->email, mobileNumber: $user->mobile_number, - city: $user->city + city: $user->city, + role: $user->role->value ); return response()->json($userDto->toArray()); diff --git a/backend/app/Http/Controllers/ProductCategoryController.php b/backend/app/Http/Controllers/ProductCategoryController.php new file mode 100644 index 0000000..f01b3f7 --- /dev/null +++ b/backend/app/Http/Controllers/ProductCategoryController.php @@ -0,0 +1,13 @@ +execute(); + } +} diff --git a/backend/app/Http/Controllers/ProductController.php b/backend/app/Http/Controllers/ProductController.php new file mode 100644 index 0000000..9081473 --- /dev/null +++ b/backend/app/Http/Controllers/ProductController.php @@ -0,0 +1,31 @@ +with(['category:id,name,slug', 'images:id,path,product_id'])->paginate(); + $paginatedDtos = $paginator->through(fn ($product) => ProductDTO::fromModel($product)); + + return ProductResource::collection($paginatedDtos); + } + + public function store(CreateProductRequest $request) + { + return Product::create($request->validated()); + } + + public function show(Product $product) {} + + public function update(Request $request, Product $product) {} + + public function destroy(Product $product) {} +} diff --git a/backend/app/Http/Controllers/ProductImagesController.php b/backend/app/Http/Controllers/ProductImagesController.php new file mode 100644 index 0000000..0ecd954 --- /dev/null +++ b/backend/app/Http/Controllers/ProductImagesController.php @@ -0,0 +1,30 @@ +execute(UploadImageDTO::fromRequest($request->validated())); + + return response()->json(['message' => 'Image uploaded successfully']); + } + + public function show(ProductImages $productImages) + { + return $productImages; + } + + public function destroy(ProductImages $productImages) + { + $productImages->delete(); + + return response()->json(); + } +} diff --git a/backend/app/Http/Requests/CreateProductRequest.php b/backend/app/Http/Requests/CreateProductRequest.php new file mode 100644 index 0000000..3b7d11c --- /dev/null +++ b/backend/app/Http/Requests/CreateProductRequest.php @@ -0,0 +1,24 @@ + 'required|string|max:255', + 'description' => 'required|string', + 'product_category_id' => 'required|exists:product_categories,id', + 'actual_price' => 'required|numeric|min:0', + 'list_price' => 'required|numeric|min:0', + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/backend/app/Http/Requests/UploadImageRequest.php b/backend/app/Http/Requests/UploadImageRequest.php new file mode 100644 index 0000000..c5b523a --- /dev/null +++ b/backend/app/Http/Requests/UploadImageRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + 'product_id' => 'required|exists:products,id', + ]; + } +} diff --git a/backend/app/Http/Resources/ProductCollection.php b/backend/app/Http/Resources/ProductCollection.php new file mode 100644 index 0000000..ae72b40 --- /dev/null +++ b/backend/app/Http/Resources/ProductCollection.php @@ -0,0 +1,16 @@ + $this->collection, + ]; + } +} diff --git a/backend/app/Http/Resources/ProductResource.php b/backend/app/Http/Resources/ProductResource.php new file mode 100644 index 0000000..ea6b9f8 --- /dev/null +++ b/backend/app/Http/Resources/ProductResource.php @@ -0,0 +1,18 @@ +resource->toArray(); + } +} diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php new file mode 100644 index 0000000..c0725a3 --- /dev/null +++ b/backend/app/Models/Product.php @@ -0,0 +1,28 @@ +belongsTo(ProductCategory::class, 'product_category_id'); + } + + public function images(): HasMany + { + return $this->hasMany(ProductImage::class, 'product_id', 'id'); + } +} diff --git a/backend/app/Models/ProductCategory.php b/backend/app/Models/ProductCategory.php new file mode 100644 index 0000000..badd25d --- /dev/null +++ b/backend/app/Models/ProductCategory.php @@ -0,0 +1,10 @@ +belongsTo(Product::class); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 9fe693c..eb74b44 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Enums\UserRoles; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -23,6 +24,7 @@ class User extends Authenticatable 'password', 'city', 'mobile_number', + 'role', ]; /** @@ -45,6 +47,7 @@ protected function casts(): array return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'role' => UserRoles::class, ]; } } diff --git a/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php b/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php new file mode 100644 index 0000000..e6d3d22 --- /dev/null +++ b/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php @@ -0,0 +1,29 @@ +id()->index(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_categories'); + } +}; diff --git a/backend/database/migrations/2026_02_26_073626_create_products_table.php b/backend/database/migrations/2026_02_26_073626_create_products_table.php new file mode 100644 index 0000000..6dc7a2f --- /dev/null +++ b/backend/database/migrations/2026_02_26_073626_create_products_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->text('description'); + $table->decimal('actual_price', 10, 2); + $table->decimal('list_price', 10, 2); + $table->foreignIdFor(ProductCategory::class)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/backend/database/migrations/2026_02_26_080304_create_product_images_table.php b/backend/database/migrations/2026_02_26_080304_create_product_images_table.php new file mode 100644 index 0000000..42940e1 --- /dev/null +++ b/backend/database/migrations/2026_02_26_080304_create_product_images_table.php @@ -0,0 +1,24 @@ +id()->index(); + $table->string('path'); + $table->foreignIdFor(Product::class)->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('product_images'); + } +}; diff --git a/backend/database/migrations/2026_02_27_044159_add_role_to_users.php b/backend/database/migrations/2026_02_27_044159_add_role_to_users.php new file mode 100644 index 0000000..7158dc2 --- /dev/null +++ b/backend/database/migrations/2026_02_27_044159_add_role_to_users.php @@ -0,0 +1,23 @@ +enum('role', array_column(UserRoles::cases(), 'value'))->default(UserRoles::Customer->value); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('role'); + }); + } +}; diff --git a/backend/database/seeders/ProductCategorySeeder.php b/backend/database/seeders/ProductCategorySeeder.php new file mode 100644 index 0000000..822835b --- /dev/null +++ b/backend/database/seeders/ProductCategorySeeder.php @@ -0,0 +1,28 @@ +map(function ($name) { + return [ + 'name' => $name, + 'slug' => Str::slug($name), + ]; + })->toArray(); + + ProductCategory::upsert($categories, ['name'], ['slug']); + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index 626d647..5d8329d 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,9 @@ group(function () { Route::get('/user', [AuthenticatedUserController::class, 'show']); Route::post('/logout', [AuthenticatedUserController::class, 'destroy']); + Route::post('/upload/images', action: [ProductImagesController::class, 'store']); }); +Route::get('/categories', [ProductCategoryController::class, 'index']); +Route::apiResource('products', ProductController::class); diff --git a/backend/stubs/action.stub b/backend/stubs/action.stub new file mode 100644 index 0000000..ba6beca --- /dev/null +++ b/backend/stubs/action.stub @@ -0,0 +1,14 @@ +