feat: add GeneratesUniqueSlug class for unique slug generation
feat: create PostCategoryFactory for generating post category data feat: create PostFactory for generating post data feat: add retry_requested_at column to services table feat: add vcenter provisioning fields to products table feat: add module_host_id to os table feat: create email_notification_templates table for email templates feat: add scenario and format fields to template table feat: add admin fields to transactions table fix: update ticket table columns and types feat: add uuid to ticket table and backfill existing records fix: make ticket_reply user_id nullable feat: create notifications table for user notifications fix: make ticket uuid nullable feat: create post_categories table for blog categories feat: create posts table for blog posts feat: add invoice email template for PDF generation
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Addon;
|
||||
|
||||
use App\Enums\AddonType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Addon;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AddonController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 100);
|
||||
|
||||
$query = Addon::when($request->get('filter'), fn($q, $f) =>
|
||||
$q->where('name', 'like', "%{$f}%")
|
||||
)->when($request->filled('type'), fn($q) =>
|
||||
$q->where('type', $request->get('type'))
|
||||
)->when($request->filled('status'), fn($q) =>
|
||||
$q->where('status', $request->boolean('status'))
|
||||
)->orderBy('sort_order')->orderBy('name');
|
||||
|
||||
if ($request->boolean('all')) {
|
||||
$items = $query->get();
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $items,
|
||||
]);
|
||||
}
|
||||
|
||||
$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
|
||||
{
|
||||
$addon = Addon::find($id);
|
||||
|
||||
if (!$addon) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy addon.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $addon,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['required', 'string', Rule::in(AddonType::values())],
|
||||
'code' => 'required|string|max:50|unique:product_addon,code',
|
||||
'name' => 'required|string|max:100',
|
||||
'prefix' => 'nullable|string|max:50',
|
||||
'min' => 'required|integer|min:0',
|
||||
'max' => 'required|integer|min:0',
|
||||
'step' => 'required|integer|min:1',
|
||||
'price' => 'required|integer|min:0',
|
||||
'input_type' => ['required', 'string', Rule::in(['number', 'slider', 'select'])],
|
||||
'status' => 'boolean',
|
||||
'sort_order' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$addon = Addon::create($validated);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Tạo addon thành công.',
|
||||
'data' => $addon,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$addon = Addon::find($id);
|
||||
|
||||
if (!$addon) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy addon.'], 404);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'type' => ['sometimes', 'required', 'string', Rule::in(AddonType::values())],
|
||||
'code' => ['sometimes', 'required', 'string', 'max:50', Rule::unique('product_addon', 'code')->ignore($id)],
|
||||
'name' => 'sometimes|required|string|max:100',
|
||||
'prefix' => 'nullable|string|max:50',
|
||||
'min' => 'sometimes|required|integer|min:0',
|
||||
'max' => 'sometimes|required|integer|min:0',
|
||||
'step' => 'sometimes|required|integer|min:1',
|
||||
'price' => 'sometimes|required|integer|min:0',
|
||||
'input_type' => ['sometimes', 'required', 'string', Rule::in(['number', 'slider', 'select'])],
|
||||
'status' => 'boolean',
|
||||
'sort_order' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$addon->update($validated);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật addon thành công.',
|
||||
'data' => $addon->fresh(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$addon = Addon::find($id);
|
||||
|
||||
if (!$addon) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy addon.'], 404);
|
||||
}
|
||||
|
||||
$addon->delete();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Xoá addon thành công.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function bulkUpdateStatus(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer',
|
||||
'status' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$updated = Addon::whereIn('id', $validated['ids'])
|
||||
->update(['status' => $validated['status']]);
|
||||
|
||||
$label = $validated['status'] ? 'kích hoạt' : 'ẩn';
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => "Đã {$label} {$updated} addon.",
|
||||
]);
|
||||
}
|
||||
|
||||
public function bulkDestroy(Request $request): JsonResponse
|
||||
{
|
||||
$ids = $request->validate(['ids' => 'required|array', 'ids.*' => 'integer'])['ids'];
|
||||
|
||||
$deleted = Addon::whereIn('id', $ids)->delete();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => "Đã xoá {$deleted} addon.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\Log;
|
||||
use App\Repositories\Customer\CustomerRepositoryInterface;
|
||||
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
||||
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
|
||||
@@ -23,6 +24,7 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CustomerController extends Controller
|
||||
@@ -370,6 +372,87 @@ class CustomerController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function addTotal(Request $request, string $uuid): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'amount' => 'required|numeric|min:1',
|
||||
'note' => 'required|string|max:1000',
|
||||
]);
|
||||
|
||||
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
||||
|
||||
if (!$customer) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find customer by UUID #" . $uuid,
|
||||
], 404);
|
||||
}
|
||||
|
||||
$admin = Auth::guard('admin')->user();
|
||||
if (!$admin) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Unauthorized',
|
||||
], 401);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$amount = (int) $request->amount;
|
||||
|
||||
$transaction = Transaction::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'trans' => 'ADMIN' . now()->format('YmdHis'),
|
||||
'amount' => $amount,
|
||||
'customer_id' => $customer->id,
|
||||
'user_id' => $admin->id,
|
||||
'status' => 1,
|
||||
'type' => 'admin_add_total',
|
||||
'note' => $request->string('note')->trim()->toString(),
|
||||
]);
|
||||
|
||||
$customer->update([
|
||||
'total' => (int) $customer->total + $amount,
|
||||
]);
|
||||
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => $request->ip(),
|
||||
'description' => sprintf(
|
||||
'%s cộng tiền trực tiếp cho customer %s: %s VNĐ - %s',
|
||||
$admin->username ?? $admin->email,
|
||||
$customer->email,
|
||||
number_format($amount),
|
||||
$transaction->note,
|
||||
),
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Total updated successfully',
|
||||
'data' => [
|
||||
'transaction' => [
|
||||
'uuid' => $transaction->uuid,
|
||||
'amount' => $transaction->amount,
|
||||
'note' => $transaction->note,
|
||||
'admin' => $admin->username ?? $admin->email,
|
||||
],
|
||||
],
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getProducts(string $uuid): JsonResponse
|
||||
{
|
||||
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
||||
@@ -446,7 +529,10 @@ class CustomerController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
$transactions = $this->transactionRepository->filter(['customer_id' => $customer->id]);
|
||||
$transactions = $this->transactionRepository->filter(
|
||||
['customer_id' => $customer->id],
|
||||
['user']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Docs;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\PostCategoryStoreRequest;
|
||||
use App\Http\Requests\Admin\PostCategoryUpdateRequest;
|
||||
use App\Http\Resources\Api\PostCategoryResource;
|
||||
use App\Http\Services\NextjsRevalidationService;
|
||||
use App\Models\PostCategory;
|
||||
use App\Support\GeneratesUniqueSlug;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostCategoryController extends Controller
|
||||
{
|
||||
public function __construct(private readonly NextjsRevalidationService $revalidation) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 100);
|
||||
|
||||
$query = PostCategory::query()
|
||||
->withCount('posts')
|
||||
->when($request->filled('keyword'), fn ($q) => $q->where('name', 'like', '%' . $request->string('keyword') . '%'))
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name');
|
||||
|
||||
if ($request->boolean('all')) {
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => PostCategoryResource::collection($query->get()),
|
||||
]);
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => PostCategoryResource::collection($paginator->items()),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PostCategoryStoreRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$data['slug'] = $data['slug'] ?: GeneratesUniqueSlug::forModel(PostCategory::class, $data['name']);
|
||||
|
||||
if ($request->filled('slug')) {
|
||||
$data['slug'] = GeneratesUniqueSlug::forModel(PostCategory::class, $data['slug']);
|
||||
}
|
||||
|
||||
$category = PostCategory::create($data);
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Tạo danh mục thành công.',
|
||||
'data' => new PostCategoryResource($category),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$category = PostCategory::withCount('posts')->find($id);
|
||||
|
||||
if (!$category) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy danh mục.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new PostCategoryResource($category),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(PostCategoryUpdateRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$category = PostCategory::find($id);
|
||||
|
||||
if (!$category) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy danh mục.'], 404);
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
$slugSource = $data['slug'] ?: $data['name'];
|
||||
$data['slug'] = GeneratesUniqueSlug::forModel(PostCategory::class, $slugSource, $category->id);
|
||||
|
||||
$category->update($data);
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật danh mục thành công.',
|
||||
'data' => new PostCategoryResource($category->fresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$category = PostCategory::withCount('posts')->find($id);
|
||||
|
||||
if (!$category) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy danh mục.'], 404);
|
||||
}
|
||||
|
||||
if ($category->posts_count > 0) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Không thể xoá danh mục đang có bài viết.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$category->delete();
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Xoá danh mục thành công.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Docs;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\PostStoreRequest;
|
||||
use App\Http\Requests\Admin\PostUpdateRequest;
|
||||
use App\Http\Resources\Api\PostResource;
|
||||
use App\Http\Services\DocsWordpressSyncService;
|
||||
use App\Http\Services\NextjsRevalidationService;
|
||||
use App\Models\Post;
|
||||
use App\Support\GeneratesUniqueSlug;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly NextjsRevalidationService $revalidation,
|
||||
private readonly DocsWordpressSyncService $wordpressSync,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$perPage = min((int) $request->get('per_page', 15), 100);
|
||||
$sort = $request->string('sort', 'latest')->toString();
|
||||
|
||||
$query = Post::query()
|
||||
->with('category')
|
||||
->when(
|
||||
$request->filled('keyword'),
|
||||
fn ($q) => $q->where(function ($subQuery) use ($request) {
|
||||
$keyword = $request->string('keyword')->toString();
|
||||
$subQuery
|
||||
->where('title', 'like', "%{$keyword}%")
|
||||
->orWhere('excerpt', 'like', "%{$keyword}%")
|
||||
->orWhere('content', 'like', "%{$keyword}%");
|
||||
})
|
||||
)
|
||||
->when($request->filled('category_id'), fn ($q) => $q->where('post_category_id', (int) $request->get('category_id')))
|
||||
->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')->toString()))
|
||||
->when($request->filled('is_featured'), fn ($q) => $q->where('is_featured', $request->boolean('is_featured')));
|
||||
|
||||
match ($sort) {
|
||||
'title_asc' => $query->orderBy('title'),
|
||||
'title_desc' => $query->orderByDesc('title'),
|
||||
'published_at_asc', 'oldest' => $query->orderBy('published_at')->orderBy('id'),
|
||||
'most_viewed' => $query->orderByDesc('view_count')->orderByDesc('id'),
|
||||
default => $query->orderByDesc('published_at')->orderByDesc('id'),
|
||||
};
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => PostResource::collection($paginator->items()),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PostStoreRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$slugSource = $data['slug'] ?: $data['title'];
|
||||
$data['slug'] = GeneratesUniqueSlug::forModel(Post::class, $slugSource);
|
||||
$data['is_featured'] = (bool) ($data['is_featured'] ?? false);
|
||||
$data['view_count'] = (int) ($data['view_count'] ?? 0);
|
||||
|
||||
$post = Post::create($data);
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Tạo bài viết thành công.',
|
||||
'data' => new PostResource($post->load('category')),
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$post = Post::with('category')->find($id);
|
||||
|
||||
if (!$post) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy bài viết.'], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new PostResource($post),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(PostUpdateRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$post = Post::find($id);
|
||||
|
||||
if (!$post) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy bài viết.'], 404);
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
$slugSource = $data['slug'] ?: $data['title'];
|
||||
$data['slug'] = GeneratesUniqueSlug::forModel(Post::class, $slugSource, $post->id);
|
||||
$data['is_featured'] = (bool) ($data['is_featured'] ?? false);
|
||||
$data['view_count'] = (int) ($data['view_count'] ?? $post->view_count);
|
||||
|
||||
$post->update($data);
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật bài viết thành công.',
|
||||
'data' => new PostResource($post->fresh()->load('category')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$post = Post::find($id);
|
||||
|
||||
if (!$post) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy bài viết.'], 404);
|
||||
}
|
||||
|
||||
$post->delete();
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Xoá bài viết thành công.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncWordpress(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'source_url' => 'nullable|url',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
'max_pages' => 'nullable|integer|min:1|max:200',
|
||||
]);
|
||||
|
||||
$result = $this->wordpressSync->sync(
|
||||
$validated['source_url'] ?? null,
|
||||
(int) ($validated['per_page'] ?? 20),
|
||||
isset($validated['max_pages']) ? (int) $validated['max_pages'] : null,
|
||||
);
|
||||
|
||||
$this->revalidation->revalidateDocs();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => "Đồng bộ hoàn tất. Tạo mới {$result['created']}, cập nhật {$result['updated']}, bỏ qua {$result['skipped']}.",
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailNotificationTemplate;
|
||||
use App\Services\EmailNotificationTemplateRenderer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailNotificationTemplateController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmailNotificationTemplateRenderer $renderer,
|
||||
) {}
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$templates = EmailNotificationTemplate::orderByDesc('is_active')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return response()->json(['error' => false, 'data' => $templates]);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$tpl = EmailNotificationTemplate::findOrFail($id);
|
||||
$tpl['block_field_definitions'] = EmailNotificationTemplate::blockFieldDefinitions();
|
||||
return response()->json(['error' => false, 'data' => $tpl]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'type' => 'required|string|in:invoice_notification,vm_credentials',
|
||||
'subject' => 'required|string|max:500',
|
||||
'header_color' => 'nullable|string|max:20',
|
||||
'header_title' => 'nullable|string|max:255',
|
||||
'blocks' => 'nullable|array',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
// Use default blocks if none provided
|
||||
if (empty($validated['blocks'])) {
|
||||
$validated['blocks'] = EmailNotificationTemplate::defaultBlocks($validated['type']);
|
||||
}
|
||||
|
||||
// If activating this template, deactivate others of the same type
|
||||
if ($validated['is_active'] ?? true) {
|
||||
EmailNotificationTemplate::where('type', $validated['type'])->update(['is_active' => false]);
|
||||
}
|
||||
|
||||
$tpl = EmailNotificationTemplate::create($validated);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $tpl], 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tpl = EmailNotificationTemplate::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'sometimes|string|max:255',
|
||||
'subject' => 'sometimes|string|max:500',
|
||||
'header_color' => 'nullable|string|max:20',
|
||||
'header_title' => 'nullable|string|max:255',
|
||||
'blocks' => 'nullable|array',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
// If activating this template, deactivate others of the same type
|
||||
if (!empty($validated['is_active'])) {
|
||||
EmailNotificationTemplate::where('type', $tpl->type)
|
||||
->where('id', '!=', $id)
|
||||
->update(['is_active' => false]);
|
||||
}
|
||||
|
||||
$tpl->update($validated);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $tpl->fresh()]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
EmailNotificationTemplate::findOrFail($id)->delete();
|
||||
return response()->json(['error' => false, 'message' => 'Đã xoá template.']);
|
||||
}
|
||||
|
||||
public function preview(int $id): JsonResponse
|
||||
{
|
||||
$tpl = EmailNotificationTemplate::findOrFail($id);
|
||||
$html = $this->renderer->renderPreview($tpl);
|
||||
return response()->json(['error' => false, 'html' => $html]);
|
||||
}
|
||||
|
||||
public function fieldDefinitions(): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'block_types' => $this->blockTypeDefinitions(),
|
||||
'block_field_definitions' => EmailNotificationTemplate::blockFieldDefinitions(),
|
||||
'default_blocks' => EmailNotificationTemplate::defaultBlocks(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function blockTypeDefinitions(): array
|
||||
{
|
||||
return [
|
||||
// Dùng cho cả 2 loại
|
||||
['type' => 'greeting', 'label' => 'Lời chào', 'description' => 'Chào hỏi khách hàng theo tên', 'for' => ['invoice_notification', 'vm_credentials']],
|
||||
['type' => 'custom_text', 'label' => 'Văn bản tùy chỉnh', 'description' => 'Nội dung HTML hoặc text tự do', 'for' => ['invoice_notification', 'vm_credentials']],
|
||||
['type' => 'footer', 'label' => 'Chữ ký kết thúc', 'description' => 'Trân trọng + tên công ty', 'for' => ['invoice_notification', 'vm_credentials']],
|
||||
// invoice_notification
|
||||
['type' => 'invoice_info', 'label' => 'Thông tin hóa đơn', 'description' => 'Số HĐ, ngày tạo, tổng tiền, trạng thái...', 'for' => ['invoice_notification']],
|
||||
['type' => 'customer_info', 'label' => 'Thông tin khách hàng', 'description' => 'Tên, email, địa chỉ khách hàng', 'for' => ['invoice_notification']],
|
||||
['type' => 'order_items', 'label' => 'Chi tiết sản phẩm', 'description' => 'Bảng sản phẩm từ đơn hàng', 'for' => ['invoice_notification']],
|
||||
['type' => 'attachments', 'label' => 'File đính kèm', 'description' => 'Danh sách file đính kèm hóa đơn', 'for' => ['invoice_notification']],
|
||||
// vm_credentials
|
||||
['type' => 'vm_credentials', 'label' => 'Thông tin truy cập VPS', 'description' => 'IP, username, port, password, OS', 'for' => ['vm_credentials']],
|
||||
['type' => 'product_info', 'label' => 'Thông tin dịch vụ', 'description' => 'Tên sản phẩm, chu kỳ, giá, hạn dùng', 'for' => ['vm_credentials']],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\Admin\EmailTemplate;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -10,36 +11,53 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmailTemplateController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$templates = Template::orderBy('type_id')->orderBy('id')->get();
|
||||
$query = Template::with('invoiceTemplate:id,name,scenario')
|
||||
->orderBy('scenario')
|
||||
->orderBy('id');
|
||||
|
||||
if ($request->filled('scenario')) {
|
||||
$query->where('scenario', $request->scenario);
|
||||
}
|
||||
|
||||
$templates = $query->get()->map(fn($t) => $this->format($t));
|
||||
|
||||
return response()->json(['error' => false, 'data' => $templates]);
|
||||
}
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$template = Template::findOrFail($id);
|
||||
|
||||
return response()->json(['error' => false, 'data' => $template]);
|
||||
$template = Template::with('invoiceTemplate:id,name,scenario')->findOrFail($id);
|
||||
return response()->json(['error' => false, 'data' => $this->format($template)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'type_id' => 'required|integer|min:1',
|
||||
'name' => 'nullable|string|max:100',
|
||||
'subject' => 'nullable|string|max:100',
|
||||
'message' => 'nullable|string',
|
||||
'status' => 'boolean',
|
||||
'name' => 'nullable|string|max:255',
|
||||
'scenario' => 'nullable|string|in:vm_credentials,renewal_reminder,news_notification,invoice_pdf',
|
||||
'format' => 'nullable|in:html,json',
|
||||
'invoice_template_id' => 'nullable|integer|exists:template,id',
|
||||
'subject' => 'nullable|string|max:255',
|
||||
'message' => 'nullable|string',
|
||||
'status' => 'boolean',
|
||||
]);
|
||||
|
||||
$template = DB::transaction(fn () => Template::create($data));
|
||||
$template = DB::transaction(fn() => Template::create([
|
||||
'type_id' => 0,
|
||||
'name' => $data['name'] ?? null,
|
||||
'scenario' => $data['scenario'] ?? null,
|
||||
'format' => $data['format'] ?? 'html',
|
||||
'invoice_template_id' => $data['invoice_template_id'] ?? null,
|
||||
'subject' => $data['subject'] ?? null,
|
||||
'message' => $data['message'] ?? null,
|
||||
'status' => $data['status'] ?? 1,
|
||||
]));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Thêm email template thành công',
|
||||
'data' => $template,
|
||||
'error' => false, 'message' => 'Tạo template thành công.',
|
||||
'data' => $this->format($template),
|
||||
], 201);
|
||||
}
|
||||
|
||||
@@ -48,31 +66,90 @@ class EmailTemplateController extends Controller
|
||||
$template = Template::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'type_id' => 'sometimes|integer|min:1',
|
||||
'name' => 'sometimes|nullable|string|max:100',
|
||||
'subject' => 'sometimes|nullable|string|max:100',
|
||||
'message' => 'sometimes|nullable|string',
|
||||
'status' => 'sometimes|boolean',
|
||||
'name' => 'sometimes|nullable|string|max:255',
|
||||
'scenario' => 'sometimes|nullable|string|in:vm_credentials,renewal_reminder,news_notification,invoice_pdf',
|
||||
'format' => 'sometimes|nullable|in:html,json',
|
||||
'invoice_template_id' => 'sometimes|nullable|integer|exists:template,id',
|
||||
'subject' => 'sometimes|nullable|string|max:255',
|
||||
'message' => 'sometimes|nullable|string',
|
||||
'status' => 'sometimes|boolean',
|
||||
]);
|
||||
|
||||
DB::transaction(fn () => $template->update($data));
|
||||
DB::transaction(fn() => $template->update($data));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật email template thành công',
|
||||
'data' => $template->fresh(),
|
||||
'error' => false, 'message' => 'Cập nhật template thành công.',
|
||||
'data' => $this->format($template->fresh(['invoiceTemplate'])),
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$template = Template::findOrFail($id);
|
||||
DB::transaction(fn() => Template::findOrFail($id)->delete());
|
||||
return response()->json(['error' => false, 'message' => 'Đã xoá template.']);
|
||||
}
|
||||
|
||||
DB::transaction(fn () => $template->delete());
|
||||
// ── Email settings (scenario assignments) ─────────────────────────────────
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã xoá email template',
|
||||
public function getEmailSettings(): JsonResponse
|
||||
{
|
||||
$setting = Setting::where('key', 'email_settings')->first();
|
||||
$value = $setting ? json_decode($setting->value, true) : [];
|
||||
|
||||
return response()->json(['error' => false, 'data' => $value]);
|
||||
}
|
||||
|
||||
public function updateEmailSettings(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'vm_template_id' => 'nullable|integer|exists:template,id',
|
||||
'renewal_template_id' => 'nullable|integer|exists:template,id',
|
||||
'news_template_id' => 'nullable|integer|exists:template,id',
|
||||
'invoice_template_id' => 'nullable|integer|exists:template,id',
|
||||
]);
|
||||
|
||||
$setting = Setting::firstOrNew(['key' => 'email_settings']);
|
||||
$current = $setting->value ? json_decode($setting->value, true) : [];
|
||||
$setting->value = json_encode(array_merge($current, $data));
|
||||
$setting->save();
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã lưu cài đặt email.', 'data' => json_decode($setting->value, true)]);
|
||||
}
|
||||
|
||||
// ── Invoice PDF preview ───────────────────────────────────────────────────
|
||||
|
||||
public function previewInvoiceJson(int $id): JsonResponse
|
||||
{
|
||||
$template = Template::findOrFail($id);
|
||||
if ($template->scenario !== 'invoice_pdf' || $template->format !== 'json') {
|
||||
return response()->json(['error' => true, 'message' => 'Template này không phải invoice JSON.'], 422);
|
||||
}
|
||||
|
||||
$config = json_decode($template->message ?? '{}', true);
|
||||
// Return the config + blade HTML preview
|
||||
$html = view('pdf.invoice-preview', compact('config'))->render();
|
||||
|
||||
return response()->json(['error' => false, 'html' => $html]);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private function format(Template $t): array
|
||||
{
|
||||
return [
|
||||
'id' => $t->id,
|
||||
'type_id' => $t->type_id,
|
||||
'name' => $t->name,
|
||||
'scenario' => $t->scenario,
|
||||
'scenario_label' => Template::SCENARIOS[$t->scenario] ?? null,
|
||||
'format' => $t->format ?? 'html',
|
||||
'invoice_template_id' => $t->invoice_template_id,
|
||||
'invoice_template_name' => $t->invoiceTemplate?->name,
|
||||
'subject' => $t->subject,
|
||||
'message' => $t->message,
|
||||
'status' => $t->status,
|
||||
'created_at' => $t->created_at,
|
||||
'updated_at' => $t->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,19 @@ class OsController extends Controller
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'version' => 'required|string|max:100',
|
||||
'os_group_id' => 'required|integer|exists:os_group,id',
|
||||
'template_id' => 'nullable|string|max:255',
|
||||
'is_active' => 'boolean',
|
||||
'version' => 'required|string|max:100',
|
||||
'os_group_id' => 'required|integer|exists:os_group,id',
|
||||
'template_id' => 'nullable|string|max:255',
|
||||
'module_host_id' => 'nullable|integer|exists:module_host,id',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$os = Os::create([
|
||||
'version' => $validated['version'],
|
||||
'os_group_id' => $validated['os_group_id'],
|
||||
'template_id' => $validated['template_id'] ?? null,
|
||||
'status' => ($validated['is_active'] ?? true) ? 1 : 0,
|
||||
'version' => $validated['version'],
|
||||
'os_group_id' => $validated['os_group_id'],
|
||||
'template_id' => $validated['template_id'] ?? null,
|
||||
'module_host_id' => $validated['module_host_id'] ?? null,
|
||||
'status' => ($validated['is_active'] ?? true) ? 1 : 0,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
@@ -42,17 +44,19 @@ class OsController extends Controller
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'version' => 'sometimes|required|string|max:100',
|
||||
'os_group_id' => 'sometimes|integer|exists:os_group,id',
|
||||
'template_id' => 'nullable|string|max:255',
|
||||
'is_active' => 'boolean',
|
||||
'version' => 'sometimes|required|string|max:100',
|
||||
'os_group_id' => 'sometimes|integer|exists:os_group,id',
|
||||
'template_id' => 'nullable|string|max:255',
|
||||
'module_host_id' => 'nullable|integer|exists:module_host,id',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$os->update([
|
||||
'version' => $validated['version'] ?? $os->version,
|
||||
'os_group_id' => $validated['os_group_id'] ?? $os->os_group_id,
|
||||
'template_id' => array_key_exists('template_id', $validated) ? $validated['template_id'] : $os->template_id,
|
||||
'status' => isset($validated['is_active']) ? ($validated['is_active'] ? 1 : 0) : $os->status,
|
||||
'version' => $validated['version'] ?? $os->version,
|
||||
'os_group_id' => $validated['os_group_id'] ?? $os->os_group_id,
|
||||
'template_id' => array_key_exists('template_id', $validated) ? $validated['template_id'] : $os->template_id,
|
||||
'module_host_id' => array_key_exists('module_host_id', $validated) ? $validated['module_host_id'] : $os->module_host_id,
|
||||
'status' => isset($validated['is_active']) ? ($validated['is_active'] ? 1 : 0) : $os->status,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
@@ -79,12 +83,19 @@ class OsController extends Controller
|
||||
|
||||
private function formatOs(Os $os): array
|
||||
{
|
||||
$os->loadMissing('host.datacenter.vCenter');
|
||||
$host = $os->host;
|
||||
|
||||
return [
|
||||
'id' => $os->id,
|
||||
'version' => $os->version,
|
||||
'template_id' => $os->template_id,
|
||||
'is_active' => (bool) $os->status,
|
||||
'os_group_id' => $os->os_group_id,
|
||||
'id' => $os->id,
|
||||
'version' => $os->version,
|
||||
'template_id' => $os->template_id,
|
||||
'is_active' => (bool) $os->status,
|
||||
'os_group_id' => $os->os_group_id,
|
||||
'module_host_id' => $os->module_host_id,
|
||||
'host_name' => $host ? $host->name : null,
|
||||
'datacenter_name' => $host?->datacenter?->name,
|
||||
'vcenter_name' => $host?->datacenter?->vCenter?->host,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class OsGroupController extends Controller
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = OsGroup::withCount('os')
|
||||
->with(['os' => fn($q) => $q->orderBy('version')])
|
||||
->with(['os' => fn($q) => $q->with('host.datacenter.vCenter')->orderBy('version')])
|
||||
->when(
|
||||
$request->get('filter'),
|
||||
fn($q, $f) => $q->where('name', 'like', "%{$f}%")
|
||||
@@ -50,7 +50,7 @@ class OsGroupController extends Controller
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$group = OsGroup::withCount('os')
|
||||
->with(['os' => fn($q) => $q->orderBy('version')])
|
||||
->with(['os' => fn($q) => $q->with('host.datacenter.vCenter')->orderBy('version')])
|
||||
->find($id);
|
||||
|
||||
if (! $group) {
|
||||
@@ -141,12 +141,18 @@ class OsGroupController extends Controller
|
||||
|
||||
private function formatOs(\App\Models\Os $os): array
|
||||
{
|
||||
$host = $os->relationLoaded('host') ? $os->host : null;
|
||||
|
||||
return [
|
||||
'id' => $os->id,
|
||||
'version' => $os->version,
|
||||
'template_id' => $os->template_id,
|
||||
'is_active' => (bool) $os->status,
|
||||
'os_group_id' => $os->os_group_id,
|
||||
'id' => $os->id,
|
||||
'version' => $os->version,
|
||||
'template_id' => $os->template_id,
|
||||
'is_active' => (bool) $os->status,
|
||||
'os_group_id' => $os->os_group_id,
|
||||
'module_host_id' => $os->module_host_id,
|
||||
'host_name' => $host?->name,
|
||||
'datacenter_name' => $host?->datacenter?->name,
|
||||
'vcenter_name' => $host?->datacenter?->vCenter?->host,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Http\Controllers\Api\Admin\Product;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\Admin\ProductResource;
|
||||
use App\Http\Services\NextjsRevalidationService;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPricings;
|
||||
use App\Repositories\Admin\Product\AdminProductRepositoryInterface;
|
||||
use App\Models\Os;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -15,6 +17,7 @@ class ProductController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminProductRepositoryInterface $products,
|
||||
private readonly NextjsRevalidationService $revalidation,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
@@ -67,10 +70,19 @@ class ProductController extends Controller
|
||||
'ram' => 'nullable|numeric|min:0',
|
||||
'disk' => 'nullable|numeric|min:0',
|
||||
'bandwidth' => 'nullable|numeric|min:0',
|
||||
'billing_type' => 'nullable|string|in:recurring,onetime',
|
||||
'currency' => 'nullable|string|max:10',
|
||||
'price_monthly' => 'nullable|numeric|min:0',
|
||||
'price_quarterly' => 'nullable|numeric|min:0',
|
||||
'price_semiannually' => 'nullable|numeric|min:0',
|
||||
'price_annually' => 'nullable|numeric|min:0',
|
||||
'price_biennially' => 'nullable|numeric|min:0',
|
||||
'price_triennially' => 'nullable|numeric|min:0',
|
||||
'price_onetime' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
$productType = $validated['product_type'] ?? null;
|
||||
|
||||
|
||||
if (!$productType && !empty($validated['product_categories_id'])) {
|
||||
$category = \App\Models\ProductCategories::find($validated['product_categories_id']);
|
||||
$productType = $category?->type;
|
||||
@@ -89,6 +101,9 @@ class ProductController extends Controller
|
||||
'bandwidth' => $validated['bandwidth'] ?? null,
|
||||
]);
|
||||
|
||||
$this->savePricings($product, $validated);
|
||||
$this->revalidation->revalidateProducts();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Tạo sản phẩm thành công.',
|
||||
@@ -116,6 +131,17 @@ class ProductController extends Controller
|
||||
'bandwidth' => 'nullable|numeric|min:0',
|
||||
'os_ids' => 'nullable|array',
|
||||
'os_ids.*' => 'integer|exists:os,id',
|
||||
'module_host_id' => 'nullable|integer|exists:module_host,id',
|
||||
'module_datastore_id' => 'nullable|integer|exists:module_datastore,id',
|
||||
'billing_type' => 'nullable|string|in:recurring,onetime',
|
||||
'currency' => 'nullable|string|max:10',
|
||||
'price_monthly' => 'nullable|numeric|min:0',
|
||||
'price_quarterly' => 'nullable|numeric|min:0',
|
||||
'price_semiannually' => 'nullable|numeric|min:0',
|
||||
'price_annually' => 'nullable|numeric|min:0',
|
||||
'price_biennially' => 'nullable|numeric|min:0',
|
||||
'price_triennially' => 'nullable|numeric|min:0',
|
||||
'price_onetime' => 'nullable|numeric|min:0',
|
||||
]);
|
||||
|
||||
if (isset($validated['name']) && $validated['name'] !== $product->name) {
|
||||
@@ -138,13 +164,49 @@ class ProductController extends Controller
|
||||
$product->operatingSystems()->sync($osIds);
|
||||
}
|
||||
|
||||
$this->savePricings($product, $validated);
|
||||
$this->revalidation->revalidateProducts();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật sản phẩm thành công.',
|
||||
'data' => new ProductResource($product->fresh(['pricings', 'category'])),
|
||||
'data' => new ProductResource($product->fresh(['pricings', 'category', 'operatingSystems'])),
|
||||
]);
|
||||
}
|
||||
|
||||
private function savePricings(Product $product, array $data): void
|
||||
{
|
||||
$currency = $data['currency'] ?? 'VND';
|
||||
$cycles = [
|
||||
'monthly' => 'price_monthly',
|
||||
'quarterly' => 'price_quarterly',
|
||||
'semiannually' => 'price_semiannually',
|
||||
'annually' => 'price_annually',
|
||||
'biennially' => 'price_biennially',
|
||||
'triennially' => 'price_triennially',
|
||||
'onetime' => 'price_onetime',
|
||||
];
|
||||
|
||||
foreach ($cycles as $cycle => $field) {
|
||||
if (!array_key_exists($field, $data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$price = $data[$field];
|
||||
|
||||
if ($price !== null && $price !== '') {
|
||||
ProductPricings::updateOrCreate(
|
||||
['product_id' => $product->id, 'billing_cycle' => $cycle],
|
||||
['price' => $price, 'currency' => $currency, 'is_active' => true]
|
||||
);
|
||||
} else {
|
||||
ProductPricings::where('product_id', $product->id)
|
||||
->where('billing_cycle', $cycle)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$product = Product::find($id);
|
||||
@@ -154,6 +216,7 @@ class ProductController extends Controller
|
||||
}
|
||||
|
||||
$product->delete();
|
||||
$this->revalidation->revalidateProducts();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
@@ -248,15 +311,19 @@ class ProductController extends Controller
|
||||
|
||||
public function getOsList(): JsonResponse
|
||||
{
|
||||
$osList = Os::with('osGroup')
|
||||
$osList = Os::with(['osGroup', 'host.datacenter.vCenter'])
|
||||
->where('status', 1)
|
||||
->get()
|
||||
->map(fn($os) => [
|
||||
'id' => $os->id,
|
||||
'name' => $os->name,
|
||||
'version' => $os->version,
|
||||
'os_group_name' => $os->osGroup?->name,
|
||||
'template_id' => $os->template_id,
|
||||
'id' => $os->id,
|
||||
'name' => $os->name,
|
||||
'version' => $os->version,
|
||||
'os_group_name' => $os->osGroup?->name,
|
||||
'template_id' => $os->template_id,
|
||||
'module_host_id' => $os->module_host_id,
|
||||
'host_name' => $os->host?->name,
|
||||
'datacenter_name' => $os->host?->datacenter?->name,
|
||||
'vcenter_name' => $os->host?->datacenter?->vCenter?->host,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Ticket;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Events\TicketReplyCreated;
|
||||
use App\Http\Resources\Api\Ticket\TicketResource;
|
||||
use App\Http\Resources\Api\Ticket\TicketReplyResource;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Ticket;
|
||||
use App\Repositories\Ticket\TicketRepositoryInterface;
|
||||
use App\Repositories\TicketReply\TicketReplyRepositoryInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected TicketRepositoryInterface $ticketRepository,
|
||||
protected TicketReplyRepositoryInterface $ticketReplyRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* List all tickets (paginated, filterable by status / customer_id / search).
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Ticket::query()
|
||||
->with(['customer'])
|
||||
->latest('id');
|
||||
|
||||
if ($request->filled('status')) {
|
||||
$query->where('status', $request->input('status'));
|
||||
}
|
||||
|
||||
if ($request->filled('customer_id')) {
|
||||
$query->where('customer_id', $request->input('customer_id'));
|
||||
}
|
||||
|
||||
if ($request->filled('search')) {
|
||||
$query->where('subject', 'like', '%' . $request->input('search') . '%');
|
||||
}
|
||||
|
||||
$paginated = $query->paginate($request->input('per_page', 15));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => TicketResource::collection($paginated->items()),
|
||||
'total' => $paginated->total(),
|
||||
'per_page' => $paginated->perPage(),
|
||||
'current_page' => $paginated->currentPage(),
|
||||
'last_page' => $paginated->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a ticket with its replies and customer info.
|
||||
*/
|
||||
public function show(string $ticket_num)
|
||||
{
|
||||
$ticket = Ticket::query()
|
||||
->with(['reply.user', 'customer'])
|
||||
->where('ticket_num', $ticket_num)
|
||||
->first();
|
||||
|
||||
if (!$ticket) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Ticket không tồn tại.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => new TicketResource($ticket),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ticket status.
|
||||
*/
|
||||
public function updateStatus(Request $request, string $ticket_num)
|
||||
{
|
||||
$request->validate([
|
||||
'status' => ['required', 'string', 'in:open,pending,resolved,closed'],
|
||||
]);
|
||||
|
||||
$ticket = Ticket::query()->where('ticket_num', $ticket_num)->first();
|
||||
|
||||
if (!$ticket) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Ticket không tồn tại.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$ticket->update(['status' => $request->input('status')]);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Trạng thái ticket đã được cập nhật.',
|
||||
'data' => new TicketResource($ticket->fresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin replies to a ticket.
|
||||
*/
|
||||
public function reply(Request $request, string $ticket_num)
|
||||
{
|
||||
$request->validate([
|
||||
'message' => ['required', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$admin = auth('admin')->user();
|
||||
|
||||
$ticket = Ticket::query()->where('ticket_num', $ticket_num)->first();
|
||||
|
||||
if (!$ticket) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Ticket không tồn tại.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$reply = $this->ticketReplyRepository->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => $admin->id,
|
||||
'message' => $request->input('message'),
|
||||
'customer_id' => $ticket->customer_id,
|
||||
]);
|
||||
|
||||
Notification::notifyCustomer($ticket->customer, [
|
||||
'type' => 'ticket_reply',
|
||||
'title' => 'Admin vừa phản hồi ticket của bạn',
|
||||
'body' => $ticket->ticket_num . ' - ' . $ticket->subject,
|
||||
'link' => '/tickets/' . $ticket->ticket_num,
|
||||
'data' => [
|
||||
'ticket_num' => $ticket->ticket_num,
|
||||
'reply_id' => $reply->id,
|
||||
],
|
||||
]);
|
||||
|
||||
// Update ticket status to pending after admin reply
|
||||
if ($ticket->status === 'open') {
|
||||
$ticket->update(['status' => 'pending']);
|
||||
}
|
||||
|
||||
$reply->load('user');
|
||||
|
||||
broadcast(new TicketReplyCreated(
|
||||
ticketNum: $ticket->ticket_num,
|
||||
customerId: $ticket->customer_id,
|
||||
replyId: $reply->id,
|
||||
));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Phản hồi đã được gửi.',
|
||||
'data' => new TicketReplyResource($reply),
|
||||
], 201);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Admin\Transaction;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Log;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -112,6 +113,18 @@ class AdminTransactionController extends Controller
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
Notification::notifyCustomer($customer, [
|
||||
'type' => 'deposit_success',
|
||||
'title' => 'Nạp tiền thành công',
|
||||
'body' => 'Giao dịch ' . $transaction->public_code . ' đã được duyệt.',
|
||||
'link' => '/lich-su-giao-dich/' . $transaction->id,
|
||||
'data' => [
|
||||
'transaction_uuid' => $transaction->uuid,
|
||||
'amount' => $transaction->amount,
|
||||
'public_code' => $transaction->public_code,
|
||||
],
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Reference in New Issue
Block a user