diff --git a/app/Http/Controllers/Api/Admin/Docs/PostCategoryController.php b/app/Http/Controllers/Api/Admin/Docs/PostCategoryController.php index bf9b2e8..abefce4 100644 --- a/app/Http/Controllers/Api/Admin/Docs/PostCategoryController.php +++ b/app/Http/Controllers/Api/Admin/Docs/PostCategoryController.php @@ -102,6 +102,75 @@ class PostCategoryController extends Controller ]); } + public function trashed(Request $request): JsonResponse + { + $perPage = min((int) $request->get('per_page', 15), 100); + + $paginator = PostCategory::onlyTrashed() + ->withCount('posts') + ->when( + $request->filled('keyword'), + fn ($q) => $q->where('name', 'like', '%' . $request->string('keyword')->toString() . '%') + ) + ->orderByDesc('deleted_at') + ->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 restore(int $id): JsonResponse + { + $category = PostCategory::onlyTrashed()->find($id); + + if (!$category) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy danh mục.'], 404); + } + + $category->restore(); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => 'Khôi phục danh mục thành công.']); + } + + public function forceDelete(int $id): JsonResponse + { + $category = PostCategory::onlyTrashed()->find($id); + + if (!$category) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy danh mục.'], 404); + } + + $category->forceDelete(); + + return response()->json(['error' => false, 'message' => 'Xoá vĩnh viễn danh mục thành công.']); + } + + public function bulkRestore(Request $request): JsonResponse + { + $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $count = PostCategory::onlyTrashed()->whereIn('id', $request->ids)->restore(); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => "Đã khôi phục {$count} danh mục."]); + } + + public function bulkForceDelete(Request $request): JsonResponse + { + $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $count = PostCategory::onlyTrashed()->whereIn('id', $request->ids)->forceDelete(); + + return response()->json(['error' => false, 'message' => "Đã xoá vĩnh viễn {$count} danh mục."]); + } + public function destroy(int $id): JsonResponse { $category = PostCategory::withCount('posts')->find($id); diff --git a/app/Http/Controllers/Api/Admin/Docs/PostController.php b/app/Http/Controllers/Api/Admin/Docs/PostController.php index 80f24e4..785fbfd 100644 --- a/app/Http/Controllers/Api/Admin/Docs/PostController.php +++ b/app/Http/Controllers/Api/Admin/Docs/PostController.php @@ -136,6 +136,97 @@ class PostController extends Controller ]); } + public function trashed(Request $request): JsonResponse + { + $perPage = min((int) $request->get('per_page', 15), 100); + + $paginator = Post::onlyTrashed() + ->with('category') + ->when( + $request->filled('keyword'), + fn ($q) => $q->where('title', 'like', '%' . $request->string('keyword')->toString() . '%') + ) + ->orderByDesc('deleted_at') + ->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 restore(int $id): JsonResponse + { + $post = Post::onlyTrashed()->find($id); + + if (!$post) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy bài viết.'], 404); + } + + $post->restore(); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => 'Khôi phục bài viết thành công.']); + } + + public function forceDelete(int $id): JsonResponse + { + $post = Post::onlyTrashed()->find($id); + + if (!$post) { + return response()->json(['error' => true, 'message' => 'Không tìm thấy bài viết.'], 404); + } + + $post->forceDelete(); + + return response()->json(['error' => false, 'message' => 'Xoá vĩnh viễn bài viết thành công.']); + } + + public function bulkRestore(Request $request): JsonResponse + { + $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $count = Post::onlyTrashed()->whereIn('id', $request->ids)->restore(); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => "Đã khôi phục {$count} bài viết."]); + } + + public function bulkForceDelete(Request $request): JsonResponse + { + $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $count = Post::onlyTrashed()->whereIn('id', $request->ids)->forceDelete(); + + return response()->json(['error' => false, 'message' => "Đã xoá vĩnh viễn {$count} bài viết."]); + } + + public function bulkDelete(Request $request): JsonResponse + { + $request->validate(['ids' => 'required|array|min:1', 'ids.*' => 'integer']); + $count = Post::whereIn('id', $request->ids)->delete(); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => "Đã xoá {$count} bài viết."]); + } + + public function bulkUpdateStatus(Request $request): JsonResponse + { + $request->validate([ + 'ids' => 'required|array|min:1', + 'ids.*' => 'integer', + 'status' => 'required|in:draft,published,hidden', + ]); + $count = Post::whereIn('id', $request->ids)->update(['status' => $request->status]); + $this->revalidation->revalidateDocs(); + + return response()->json(['error' => false, 'message' => "Đã cập nhật trạng thái {$count} bài viết."]); + } + public function syncWordpress(Request $request): JsonResponse { $validated = $request->validate([ diff --git a/app/Http/Controllers/Api/Admin/Ticket/TicketController.php b/app/Http/Controllers/Api/Admin/Ticket/TicketController.php index c3f82e7..b7c2825 100644 --- a/app/Http/Controllers/Api/Admin/Ticket/TicketController.php +++ b/app/Http/Controllers/Api/Admin/Ticket/TicketController.php @@ -110,7 +110,7 @@ class TicketController extends Controller public function reply(Request $request, string $ticket_num) { $request->validate([ - 'message' => ['required', 'string', 'max:2000'], + 'message' => ['required', 'string'], ]); $admin = auth('admin')->user(); diff --git a/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php b/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php index c49d7af..1e32ac3 100644 --- a/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php +++ b/app/Http/Controllers/Api/Admin/VCenter/VCenterController.php @@ -93,6 +93,7 @@ class VCenterController extends Controller ]); } + public function storeNode(Request $request, string $type): JsonResponse { $rules = match ($type) { @@ -187,9 +188,9 @@ class VCenterController extends Controller $allHosts = ModuleHost::with(['datastore', 'network'])->get()->groupBy('module_datacenter_id'); $allFolders = ModuleFolder::all()->groupBy('module_datacenter_id'); // VMs with a host assignment — grouped by host DB id - $allVMs = VirtualMachines::whereNotNull('host_id')->get()->groupBy('host_id'); + $allVMs = VirtualMachines::with('vmware')->whereNotNull('host_id')->get()->groupBy('host_id'); // VMs without a host assignment (synced before host_id was tracked) — grouped by datacenter DB id - $orphanVMs = VirtualMachines::whereNull('host_id')->get()->groupBy('datacenter_id'); + $orphanVMs = VirtualMachines::with('vmware')->whereNull('host_id')->get()->groupBy('datacenter_id'); $tree = $vcenters->map(function ($vc) use ($allHosts, $allFolders, $allVMs, $orphanVMs) { $dcGroups = ($vc->datacenters ?? collect())->groupBy('datacenter_id'); @@ -227,8 +228,8 @@ class VCenterController extends Controller 'uuid' => $vm->uuid, 'vm_id' => $vm->vm_id, 'ip' => $vm->ip, - 'username' => $vm->username, - 'password' => $vm->password, + 'username' => $vm->vmware?->user, + 'password' => $vm->vmware?->plain_password, 'power_state' => $vm->power_state, 'folder' => $vm->folder, 'cpu' => $vm->cpu, @@ -304,6 +305,8 @@ class VCenterController extends Controller 'uuid' => $vm->uuid, 'vm_id' => $vm->vm_id, 'ip' => $vm->ip, + 'username' => $vm->vmware?->user, + 'password' => $vm->vmware?->plain_password, 'power_state' => $vm->power_state, 'folder' => $vm->folder, 'cpu' => $vm->cpu, diff --git a/app/Http/Controllers/Api/Client/Ticket/TicketController.php b/app/Http/Controllers/Api/Client/Ticket/TicketController.php index 0040b1d..527dee5 100644 --- a/app/Http/Controllers/Api/Client/Ticket/TicketController.php +++ b/app/Http/Controllers/Api/Client/Ticket/TicketController.php @@ -5,6 +5,10 @@ namespace App\Http\Controllers\Api\Client\Ticket; use App\Http\Controllers\Controller; use App\Http\Resources\Api\Ticket\TicketResource; use App\Http\Resources\Api\Ticket\TicketReplyResource; +use App\Jobs\Ticket\TicketJobs; +use App\Mail\TicketCreated; +use App\Mail\Admin\TicketAdminNotification; +use App\Models\Setting; use App\Models\Ticket; use App\Models\Notification; use App\Models\TicketReply; @@ -12,6 +16,8 @@ use App\Events\TicketReplyCreated; use App\Repositories\Ticket\TicketRepositoryInterface; use App\Repositories\TicketReply\TicketReplyRepositoryInterface; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Mail; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; class TicketController extends Controller @@ -60,22 +66,41 @@ class TicketController extends Controller 'description' => ['required', 'string', 'min:20'], 'room' => ['nullable', 'string'], 'data' => ['nullable', 'array'], + 'images' => ['nullable', 'array', 'max:5'], + 'images.*' => ['image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'], ]); $customer = auth('client')->user(); $ticketNum = 'TKT-' . now()->format('Ymd') . '-' . strtoupper(Str::random(5)); + $gallery = []; + foreach ($request->file('images', []) as $image) { + $path = $image->store('tickets', 'public'); + $gallery[] = Storage::disk('public')->url($path); + } + $ticket = $this->ticketRepository->create([ + 'uuid' => (string) Str::uuid(), 'ticket_num' => $ticketNum, 'subject' => $request->input('subject'), 'description' => $request->input('description'), 'status' => 'open', 'room' => $request->input('room'), 'data' => $request->input('data') ? json_encode($request->input('data')) : null, + 'gallery' => $gallery ?: null, 'customer_id' => $customer->id, ]); + TicketJobs::dispatch($ticket, $customer); + Mail::to($customer->email)->queue(new TicketCreated($ticket, $customer)); + + $setting = Setting::where('key', 'setting')->first(); + $adminEmail = $setting ? (json_decode((string) $setting->value)?->admin_email ?? null) : null; + if ($adminEmail) { + Mail::to($adminEmail)->queue(new TicketAdminNotification($ticket, $customer)); + } + return response()->json([ 'error' => false, 'message' => 'Ticket đã được tạo thành công.', @@ -115,7 +140,7 @@ class TicketController extends Controller public function reply(Request $request, string $ticket_num) { $request->validate([ - 'message' => ['required', 'string', 'max:2000'], + 'message' => ['required', 'string'], ]); $customer = auth('client')->user(); diff --git a/app/Http/Controllers/Api/Service/OrderControllerRefactor.php b/app/Http/Controllers/Api/Service/OrderControllerRefactor.php index e05f143..8682584 100644 --- a/app/Http/Controllers/Api/Service/OrderControllerRefactor.php +++ b/app/Http/Controllers/Api/Service/OrderControllerRefactor.php @@ -20,8 +20,10 @@ use App\Services\ActivityLogger; use App\Services\Api\Order\OrderQueryService; use App\Services\Api\Order\OrderService; use App\Services\InvoicePaymentService; +use App\Services\InvoicePdfService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; class OrderControllerRefactor extends Controller { @@ -225,6 +227,34 @@ class OrderControllerRefactor extends Controller } } + public function getClientOrderDetailByOrderNum(Request $request, string $orderNum): JsonResponse + { + $order = $this->orderQueryService->findCustomerInvoiceByOrderNum((int) $request->user()->id, $orderNum); + + if (! $order) { + return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404); + } + + return response()->json(['success' => true, 'data' => new OrderDetailResource($order)]); + } + + public function downloadInvoicePdfByOrderNum(Request $request, string $orderNum, InvoicePdfService $pdfService): Response + { + $order = $this->orderQueryService->findCustomerInvoiceByOrderNum((int) $request->user()->id, $orderNum); + + if (! $order?->invoice) { + abort(404, 'Hóa đơn không tồn tại.'); + } + + $pdf = $pdfService->generate($order->invoice); + $filename = $pdfService->generateFileName($order->invoice); + + return response($pdf, 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + ]); + } + public function getClientServices(Request $request): JsonResponse { $services = $this->orderQueryService->getCustomerServices((int) $request->user()->id); diff --git a/app/Http/Resources/Api/Ticket/TicketResource.php b/app/Http/Resources/Api/Ticket/TicketResource.php index c76bf33..127e8af 100644 --- a/app/Http/Resources/Api/Ticket/TicketResource.php +++ b/app/Http/Resources/Api/Ticket/TicketResource.php @@ -11,15 +11,18 @@ class TicketResource extends JsonResource { return [ 'id' => $this->id, + 'uuid' => $this->uuid, 'ticket_num' => $this->ticket_num, 'subject' => $this->subject, 'description' => $this->description, 'status' => $this->status, 'room' => $this->room, 'data' => $this->data ? (is_string($this->data) ? json_decode($this->data, true) : $this->data) : null, + 'gallery' => $this->gallery ?? [], 'customer_id' => $this->customer_id, 'customer' => $this->whenLoaded('customer', fn() => [ 'id' => $this->customer->id, + 'uuid' => $this->customer->uuid, 'fullname' => $this->customer->fullname, 'email' => $this->customer->email, ]), diff --git a/app/Jobs/VirtualMachine/ChangeVmPasswordJob.php b/app/Jobs/VirtualMachine/ChangeVmPasswordJob.php index 0001f2f..f79f04e 100644 --- a/app/Jobs/VirtualMachine/ChangeVmPasswordJob.php +++ b/app/Jobs/VirtualMachine/ChangeVmPasswordJob.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Jobs\VirtualMachine; use App\Http\Facade\SignRSA; +use App\Mail\Customer\VmPasswordChanged; +use App\Models\ModuleVmware; use App\Models\vCenter; use App\Services\VmProvisionService; use App\Services\VsphereHostSshService; @@ -14,6 +16,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; /** * ChangeVmPasswordJob @@ -71,6 +74,8 @@ class ChangeVmPasswordJob implements ShouldQueue // Persist encrypted password to DB $provision->updatePassword($this->moduleVmwareId, $this->newPassword); + $this->notifyCustomer(); + Log::info('ChangeVmPasswordJob: password changed successfully', [ 'module_vmware_id' => $this->moduleVmwareId, 'vm_id' => $this->vmId, @@ -86,6 +91,19 @@ class ChangeVmPasswordJob implements ShouldQueue ]); } + private function notifyCustomer(): void + { + $vm = ModuleVmware::with('customer')->find($this->moduleVmwareId); + if (!$vm?->customer?->email) { + return; + } + + Mail::to($vm->customer->email)->queue(new VmPasswordChanged([ + 'fullname' => $vm->customer->fullname, + 'vm_name' => $vm->name, + ])); + } + private function assertSuccess(string $output): void { if (str_contains(strtoupper(trim($output)), 'SUCCESS')) { diff --git a/app/Mail/Admin/TicketAdminNotification.php b/app/Mail/Admin/TicketAdminNotification.php new file mode 100644 index 0000000..cd51c92 --- /dev/null +++ b/app/Mail/Admin/TicketAdminNotification.php @@ -0,0 +1,34 @@ +subject("[ADMIN] Ticket mới #{$this->ticket->ticket_num} — {$this->customer->email}") + ->view('emails.admin.ticket-notification') + ->with([ + 'ticketNum' => $this->ticket->ticket_num, + 'subject' => $this->ticket->subject, + 'room' => $this->ticket->room, + 'description' => strip_tags($this->ticket->description), + 'customerName' => trim($this->customer->firstname . ' ' . $this->customer->lastname), + 'customerEmail' => $this->customer->email, + 'ticketUrl' => config('app.frontend_url') . '/tickets/' . $this->ticket->ticket_num, + ]); + } +} diff --git a/app/Mail/Customer/VmPasswordChanged.php b/app/Mail/Customer/VmPasswordChanged.php new file mode 100644 index 0000000..7094264 --- /dev/null +++ b/app/Mail/Customer/VmPasswordChanged.php @@ -0,0 +1,65 @@ +payload()['company_name'] ?? config('app.name', 'The Gioi So'); + + return new Envelope( + subject: 'Mật khẩu VM của bạn đã được thay đổi — ' . $appName, + ); + } + + public function content(): Content + { + $data = $this->payload(); + + return new Content( + view: 'emails.customer.vm-password-changed', + with: [ + 'appName' => $data['company_name'] ?? config('app.name', 'The Gioi So'), + 'fullName' => $data['fullname'] ?? 'Quý khách', + 'vmName' => $data['vm_name'] ?? '', + 'loginUrl' => $data['login_url'] ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap', + 'supportEmail' => $data['support_email'] ?? config('mail.support_email', 'support@thegioiso.com'), + 'supportPhone' => $data['support_phone'] ?? config('mail.support_phone', '1900.xxxx'), + ], + ); + } + + public function attachments(): array + { + return []; + } + + private function payload(): array + { + try { + $setting = Setting::where('key', 'setting')->first(); + $settingValue = $setting ? json_decode((string) $setting->value, true) : []; + } catch (\Throwable) { + $settingValue = []; + } + + return array_merge([ + 'company_name' => $settingValue['company'] ?? config('app.name', 'The Gioi So'), + 'support_email' => $settingValue['email'] ?? config('mail.support_email', 'support@thegioiso.com'), + 'support_phone' => $settingValue['hotline'] ?? ($settingValue['phone'] ?? config('mail.support_phone', '1900.xxxx')), + 'login_url' => rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap', + ], $this->data); + } +} diff --git a/app/Mail/TicketCreated.php b/app/Mail/TicketCreated.php new file mode 100644 index 0000000..7282bd8 --- /dev/null +++ b/app/Mail/TicketCreated.php @@ -0,0 +1,46 @@ +ticket->ticket_num}] Yêu cầu hỗ trợ của bạn đã được tiếp nhận", + ); + } + + public function content(): Content + { + return new Content( + view: 'emails.customer.ticket-created', + with: [ + 'fullName' => trim("{$this->customer->firstname} {$this->customer->lastname}") ?: 'Quý khách', + 'ticketNum' => $this->ticket->ticket_num, + 'subject' => $this->ticket->subject, + 'room' => $this->ticket->room, + 'ticketUrl' => rtrim(config('app.frontend_url', ''), '/') . '/tickets/' . $this->ticket->ticket_num, + ], + ); + } + + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index b389f67..1749f95 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -13,6 +13,7 @@ class Ticket extends Model protected $table = 'ticket'; protected $fillable = [ + 'uuid', 'ticket_num', 'subject', 'customer_id', @@ -20,6 +21,11 @@ class Ticket extends Model 'status', 'room', 'data', + 'gallery', + ]; + + protected $casts = [ + 'gallery' => 'array', ]; public function customer() diff --git a/app/Models/VirtualMachines.php b/app/Models/VirtualMachines.php index 962f9f3..f3a25fc 100644 --- a/app/Models/VirtualMachines.php +++ b/app/Models/VirtualMachines.php @@ -25,8 +25,6 @@ class VirtualMachines extends Model 'host_id', 'ip_pool_id', 'ip', - 'username', - 'password', ]; public function datacenter() @@ -43,4 +41,9 @@ class VirtualMachines extends Model { return $this->belongsTo(IpPool::class, 'ip_pool_id'); } + + public function vmware() + { + return $this->hasOne(ModuleVmware::class, 'vmware_id', 'vm_id'); + } } diff --git a/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php b/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php deleted file mode 100644 index c7359d3..0000000 --- a/database/migrations/2026_07_14_000001_add_credentials_to_virtual_machines_table.php +++ /dev/null @@ -1,23 +0,0 @@ -string('username')->nullable()->after('ip'); - $table->string('password')->nullable()->after('username'); - }); - } - - public function down(): void - { - Schema::table('virtual_machines', function (Blueprint $table) { - $table->dropColumn(['username', 'password']); - }); - } -}; diff --git a/database/migrations/2026_07_14_000001_add_gallery_and_fk_to_ticket_table.php b/database/migrations/2026_07_14_000001_add_gallery_and_fk_to_ticket_table.php new file mode 100644 index 0000000..60bc446 --- /dev/null +++ b/database/migrations/2026_07_14_000001_add_gallery_and_fk_to_ticket_table.php @@ -0,0 +1,29 @@ +json('gallery')->nullable()->after('data'); + } + // customer_id was bigInteger (signed); must match customers.id (unsignedBigInteger) + $table->unsignedBigInteger('customer_id')->change(); + $table->foreign('customer_id')->references('id')->on('customers')->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::table('ticket', function (Blueprint $table) { + $table->dropForeign(['customer_id']); + $table->bigInteger('customer_id')->default(0)->change(); + $table->dropColumn('gallery'); + }); + } +}; diff --git a/database/migrations/2026_07_14_000002_change_ticket_reply_message_to_longtext.php b/database/migrations/2026_07_14_000002_change_ticket_reply_message_to_longtext.php new file mode 100644 index 0000000..c491933 --- /dev/null +++ b/database/migrations/2026_07_14_000002_change_ticket_reply_message_to_longtext.php @@ -0,0 +1,22 @@ +longText('message')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('ticket_reply', function (Blueprint $table) { + $table->text('message')->nullable()->change(); + }); + } +}; diff --git a/resources/views/emails/admin/ticket-notification.blade.php b/resources/views/emails/admin/ticket-notification.blade.php new file mode 100644 index 0000000..d69346c --- /dev/null +++ b/resources/views/emails/admin/ticket-notification.blade.php @@ -0,0 +1,83 @@ + + + + + + Thông báo ticket mới + + + +
+
+
+
🎫
+

Ticket mới cần xử lý

+

Ticket #{{ $ticketNum ?? 'N/A' }}

+
+ +
+ + + + + + + + + + + + + + +
Mã ticket#{{ $ticketNum ?? 'N/A' }}
Tiêu đề{{ $subject ?? 'N/A' }}
Phòng ban{{ $room ?? 'N/A' }}
+ + + + + + + + + + + +
Khách hàng{{ $customerName ?? 'N/A' }}
Email khách{{ $customerEmail ?? 'N/A' }}
+ + +
{{ mb_substr($description ?? '', 0, 500) }}{{ mb_strlen($description ?? '') > 500 ? '…' : '' }}
+ + +
+ + +
+
+ + diff --git a/resources/views/emails/customer/ticket-created.blade.php b/resources/views/emails/customer/ticket-created.blade.php new file mode 100644 index 0000000..3582c47 --- /dev/null +++ b/resources/views/emails/customer/ticket-created.blade.php @@ -0,0 +1,209 @@ +@php + $appName = $appName ?? config('app.name', 'Thế Giới Số'); + $fullName = $fullName ?? 'Quý khách'; + $supportEmail = $supportEmail ?? 'support@tgs.com.vn'; + $supportPhone = $supportPhone ?? '1900. 6119'; +@endphp + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + +
+
🎫
+

+ Yêu cầu hỗ trợ đã được tiếp nhận +

+
+ Chúng tôi sẽ phản hồi trong thời gian sớm nhất +
+
+ + + + + + +
+
 
+
Xin chào {{ $fullName }},
+
 
+
+ Chúng tôi đã nhận được yêu cầu hỗ trợ của Quý khách và sẽ xử lý trong thời gian sớm nhất. Dưới đây là thông tin ticket của Quý khách: +
+
 
+ + {{-- Ticket info box --}} + + + + + + +
+ + + + + + + + + + + @if($room) + + + + + @endif + + + + + +
Mã ticket:#{{ $ticketNum }}
Tiêu đề:{{ $subject }}
Phòng ban:{{ $room }}
Trạng thái: + Đang mở +
+
+ +
 
+ +
 
+
+ Đội ngũ hỗ trợ của {{ $appName }} sẽ liên hệ với Quý khách qua email hoặc cập nhật trực tiếp trên hệ thống. Cảm ơn Quý khách đã tin tưởng sử dụng dịch vụ của chúng tôi. +
+
 
+
+ Email này được gửi tự động. Vui lòng không trả lời email này. +
+
 
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + +
 
+
 
+ + + + + + + + + + + + + + +
  + Premium HOSTING +   + Cloud SERVER +   + EMAIL DOANH NGHIỆP +   + Thiết Kế WEBSITE +  
+
 
+ + + + + + +
 
+
+
+ + + + + + + + + + + + +
 
+

+ CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ
+ Địa chỉ: 128/38 Tân Hương, Phường Phú Thọ Hòa, Thành Phố Hồ Chí Minh
+ Tel: (028) 7309. 7379 - Tổng đài: {{ $supportPhone }}
+ Email: {{ $supportEmail }}
+ + www.TGS.com.vn & + www.thegioiso.CLOUD & + www.thegioiSERVER.com & + www.CloudEmail.vn + +

+

+ + + + 🥇Thế Giới Số🥇- Premium HOSTING • CLOUD • SERVER & WEBSITE | 15+ năm Phụng Sự 🚀 + + + +

+
 
+
+
diff --git a/resources/views/emails/customer/vm-password-changed.blade.php b/resources/views/emails/customer/vm-password-changed.blade.php new file mode 100644 index 0000000..04f7427 --- /dev/null +++ b/resources/views/emails/customer/vm-password-changed.blade.php @@ -0,0 +1,190 @@ +@php + $appName = $appName ?? config('app.name', 'Thế Giới Số'); + $fullName = $fullName ?? 'Quý khách'; + $vmName = $vmName ?? ''; + $loginUrl = $loginUrl ?? rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap'; + $supportEmail = $supportEmail ?? 'support@tgs.com.vn'; + $supportPhone = $supportPhone ?? '1900. 6119'; +@endphp + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + +
+
🔑
+

+ Mật khẩu VM đã được thay đổi +

+
+ Mật khẩu máy chủ ảo của bạn đã được cập nhật thành công +
+
+ + + + + + +
+
 
+
Xin chào {{ $fullName }},
+
 
+
+ Mật khẩu đăng nhập vào máy chủ ảo (VM) của bạn tại {{ $appName }} đã được thay đổi thành công. +
+
 
+ + + + + + +
+
Máy chủ: {{ $vmName }}
+
Trạng thái: Đổi mật khẩu thành công ✓
+
+
 
+
+ Bạn có thể đăng nhập vào cổng quản lý để kiểm tra trạng thái dịch vụ. +
+
 
+ +
 
+
+ Nếu bạn không thực hiện thao tác này, vui lòng liên hệ ngay với chúng tôi qua {{ $supportEmail }} hoặc {{ $supportPhone }}. +
+
 
+
+ Email này được gửi tự động. Vui lòng không trả lời email này. +
+
 
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + +
 
+
 
+ + + + + + + + + + + + + + +
  + Premium HOSTING +   + Cloud SERVER +   + EMAIL DOANH NGHIỆP +   + Thiết Kế WEBSITE +  
+
 
+ + + + + + +
 
+
+
+ + + + + + + + + + + + +
 
+

+ CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ
+ Địa chỉ: 128/38 Tân Hương, Phường Phú Thọ Hòa, Thành Phố Hồ Chí Minh
+ Tel: (028) 7309. 7379 - Tổng đài: {{ $supportPhone }}
+ Email: {{ $supportEmail }}
+ + www.TGS.com.vn & + www.thegioiso.CLOUD & + www.thegioiSERVER.com & + www.CloudEmail.vn + +

+

+ + + + 🥇Thế Giới Số🥇- Premium HOSTING • CLOUD • SERVER & WEBSITE | 15+ năm Phụng Sự 🚀 + + + +

+
 
+
+
diff --git a/routes/api.php b/routes/api.php index 93911d3..0bfe3e8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -244,6 +244,11 @@ Route::prefix('v1') ->group(function () { Route::get('/', 'index')->name('index'); Route::post('/', 'store')->name('store'); + Route::get('trashed', 'trashed')->name('trashed'); + Route::post('bulk-restore', 'bulkRestore')->name('bulk-restore'); + Route::post('bulk-force-delete', 'bulkForceDelete')->name('bulk-force-delete'); + Route::post('{id}/restore', 'restore')->name('restore'); + Route::delete('{id}/force-delete', 'forceDelete')->name('force-delete'); Route::get('{id}', 'show')->name('show'); Route::put('{id}', 'update')->name('update'); Route::delete('{id}', 'destroy')->name('destroy'); @@ -256,6 +261,13 @@ Route::prefix('v1') Route::get('/', 'index')->name('index'); Route::post('/', 'store')->name('store'); Route::post('sync-wordpress', 'syncWordpress')->name('sync-wordpress'); + Route::post('bulk-delete', 'bulkDelete')->name('bulk-delete'); + Route::post('bulk-update-status', 'bulkUpdateStatus')->name('bulk-update-status'); + Route::get('trashed', 'trashed')->name('trashed'); + Route::post('bulk-restore', 'bulkRestore')->name('bulk-restore'); + Route::post('bulk-force-delete', 'bulkForceDelete')->name('bulk-force-delete'); + Route::post('{id}/restore', 'restore')->name('restore'); + Route::delete('{id}/force-delete', 'forceDelete')->name('force-delete'); Route::get('{id}', 'show')->name('show'); Route::put('{id}', 'update')->name('update'); Route::delete('{id}', 'destroy')->name('destroy'); diff --git a/routes/client.php b/routes/client.php index bc87645..d1acf93 100644 --- a/routes/client.php +++ b/routes/client.php @@ -50,6 +50,7 @@ Route::prefix('v1') ->controller(OrderControllerRefactor::class) ->group(function () { Route::get('orders', 'getClientOrders')->name('orders.list'); + Route::get('orders/num/{orderNum}', 'getClientOrderDetailByOrderNum')->name('orders.detailByOrderNum'); Route::get('orders/{id}', 'getClientOrderDetail')->name('orders.detail'); Route::post('orders/{id}/retry', 'retryOrderProvisioning')->name('orders.retry'); Route::get('invoices', 'getClientInvoices')->name('invoices.list'); @@ -62,6 +63,9 @@ Route::prefix('v1') Route::get('invoices/order/{orderNum}', 'getClientInvoiceDetailByOrderNum')->name( 'invoices.detailByOrderNum', ); + Route::get('invoices/order/{orderNum}/pdf', 'downloadInvoicePdfByOrderNum')->name( + 'invoices.pdfByOrderNum', + ); Route::get('invoices/{id}', 'getClientInvoiceDetail')->name('invoices.detail'); Route::get('dich-vu', 'getClientServices')->name('services.list'); Route::patch('dich-vu/{id}/auto-renew', 'updateClientServiceAutoRenew')->name( diff --git a/storage/app/public/tickets/f6cAr67eqedaG5YahnL3dXECkY2npfBQw4BNHHaB.png b/storage/app/public/tickets/f6cAr67eqedaG5YahnL3dXECkY2npfBQw4BNHHaB.png new file mode 100644 index 0000000..b9d7a0e Binary files /dev/null and b/storage/app/public/tickets/f6cAr67eqedaG5YahnL3dXECkY2npfBQw4BNHHaB.png differ diff --git a/storage/app/public/tickets/iC9NQbtmGh5MbvJGyPX5cg34vPojpfIvpEG2d5eK.png b/storage/app/public/tickets/iC9NQbtmGh5MbvJGyPX5cg34vPojpfIvpEG2d5eK.png new file mode 100644 index 0000000..631bc82 Binary files /dev/null and b/storage/app/public/tickets/iC9NQbtmGh5MbvJGyPX5cg34vPojpfIvpEG2d5eK.png differ diff --git a/storage/app/public/tickets/smN4so93ITuBlpwUUcM9hH6UWL8ntDUlrNhGaNSY.jpg b/storage/app/public/tickets/smN4so93ITuBlpwUUcM9hH6UWL8ntDUlrNhGaNSY.jpg new file mode 100644 index 0000000..0a5180a Binary files /dev/null and b/storage/app/public/tickets/smN4so93ITuBlpwUUcM9hH6UWL8ntDUlrNhGaNSY.jpg differ