This commit is contained in:
pbao9
2026-02-11 11:18:35 +07:00
parent d59b2a0840
commit 6b6385f441
27 changed files with 386 additions and 30 deletions
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Enums\Client;
enum ClientGender: int
{
case MALE = 0;
case FEMALE = 1;
public function label(): string
{
return match ($this) {
self::MALE => 'Nam',
self::FEMALE => 'Nữ',
};
}
}
+27 -2
View File
@@ -34,8 +34,33 @@ class Handler extends ExceptionHandler
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
$this->renderable(function (\Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, $request) {
if (!$request->expectsJson() && !$request->is('v1/*') && !$request->is('api/*')) {
return null;
}
$previous = $e->getPrevious();
if ($previous instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
$modelClass = $previous->getModel();
$pageType = match ($modelClass) {
\App\Models\Product::class => 'service',
\App\Models\Customer::class => 'client',
\App\Models\Post::class => 'post',
default => 'unknown',
};
return response()->json([
'error' => true,
'message' => 'Dữ liệu không tồn tại',
'page_type' => $pageType,
], 404);
}
return response()->json([
'error' => true,
'message' => 'API Route not found',
], 404);
});
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api\Service;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Service\ServiceResource;
use App\Models\Product;
use App\Services\Api\Service\ServiceService;
class ServiceController extends Controller
{
protected $serviceService;
public function __construct(ServiceService $serviceService)
{
$this->serviceService = $serviceService;
}
/**
* Lấy danh sách dịch vụ (sản phẩm) đang hoạt động
*/
public function getService()
{
$services = $this->serviceService->getActiveServices();
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service',
'data' => ServiceResource::collection($services),
]);
}
public function getDetailService(Product $product)
{
$product->load(['osVersions.osGroup', 'category', 'pricings' => fn ($q) => $q->where('is_active', 1)]);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'page_type' => 'service',
'data' => new ServiceResource($product),
]);
}
}
@@ -131,7 +131,7 @@ class Datacenter extends Controller
->addColumn(
'action',
function ($d) {
return view('administrator.pages.package.datacenter.datatable.edit', ['row' => $d])->render();
return view('administrator.pages.package.datacenter.datatable.action', ['row' => $d])->render();
}
)
->filterColumn('v_center.host', function ($query, $keyword) {
@@ -25,19 +25,15 @@ class ClientResource extends JsonResource
'phone' => $this->phone,
'address' => $this->address,
'city' => $this->city,
'ward' => $this->ward,
'province' => $this->province,
'active_phone' => 1,
'active_zalo' => 1,
'gender' => $this->gender,
'ward' => $this->ward->name,
'province' => $this->province->name,
'gender' => $this->gender_label,
'date' => $this->created_at ? $this->created_at->format('Y-m-d H:i:s') : null,
'company' => $this->company ?? '',
'mst' => null,
'avatar' => $this->avatar,
'cmnd_after' => $this->cmnd_after,
'cmnd_before' => $this->cmnd_before,
'cmnd_active' => (bool) $this->identity_number,
'cmnd' => $this->cmnd,
'cmnd_after' => $this->front_identity_number,
'cmnd_before' => $this->back_identity_number,
'cmnd' => $this->identity_number,
'fb_link' => null,
'active_2fa' => 0,
'off_mail_marketing' => $this->off_mail_marketing,
@@ -0,0 +1,49 @@
<?php
namespace App\Http\Resources\Api\Service;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class ServiceResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'uuid' => $this->uuid,
'name' => $this->name,
'slug' => $this->slug,
'short_description' => $this->short_description,
'description' => $this->description,
'thumbnail' => $this->thumbnail ? asset($this->thumbnail) : null,
'product_type' => $this->product_type,
'specs' => [
'cpu' => $this->cpu,
'ram' => $this->ram,
'disk' => $this->disk,
'bandwidth' => $this->bandwidth,
'gpu' => $this->gpu
],
'stock' => $this->stock,
'price' => $this->price,
'price_sale' => $this->price_sale,
'group' => $this->category->name,
'os' => $this->osVersions->groupBy('os_group_id')->map(function ($versions) {
$group = $versions->first()->osGroup;
return [
'name' => $group?->name,
'thumbnail' => $group?->thumbnail,
'versions' => $versions->map(fn($os) => [
'version' => $os->version,
'name' => $os->name,
])->values(),
];
})->values(),
];
}
}
+10
View File
@@ -57,6 +57,11 @@ class Customer extends Authenticatable
'first_name',
'last_name',
'point',
'gender_label',
];
protected $casts = [
'gender' => \App\Enums\Client\ClientGender::class,
];
/**
@@ -219,4 +224,9 @@ class Customer extends Authenticatable
{
return $this->belongsTo(Ward::class, 'ward_code', 'code');
}
public function getGenderLabelAttribute()
{
return $this->gender?->label() ?? 'Khác';
}
}
+6
View File
@@ -365,6 +365,12 @@ class RepositoryProvider extends ServiceProvider
\App\Repositories\Api\Client\ClientRepositoryInterface::class,
\App\Repositories\Api\Client\ClientRepository::class
);
// Service API
$this->app->bind(
\App\Repositories\Api\Service\ServiceRepositoryInterface::class,
\App\Repositories\Api\Service\ServiceRepository::class
);
}
/**
@@ -0,0 +1,35 @@
<?php
namespace App\Repositories\Api\Service;
use App\Models\Product;
use App\Repositories\Api\BaseApiRepository;
class ServiceRepository extends BaseApiRepository implements ServiceRepositoryInterface
{
public function getModel()
{
return Product::class;
}
/**
* Lấy danh sách sản phẩm đang hoạt động
*/
public function getActiveServices()
{
return $this->model
->with(['osVersions.osGroup', 'category'])
->where('is_active', 1)
->orderBy('sort_order', 'asc')
->get();
}
public function findActiveBySlug(string $slug)
{
return $this->model
->with(['osVersions.osGroup', 'category', 'pricings' => fn ($q) => $q->where('is_active', 1)])
->where('slug', $slug)
->where('is_active', 1)
->first();
}
}
@@ -0,0 +1,15 @@
<?php
namespace App\Repositories\Api\Service;
use App\Repositories\BaseRepositoryInterface;
interface ServiceRepositoryInterface extends BaseRepositoryInterface
{
/**
* Lấy danh sách sản phẩm đang hoạt động
*/
public function getActiveServices();
public function findActiveBySlug(string $slug);
}
@@ -0,0 +1,28 @@
<?php
namespace App\Services\Api\Service;
use App\Repositories\Api\Service\ServiceRepositoryInterface;
class ServiceService
{
protected $serviceRepository;
public function __construct(ServiceRepositoryInterface $serviceRepository)
{
$this->serviceRepository = $serviceRepository;
}
/**
* Lấy danh sách dịch vụ đang hoạt động
*/
public function getActiveServices()
{
return $this->serviceRepository->getActiveServices();
}
public function findActiveBySlug(string $slug)
{
return $this->serviceRepository->findActiveBySlug($slug);
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\OsGroup;
use App\Models\Os;
class OsSeeder extends Seeder
{
public function run(): void
{
/**
* OS Groups + Versions
*/
$data = [
[
'name' => 'Ubuntu',
'thumbnail' => 'ubuntu.png',
'module_host_id' => 1,
'status' => 1,
'versions' => [
'20.04 LTS',
'22.04 LTS',
'24.04 LTS',
],
],
[
'name' => 'CentOS',
'thumbnail' => 'centos.png',
'module_host_id' => 1,
'status' => 1,
'versions' => [
'7',
'8 Stream',
],
],
[
'name' => 'Debian',
'thumbnail' => 'debian.png',
'module_host_id' => 1,
'status' => 1,
'versions' => [
'10',
'11',
'12',
],
],
[
'name' => 'Windows Server',
'thumbnail' => 'windows.png',
'module_host_id' => 1,
'status' => 1,
'versions' => [
'2016',
'2019',
'2022',
],
],
];
foreach ($data as $groupData) {
/**
* Create or Update OS Group
*/
$group = OsGroup::updateOrCreate(
['name' => $groupData['name']],
[
'thumbnail' => $groupData['thumbnail'],
'module_host_id' => $groupData['module_host_id'],
'status' => $groupData['status'],
]
);
/**
* Create OS versions
*/
foreach ($groupData['versions'] as $version) {
Os::updateOrCreate(
[
'os_group_id' => $group->id,
'version' => $version,
],
[
'template_id' => null,
'status' => 1,
]
);
}
}
}
}

Before

Width:  |  Height:  |  Size: 890 B

After

Width:  |  Height:  |  Size: 890 B

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.8179 33.5987L33.1974 35.9642L30.8179 38.3297H14.0705V46.2117L3.85714 35.9642L14.0705 25.8894V33.5986L30.8179 33.5987V33.5987Z" fill="#932279"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4016 30.8181L36.036 33.1976L33.6705 30.8181V14.0706H25.7886L36.036 3.85718L46.1108 14.0706H38.4016L38.4016 30.8181V30.8181Z" fill="#EFA724"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.182 38.4002L38.8026 36.0347L41.182 33.6692H57.9295V25.7872L68.1429 36.0347L57.9295 46.1095V38.4003L41.182 38.4002V38.4002Z" fill="#262577"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.5972 41.1807L35.9628 38.8012L38.3283 41.1807V57.9282H46.2102L35.9628 68.1416L25.888 57.9282H33.5972L33.5972 41.1807V41.1807Z" fill="#9CCD2A"/>
<path d="M13.4708 13.4724H33.82V33.8217H13.4708V13.4724Z" fill="#9CCD2A" stroke="white"/>
<path d="M38.1805 13.4724H58.5296V33.8217H38.1805V13.4724Z" fill="#932279" stroke="white"/>
<path d="M38.1805 38.1847H58.5298V58.534H38.1805V38.1847Z" fill="#EFA724" stroke="white"/>
<path d="M13.4708 38.1847H33.82V58.534H13.4708V38.1847Z" fill="#262577" stroke="white"/>
<path d="M4.13776 36.0001L18.5269 21.611L32.9159 36.0001L18.5269 50.3893L4.13776 36.0001Z" stroke="white"/>
<path d="M21.6109 18.5248L36 4.13565L50.389 18.5248L36 32.9139L21.6109 18.5248Z" stroke="white"/>
<path d="M39.0824 35.9965L53.4715 21.6073L67.8605 35.9965L53.4715 50.3856L39.0824 35.9965Z" stroke="white"/>
<path d="M21.6107 53.474L35.9998 39.0849L50.3889 53.474L35.9998 67.8631L21.6107 53.474Z" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#DD4814" fill-rule="evenodd" d="M24,12.000127 C24,18.6272309 18.6273155,23.9999153 11.999873,23.9999153 C5.37243046,23.9999153 0,18.6273155 0,12.000127 C0,5.37251513 5.37234579,0 11.9997883,0 C18.6272309,0 24,5.37251513 24,12.000127 L24,12.000127 Z M3.84013547,10.3980668 C2.95511615,10.3980668 2.23799058,11.1151924 2.23799058,12.0002117 C2.23799058,12.8848923 2.95511615,13.6021026 3.84013547,13.6021026 C4.72481611,13.6021026 5.44202635,12.8848923 5.44202635,12.0002117 C5.44202635,11.1151077 4.72490078,10.3980668 3.84013547,10.3980668 L3.84013547,10.3980668 Z M15.2791773,17.6795583 C14.5128605,18.121941 14.2505636,19.1013635 14.6927769,19.8673416 C15.1352442,20.6336585 16.114582,20.8961247 16.8808989,20.4537421 C17.6469617,20.0114441 17.909428,19.0320216 17.4670453,18.2657894 C17.0247473,17.4998113 16.0452402,17.2372603 15.2791773,17.6795583 L15.2791773,17.6795583 Z M7.32009948,11.9999577 C7.32009948,10.4166088 8.10690561,9.01808689 9.31010178,8.17125218 L8.1388249,6.20927451 C6.73674704,7.14602508 5.69382463,8.57807489 5.26058596,10.255319 C5.76663786,10.6678144 6.08997936,11.2959554 6.08997936,12.000127 C6.08997936,12.7041293 5.76663786,13.3322703 5.2605013,13.744681 C5.6936553,15.4220944 6.73666237,16.8541443 8.13874023,17.7908102 L9.31010178,15.8286632 C8.10690561,14.9819978 7.32009948,13.5835606 7.32009948,11.9999577 L7.32009948,11.9999577 Z M12.0000423,7.32001482 C14.4449579,7.32001482 16.4504542,9.19461662 16.6606812,11.5849223 L18.9439684,11.5515637 C18.8316159,9.78677438 18.0607271,8.20198614 16.8759882,7.03722858 C16.2668971,7.26735223 15.5632336,7.23246962 14.9550738,6.88127282 C14.3461521,6.52973736 13.964052,5.93673293 13.8594042,5.29318258 C13.2674157,5.12935283 12.6443547,5.04002963 12.000127,5.04002963 C10.8921807,5.04002963 9.84544829,5.30012524 8.91564038,5.7607112 L10.0286667,7.75545482 C10.6278518,7.47664791 11.2955321,7.32001482 12.0000423,7.32001482 L12.0000423,7.32001482 Z M12.0000423,16.6799852 C11.2954474,16.6799852 10.6278518,16.5233521 10.028582,16.2445452 L8.91538638,18.2394581 C9.84527896,18.6998748 10.892096,18.960055 12.0000423,18.960055 C12.64427,18.960055 13.2673311,18.8707318 13.8594042,18.7069021 C13.964052,18.0633517 14.3462367,17.4703473 14.9552432,17.1185578 C15.5634029,16.7675304 16.2670665,16.7326478 16.8761576,16.9628561 C18.0608117,15.7980139 18.8317006,14.2132256 18.9438837,12.4484363 L16.6605965,12.4150777 C16.4505389,14.805722 14.4449579,16.6799852 12.0000423,16.6799852 L12.0000423,16.6799852 Z M15.2789233,6.32027234 C16.0451555,6.76282433 17.0246627,6.5002734 17.466876,5.73429524 C17.9093433,4.96797841 17.6470464,3.98855591 16.8807295,3.54608858 C16.114582,3.10379059 15.1351595,3.36625686 14.6926922,4.13257369 C14.2503942,4.89855185 14.5128605,5.87805902 15.2789233,6.32027234 L15.2789233,6.32027234 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 16.6455L31.0681 13.6493L31.0777 34.8702L9.02016 34.9954L9 16.6455ZM31.0576 37.3152L31.0747 58.5546L9.01713 55.5314L9.01589 37.1728L31.0576 37.3152ZM33.7327 13.2574L62.9932 9V34.6003L33.7327 34.8319V13.2574ZM63 37.515L62.9931 63L33.7327 58.8829L33.6917 37.4673L63 37.515Z" fill="#3377BA"/>
</svg>

After

Width:  |  Height:  |  Size: 381 B

@@ -1 +1,2 @@
<x-button-crm type="button" variant="primary" size="sm" route="package.sync.all" icon="refresh-cw">Sync All Package</x-button-crm>
<x-button-crm type="button" variant="outline-primary" size="sm" route="package.sync.all" icon="refresh-cw">Sync All
Package</x-button-crm>
@@ -1,5 +1,7 @@
<x-button-crm type="button" variant="primary" size="sm" route="package.vm.create" icon="plus-circle">Add
VM</x-button-crm>
@include("administrator.pages.block.Sync.All")
<x-button-crm type="button" variant="secondary" size="sm" route="package.vm.sync" icon="refresh-cw">Sync
VM</x-button-crm>
<div class="d-flex gap-2 justify-content-end">
<x-button-crm type="button" variant="outline-primary" size="sm" route="package.vm.create" icon="plus-circle">Add
VM</x-button-crm>
@include("administrator.pages.block.Sync.All")
<x-button-crm type="button" variant="outline-secondary" size="sm" route="package.vm.sync" icon="refresh-cw">Sync
VM</x-button-crm>
</div>
@@ -1,5 +1,4 @@
<div class="d-flex gap-2">
<x-button-crm type="button" variant="primary" size="sm" route="module.vcenter.edit" :params="['id' => $row->id]"
icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" route="module.vcenter.destroy" :params="['id' => $row->id]"
@@ -0,0 +1,5 @@
<div class="d-flex gap-2">
<x-button-crm type="button" variant="outline-primary" size="sm" route="package.datacenter.edit" :params="['id' => $row->id]"
icon="pencil" />
<x-button-crm type="button" variant="outline-danger" size="sm" route="package.datacenter.destroy" :params="['id' => $row->id]" icon="trash" />
</div>
@@ -1,2 +0,0 @@
<x-button-crm type="button" variant="primary" size="sm" route="package.datacenter.edit" :params="['id' => $row->id]" icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" route="package.datacenter.destroy" :params="['id' => $row->id]" icon="trash">Delete</x-button-crm>
@@ -18,15 +18,15 @@
@section('content')
@if(auth()->guard('admins')->user()->can("manager package module"))
<div class="card card-primary card-outline">
<div class="card-header">
<div class="card-header d-flex justify-content-end gap-2">
@include("administrator.pages.block.Sync.Network")
</div>
<div class="card-body">
<x-datatable id="networkTable" ajax="{{ route('package.network.datatable') }}" :columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'host.datacenter.v_center.host', 'label' => 'Vcenter Host'],
['key' => 'action', 'label' => 'Action']
]" />
['key' => 'name', 'label' => 'Name'],
['key' => 'host.datacenter.v_center.host', 'label' => 'Vcenter Host'],
['key' => 'action', 'label' => 'Action']
]" />
</div>
</div>
@else
@@ -1,2 +1,6 @@
<x-button-crm type="button" variant="primary" size="sm" route="package.vm.edit" :params="['id' => $row->id]" icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" route="package.vm.destroy" :params="['id' => $row->id]" icon="trash">Delete</x-button-crm>
<div class="d-flex gap-2">
<x-button-crm type="button" variant="primary" size="sm" route="package.vm.edit" :params="['id' => $row->id]"
icon="pencil">Edit</x-button-crm>
<x-button-crm type="button" variant="danger" size="sm" route="package.vm.destroy" :params="['id' => $row->id]"
icon="trash">Delete</x-button-crm>
</div>
+8 -1
View File
@@ -1,7 +1,7 @@
<?php
use App\Http\Controllers\Api\Client\ClientController;
use App\Http\Controllers\Api\Service\ServiceController;
Route::prefix('v1')->group(function () {
@@ -18,6 +18,13 @@ Route::prefix('v1')->group(function () {
});
});
});
Route::prefix('services')->middleware('api.auth')->as('services.')->group(function () {
Route::controller(ServiceController::class)->group(function () {
Route::get('get-list', 'getService')->name('getList');
Route::get('{product:slug}', 'getDetailService')->name('detail');
});
});
});