get('per_page', 15), 100); $query = WebDesignQuote::with('creator:id,fullname') ->when($request->get('filter'), fn($q, $f) => $q->where(fn($sub) => $sub ->where('quote_number', 'like', "%{$f}%") ->orWhere('customer_name', 'like', "%{$f}%") ->orWhere('website_name', 'like', "%{$f}%") ->orWhere('customer_email', 'like', "%{$f}%")) )->when($request->filled('status'), fn($q) => $q->where('status', $request->get('status')) )->when($request->filled('platform'), fn($q) => $q->where('platform', $request->get('platform')) )->orderByDesc('id'); $paginator = $query->paginate($perPage); return response()->json([ 'error' => false, 'data' => [ 'items' => $paginator->items(), 'total' => $paginator->total(), 'per_page' => $paginator->perPage(), 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), ], ]); } public function show(int $id): JsonResponse { $quote = WebDesignQuote::with('creator:id,fullname')->find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } return response()->json([ 'error' => false, 'data' => $quote, ]); } public function store(Request $request): JsonResponse { $validated = $this->validateQuote($request); $validated['quote_number'] = WebDesignQuote::generateQuoteNumber(); $validated['created_by'] = $request->user()?->id; $validated = $this->applyTotals($validated); $quote = WebDesignQuote::create($validated); return response()->json([ 'error' => false, 'message' => 'Tạo báo giá thành công.', 'data' => $quote, ], 201); } public function update(Request $request, int $id): JsonResponse { $quote = WebDesignQuote::find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } $validated = $this->validateQuote($request); $validated = $this->applyTotals($validated); $quote->update($validated); return response()->json([ 'error' => false, 'message' => 'Cập nhật báo giá thành công.', 'data' => $quote->fresh('creator:id,fullname'), ]); } public function updateStatus(Request $request, int $id): JsonResponse { $quote = WebDesignQuote::find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } $validated = $request->validate([ 'status' => ['required', 'string', Rule::in(WebDesignQuoteStatus::values())], ]); $quote->update($validated); return response()->json([ 'error' => false, 'message' => 'Cập nhật trạng thái báo giá thành công.', 'data' => $quote->fresh(), ]); } public function destroy(int $id): JsonResponse { $quote = WebDesignQuote::find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } $quote->delete(); return response()->json([ 'error' => false, 'message' => 'Xoá báo giá thành công.', ]); } public function downloadPdf(int $id, WebDesignQuotePdfService $pdfService): Response|JsonResponse { $quote = WebDesignQuote::find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } $fileName = $pdfService->generateFileName($quote); return response($pdfService->generate($quote), 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', ]); } public function downloadXlsx(int $id, WebDesignQuoteSpreadsheet $spreadsheet): BinaryFileResponse|JsonResponse { $quote = WebDesignQuote::find($id); if (!$quote) { return response()->json(['error' => true, 'message' => 'Không tìm thấy báo giá.'], 404); } return $spreadsheet->download($quote); } /** * Danh sách cấu hình nền tảng cho form báo giá (label, frontend, custom admin, phí bản quyền). */ public function platformOptions(): JsonResponse { $platforms = array_map(fn(WebDesignPlatform $p) => [ 'value' => $p->value, 'label' => $p->label(), 'frontend_options' => $p->frontendOptions(), 'default_frontend' => $p->defaultFrontend(), 'supports_custom_admin' => $p->supportsCustomAdmin(), 'annual_license_fee_usd' => $p->annualLicenseFeeUsd(), ], WebDesignPlatform::cases()); return response()->json([ 'error' => false, 'data' => $platforms, ]); } private function validateQuote(Request $request): array { $platform = WebDesignPlatform::tryFrom((string) $request->get('platform')); $validated = $request->validate([ 'status' => ['nullable', 'string', Rule::in(WebDesignQuoteStatus::values())], 'customer_name' => 'required|string|max:150', 'customer_email' => 'nullable|email|max:150', 'customer_phone' => 'nullable|string|max:30', 'customer_company' => 'nullable|string|max:200', 'website_name' => 'required|string|max:200', 'website_category' => 'required|string|max:100', 'description' => 'nullable|string|max:5000', 'requirements' => 'nullable|string|max:5000', 'reference_websites' => 'nullable|array|max:20', 'reference_websites.*' => 'string|max:500', 'platform' => ['required', 'string', Rule::in(WebDesignPlatform::values())], 'frontend_framework' => [ Rule::requiredIf(fn() => $platform && count($platform->frontendOptions()) > 0), 'nullable', 'string', Rule::in($platform?->frontendOptions() ?? []), ], 'custom_admin' => 'boolean', 'items' => 'required|array|min:1', 'items.*.name' => 'required|string|max:300', 'items.*.description' => 'nullable|string|max:1000', 'items.*.quantity' => 'required|numeric|min:0', 'items.*.unit_price' => 'required|numeric|min:0', 'discount' => 'nullable|numeric|min:0', 'vat_percent' => 'nullable|numeric|min:0|max:100', 'currency' => ['nullable', 'string', Rule::in(['VND', 'USD'])], 'valid_until' => 'nullable|date', 'note' => 'nullable|string|max:2000', ]); if ($platform && !$platform->supportsCustomAdmin()) { $validated['custom_admin'] = false; } if ($platform && count($platform->frontendOptions()) === 0) { $validated['frontend_framework'] = null; } return $validated; } private function applyTotals(array $data): array { $subtotal = collect($data['items']) ->sum(fn($item) => (float) $item['quantity'] * (float) $item['unit_price']); $discount = (float) ($data['discount'] ?? 0); $vatPercent = (float) ($data['vat_percent'] ?? 0); $taxable = max($subtotal - $discount, 0); $data['subtotal'] = round($subtotal, 2); $data['total'] = round($taxable + $taxable * $vatPercent / 100, 2); return $data; } }