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,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailNotificationTemplate extends Model
|
||||
{
|
||||
protected $table = 'email_notification_templates';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'type',
|
||||
'subject',
|
||||
'header_color',
|
||||
'header_title',
|
||||
'blocks',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'blocks' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/** Default blocks by template type */
|
||||
public static function defaultBlocks(string $type = 'invoice_notification'): array
|
||||
{
|
||||
if ($type === 'vm_credentials') {
|
||||
return [
|
||||
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
|
||||
['id' => 'vm_credentials', 'type' => 'vm_credentials', 'label' => 'Thông tin truy cập VPS','enabled' => true, 'order' => 2, 'config' => [
|
||||
'fields' => ['service_host', 'service_username', 'service_port', 'service_password', 'os_name'],
|
||||
]],
|
||||
['id' => 'product_info', 'type' => 'product_info', 'label' => 'Thông tin sản phẩm', 'enabled' => true, 'order' => 3, 'config' => [
|
||||
'fields' => ['product_name', 'product_billing', 'product_price', 'service_due_exp'],
|
||||
]],
|
||||
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 4, 'config' => [
|
||||
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
|
||||
]],
|
||||
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 5, 'config' => []],
|
||||
];
|
||||
}
|
||||
|
||||
// invoice_notification (default)
|
||||
return [
|
||||
['id' => 'greeting', 'type' => 'greeting', 'label' => 'Lời chào', 'enabled' => true, 'order' => 1, 'config' => []],
|
||||
['id' => 'invoice_info', 'type' => 'invoice_info', 'label' => 'Thông tin hóa đơn', 'enabled' => true, 'order' => 2, 'config' => [
|
||||
'fields' => ['invoice_number', 'created_at', 'due_date', 'total', 'status'],
|
||||
]],
|
||||
['id' => 'customer_info','type' => 'customer_info','label' => 'Thông tin khách hàng', 'enabled' => false, 'order' => 3, 'config' => [
|
||||
'fields' => ['name', 'email'],
|
||||
]],
|
||||
['id' => 'order_items', 'type' => 'order_items', 'label' => 'Chi tiết sản phẩm', 'enabled' => true, 'order' => 4, 'config' => []],
|
||||
['id' => 'attachments', 'type' => 'attachments', 'label' => 'File đính kèm', 'enabled' => true, 'order' => 5, 'config' => []],
|
||||
['id' => 'custom_text', 'type' => 'custom_text', 'label' => 'Lời nhắn', 'enabled' => true, 'order' => 6, 'config' => [
|
||||
'content' => 'Nếu bạn có bất kỳ thắc mắc nào, vui lòng liên hệ với chúng tôi để được hỗ trợ.',
|
||||
]],
|
||||
['id' => 'footer', 'type' => 'footer', 'label' => 'Footer', 'enabled' => true, 'order' => 7, 'config' => []],
|
||||
];
|
||||
}
|
||||
|
||||
/** Field definitions exposed per block type */
|
||||
public static function blockFieldDefinitions(): array
|
||||
{
|
||||
return [
|
||||
'invoice_info' => [
|
||||
['key' => 'invoice_number', 'label' => 'Số hóa đơn'],
|
||||
['key' => 'created_at', 'label' => 'Ngày tạo'],
|
||||
['key' => 'due_date', 'label' => 'Hạn thanh toán'],
|
||||
['key' => 'total', 'label' => 'Tổng tiền'],
|
||||
['key' => 'sub_total', 'label' => 'Tiền dịch vụ'],
|
||||
['key' => 'tax', 'label' => 'Thuế'],
|
||||
['key' => 'status', 'label' => 'Trạng thái'],
|
||||
['key' => 'payment_method', 'label' => 'Phương thức TT'],
|
||||
],
|
||||
'customer_info' => [
|
||||
['key' => 'name', 'label' => 'Tên khách hàng'],
|
||||
['key' => 'email', 'label' => 'Email'],
|
||||
['key' => 'address', 'label' => 'Địa chỉ'],
|
||||
],
|
||||
'vm_credentials' => [
|
||||
['key' => 'service_host', 'label' => 'Địa chỉ IP'],
|
||||
['key' => 'service_username', 'label' => 'Username'],
|
||||
['key' => 'service_port', 'label' => 'Port'],
|
||||
['key' => 'service_password', 'label' => 'Mật khẩu'],
|
||||
['key' => 'os_name', 'label' => 'Hệ điều hành'],
|
||||
],
|
||||
'product_info' => [
|
||||
['key' => 'product_name', 'label' => 'Tên sản phẩm'],
|
||||
['key' => 'product_billing', 'label' => 'Chu kỳ thanh toán'],
|
||||
['key' => 'product_price', 'label' => 'Giá dịch vụ'],
|
||||
['key' => 'service_due_exp', 'label' => 'Ngày hết hạn'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Models\Customer;
|
||||
use App\Models\User;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'recipient_type',
|
||||
'recipient_id',
|
||||
'type',
|
||||
'title',
|
||||
'body',
|
||||
'link',
|
||||
'is_read',
|
||||
'read_at',
|
||||
'data',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_read' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
'data' => 'array',
|
||||
];
|
||||
|
||||
public static function notifyCustomer(Customer $customer, array $attributes): void
|
||||
{
|
||||
self::create([
|
||||
'recipient_type' => 'client',
|
||||
'recipient_id' => $customer->id,
|
||||
] + $attributes);
|
||||
}
|
||||
|
||||
public static function notifyAdmins(array $attributes): void
|
||||
{
|
||||
$adminIds = User::query()
|
||||
->whereHas('roles', fn ($query) => $query->where('guard_name', 'admin'))
|
||||
->pluck('id');
|
||||
|
||||
foreach ($adminIds as $adminId) {
|
||||
self::create([
|
||||
'recipient_type' => 'admin',
|
||||
'recipient_id' => $adminId,
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -15,13 +15,19 @@ class Os extends Model
|
||||
'version',
|
||||
'os_group_id',
|
||||
'template_id',
|
||||
'status'
|
||||
'module_host_id',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function osGroup()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\OsGroup::class);
|
||||
}
|
||||
|
||||
public function host()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\ModuleHost::class, 'module_host_id');
|
||||
}
|
||||
public function getStatus()
|
||||
{
|
||||
return match ($this->attributes['status']){
|
||||
|
||||
+34
-10
@@ -2,28 +2,52 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'post';
|
||||
protected $table = 'posts';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'post_category_id',
|
||||
'title',
|
||||
'slug',
|
||||
'thumbnail',
|
||||
'excerpt',
|
||||
'content',
|
||||
'status'
|
||||
'thumbnail',
|
||||
'status',
|
||||
'is_featured',
|
||||
'view_count',
|
||||
'reading_time',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'published_at',
|
||||
];
|
||||
public function getStatus()
|
||||
|
||||
protected $casts = [
|
||||
'is_featured' => 'boolean',
|
||||
'view_count' => 'integer',
|
||||
'reading_time' => 'integer',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return match ((int)$this->status) {
|
||||
1 => '<span class="badge bg-success text-success-fg">Published</span>',
|
||||
2 => '<span class="badge bg-secondary text-secondary-fg">Draft</span>',
|
||||
default => '<span class="badge bg-warning text-warning-fg">Pending</span>',
|
||||
};
|
||||
return $this->belongsTo(PostCategory::class, 'post_category_id');
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->where('status', 'published')
|
||||
->whereNotNull('published_at')
|
||||
->where('published_at', '<=', now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PostCategory extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'post_categories';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'slug',
|
||||
'description',
|
||||
'icon',
|
||||
'sort_order',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function posts(): HasMany
|
||||
{
|
||||
return $this->hasMany(Post::class, 'post_category_id');
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ class Product extends Model
|
||||
'sort_order',
|
||||
'product_categories_id',
|
||||
'module_ip_address',
|
||||
'module_host_id',
|
||||
'module_datastore_id',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
@@ -137,6 +139,16 @@ class Product extends Model
|
||||
return $this->belongsTo(ModuleIpAddress::class, 'module_ip_address');
|
||||
}
|
||||
|
||||
public function provisioningHost()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\ModuleHost::class, 'module_host_id');
|
||||
}
|
||||
|
||||
public function provisioningDatastore()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\ModuleDatastore::class, 'module_datastore_id');
|
||||
}
|
||||
|
||||
public function addons()
|
||||
{
|
||||
return $this->hasMany(Addon::class, 'type', 'product_type')
|
||||
|
||||
@@ -28,6 +28,7 @@ class Service extends Model
|
||||
'expires_at' => 'datetime',
|
||||
'suspended_at' => 'datetime',
|
||||
'cancelled_at' => 'datetime',
|
||||
'retry_requested_at' => 'datetime',
|
||||
];
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -77,6 +78,21 @@ class Service extends Model
|
||||
return $this->status === 'active';
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === 'pending';
|
||||
}
|
||||
|
||||
public function canRetryProvisioning(): bool
|
||||
{
|
||||
return $this->isPending() && $this->retry_requested_at === null;
|
||||
}
|
||||
|
||||
public function retryRequested(): bool
|
||||
{
|
||||
return $this->retry_requested_at !== null;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at !== null && $this->expires_at->isPast();
|
||||
|
||||
+20
-1
@@ -13,12 +13,31 @@ class Template extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'type_id',
|
||||
'scenario',
|
||||
'format',
|
||||
'invoice_template_id',
|
||||
'name',
|
||||
'subject',
|
||||
'message',
|
||||
'status'
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => 'integer',
|
||||
];
|
||||
|
||||
public const SCENARIOS = [
|
||||
'vm_credentials' => 'Khởi tạo dịch vụ',
|
||||
'renewal_reminder' => 'Nhắc gia hạn',
|
||||
'news_notification'=> 'Thông báo tin tức',
|
||||
'invoice_pdf' => 'Hoá đơn điện tử',
|
||||
];
|
||||
|
||||
public function invoiceTemplate()
|
||||
{
|
||||
return $this->belongsTo(self::class, 'invoice_template_id');
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Type::class);
|
||||
|
||||
@@ -19,7 +19,7 @@ class Ticket extends Model
|
||||
'description',
|
||||
'status',
|
||||
'room',
|
||||
'data'
|
||||
'data',
|
||||
];
|
||||
|
||||
public function customer()
|
||||
|
||||
@@ -19,6 +19,8 @@ class Transaction extends Model
|
||||
'trans',
|
||||
'amount',
|
||||
'customer_id',
|
||||
'user_id',
|
||||
'note',
|
||||
'status',
|
||||
'type',
|
||||
'proof_image',
|
||||
@@ -29,4 +31,9 @@ class Transaction extends Model
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id');
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user