[FEAT] 重構機台日誌 UI 與增加多語系支援,並整合 IoT API 核心架構

- 機台日誌:對齊 Luxury UI 規範,實作整合式佈局與分頁組件。
- 多語系:完成機台日誌繁、英、日三語系翻譯與動態處理。
- UI 規範:更新 SKILL.md 定義「標準列表 Bible」。
- 後端:完善 TenantScoped 隔離邏輯,修復儀表板死循環與 User Model 缺失。
- IoT:擴展機台、會員 Model 並建立交易、商品、狀態等核心表結構。
- 基礎設施:設置台北時區與 Docker 環境變數同步。
This commit is contained in:
2026-03-16 17:29:15 +08:00
parent 1851e91c86
commit 3ce88ed342
54 changed files with 2015 additions and 227 deletions

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CoinInventory extends Model
{
use HasFactory;
protected $fillable = [
'machine_id',
'denomination',
'count',
'type',
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
}

View File

@@ -10,14 +10,20 @@ use App\Traits\TenantScoped;
class Machine extends Model
{
use HasFactory, TenantScoped;
use \Illuminate\Database\Eloquent\SoftDeletes;
protected $fillable = [
'company_id',
'name',
'serial_no',
'model',
'location',
'status',
'current_page',
'door_status',
'temperature',
'firmware_version',
'api_token',
'last_heartbeat_at',
];

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Product\Product;
class MachineSlot extends Model
{
use HasFactory;
protected $fillable = [
'machine_id',
'product_id',
'slot_no',
'slot_name',
'capacity',
'stock',
'price',
'status',
'last_restocked_at',
];
protected $casts = [
'price' => 'decimal:2',
'last_restocked_at' => 'datetime',
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class RemoteCommand extends Model
{
use HasFactory;
protected $fillable = [
'machine_id',
'command',
'payload',
'status',
'response_payload',
'executed_at',
];
protected $casts = [
'payload' => 'array',
'response_payload' => 'array',
'executed_at' => 'datetime',
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Models\Machine;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TimerStatus extends Model
{
use HasFactory;
protected $fillable = [
'machine_id',
'slot_no',
'status',
'remaining_seconds',
'end_at',
];
protected $casts = [
'end_at' => 'datetime',
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
}

View File

@@ -31,6 +31,10 @@ class Member extends Authenticatable
'avatar',
'is_active',
'email_verified_at',
'company_id',
'barcode',
'points',
'wallet_balance',
];
/**
@@ -49,6 +53,8 @@ class Member extends Authenticatable
'birthday' => 'date',
'is_active' => 'boolean',
'password' => 'hashed',
'points' => 'integer',
'wallet_balance' => 'decimal:2',
];
/**

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Models\Product;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\TenantScoped;
class Product extends Model
{
use HasFactory, SoftDeletes, TenantScoped;
protected $fillable = [
'company_id',
'category_id',
'name',
'sku',
'barcode',
'description',
'price',
'cost',
'type',
'image_url',
'status',
'name_dictionary_key',
'metadata',
];
protected $casts = [
'price' => 'decimal:2',
'cost' => 'decimal:2',
'metadata' => 'array',
];
public function category()
{
return $this->belongsTo(ProductCategory::class, 'category_id');
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models\Product;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\TenantScoped;
class ProductCategory extends Model
{
use HasFactory, SoftDeletes, TenantScoped;
protected $fillable = [
'company_id',
'name',
'name_dictionary_key',
];
public function products()
{
return $this->hasMany(Product::class, 'category_id');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Translation extends Model
{
use HasFactory;
protected $fillable = [
'group',
'key',
'locale',
'value',
];
}

View File

@@ -8,11 +8,13 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use App\Traits\TenantScoped;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable, HasRoles;
use HasApiTokens, HasFactory, Notifiable, HasRoles, TenantScoped, SoftDeletes;
/**
* The attributes that are mass assignable.

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Machine\Machine;
use App\Models\Product\Product;
class DispenseRecord extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'company_id',
'order_id',
'flow_id',
'machine_id',
'product_id',
'slot_no',
'amount',
'remaining_stock',
'dispense_status',
'member_barcode',
'machine_time',
'points_used',
];
protected $casts = [
'amount' => 'decimal:2',
'machine_time' => 'datetime',
'dispense_status' => 'integer',
];
public function order()
{
return $this->belongsTo(Order::class);
}
public function machine()
{
return $this->belongsTo(Machine::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Invoice extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'company_id',
'order_id',
'machine_id',
'flow_id',
'invoice_no',
'amount',
'carrier_id',
'invoice_date',
'random_number',
'love_code',
'rtn_code',
'rtn_msg',
'metadata',
];
protected $casts = [
'total_amount' => 'decimal:2',
'tax_amount' => 'decimal:2',
'metadata' => 'array',
];
public function order()
{
return $this->belongsTo(Order::class);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\TenantScoped;
use App\Models\Machine\Machine;
use App\Models\Member\Member;
class Order extends Model
{
use HasFactory, SoftDeletes, TenantScoped;
protected $fillable = [
'company_id',
'flow_id',
'order_no',
'machine_id',
'member_id',
'total_amount',
'discount_amount',
'pay_amount',
'payment_type',
'payment_status',
'payment_at',
'status',
'metadata',
];
protected $casts = [
'total_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'pay_amount' => 'decimal:2',
'payment_at' => 'datetime',
'metadata' => 'array',
];
public function machine()
{
return $this->belongsTo(Machine::class);
}
public function member()
{
return $this->belongsTo(Member::class);
}
public function items()
{
return $this->hasMany(OrderItem::class);
}
public function invoice()
{
return $this->hasOne(Invoice::class);
}
public function dispenseRecords()
{
return $this->hasMany(DispenseRecord::class);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Product\Product;
class OrderItem extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'product_id',
'product_name',
'sku',
'price',
'quantity',
'subtotal',
'metadata',
];
protected $casts = [
'price' => 'decimal:2',
'subtotal' => 'decimal:2',
'metadata' => 'array',
];
public function order()
{
return $this->belongsTo(Order::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models\Transaction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class PaymentType extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'code',
'config',
'status',
];
protected $casts = [
'config' => 'array',
];
}