diff --git a/src/Controllers/Admin/CouponController.php b/src/Controllers/Admin/CouponController.php new file mode 100644 index 0000000..89321cb --- /dev/null +++ b/src/Controllers/Admin/CouponController.php @@ -0,0 +1,86 @@ +view('admin.coupons.index', [ + 'coupons' => $this->ctx->coupons->all(), + ], 'admin.layout'); + } + + public function create(): Response + { + return $this->view('admin.coupons.form', ['errors' => []], 'admin.layout'); + } + + public function store(): Response + { + [$fields, $errors] = $this->validate(); + if ($errors !== []) { + return $this->view('admin.coupons.form', ['errors' => $errors], 'admin.layout', 422); + } + + $this->ctx->coupons->create($fields); + $this->ctx->session->flash('success', 'Coupon created.'); + return $this->redirect('/admin/coupons'); + } + + public function destroy(string $id): Response + { + $this->ctx->coupons->delete((int) $id); + $this->ctx->session->flash('success', 'Coupon deleted.'); + return $this->redirect('/admin/coupons'); + } + + private function validate(): array + { + $request = $this->ctx->request; + $code = strtoupper(trim((string) $request->input('code', ''))); + $type = $request->input('type') === 'fixed' ? 'fixed' : 'percent'; + $rawValue = (float) $request->input('value', 0); + $value = $type === 'percent' ? (int) round($rawValue) : (int) round($rawValue * 100); + $maxRedemptions = $request->input('maxRedemptions', ''); + + $errors = []; + if ($code === '') { + $errors['code'] = 'Code is required.'; + } elseif ($this->ctx->coupons->findByCode($code) !== null) { + $errors['code'] = 'A coupon with this code already exists.'; + } + if ($type === 'percent' && ($value < 1 || $value > 100)) { + $errors['value'] = 'Percent must be between 1 and 100.'; + } + if ($type === 'fixed' && $value < 1) { + $errors['value'] = 'Amount must be greater than zero.'; + } + + $fields = [ + 'code' => $code, + 'type' => $type, + 'value' => $value, + 'minSubtotalCents' => (int) round((float) $request->input('minSubtotal', 0) * 100), + 'maxRedemptions' => is_numeric($maxRedemptions) ? (int) $maxRedemptions : null, + 'startsAt' => $this->normalizeDate((string) $request->input('startsAt', '')), + 'expiresAt' => $this->normalizeDate((string) $request->input('expiresAt', '')), + 'isActive' => $request->input('isActive') !== null, + ]; + + return [$fields, $errors]; + } + + private function normalizeDate(string $value): ?string + { + $value = trim($value); + if ($value === '') { + return null; + } + $timestamp = strtotime($value); + return $timestamp === false ? null : gmdate('c', $timestamp); + } +} diff --git a/src/Controllers/Admin/OrderController.php b/src/Controllers/Admin/OrderController.php new file mode 100644 index 0000000..fb648d3 --- /dev/null +++ b/src/Controllers/Admin/OrderController.php @@ -0,0 +1,40 @@ +view('admin.orders.index', [ + 'orders' => $this->ctx->orders->all(), + ], 'admin.layout'); + } + + public function show(string $id): Response + { + $order = $this->ctx->orders->find((int) $id); + if ($order === null) { + return $this->notFound(); + } + + return $this->view('admin.orders.show', [ + 'order' => $order, + 'items' => $this->ctx->orders->items((int) $id), + ], 'admin.layout'); + } + + public function fulfill(string $id): Response + { + try { + $this->ctx->orderService->markFulfilled((int) $id); + $this->ctx->session->flash('success', 'Order marked as fulfilled.'); + } catch (\RuntimeException $error) { + $this->ctx->session->flash('error', $error->getMessage()); + } + return $this->redirect('/admin/orders/' . (int) $id); + } +} diff --git a/src/Controllers/Admin/ReviewController.php b/src/Controllers/Admin/ReviewController.php new file mode 100644 index 0000000..7329cf4 --- /dev/null +++ b/src/Controllers/Admin/ReviewController.php @@ -0,0 +1,32 @@ +view('admin.reviews.index', [ + 'pending' => $this->ctx->reviews->moderationQueue('pending'), + 'approved' => $this->ctx->reviews->moderationQueue('approved'), + 'rejected' => $this->ctx->reviews->moderationQueue('rejected'), + ], 'admin.layout'); + } + + public function approve(string $id): Response + { + $this->ctx->reviewService->approve((int) $id); + $this->ctx->session->flash('success', 'Review approved.'); + return $this->redirect('/admin/reviews'); + } + + public function reject(string $id): Response + { + $this->ctx->reviewService->reject((int) $id); + $this->ctx->session->flash('success', 'Review rejected.'); + return $this->redirect('/admin/reviews'); + } +}