DB 滙入
User 介面 只能看資料不能做修改 Role 介面移除 Branch介面 只能看資料不能做修改 Room 介面 可操控 包廂開關台 Sqgger API 可操控API Room 有操控異動記錄 machine_statuses 需做資料留存需留7 天 20250528
This commit is contained in:
parent
dcb27b8c9c
commit
7c8c3fe69b
17
.env.example
17
.env.example
@ -3,8 +3,8 @@ APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=Asia/Taipei
|
||||
APP_URL=https://shop_12_wireui.test
|
||||
L5_SWAGGER_CONST_HOST=https://shop_12_wireui.test/
|
||||
APP_URL=https://KTVCentral.test
|
||||
L5_SWAGGER_CONST_HOST=https://KTVCentral.test/
|
||||
|
||||
APP_LOCALE=zh-tw
|
||||
APP_FALLBACK_LOCALE=zh-tw
|
||||
@ -22,12 +22,13 @@ LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
#DB_CONNECTION=sqlite
|
||||
DB_CONNECTION=mariadb
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3307
|
||||
DB_DATABASE=Karaoke-Kingpin_Central
|
||||
DB_USERNAME=Karaoke-Kingpin
|
||||
DB_PASSWORD=ESM7yTPMnavFmbBH
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
46
app/Console/Commands/ClearMachineStatuses.php
Normal file
46
app/Console/Commands/ClearMachineStatuses.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ClearMachineStatuses extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:clear-machine-statuses';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '備份並清空 machine_statuses 表,每週保留一次';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$day = now()->format('l'); // e.g. "Monday"
|
||||
$targetTable = "machine_statuses_" . $day;
|
||||
|
||||
DB::statement("CREATE TABLE IF NOT EXISTS _machine_statuses LIKE machine_statuses");
|
||||
|
||||
// 先刪除舊表(如存在)
|
||||
DB::statement("DROP TABLE IF EXISTS {$targetTable}");
|
||||
|
||||
// 改名備份
|
||||
DB::statement("RENAME TABLE machine_statuses TO {$targetTable}");
|
||||
|
||||
// 空表回命名
|
||||
DB::statement("RENAME TABLE _machine_statuses TO machine_statuses");
|
||||
|
||||
$this->info("Machine statuses backed up to {$targetTable} and table cleared.");
|
||||
}
|
||||
}
|
50
app/Console/Commands/TransferSqliteToMysql.php
Normal file
50
app/Console/Commands/TransferSqliteToMysql.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Jobs\TransferSqliteTableJob;
|
||||
|
||||
class TransferSqliteToMysql extends Command
|
||||
{
|
||||
protected $signature = 'transfer:sqlite
|
||||
{path : SQLite 相對路徑(例:sqlite/song.sqlite)}
|
||||
{--sync : 同步執行}';
|
||||
|
||||
protected $description = '將 SQLite 中的資料表轉移到 MySQL 資料庫中';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$start = now();
|
||||
$path = ltrim($this->argument('path'), '/');
|
||||
$fullPath = Storage::disk('local')->path($path);
|
||||
|
||||
$this->info("[Transfer] 開始轉移 SQLite 資料:{$fullPath}");
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
$this->error("[Transfer] 找不到 SQLite 檔案:{$fullPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->option('sync')) {
|
||||
$this->warn('[Transfer] 使用同步模式執行...');
|
||||
(new TransferSqliteTableJob($fullPath))->handle();
|
||||
$this->info('[Transfer] 匯出完成(同步)');
|
||||
} else {
|
||||
TransferSqliteTableJob::dispatch($fullPath);
|
||||
$this->info('[Transfer] 匯出任務已派送至 queue');
|
||||
}
|
||||
|
||||
$duration = now()->diffInSeconds($start);
|
||||
$this->info("[Transfer] 執行完成,用時 {$duration} 秒");
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('[Transfer] 發生錯誤:' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
33
app/Enums/RoomStatus.php
Normal file
33
app/Enums/RoomStatus.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Traits\HasLabels;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="RoomStatus",
|
||||
* type="string",
|
||||
* enum={"active", "closed", "fire", "maintenance"},
|
||||
* example="error"
|
||||
* )
|
||||
*/
|
||||
enum RoomStatus: string {
|
||||
use HasLabels;
|
||||
|
||||
case Active = 'active';
|
||||
case Closed = 'closed';
|
||||
case Fire ='fire';
|
||||
case Error = 'error';
|
||||
|
||||
// 返回對應的顯示文字
|
||||
public function labels(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::Active => __('enums.room.status.Active'),
|
||||
self::Closed => __('enums.room.status.Closed'),
|
||||
self::Fire => __('enums.room.status.Fire'),
|
||||
self::Error => __('enums.room.status.Error'),
|
||||
};
|
||||
}
|
||||
}
|
31
app/Enums/RoomType.php
Normal file
31
app/Enums/RoomType.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Traits\HasLabels;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="RoomType",
|
||||
* type="string",
|
||||
* enum={"unset", "pc", "svr"},
|
||||
* example="error"
|
||||
* )
|
||||
*/
|
||||
enum RoomType: string {
|
||||
use HasLabels;
|
||||
|
||||
case Unset = 'unset';
|
||||
case PC = 'pc';
|
||||
case SVR ='svr';
|
||||
|
||||
// 返回對應的顯示文字
|
||||
public function labels(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::Unset => __('enums.room.status.Unset'),
|
||||
self::PC => "PC",
|
||||
self::SVR => "SVR",
|
||||
};
|
||||
}
|
||||
}
|
@ -4,6 +4,14 @@ namespace App\Enums;
|
||||
|
||||
use App\Enums\Traits\HasLabels;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="UserGender",
|
||||
* type="string",
|
||||
* enum={"male", "female", "other", "unset"},
|
||||
* example="male"
|
||||
* )
|
||||
*/
|
||||
enum UserGender: string
|
||||
{
|
||||
use HasLabels;
|
||||
|
@ -4,6 +4,15 @@ namespace App\Enums;
|
||||
|
||||
use App\Enums\Traits\HasLabels;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="UserStatus",
|
||||
* type="string",
|
||||
* enum={"0", "1", "2"},
|
||||
* description="User status: 0=Active, 1=Suspended, 2=Deleting",
|
||||
* example="0"
|
||||
* )
|
||||
*/
|
||||
enum UserStatus: int
|
||||
{
|
||||
use HasLabels;
|
||||
|
305
app/Http/Controllers/RoomControlController.php
Normal file
305
app/Http/Controllers/RoomControlController.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\SendRoomSwitchCommandRequest;
|
||||
use App\Http\Requests\ReceiveRoomRegisterRequest;
|
||||
use App\Http\Requests\ReceiveRoomStatusDataRequest;
|
||||
use App\Services\TcpSocketClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Room;
|
||||
use App\Models\MachineStatus;
|
||||
use App\Enums\RoomStatus;
|
||||
use App\Http\Responses\ApiResponse;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="Auth",
|
||||
* description="包廂控制"
|
||||
* )
|
||||
*/
|
||||
class RoomControlController extends Controller
|
||||
{
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/room/receiveRegister",
|
||||
* summary="包廂註冊控制指令",
|
||||
* description="依據傳入的 branch_id 與 room_name,知道過來的設備來之於那個IP設備。",
|
||||
* operationId="registerRoomCommand",
|
||||
* tags={"Room Control"},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/ReceiveRoomRegisterRequest")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="成功傳送指令並回傳 TCP 回應",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Room")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="Accept",
|
||||
* in="header",
|
||||
* required=true,
|
||||
* @OA\Schema(type="string", default="application/json")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function receiveRegister(ReceiveRoomRegisterRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->only(['branch', 'room_name', 'room_ip', 'email']); // 不記錄密碼
|
||||
Log::info('Token Request Payload:', $data);
|
||||
|
||||
// 1. 驗證帳密(登入用)
|
||||
$credentials = $request->only('email', 'password');
|
||||
|
||||
if (!Auth::attempt($credentials)) {
|
||||
return ApiResponse::unauthorized();
|
||||
}
|
||||
|
||||
// 2. 取得登入使用者
|
||||
$user = Auth::user();
|
||||
|
||||
// 3. 產生或取得 Token
|
||||
if (empty($user->api_plain_token)) {
|
||||
$token = $user->createToken('pc-heartbeat')->plainTextToken;
|
||||
$user->api_plain_token = $token;
|
||||
$user->save();
|
||||
} else {
|
||||
$token = $user->api_plain_token;
|
||||
}
|
||||
|
||||
// 4. 驗證其他註冊欄位
|
||||
$validated = $request->validated(); // branch_id, room_name, room_ip
|
||||
|
||||
// 5. 找出對應包廂
|
||||
$roomType = null;
|
||||
$roomName = null;
|
||||
// 從 room_name(例如 PC101, SVR01)中擷取 type 與 name
|
||||
if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['room_name'], $matches)) {
|
||||
$roomType = strtolower($matches[1]); // 'PC' → 'pc'
|
||||
$roomName = $matches[2]; // '101'
|
||||
}
|
||||
$branch=Branch::where('name',$validated['branch_name'])->first();
|
||||
$room = Room::where('branch_id', $branch->id)
|
||||
->where('name', $roomName)
|
||||
->where('type', $roomType)
|
||||
->first();
|
||||
|
||||
if (!$room) {
|
||||
return ApiResponse::error('找不到對應包廂');
|
||||
}
|
||||
|
||||
// 6. 更新包廂資訊
|
||||
$room->internal_ip = $validated['room_ip'];
|
||||
$room->port = 1000; // 預設值
|
||||
$room->is_online =1;
|
||||
$room->status = RoomStatus::Closed;
|
||||
$room->touch(); // 更新 updated_at
|
||||
$room->save();
|
||||
|
||||
// 7. 回傳 token 與包廂資料
|
||||
return ApiResponse::success([
|
||||
'token' => $token,
|
||||
'room' => $room,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/room/heartbeat",
|
||||
* summary="包廂心跳封包指令",
|
||||
* description="記錄設備連線狀況",
|
||||
* operationId="heartbeatRoomCommand",
|
||||
* tags={"Room Control"},
|
||||
* security={{"Authorization":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/ReceiveRoomStatusDataRequest")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="成功傳送指令並回傳 TCP 回應",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="data", ref="#/components/schemas/MachineStatus")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="Accept",
|
||||
* in="header",
|
||||
* required=true,
|
||||
* @OA\Schema(type="string", default="application/json")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function StatusReport(ReceiveRoomStatusDataRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$roomType = null;
|
||||
$roomName = null;
|
||||
// 從 room_name(例如 PC101, SVR01)中擷取 type 與 name
|
||||
if (preg_match('/^([A-Za-z]+)(\d+)$/', $validated['hostname'], $matches)) {
|
||||
$roomType = strtolower($matches[1]); // 'PC' → 'pc'
|
||||
$roomName = $matches[2]; // '101'
|
||||
}
|
||||
$branch=Branch::where('name',$validated['branch_name'])->first();
|
||||
$room = Room::where('branch_id', $branch->id)
|
||||
->where('name', $roomName)
|
||||
->where('type', $roomType)
|
||||
->first();
|
||||
// 決定 status 欄位值
|
||||
$validated['status']= 'error';
|
||||
if($room){
|
||||
$validated['status']= 'online';
|
||||
if($room->internal_ip != $validated['ip']){
|
||||
$room->internal_ip = $validated['ip'];
|
||||
$validated['status']='error';
|
||||
}
|
||||
$room->is_online=1;
|
||||
$room->touch(); // 更新 updated_at
|
||||
$room->save();
|
||||
}
|
||||
|
||||
return ApiResponse::success([
|
||||
'data' => MachineStatus::create($validated),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/room/sendSwitch",
|
||||
* summary="送出包廂控制指令",
|
||||
* description="依據傳入的 room_id 與 command,透過 TCP 傳送對應指令給包廂電腦。",
|
||||
* operationId="sendRoomSwitchCommand",
|
||||
* tags={"Room Control"},
|
||||
* security={{"Authorization":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\JsonContent(ref="#/components/schemas/SendRoomSwitchCommandRequest")
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="成功傳送指令並回傳 TCP 回應",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="data", ref="#/components/schemas/Room")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=401,
|
||||
* description="Unauthorized",
|
||||
* @OA\JsonContent(
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||
* @OA\Schema(
|
||||
* @OA\Property(property="code", type="string", example="UNAUTHORIZED"),
|
||||
* @OA\Property(property="message", type="string", example="Unauthorized"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
* ),
|
||||
* @OA\Parameter(
|
||||
* name="Accept",
|
||||
* in="header",
|
||||
* required=true,
|
||||
* @OA\Schema(type="string", default="application/json")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function sendSwitch(SendRoomSwitchCommandRequest $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$room = Room::where([
|
||||
['branch_id', $validated['branch_id']],
|
||||
['name', $validated['room_name']],
|
||||
])->first();
|
||||
|
||||
if (!$room) {
|
||||
return ApiResponse::error('房間不存在');
|
||||
}
|
||||
|
||||
// 檢查必要欄位是否缺失或狀態為錯誤
|
||||
if (empty($room->internal_ip) || empty($room->port)) {
|
||||
return ApiResponse::error('房間未設定 IP 或 Port');
|
||||
}
|
||||
|
||||
if ($room->status === RoomStatus::Error) {
|
||||
return ApiResponse::error('房間目前處於錯誤狀態,無法操作');
|
||||
}
|
||||
|
||||
$suffix = substr($room->name, -3) ?: $room->name;
|
||||
$signal = match ($validated['command']) {
|
||||
'active' => 'O',
|
||||
'closed' => 'X',
|
||||
'fire' => 'F',
|
||||
default => 'X', // fallback 保險起見
|
||||
};
|
||||
$data = $suffix . "," . $signal;
|
||||
|
||||
//dd($data);
|
||||
$client = new TcpSocketClient($room->internal_ip, $room->port);
|
||||
try {
|
||||
$response = $client->send($data);
|
||||
|
||||
$room->status=$validated['command'];
|
||||
$room->started_at=$validated['started_at'];
|
||||
$room->ended_at=$validated['ended_at'];
|
||||
$room->save();
|
||||
|
||||
return ApiResponse::success($room);
|
||||
} catch (\Throwable $e) {
|
||||
$room->status=RoomStatus::Error;
|
||||
$room->started_at=null;
|
||||
$room->ended_at=null;
|
||||
$room->save();
|
||||
return ApiResponse::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
75
app/Http/Controllers/SqliteUploadController.php
Normal file
75
app/Http/Controllers/SqliteUploadController.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Jobs\TransferSqliteTableJob;
|
||||
|
||||
/**
|
||||
* @OA\Tag(
|
||||
* name="SqliteUpload",
|
||||
* description="Sqlite 檔案上傳"
|
||||
* )
|
||||
*/
|
||||
class SqliteUploadController extends Controller
|
||||
{
|
||||
/**
|
||||
* 上傳 Sqlite 檔案
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/upload-sqlite",
|
||||
* tags={"SqliteUpload"},
|
||||
* summary="上傳 SQLite 檔案",
|
||||
* description="接收一個 SQLite 檔案並儲存至 storage/app/sqlite/,並派送資料轉移任務",
|
||||
* operationId="uploadSqlite",
|
||||
* security={{"Authorization":{}}},
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
* @OA\MediaType(
|
||||
* mediaType="multipart/form-data",
|
||||
* @OA\Schema(
|
||||
* required={"file"},
|
||||
* @OA\Property(
|
||||
* property="file",
|
||||
* type="string",
|
||||
* format="binary",
|
||||
* description="要上傳的 SQLite 檔案"
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="上傳成功",
|
||||
* @OA\JsonContent(
|
||||
* @OA\Property(property="message", type="string", example="上傳成功"),
|
||||
* @OA\Property(property="path", type="string", example="sqlite/tempUser.sqlite")
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="驗證錯誤"
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function upload(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file',
|
||||
]);
|
||||
|
||||
if ($request->file('file')->getClientOriginalExtension() !== 'sqlite') {
|
||||
return response()->json(['message' => '只允許上傳 .sqlite 檔案'], 422);
|
||||
}
|
||||
|
||||
$filename = $request->file('file')->getClientOriginalName();
|
||||
$path = $request->file('file')->storeAs('sqlite', $filename, 'local');
|
||||
TransferSqliteTableJob::dispatch(Storage::disk('local')->path($path));
|
||||
return response()->json([
|
||||
'message' => '上傳成功,已派送資料處理任務',
|
||||
'path' => $path,
|
||||
]);
|
||||
}
|
||||
}
|
31
app/Http/Requests/ApiRequest.php
Normal file
31
app/Http/Requests/ApiRequest.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use App\Http\Requests\Traits\FailedValidationJsonResponse;
|
||||
|
||||
class ApiRequest extends FormRequest
|
||||
{
|
||||
|
||||
use FailedValidationJsonResponse;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array{
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
35
app/Http/Requests/ReceiveRoomRegisterRequest.php
Normal file
35
app/Http/Requests/ReceiveRoomRegisterRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="ReceiveRoomRegisterRequest",
|
||||
* required={"branch", "room_name", "email" ,"password"},
|
||||
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||
* @OA\Property(property="room_name", type="string", example="PC101"),
|
||||
* @OA\Property(property="room_ip", type="string", example="192.168.1.1"),
|
||||
* @OA\Property(property="email", type="string", example="XX@gmail.com"),
|
||||
* @OA\Property(property="password", type="string", example="XXX"),
|
||||
* )
|
||||
*/
|
||||
class ReceiveRoomRegisterRequest extends ApiRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'branch_name' =>'required|string|exists:branches,name',
|
||||
'room_name' => 'required|string',
|
||||
'room_ip' => 'required|string',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
];
|
||||
}
|
||||
}
|
37
app/Http/Requests/ReceiveRoomStatusDataRequest.php
Normal file
37
app/Http/Requests/ReceiveRoomStatusDataRequest.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="ReceiveRoomStatusDataRequest",
|
||||
* required={"branch_name","hostname", "ip", "status"},
|
||||
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||
* @OA\Property(property="hostname", type="string", example="PC101"),
|
||||
* @OA\Property(property="ip", type="string", example="192.168.XX.XX"),
|
||||
* @OA\Property(property="cpu", type="numeric", example="0.00"),
|
||||
* @OA\Property(property="memory", type="numeric", example="25603"),
|
||||
* @OA\Property(property="disk", type="numeric", example="158266.49"),
|
||||
* )
|
||||
*/
|
||||
class ReceiveRoomStatusDataRequest extends ApiRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'branch_name' =>'required|string|exists:branches,name',
|
||||
'hostname' => 'required|string',
|
||||
'ip' => 'required|string',
|
||||
'cpu' => 'nullable|numeric',
|
||||
'memory' => 'nullable|numeric',
|
||||
'disk' => 'nullable|numeric',
|
||||
];
|
||||
}
|
||||
}
|
35
app/Http/Requests/SendRoomSwitchCommandRequest.php
Normal file
35
app/Http/Requests/SendRoomSwitchCommandRequest.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="SendRoomSwitchCommandRequest",
|
||||
* required={"branch_id", "room_name", "command"},
|
||||
* @OA\Property(property="branch_id", type="integer", example="5"),
|
||||
* @OA\Property(property="room_name", type="string", example="pc102"),
|
||||
* @OA\Property(property="command", type="string", enum={"active", "closed", "fire", "maintenance"}, example="active"),
|
||||
* @OA\Property(property="started_at", type="string", nullable=true, example="2025-05-19 09:31:00"),
|
||||
* @OA\Property(property="ended_at", type="string", nullable=true, example="2025-05-19 09:31:00")
|
||||
* )
|
||||
*/
|
||||
class SendRoomSwitchCommandRequest extends ApiRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'branch_id' => 'required|integer',
|
||||
'room_name' => 'required|string',
|
||||
'command' => 'required|string',
|
||||
'started_at' => 'nullable|date_format:Y-m-d H:i:s',
|
||||
'ended_at' => 'nullable|date_format:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
}
|
20
app/Http/Requests/Traits/FailedValidationJsonResponse.php
Normal file
20
app/Http/Requests/Traits/FailedValidationJsonResponse.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Traits;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
trait FailedValidationJsonResponse
|
||||
{
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
throw new HttpResponseException(response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
'code' => 'ERROR',
|
||||
'token' => ''
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY));
|
||||
}
|
||||
}
|
39
app/Http/Responses/ApiResponse.php
Normal file
39
app/Http/Responses/ApiResponse.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Responses;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="ApiResponse",
|
||||
* type="object",
|
||||
* @OA\Property(property="code", type="string", example="OK"),
|
||||
* @OA\Property(property="message", type="string", example="Success"),
|
||||
* @OA\Property(property="data", type="object", nullable=true)
|
||||
* )
|
||||
*/
|
||||
|
||||
class ApiResponse
|
||||
{
|
||||
public static function success($data = null, string $message = 'Success', string $code = 'OK', int $status = Response::HTTP_OK): JsonResponse
|
||||
{
|
||||
return self::respond($code, $message, $data, $status);
|
||||
}
|
||||
|
||||
public static function error(string $message = 'Error', string $code = 'ERROR', int $status = Response::HTTP_BAD_REQUEST): JsonResponse
|
||||
{
|
||||
return self::respond($code, $message, null, $status);
|
||||
}
|
||||
|
||||
public static function unauthorized(string $message = 'Unauthorized'): JsonResponse
|
||||
{
|
||||
return self::error($message, 'UNAUTHORIZED', Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
private static function respond(string $code, string $message, $data, int $status): JsonResponse
|
||||
{
|
||||
return response()->json(['code' => $code,'message' => $message,'data' => $data,], $status);
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithChunkReading;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Jobs\ImportUserChunkJob;
|
||||
|
||||
class DataImport implements ToCollection, WithHeadingRow, WithChunkReading
|
||||
{
|
||||
protected int $con=0;
|
||||
protected string $modelName;
|
||||
public function __construct(string $modelName)
|
||||
{
|
||||
HeadingRowFormatter::default('none');
|
||||
$this->modelName= $modelName;
|
||||
}
|
||||
public function collection(Collection $rows)
|
||||
{
|
||||
|
||||
Log::warning('匯入啟動', [
|
||||
'model' => $this->modelName,
|
||||
'rows_id' =>++$this->con,
|
||||
'rows_count' => $rows->count()
|
||||
]);
|
||||
if($this->modelName=='User'){
|
||||
ImportUserChunkJob::dispatch($rows,$this->con);
|
||||
}else{
|
||||
Log::warning('未知的 modelName', ['model' => $this->modelName]);
|
||||
}
|
||||
}
|
||||
public function chunkSize(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function headingRow(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Imports\DataImport;
|
||||
|
||||
class ImportJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected string $modelName;
|
||||
protected string $filePath;
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $filePath,string $modelName)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->modelName= $modelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
ini_set('memory_limit', '512M'); // ✅ 增加記憶體限制
|
||||
Log::info('[ImportJob] 開始處理檔案:' . $this->filePath);
|
||||
|
||||
try {
|
||||
if (!Storage::exists($this->filePath)) {
|
||||
Log::warning('[ImportJob] 檔案不存在:' . $this->filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
Excel::import(new DataImport($this->modelName), $this->filePath);
|
||||
Log::info('[ImportJob] 已提交所有 chunk 匯入任務。');
|
||||
|
||||
Storage::delete($this->filePath);
|
||||
Log::info('[ImportJob] 已刪除檔案:' . $this->filePath);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error("[ImportJob] 匯入失敗:{$e->getMessage()}", [
|
||||
'file' => $this->filePath,
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ImportUserChunkJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected Collection $rows;
|
||||
protected String $id;
|
||||
|
||||
public function __construct(Collection $rows,String $id)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
Log::warning('匯入啟動', [
|
||||
'model' => "ImportUserChunkJob",
|
||||
'rows_id' =>$this->id,
|
||||
]);
|
||||
$now = now();
|
||||
foreach ($this->rows as $index => $row) {
|
||||
try {
|
||||
$name = $this->normalizeName($row['歌手姓名'] ?? '');
|
||||
|
||||
if (empty($name) || User::where('name', $name)->exists()) {
|
||||
continue;
|
||||
}
|
||||
// 準備 song 資料
|
||||
$toInsert[] = [
|
||||
'name' => $name,
|
||||
'category' => ArtistCategory::tryFrom(trim($row['歌手分類'] ?? '未定義')) ?? ArtistCategory::Unset,
|
||||
'simplified' => $simplified,
|
||||
'phonetic_abbr' => $phoneticAbbr,
|
||||
'pinyin_abbr' => $pinyinAbbr,
|
||||
'strokes_abbr' => $strokesAbbr,
|
||||
'enable' =>trim($row['狀態'] ?? 1),
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error("Row {$index} failed: {$e->getMessage()}", [
|
||||
'row' => $row,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
User::insert($toInsert);
|
||||
}
|
||||
|
||||
public function normalizeName(?string $str): string
|
||||
{
|
||||
return strtoupper(mb_convert_kana(trim($str ?? ''), 'as'));
|
||||
}
|
||||
}
|
101
app/Jobs/TransferSqliteTableJob.php
Normal file
101
app/Jobs/TransferSqliteTableJob.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class TransferSqliteTableJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected string $sqlitePath;
|
||||
|
||||
public function __construct(string $sqlitePath){
|
||||
$this->sqlitePath=$sqlitePath;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!file_exists($this->sqlitePath)) {
|
||||
logger()->error("❌ SQLite file not found: {$this->sqlitePath}");
|
||||
return;
|
||||
}
|
||||
// ✅ 動態產生唯一 connection 名稱
|
||||
$connectionName = 'tempsqlite_' . md5($this->sqlitePath . microtime());
|
||||
config(["database.connections.{$connectionName}" => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => $this->sqlitePath,
|
||||
'prefix' => '',
|
||||
]]);
|
||||
|
||||
$mysqlConnection = config('database.default');
|
||||
|
||||
$sqliteTables = DB::connection($connectionName)->select("
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||
");
|
||||
|
||||
if (empty($sqliteTables)) {
|
||||
logger()->error("❌ No tables found in SQLite database.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($sqliteTables as $tableObj) {
|
||||
$table = $tableObj->name;
|
||||
|
||||
if ($table === 'migrations') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::connection($mysqlConnection)->statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
DB::statement("CREATE TABLE IF NOT EXISTS _{$table} LIKE {$table}");
|
||||
|
||||
$rows = DB::connection($connectionName)->table($table)->cursor();
|
||||
|
||||
$buffer = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$buffer[] = (array) $row;
|
||||
$count++;
|
||||
|
||||
if ($count % 500 === 0) {
|
||||
DB::connection($mysqlConnection)->table("_" . $table)->insert($buffer);
|
||||
$buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($buffer)) {
|
||||
DB::connection($mysqlConnection)->table("_" . $table)->insert($buffer);
|
||||
}
|
||||
|
||||
DB::statement("RENAME TABLE {$table} TO {$table}_");
|
||||
DB::statement("RENAME TABLE _{$table} TO {$table}");
|
||||
DB::statement("DROP TABLE IF EXISTS {$table}_");
|
||||
|
||||
logger()->info("✅ Done: {$table} ({$count} records)");
|
||||
} catch (\Exception $e) {
|
||||
logger()->error("❌ Failed to transfer {$table}: " . $e->getMessage());
|
||||
} finally {
|
||||
DB::connection($mysqlConnection)->statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 結束後刪檔與釋放 connection
|
||||
DB::purge($connectionName);
|
||||
if (file_exists($this->sqlitePath)) {
|
||||
sleep(1);
|
||||
unlink($this->sqlitePath);
|
||||
logger()->info("🧹 Temp SQLite file deleted: {$this->sqlitePath}");
|
||||
}
|
||||
|
||||
logger()->info("🎉 All tables transferred.");
|
||||
}
|
||||
}
|
89
app/Livewire/Admin/BranchTable.php
Normal file
89
app/Livewire/Admin/BranchTable.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\Branch;
|
||||
use App\Jobs\ExportSqliteBranchJob;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use PowerComponents\LivewirePowerGrid\Button;
|
||||
use PowerComponents\LivewirePowerGrid\Column;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||
use Livewire\Attributes\On;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
final class BranchTable extends PowerGridComponent
|
||||
{
|
||||
use WithExport, WireUiActions;
|
||||
public string $tableName = 'branch-table';
|
||||
|
||||
public bool $showFilters = false;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
config(['livewire-powergrid.filter' => 'outside']);
|
||||
}
|
||||
|
||||
public function setUp(): array
|
||||
{
|
||||
$actions = [];
|
||||
$header = PowerGrid::header()
|
||||
->withoutLoading()
|
||||
->showToggleColumns();
|
||||
$header->includeViewOnTop('livewire.admin.branch-header') ;
|
||||
|
||||
$actions[]=$header;
|
||||
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function datasource(): Builder
|
||||
{
|
||||
return Branch::query();
|
||||
}
|
||||
|
||||
public function relationSearch(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fields(): PowerGridFields
|
||||
{
|
||||
return PowerGrid::fields()
|
||||
->add('id')
|
||||
->add('name')
|
||||
->add('external_ip')
|
||||
->add('enable')
|
||||
->add('created_at_formatted', fn (Branch $model) => Carbon::parse($model->created_at)->format('d/m/Y H:i:s'));
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
$column=[];
|
||||
$column[]=Column::make(__('branches.no'), 'id');
|
||||
$column[]=Column::make(__('branches.name'), 'name')->sortable()->searchable();
|
||||
$column[]=Column::make(__('branches.external_ip'), 'external_ip')->sortable()->searchable();
|
||||
$column[]=Column::make(__('branches.enable'), 'enable');
|
||||
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable()->hidden(true, false);
|
||||
return $column;
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
return [
|
||||
Filter::inputText('name')->placeholder(__('branches.name')),
|
||||
Filter::inputText('external_ip')->placeholder(__('branches.external_ip')),
|
||||
Filter::boolean('enable')->label('✅', '❌'),
|
||||
Filter::datetimepicker('created_at'),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use Livewire\Component;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class RoleForm extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
|
||||
protected $listeners = ['openCreateRoleModal','openEditRoleModal', 'deleteRole'];
|
||||
|
||||
public bool $canCreate;
|
||||
public bool $canEdit;
|
||||
public bool $canDelect;
|
||||
|
||||
public $showCreateModal=false;
|
||||
public ?int $roleId = null;
|
||||
public $name = '';
|
||||
public $permissions = []; // 所有權限清單
|
||||
public $selectedPermissions = []; // 表單中選到的權限
|
||||
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->permissions = Permission::all();
|
||||
$this->canCreate = Auth::user()?->can('role-edit') ?? false;
|
||||
$this->canEdit = Auth::user()?->can('role-edit') ?? false;
|
||||
$this->canDelect = Auth::user()?->can('role-delete') ?? false;
|
||||
}
|
||||
|
||||
public function openCreateRoleModal()
|
||||
{
|
||||
$this->resetFields();
|
||||
$this->showCreateModal = true;
|
||||
}
|
||||
|
||||
public function openEditRoleModal($id)
|
||||
{
|
||||
$role = Role::findOrFail($id);
|
||||
$this->roleId = $role->id;
|
||||
$this->name = $role->name;
|
||||
$this->selectedPermissions = $role->permissions()->pluck('id')->toArray();
|
||||
$this->showCreateModal = true;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'selectedPermissions' => 'array',
|
||||
]);
|
||||
|
||||
if ($this->roleId) {
|
||||
if ($this->canEdit) {
|
||||
$role = Role::findOrFail($this->roleId);
|
||||
$role->update(['name' => $this->name]);
|
||||
$role->syncPermissions($this->selectedPermissions);
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '角色已更新',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($this->canCreate) {
|
||||
$role = Role::create(['name' => $this->name]);
|
||||
$role->syncPermissions($this->selectedPermissions);
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '角色已新增',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetFields();
|
||||
$this->showCreateModal = false;
|
||||
$this->dispatch('pg:eventRefresh-role-table');
|
||||
}
|
||||
|
||||
public function deleteRole($id)
|
||||
{
|
||||
if ($this->canDelect) {
|
||||
Role::findOrFail($id)->delete();
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '角色已刪除',
|
||||
]);
|
||||
$this->dispatch('pg:eventRefresh-role-table');
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFields()
|
||||
{
|
||||
$this->name = '';
|
||||
$this->selectedPermissions = [];
|
||||
$this->roleId = null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.role-form');
|
||||
}
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use PowerComponents\LivewirePowerGrid\Button;
|
||||
use PowerComponents\LivewirePowerGrid\Column;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||
use Livewire\Attributes\On;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
final class RoleTable extends PowerGridComponent
|
||||
{
|
||||
use WireUiActions;
|
||||
|
||||
public string $tableName = 'role-table';
|
||||
public bool $canCreate;
|
||||
public bool $canEdit;
|
||||
public bool $canDelect;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
config(['livewire-powergrid.filter' => 'outside']);
|
||||
//權限設定
|
||||
$this->canCreate = Auth::user()?->can('role-edit') ?? false;
|
||||
$this->canEdit = Auth::user()?->can('role-edit') ?? false;
|
||||
$this->canDelect = Auth::user()?->can('role-delete') ?? false;
|
||||
}
|
||||
|
||||
public function setUp(): array
|
||||
{
|
||||
if($this->canDelect){
|
||||
$this->showCheckBox();
|
||||
}
|
||||
$actions = [];
|
||||
$header =PowerGrid::header();
|
||||
if($this->canCreate){
|
||||
$header->includeViewOnTop('livewire.admin.role-header');
|
||||
}
|
||||
$actions[]=$header;
|
||||
$actions[]=PowerGrid::footer()
|
||||
->showPerPage()
|
||||
->showRecordCount();
|
||||
return $actions;
|
||||
}
|
||||
public function header(): array
|
||||
{
|
||||
$actions = [];
|
||||
if ($this->canDelect) {
|
||||
$actions[]=Button::add('bulk-delete')
|
||||
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
|
||||
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatch('bulkDelete.' . $this->tableName, []);
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function datasource(): Builder
|
||||
{
|
||||
//dd(Role::with('permissions'));
|
||||
return Role::with('permissions');
|
||||
}
|
||||
|
||||
public function relationSearch(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fields(): PowerGridFields
|
||||
{
|
||||
$allPermissions = Permission::pluck('name')->sort()->values();
|
||||
return PowerGrid::fields()
|
||||
->add('id')
|
||||
->add('name')
|
||||
->add('permissions_list', function (Role $model) use ($allPermissions) {
|
||||
$rolePermissions = $model->permissions->pluck('name')->sort()->values();
|
||||
|
||||
if ($rolePermissions->count() === $allPermissions->count() && $rolePermissions->values()->all() === $allPermissions->values()->all()) {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
return $rolePermissions->implode(', ');
|
||||
})
|
||||
->add('created_at_formatted', fn (Role $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
$column=[];
|
||||
$column[]=Column::make(__('roles.no'), 'id')->sortable()->searchable();
|
||||
$column[]=Column::make(__('roles.name'), 'name')->sortable()->searchable()
|
||||
->editOnClick(
|
||||
hasPermission: $this->canEdit,
|
||||
dataField: 'name',
|
||||
fallback: 'N/A',
|
||||
saveOnMouseOut: true
|
||||
);
|
||||
$column[]=Column::make(__('roles.permissions'), 'permissions_list');
|
||||
$column[]=Column::make('Created at', 'created_at_formatted', 'created_at')->sortable();
|
||||
$column[]=Column::action('Action');
|
||||
return $column;
|
||||
}
|
||||
#[On('bulkDelete.{tableName}')]
|
||||
public function bulkDelete(): void
|
||||
{
|
||||
if ($this->canDelect) {
|
||||
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
|
||||
if($this->checkboxValues){
|
||||
Role::destroy($this->checkboxValues);
|
||||
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
|
||||
}
|
||||
}
|
||||
}
|
||||
#[On('onUpdatedEditable')]
|
||||
public function onUpdatedEditable($id, $field, $value): void
|
||||
{
|
||||
if ($field === 'name' && $this->canEdit) {
|
||||
$this->noUpdated($id,$field,$value);
|
||||
}
|
||||
}
|
||||
private function noUpdated($id,$field,$value){
|
||||
$role = Role::find($id);
|
||||
if ($role) {
|
||||
$role->{$field} = $value;
|
||||
$role->save(); // 明確觸發 saving
|
||||
}
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => $id.'.'.__('roles.'.$field).':'.$value,
|
||||
'description' => '已經寫入',
|
||||
]);
|
||||
}
|
||||
public function filters(): array
|
||||
{
|
||||
return [
|
||||
Filter::inputText('name')->placeholder(__('roles.name')),
|
||||
Filter::datetimepicker('created_at'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function actions(Role $row): array
|
||||
{
|
||||
$actions = [];
|
||||
if ($this->canEdit) {
|
||||
$actions[] =Button::add('edit')
|
||||
->slot(__('roles.edit'))
|
||||
->icon('solid-pencil-square')
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatchTo('admin.role-form', 'openEditRoleModal', ['id' => $row->id]);
|
||||
}
|
||||
if($this->canDelect){
|
||||
$actions[] =Button::add('delete')
|
||||
->slot(__('roles.delete'))
|
||||
->icon('solid-trash')
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatchTo('admin.role-form', 'deleteRole', ['id' => $row->id]);
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/*
|
||||
public function actionRules($row): array
|
||||
{
|
||||
return [
|
||||
// Hide button edit for ID 1
|
||||
Rule::button('edit')
|
||||
->when(fn($row) => $row->id === 1)
|
||||
->hide(),
|
||||
];
|
||||
}
|
||||
*/
|
||||
}
|
114
app/Livewire/Admin/RoomDetailModal.php
Normal file
114
app/Livewire/Admin/RoomDetailModal.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\Room;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Services\ApiClient;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
class RoomDetailModal extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
|
||||
protected $listeners = [
|
||||
'openModal', 'closeModal',
|
||||
'startNotify', 'stopNotify', 'fireNotify',
|
||||
'openAccountNotify','closeAccountNotify'
|
||||
];
|
||||
|
||||
public $room;
|
||||
public bool $showModal = false;
|
||||
|
||||
public function openModal($roomId)
|
||||
{
|
||||
$this->room = Room::find($roomId);
|
||||
$this->showModal = true;
|
||||
}
|
||||
public function closeModal()
|
||||
{
|
||||
$this->showModal = false;
|
||||
}
|
||||
public function startNotify()
|
||||
{
|
||||
$data = $this->buildNotifyData('active', now(), null);
|
||||
$this->send($data);
|
||||
}
|
||||
|
||||
public function stopNotify()
|
||||
{
|
||||
$data = $this->buildNotifyData('closed', null, null);
|
||||
$chk =$this->send($data);
|
||||
|
||||
}
|
||||
|
||||
public function fireNotify()
|
||||
{
|
||||
$data = $this->buildNotifyData('fire', null, null);
|
||||
$this->send($data);
|
||||
}
|
||||
|
||||
public function openAccountNotify()
|
||||
{
|
||||
$data = $this->buildNotifyData('active', now(), null);
|
||||
$this->send($data);
|
||||
}
|
||||
|
||||
public function closeAccountNotify()
|
||||
{
|
||||
$data = $this->buildNotifyData('closed', now(), null);
|
||||
$this->send($data);
|
||||
}
|
||||
protected function buildNotifyData(string $command, $startedAt = null, $endedAt = null): array
|
||||
{
|
||||
return [
|
||||
'branch_id' => $this->room->branch_id ?? 0,
|
||||
'room_name' => $this->room->name ?? '',
|
||||
'command' => $command,
|
||||
'started_at' => $startedAt ? $startedAt->toDateTimeString() : null,
|
||||
'ended_at' => $endedAt ? $endedAt->toDateTimeString() : null,
|
||||
];
|
||||
}
|
||||
|
||||
function send(array $data){
|
||||
|
||||
$user = Auth::user();
|
||||
$token = $user->api_plain_token ?? null;
|
||||
|
||||
if (!$token) {
|
||||
$this->sendErrorNotification('api', 'API token is missing.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$apiClient = new ApiClient();
|
||||
$response = $apiClient->setToken($token)->post('/room/sendSwitch', $data);
|
||||
if ($response->failed()) {
|
||||
$this->sendErrorNotification('api', 'API request failed: ' . $response->body());
|
||||
return false;
|
||||
}
|
||||
// ✅ 成功提示
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '命令已成功發送:' . $data['command'],
|
||||
]);
|
||||
// ✅ 關閉 modal
|
||||
$this->showModal = false;
|
||||
return true;
|
||||
}
|
||||
public function sendErrorNotification(string $title = '錯誤', string $description = '發生未知錯誤')
|
||||
{
|
||||
$this->notification()->send([
|
||||
'icon' => 'error',
|
||||
'title' => $title,
|
||||
'description' =>$description,
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.room-detail-modal');
|
||||
}
|
||||
}
|
43
app/Livewire/Admin/RoomGrid.php
Normal file
43
app/Livewire/Admin/RoomGrid.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use App\Models\Room;
|
||||
use App\Models\Branch;
|
||||
use App\Enums\RoomType;
|
||||
|
||||
use Livewire\Component;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
|
||||
|
||||
class RoomGrid extends Component
|
||||
{
|
||||
public $branchName="";
|
||||
public array $roomTypes;
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->roomTypes = ['all' => '全部'] + collect(RoomType::cases())->mapWithKeys(fn($e) => [$e->value => $e->labels()])->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$branch = Branch::first();
|
||||
$this->branchName = $branch->name ?? '';
|
||||
|
||||
$rooms = collect(); // 預設為空集合
|
||||
$floors = [];
|
||||
|
||||
if ($branch) {
|
||||
$rooms = Room::where('branch_id', $branch->id)->get();
|
||||
$floors = $rooms->pluck('floor')->unique()->sort()->values()->toArray();
|
||||
}
|
||||
|
||||
return view('livewire.admin.room-grid', [
|
||||
'rooms' => $rooms,
|
||||
'floors' => $floors,
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use Livewire\Component;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Enums\UserGender;
|
||||
use App\Enums\UserStatus;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class UserForm extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
|
||||
protected $listeners = ['openModal','closeModal', 'deleteUser'];
|
||||
|
||||
public bool $canCreate;
|
||||
public bool $canEdit;
|
||||
public bool $canDelect;
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public array $genderOptions =[];
|
||||
public array $statusOptions =[];
|
||||
public $rolesOptions = []; // 所有角色清單
|
||||
public $selectedRoles = []; // 表單中選到的權限
|
||||
public ?int $userId = null;
|
||||
public array $fields = [
|
||||
'name' =>'',
|
||||
'email' => '',
|
||||
'phone' => '',
|
||||
'birthday' => '',
|
||||
'gender' => 'unset',
|
||||
'status' => 0,
|
||||
];
|
||||
|
||||
|
||||
protected $rules = [
|
||||
'fields.name' => 'required|string|max:255',
|
||||
'fields.email' => 'required|string|email|max:255',
|
||||
'fields.phone' => 'nullable|regex:/^09\d{8}$/',
|
||||
'fields.birthday' =>'nullable|date',
|
||||
'fields.gender' => 'required|in:male,female,other,unset',
|
||||
'fields.status' => 'required|integer|in:0,1,2',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->fields['birthday'] = now()->toDateString();
|
||||
$this->genderOptions = collect(UserGender::cases())->map(fn ($gender) => [
|
||||
'name' => $gender->labels(),
|
||||
'value' => $gender->value,
|
||||
])->toArray();
|
||||
$this->statusOptions = collect(UserStatus::cases())->map(fn ($status) => [
|
||||
'name' => $status->labels(),
|
||||
'value' => $status->value,
|
||||
])->toArray();
|
||||
$this->rolesOptions = Role::all();
|
||||
$this->canCreate = Auth::user()?->can('user-edit') ?? false;
|
||||
$this->canEdit = Auth::user()?->can('user-edit') ?? false;
|
||||
$this->canDelect = Auth::user()?->can('user-delete') ?? false;
|
||||
}
|
||||
|
||||
public function openModal($id = null)
|
||||
{
|
||||
$this->resetFields();
|
||||
if($id){
|
||||
$obj = User::findOrFail($id);
|
||||
$this->userId = $obj->id;
|
||||
$this->fields = $obj->only(array_keys($this->fields));
|
||||
$this->selectedRoles = $obj->roles()->pluck('id')->toArray();
|
||||
}
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function closeModal()
|
||||
{
|
||||
$this->resetFields();
|
||||
$this->showModal = false;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
//$this->validate();
|
||||
|
||||
if ($this->userId) {
|
||||
if ($this->canEdit) {
|
||||
$obj = User::findOrFail($this->userId);
|
||||
$obj->update($this->fields);
|
||||
$obj->syncRoles($this->selectedRoles);
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '使用者已更新',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($this->canCreate) {
|
||||
$obj = User::create($this->fields);
|
||||
$obj->syncRoles($this->selectedRoles);
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '使用者已新增',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetFields();
|
||||
$this->showModal = false;
|
||||
$this->dispatch('pg:eventRefresh-user-table');
|
||||
}
|
||||
|
||||
public function deleteUser($id)
|
||||
{
|
||||
if ($this->canDelect) {
|
||||
User::findOrFail($id)->delete();
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => '成功',
|
||||
'description' => '使用者已刪除',
|
||||
]);
|
||||
$this->dispatch('pg:eventRefresh-user-table');
|
||||
}
|
||||
}
|
||||
|
||||
public function resetFields()
|
||||
{
|
||||
foreach ($this->fields as $key => $value) {
|
||||
if ($key == 'gender') {
|
||||
$this->fields[$key] = 'unset';
|
||||
} elseif ($key == 'status') {
|
||||
$this->fields[$key] = 0;
|
||||
} elseif ($key == 'birthday') {
|
||||
$this->fields[$key] = now()->toDateString();
|
||||
} else {
|
||||
$this->fields[$key] = '';
|
||||
}
|
||||
}
|
||||
$this->userId = null;
|
||||
$this->selectedRoles = [];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.user-form');
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Admin;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithFileUploads;
|
||||
use App\Jobs\ImportJob;
|
||||
|
||||
|
||||
class UserImportData extends Component
|
||||
{
|
||||
use WithFileUploads, WireUiActions;
|
||||
|
||||
protected $listeners = ['openModal','closeModal'];
|
||||
|
||||
public bool $canCreate;
|
||||
|
||||
public bool $showModal = false;
|
||||
|
||||
public $file;
|
||||
public string $maxUploadSize;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->canCreate = Auth::user()?->can('user-edit') ?? false;
|
||||
$this->maxUploadSize = $this->getMaxUploadSize();
|
||||
}
|
||||
|
||||
public function openModal()
|
||||
{
|
||||
$this->showModal = true;
|
||||
}
|
||||
|
||||
public function closeModal()
|
||||
{
|
||||
$this->deleteTmpFile(); // 關閉 modal 時刪除暫存檔案
|
||||
$this->reset(['file']);
|
||||
$this->showModal = false;
|
||||
}
|
||||
|
||||
public function import()
|
||||
{
|
||||
// 檢查檔案是否有上傳
|
||||
$this->validate([
|
||||
'file' => 'required|file|mimes:csv,xlsx,xls'
|
||||
]);
|
||||
if ($this->canCreate) {
|
||||
// 儲存檔案至 storage
|
||||
$path = $this->file->storeAs('imports', uniqid() . '_' . $this->file->getClientOriginalName());
|
||||
|
||||
// 丟到 queue 執行
|
||||
ImportJob::dispatch($path,'User');
|
||||
|
||||
$this->notification()->send([
|
||||
'icon' => 'info',
|
||||
'title' => $this->file->getClientOriginalName(),
|
||||
'description' => '已排入背景匯入作業,請稍候查看結果',
|
||||
]);
|
||||
$this->deleteTmpFile(); // 匯入後也順便刪除 tmp 檔
|
||||
$this->reset(['file']);
|
||||
$this->showModal = false;
|
||||
}
|
||||
}
|
||||
protected function deleteTmpFile()
|
||||
{
|
||||
if($this->file!=null){
|
||||
$Path = $this->file->getRealPath();
|
||||
if ($Path && File::exists($Path)) {
|
||||
File::delete($Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getMaxUploadSize(): string
|
||||
{
|
||||
$uploadMax = $this->convertPHPSizeToBytes(ini_get('upload_max_filesize'));
|
||||
$postMax = $this->convertPHPSizeToBytes(ini_get('post_max_size'));
|
||||
$max = min($uploadMax, $postMax);
|
||||
return $this->humanFileSize($max);
|
||||
}
|
||||
|
||||
private function convertPHPSizeToBytes(string $s): int
|
||||
{
|
||||
$s = trim($s);
|
||||
$unit = strtolower($s[strlen($s) - 1]);
|
||||
$bytes = (int) $s;
|
||||
switch ($unit) {
|
||||
case 'g':
|
||||
$bytes *= 1024;
|
||||
case 'm':
|
||||
$bytes *= 1024;
|
||||
case 'k':
|
||||
$bytes *= 1024;
|
||||
}
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
private function humanFileSize(int $bytes, int $decimals = 2): string
|
||||
{
|
||||
$sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
$factor = floor((strlen((string) $bytes) - 1) / 3);
|
||||
return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . $sizes[$factor];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.user-import-data');
|
||||
}
|
||||
|
||||
}
|
@ -6,73 +6,35 @@ use App\Models\User;
|
||||
use App\Enums\UserGender;
|
||||
use App\Enums\UserStatus;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use PowerComponents\LivewirePowerGrid\Button;
|
||||
use PowerComponents\LivewirePowerGrid\Column;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\Filter;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridFields;
|
||||
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
|
||||
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
|
||||
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
|
||||
use PowerComponents\LivewirePowerGrid\Facades\Rule;
|
||||
use Livewire\Attributes\On;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
final class UserTable extends PowerGridComponent
|
||||
{
|
||||
use WithExport, WireUiActions;
|
||||
|
||||
public string $tableName = 'user-table';
|
||||
|
||||
public bool $showFilters = false;
|
||||
public bool $canCreate;
|
||||
public bool $canEdit;
|
||||
public bool $canDownload;
|
||||
public bool $canDelect;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
config(['livewire-powergrid.filter' => 'outside']);
|
||||
//權限設定
|
||||
$this->canCreate = Auth::user()?->can('user-edit') ?? false;
|
||||
$this->canEdit = Auth::user()?->can('user-edit') ?? false;
|
||||
$this->canDownload=Auth::user()?->can('user-delete') ?? false;
|
||||
$this->canDelect = Auth::user()?->can('user-delete') ?? false;
|
||||
}
|
||||
|
||||
|
||||
public function setUp(): array
|
||||
{
|
||||
if($this->canDownload || $this->canDelect){
|
||||
$this->showCheckBox();
|
||||
}
|
||||
$actions = [];
|
||||
$actions[] =PowerGrid::exportable(fileName: $this->tableName.'-file')
|
||||
->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV);
|
||||
$header = PowerGrid::header()
|
||||
->showToggleColumns();
|
||||
if($this->canCreate){
|
||||
$header->includeViewOnTop('livewire.admin.user-header');
|
||||
}
|
||||
$header->includeViewOnTop('livewire.admin.user-header');
|
||||
$actions[]=$header;
|
||||
$actions[]=PowerGrid::footer()->showPerPage()->showRecordCount();
|
||||
return $actions;
|
||||
}
|
||||
public function header(): array
|
||||
{
|
||||
$actions = [];
|
||||
if ($this->canDelect) {
|
||||
$actions[]=Button::add('bulk-delete')
|
||||
->slot('Bulk delete (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
|
||||
->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold'])
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatch('bulkDelete.' . $this->tableName, []);
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function datasource(): Builder
|
||||
{
|
||||
@ -93,36 +55,10 @@ final class UserTable extends PowerGridComponent
|
||||
->add('phone')
|
||||
->add('birthday_formatted',fn (User $model) => Carbon::parse($model->birthday)->format('Y-m-d'))
|
||||
->add('gender_str', function (User $model) {
|
||||
if ($this->canEdit) {
|
||||
return Blade::render(
|
||||
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||
[
|
||||
'options' => UserGender::options(),
|
||||
'modelId' => intval($model->id),
|
||||
'fieldName'=>'gender',
|
||||
'selected' => $model->gender->value
|
||||
]
|
||||
);
|
||||
}
|
||||
// 沒有權限就顯示對應的文字
|
||||
|
||||
return $model->gender->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
|
||||
return $model->gender->labelPowergridFilter();
|
||||
} )
|
||||
->add('status_str', function (User $model) {
|
||||
if ($this->canEdit) {
|
||||
return Blade::render(
|
||||
'<x-select-category type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
|
||||
[
|
||||
'options' => UserStatus::options(),
|
||||
'modelId' => intval($model->id),
|
||||
'fieldName'=>'status',
|
||||
'selected' => $model->status->value
|
||||
]
|
||||
);
|
||||
}
|
||||
// 沒有權限就顯示對應的文字
|
||||
|
||||
return $model->status->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
|
||||
return $model->status->labelPowergridFilter();
|
||||
} )
|
||||
->add('roles' ,fn(User $model)=> $model->roles->pluck('name')->implode(', '))
|
||||
->add('created_at_formatted', fn (User $model) => Carbon::parse($model->created_at)->format('Y-m-d H:i:s'));
|
||||
@ -134,85 +70,22 @@ final class UserTable extends PowerGridComponent
|
||||
Column::make('ID', 'id'),
|
||||
Column::make(__('users.name'), 'name')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->editOnClick(
|
||||
hasPermission: $this->canEdit,
|
||||
dataField: 'name',
|
||||
fallback: 'N/A',
|
||||
saveOnMouseOut: true
|
||||
),
|
||||
->searchable(),
|
||||
Column::make('Email', 'email')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->editOnClick(
|
||||
hasPermission: $this->canEdit,
|
||||
dataField: 'email',
|
||||
fallback: 'N/A',
|
||||
saveOnMouseOut: true
|
||||
),
|
||||
->searchable(),
|
||||
Column::make(__('users.phone'), 'phone')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->editOnClick(
|
||||
hasPermission: $this->canEdit,
|
||||
dataField: 'phone',
|
||||
fallback: 'N/A',
|
||||
saveOnMouseOut: true
|
||||
),
|
||||
->searchable(),
|
||||
|
||||
Column::make(__('users.gender'), 'gender_str','users.gender'),
|
||||
Column::make(__('users.birthday'), 'birthday_formatted')->sortable()->searchable(),
|
||||
Column::make(__('users.status'), 'status_str','users.status'),
|
||||
Column::make(__('users.role'), 'roles'),
|
||||
Column::make('建立時間', 'created_at_formatted', 'created_at')->sortable(),
|
||||
Column::action('操作')
|
||||
|
||||
];
|
||||
}
|
||||
#[On('bulkDelete.{tableName}')]
|
||||
public function bulkDelete(): void
|
||||
{
|
||||
if ($this->canDelect) {
|
||||
$this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))');
|
||||
if($this->checkboxValues){
|
||||
User::destroy($this->checkboxValues);
|
||||
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
|
||||
}
|
||||
}
|
||||
}
|
||||
#[On('categoryChanged')]
|
||||
public function categoryChanged($value,$fieldName, $modelId): void
|
||||
{
|
||||
// dd($value,$fieldName, $modelId);
|
||||
if (in_array($fieldName,['gender','status']) && $this->canEdit) {
|
||||
$this->noUpdated($modelId,$fieldName,$value);
|
||||
}
|
||||
}
|
||||
#[On('onUpdatedEditable')]
|
||||
public function onUpdatedEditable($id, $field, $value): void
|
||||
{
|
||||
if (in_array($field,['name','email','phone']) && $this->canEdit) {
|
||||
$this->noUpdated($id,$field,$value);
|
||||
}
|
||||
}
|
||||
#[On('onUpdatedToggleable')]
|
||||
public function onUpdatedToggleable($id, $field, $value): void
|
||||
{
|
||||
if (in_array($field,[]) && $this->canEdit) {
|
||||
$this->noUpdated($id,$field,$value);
|
||||
}
|
||||
}
|
||||
private function noUpdated($id,$field,$value){
|
||||
$user = User::find($id);
|
||||
if ($user) {
|
||||
$user->{$field} = $value;
|
||||
$user->save(); // 明確觸發 saving
|
||||
}
|
||||
$this->notification()->send([
|
||||
'icon' => 'success',
|
||||
'title' => $id.'.'.__('users.'.$field).':'.$value,
|
||||
'description' => '已經寫入',
|
||||
]);
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
@ -231,35 +104,4 @@ final class UserTable extends PowerGridComponent
|
||||
];
|
||||
}
|
||||
|
||||
public function actions(User $row): array
|
||||
{
|
||||
$actions = [];
|
||||
if ($this->canEdit) {
|
||||
$actions[]=Button::add('edit')
|
||||
->slot(__('users.edit'))
|
||||
->icon('solid-pencil-square')
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatchTo('admin.user-form', 'openModal', ['id' => $row->id]);
|
||||
}
|
||||
if($this->canDelect){
|
||||
$actions[]=Button::add('delete')
|
||||
->slot(__('users.delete'))
|
||||
->icon('solid-trash')
|
||||
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
|
||||
->dispatchTo('admin.user-form', 'deleteUser', ['id' => $row->id]);
|
||||
}
|
||||
return $actions;
|
||||
}
|
||||
|
||||
|
||||
/* public function actionRules($row): array
|
||||
{
|
||||
return [
|
||||
// Hide button edit for ID 1
|
||||
Rule::button('edit')
|
||||
->when(fn($row) => $row->id === 1)
|
||||
->hide(),
|
||||
];
|
||||
} */
|
||||
|
||||
}
|
||||
|
30
app/Models/Artist.php
Normal file
30
app/Models/Artist.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\LogsModelActivity;
|
||||
|
||||
class Artist extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||
use HasFactory, LogsModelActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'category',
|
||||
'name',
|
||||
'simplified',
|
||||
'phonetic_abbr',
|
||||
'pinyin_abbr',
|
||||
'strokes_abbr',
|
||||
'enable',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'category' => \App\Enums\ArtistCategory::class,
|
||||
];
|
||||
}
|
||||
}
|
34
app/Models/Branch.php
Normal file
34
app/Models/Branch.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\LogsModelActivity;
|
||||
|
||||
class Branch extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||
use HasFactory, LogsModelActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'external_ip',
|
||||
'enable',
|
||||
];
|
||||
|
||||
public function rooms() {
|
||||
return $this->hasMany(Room::class);
|
||||
}
|
||||
public function songs(){
|
||||
return $this->belongsToMany(Song::class)
|
||||
->withPivot('counts')
|
||||
->withTimestamps();
|
||||
}
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function (Branch $branch) {
|
||||
$branch->rooms()->delete();
|
||||
});
|
||||
}
|
||||
}
|
31
app/Models/MachineStatus.php
Normal file
31
app/Models/MachineStatus.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="MachineStatus",
|
||||
* type="object",
|
||||
* @OA\Property(property="branch_name", type="string", example="測試"),
|
||||
* @OA\Property(property="hostname", type="string", example="PC101"),
|
||||
* @OA\Property(property="ip", type="string", example="192.168.XX.XX"),
|
||||
* @OA\Property(property="cpu", type="numeric", example="0.00"),
|
||||
* @OA\Property(property="memory", type="numeric", example="25603"),
|
||||
* @OA\Property(property="disk", type="numeric", example="158266.49"),
|
||||
* @OA\Property(property="status", type="string", example="online,error"),
|
||||
* )
|
||||
*/
|
||||
class MachineStatus extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'branch_name',
|
||||
'hostname',
|
||||
'ip',
|
||||
'cpu',
|
||||
'memory',
|
||||
'disk',
|
||||
'status',
|
||||
];
|
||||
}
|
82
app/Models/Room.php
Normal file
82
app/Models/Room.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Traits\LogsModelActivity;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="Room",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer", example=16),
|
||||
* @OA\Property(property="floor", type="integer", example="1"),
|
||||
* @OA\Property(property="type", ref="#/components/schemas/RoomType"),
|
||||
* @OA\Property(property="name", type="string", example="pc102"),
|
||||
* @OA\Property(property="internal_ip", type="string", example="192.168.11.7"),
|
||||
* @OA\Property(property="port", type="int", example="9000"),
|
||||
* @OA\Property(property="status", ref="#/components/schemas/RoomStatus"),
|
||||
* @OA\Property(property="started_at", type="string", format="date-time", example="2025-05-11T16:00:00.000000Z"),
|
||||
* @OA\Property(property="ended_at", type="string", format="date-time", example=null),
|
||||
* )
|
||||
*/
|
||||
class Room extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||
use HasFactory, LogsModelActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'floor',
|
||||
'type',
|
||||
'name',
|
||||
'internal_ip',
|
||||
'port',
|
||||
'is_online',
|
||||
'status',
|
||||
'started_at',
|
||||
'ended_at',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'internal_ip',
|
||||
'port',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'floor' => 'int',
|
||||
'type' => \App\Enums\RoomType::class,
|
||||
'name' => 'string',
|
||||
'internal_ip' =>'string',
|
||||
'port' => 'int',
|
||||
'is_online' => 'boolean',
|
||||
'status' => \App\Enums\RoomStatus::class,
|
||||
'started_at' => 'datetime',
|
||||
'ended_at' => 'datetime',
|
||||
|
||||
];
|
||||
|
||||
public function str_started_at(){
|
||||
$str ="Not Set";
|
||||
if($this->started_at !=null){
|
||||
$str = $this->started_at;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
public function str_ended_at(){
|
||||
$str ="Not Set";
|
||||
if($this->ended_at !=null){
|
||||
$str = $this->ended_at;
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
public function branch() {
|
||||
return $this->belongsTo(Branch::class);
|
||||
}
|
||||
|
||||
public function statusLogs() {
|
||||
return $this->hasMany(RoomStatusLog::class);
|
||||
}
|
||||
}
|
30
app/Models/RoomStatusLog.php
Normal file
30
app/Models/RoomStatusLog.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RoomStatusLog extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ArtistFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $fillable =
|
||||
[
|
||||
'room_id',
|
||||
'user_id',
|
||||
'status',
|
||||
'message',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'status' => \App\Enums\RoomStatus::class,
|
||||
];
|
||||
|
||||
public function room() {
|
||||
return $this->belongsTo(Room::class);
|
||||
}
|
||||
}
|
@ -9,11 +9,25 @@ use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use App\Traits\LogsModelActivity;
|
||||
use Spatie\Activitylog\Traits\CausesActivity;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
/**
|
||||
* @OA\Schema(
|
||||
* schema="User",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="name", type="string", example="John Doe"),
|
||||
* @OA\Property(property="email", type="string", example="john@example.com"),
|
||||
* @OA\Property(property="phone", type="string", example="0900000000"),
|
||||
* @OA\Property(property="birthday", type="string", format="date-time", example="2025-05-11T16:00:00.000000Z"),
|
||||
* @OA\Property(property="gender", ref="#/components/schemas/UserGender"),
|
||||
* @OA\Property(property="status", ref="#/components/schemas/UserStatus"),
|
||||
* )
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, HasRoles, LogsModelActivity,CausesActivity;
|
||||
use HasApiTokens, HasFactory, Notifiable, HasRoles, LogsModelActivity,CausesActivity;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@ -38,6 +52,7 @@ class User extends Authenticatable
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'api_plain_token',
|
||||
];
|
||||
|
||||
/**
|
||||
|
59
app/Observers/RoomObserver.php
Normal file
59
app/Observers/RoomObserver.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use App\Models\Room;
|
||||
use App\Models\RoomStatusLog;
|
||||
|
||||
class RoomObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Room "created" event.
|
||||
*/
|
||||
public function created(Room $room): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Room "updated" event.
|
||||
*/
|
||||
public function updated(Room $room): void
|
||||
{
|
||||
// 檢查是否有變更狀態
|
||||
if ($room->wasChanged('status')) {
|
||||
RoomStatusLog::create([
|
||||
'room_id' => $room->id,
|
||||
'user_id' => Auth::id(), // 若是 console 或系統自動操作可能為 null
|
||||
'status' => $room->status,
|
||||
'message' => 'started_at:'.$room->started_at.',ended_at:'.$room->ended_at,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Room "deleted" event.
|
||||
*/
|
||||
public function deleted(Room $room): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Room "restored" event.
|
||||
*/
|
||||
public function restored(Room $room): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Room "force deleted" event.
|
||||
*/
|
||||
public function forceDeleted(Room $room): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -4,6 +4,9 @@ namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
use App\Models\Room;
|
||||
use App\Observers\RoomObserver;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
@ -19,6 +22,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
Room::observe(RoomObserver::class);
|
||||
}
|
||||
}
|
||||
|
41
app/Services/ApiClient.php
Normal file
41
app/Services/ApiClient.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class ApiClient
|
||||
{
|
||||
protected string $baseUrl;
|
||||
protected string $token;
|
||||
|
||||
public function __construct(string $token = null)
|
||||
{
|
||||
$this->baseUrl = config('services.room_api.base_url', 'https://ktv.test/api');
|
||||
$this->token = $token ?? config('services.room_api.token','');
|
||||
}
|
||||
public function setToken(string $token): self
|
||||
{
|
||||
$this->token = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withDefaultHeaders(): \Illuminate\Http\Client\PendingRequest
|
||||
{
|
||||
return Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $this->token,
|
||||
'Content-Type' => 'application/json',
|
||||
]);
|
||||
}
|
||||
|
||||
public function post(string $endpoint, array $data = [])
|
||||
{
|
||||
return $this->withDefaultHeaders()->post($this->baseUrl . $endpoint, $data);
|
||||
}
|
||||
|
||||
public function get(string $endpoint, array $query = [])
|
||||
{
|
||||
return $this->withDefaultHeaders()->get($this->baseUrl . $endpoint, $query);
|
||||
}
|
||||
|
||||
}
|
43
app/Services/TcpSocketClient.php
Normal file
43
app/Services/TcpSocketClient.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
class TcpSocketClient
|
||||
{
|
||||
protected $ip;
|
||||
protected $port;
|
||||
protected $timeout;
|
||||
|
||||
public function __construct(string $ip, int $port, int $timeout = 5)
|
||||
{
|
||||
$this->ip = $ip;
|
||||
$this->port = $port;
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function send(string $data): string
|
||||
{
|
||||
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
|
||||
if ($socket === false) {
|
||||
throw new \Exception("Socket create failed: " . socket_strerror(socket_last_error()));
|
||||
}
|
||||
|
||||
$result = socket_connect($socket, $this->ip, $this->port);
|
||||
if ($result === false) {
|
||||
throw new \Exception("Socket connect failed: " . socket_strerror(socket_last_error($socket)));
|
||||
}
|
||||
|
||||
socket_write($socket, $data, strlen($data));
|
||||
|
||||
$response = '';
|
||||
while ($out = socket_read($socket, 2048)) {
|
||||
$response .= $out;
|
||||
// 根據協議判斷是否結束接收,可以自行調整
|
||||
if (strpos($response, "\n") !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
socket_close($socket);
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
@ -18,5 +19,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
//
|
||||
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, $request) {
|
||||
if ($request->expectsJson()) {
|
||||
return \App\Http\Responses\ApiResponse::unauthorized();
|
||||
}
|
||||
// 其他非 JSON 請求的處理方式(可選)
|
||||
return redirect()->guest(route('login'));
|
||||
});
|
||||
})->create();
|
||||
|
493
composer.lock
generated
493
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,9 @@ return [
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'room_api' => [
|
||||
'base_url' => env('ROOM_API_BASE_URL', env('APP_URL') . '/api'),
|
||||
],
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
@ -22,6 +22,7 @@ return new class extends Migration
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->text('api_plain_token')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('artists', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->enum('category', ['未定義','男', '女','團','外', '其他'])->default('未定義')->index()->comment('歌星類別');
|
||||
$table->string('name')->unique()->comment('歌星名稱');
|
||||
$table->string('simplified')->index()->comment('歌星簡體');
|
||||
$table->string('phonetic_abbr')->index()->comment('歌星注音');
|
||||
$table->string('pinyin_abbr')->index()->comment('歌星拼音');
|
||||
$table->integer('strokes_abbr')->index()->comment('歌星筆劃');
|
||||
$table->tinyInteger('enable')->default(1)->comment('狀態'); // 1,可看,0,不可看
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artists');
|
||||
}
|
||||
};
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('song_library_cache', function (Blueprint $table) {
|
||||
$table->bigIncrements('song_id')->comment('歌曲編號');
|
||||
$table->string('song_name')->nullable()->index()->comment('歌曲檔名');
|
||||
$table->string('song_simplified')->nullable()->index()->comment('歌曲簡體');
|
||||
$table->string('phonetic_abbr')->nullable()->index()->comment('歌曲注音');
|
||||
$table->string('pinyin_abbr')->nullable()->index()->comment('歌曲拼音');
|
||||
$table->integer('strokes_abbr')->default(0)->index()->comment('歌曲筆劃');
|
||||
$table->integer('song_number')->default(0)->index()->comment('歌曲字數');
|
||||
$table->string('artistA')->nullable()->index()->comment('歌星名稱A');
|
||||
$table->string('artistB')->nullable()->index()->comment('歌星名稱B');
|
||||
$table->string('artistA_simplified')->nullable()->index()->comment('歌星簡體名稱A');
|
||||
$table->string('artistB_simplified')->nullable()->index()->comment('歌星簡體名稱B');
|
||||
$table->enum('artistA_category', ['未定義','男', '女','團','外', '其他'])->default('未定義')->index()->comment('歌星類別A');
|
||||
$table->enum('artistB_category', ['未定義','男', '女','團','外', '其他'])->default('未定義')->index()->comment('歌星類別B');
|
||||
$table->enum('artist_category', ['未定義','團'])->default('未定義')->index()->comment('歌星類別');
|
||||
$table->string('song_filename')->nullable()->comment('歌曲檔名');
|
||||
$table->string('song_category')->nullable()->comment('歌曲分類');
|
||||
$table->enum('language_name', ['未定義','國語','台語','英語','日語','粵語','韓語','越語','客語','其他'])->default('未定義')->index()->comment('語別');
|
||||
$table->date('add_date')->nullable()->index()->comment('新增日期');
|
||||
$table->enum('situation', ['未定義','浪漫', '柔和','動感','明亮'])->default('未定義')->index()->comment('情境');
|
||||
$table->tinyInteger('vocal')->index()->comment('人聲'); // 0,1
|
||||
$table->integer('db_change')->default(0)->index()->comment('DB加減');
|
||||
$table->integer('song_counts')->default(0)->index()->comment('點播次數');
|
||||
$table->dateTime('updated_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('song_library_cache');
|
||||
}
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('FavoriteSongs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('songNumber',20);
|
||||
$table->string('userPhone', 10);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('FavoriteSongs');
|
||||
}
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('branches', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('店名');
|
||||
$table->ipAddress('external_ip')->comment('對外IP'); // 對外 IP
|
||||
$table->tinyInteger('enable')->default(1)->comment('狀態');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('branches');
|
||||
}
|
||||
};
|
37
database/migrations/2025_05_23_055307_create_rooms_table.php
Normal file
37
database/migrations/2025_05_23_055307_create_rooms_table.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rooms', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('branch_id')->constrained()->onDelete('cascade')->comment('關聯分店');
|
||||
$table->unsignedTinyInteger('floor')->default(1)->comment('樓層'); // 可根據實際狀況決定預設值
|
||||
$table->enum('type',['unset', 'pc','svr'])->default('unset')->comment('包廂類別');
|
||||
$table->string('name')->comment('包廂名稱');
|
||||
$table->string('internal_ip')->nullable()->comment('內部 IP');
|
||||
$table->unsignedSmallInteger('port')->nullable()->comment('通訊 Port');
|
||||
$table->tinyInteger('is_online')->default(0)->comment('連線狀態');
|
||||
$table->enum('status', ['active', 'closed','fire', 'error'])->default('error')->comment('狀態'); // :啟用中 / 已結束
|
||||
$table->dateTime('started_at')->nullable()->comment('開始時間'); //
|
||||
$table->dateTime('ended_at')->nullable()->comment('結束時間'); //
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rooms');
|
||||
}
|
||||
};
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('room_status_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('room_id')->nullable();
|
||||
$table->foreignId('user_id')->nullable(); // 操作者,可為 null(系統)
|
||||
$table->enum('status', ['active', 'closed','fire', 'error', 'maintenance']);
|
||||
$table->text('message')->nullable(); // 可填異常原因或操作說明
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('room_status_logs');
|
||||
}
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('machine_statuses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('branch_name');
|
||||
$table->string('hostname');
|
||||
$table->string('ip')->nullable();
|
||||
$table->decimal('cpu', 5, 2)->nullable();
|
||||
$table->unsignedInteger('memory')->nullable();
|
||||
$table->decimal('disk', 10, 2)->nullable();
|
||||
$table->string('status');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('machine_statuses');
|
||||
}
|
||||
};
|
@ -16,6 +16,7 @@ class DatabaseSeeder extends Seeder
|
||||
$this->call([
|
||||
PermissionTableSeeder::class,
|
||||
CreateAdminUserSeeder::class,
|
||||
FavoriteSongsSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
23
database/seeders/FavoriteSongsSeeder.php
Normal file
23
database/seeders/FavoriteSongsSeeder.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class FavoriteSongsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
DB::table('FavoriteSongs')->insert([
|
||||
'songNumber' => 999996,
|
||||
'userPhone' => '0912345678',
|
||||
'created_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now(),
|
||||
]);
|
||||
}
|
||||
}
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "shop_12_wireui",
|
||||
"name": "KTVCentral",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
23
resources/lang/zh-tw/branches.php
Normal file
23
resources/lang/zh-tw/branches.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'management' => '分店管理',
|
||||
'list' => '分店列表',
|
||||
'CreateNew' => '新增分店',
|
||||
'EditBranch' => '編輯分店',
|
||||
'ImportData' => '滙入分店',
|
||||
'create_edit' => '新增 / 編輯',
|
||||
'create' => '新增',
|
||||
'edit' => '編輯',
|
||||
'delete' => '刪除',
|
||||
'no' => '編號',
|
||||
'name' => '名稱',
|
||||
'external_ip' => '對外IP',
|
||||
'enable' => '狀態',
|
||||
|
||||
|
||||
'actions' => '操作',
|
||||
'view' => '查看',
|
||||
'submit' => '提交',
|
||||
'cancel' => '取消',
|
||||
];
|
@ -8,4 +8,9 @@ return [
|
||||
'user.status.Active' => '正常',
|
||||
'user.status.Suspended' => '停權',
|
||||
'user.status.Deleting' => '刪除中',
|
||||
|
||||
'room.status.Active' => '已占用',
|
||||
'room.status.Closed' => '可用',
|
||||
'room.status.Fire' => '火災',
|
||||
'room.status.Error' => '維修',
|
||||
];
|
22
resources/views/components/room-card-svr.blade.php
Normal file
22
resources/views/components/room-card-svr.blade.php
Normal file
@ -0,0 +1,22 @@
|
||||
@php
|
||||
use App\Enums\RoomStatus;
|
||||
$statusColors = [
|
||||
RoomStatus::Active->value => 'green-600',
|
||||
RoomStatus::Closed->value => 'gray-600',
|
||||
RoomStatus::Error->value => 'red-600',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="border p-2 rounded shadow-md h-32 relative cursor-pointer bg-amber-50">
|
||||
{{-- 房間名稱 + 線上狀態圓點 --}}
|
||||
<div class="font-bold flex items-center gap-1">
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block
|
||||
{{ $room->is_online ? 'bg-green-500' : 'bg-red-500' }}">
|
||||
</span>
|
||||
<span>{{ $room->type->labels().".".$room->name }}</span>
|
||||
|
||||
</div>
|
||||
<div class="text-sm text-{{ $statusColors[$room->status->value] ?? 'gray-500' }} text-center">
|
||||
{{ $room->status->labels() }}
|
||||
</div>
|
||||
</div>
|
26
resources/views/components/room-card.blade.php
Normal file
26
resources/views/components/room-card.blade.php
Normal file
@ -0,0 +1,26 @@
|
||||
@php
|
||||
use App\Enums\RoomStatus;
|
||||
$statusColors = [
|
||||
RoomStatus::Active->value => 'green-600',
|
||||
RoomStatus::Closed->value => 'gray-600',
|
||||
RoomStatus::Error->value => 'red-600',
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="border p-2 rounded shadow-md h-32 relative cursor-pointer bg-amber-50"
|
||||
wire:click="$dispatchTo('admin.room-detail-modal','openModal', { roomId: {{ $room->id }} })">
|
||||
{{-- 房間名稱 + 線上狀態圓點 --}}
|
||||
<div class="font-bold flex items-center gap-1">
|
||||
<span class="w-2.5 h-2.5 rounded-full inline-block
|
||||
{{ $room->is_online ? 'bg-green-500' : 'bg-red-500' }}">
|
||||
</span>
|
||||
<span>{{ $room->type->labels().".".$room->name }}</span>
|
||||
|
||||
</div>
|
||||
<div class="text-sm text-{{ $statusColors[$room->status->value] ?? 'gray-500' }} text-center">
|
||||
{{ $room->status->labels() }}
|
||||
</div>
|
||||
<div class="text-xs text-center whitespace-nowrap ">{{ $room->str_started_at() }}</div>
|
||||
<div class="text-xs text-center whitespace-nowrap ">{{ $room->str_ended_at() }}</div>
|
||||
|
||||
</div>
|
2
resources/views/livewire/admin/branch-header.blade.php
Normal file
2
resources/views/livewire/admin/branch-header.blade.php
Normal file
@ -0,0 +1,2 @@
|
||||
<x-admin.section-header title="{{ __('branches.list') }}">
|
||||
</x-admin.section-header>
|
4
resources/views/livewire/admin/branches.blade.php
Normal file
4
resources/views/livewire/admin/branches.blade.php
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
<x-layouts.admin>
|
||||
<livewire:admin.branch-table />
|
||||
</x-layouts.admin>
|
@ -1,21 +0,0 @@
|
||||
<x-wireui:modal-card title="{{ $roleId ? __('roles.EditRole') : __('roles.CreateNew') }}" blur wire:model.defer="showCreateModal">
|
||||
<div class="space-y-4">
|
||||
<x-wireui:input label="{{__('roles.role_name')}}" wire:model.defer="name" />
|
||||
<x-wireui:select
|
||||
label="{{__('roles.permissions')}}"
|
||||
wire:model.defer="selectedPermissions"
|
||||
placeholder="{{__('roles.select_permissions')}}"
|
||||
multiselect
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="$permissions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex justify-between w-full">
|
||||
<x-wireui:button flat label="{{__('roles.cancel')}}" @click="$wire.showCreateModal = false" />
|
||||
<x-wireui:button primary label="{{__('roles.submit')}}" wire:click="save" />
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-wireui:modal-card>
|
@ -1,8 +0,0 @@
|
||||
<x-admin.section-header title="{{ __('roles.list') }}">
|
||||
<x-wireui:button
|
||||
wire:click="$dispatchTo('admin.role-form', 'openCreateRoleModal')"
|
||||
icon="plus"
|
||||
label="{{ __('roles.CreateNew') }}"
|
||||
class="bg-blue-600 text-white"
|
||||
/>
|
||||
</x-admin.section-header>
|
@ -1,5 +0,0 @@
|
||||
<x-layouts.admin>
|
||||
<x-wireui:notifications/>
|
||||
<livewire:admin.role-table />
|
||||
<livewire:admin.role-form />
|
||||
</x-layouts.admin>
|
29
resources/views/livewire/admin/room-detail-modal.blade.php
Normal file
29
resources/views/livewire/admin/room-detail-modal.blade.php
Normal file
@ -0,0 +1,29 @@
|
||||
<x-wireui:modal id="room-detail-modal" wire:model.defer="showModal" persistent>
|
||||
<x-wireui:card class="border border-gray-200">
|
||||
<x-slot name="action">
|
||||
<button class="cursor-pointer p-1 rounded-full focus:outline-none focus:outline-hidden focus:ring-2 focus:ring-secondary-200 text-secondary-300"
|
||||
wire:click="closeModal"
|
||||
tabindex="-1"
|
||||
>
|
||||
<x-dynamic-component
|
||||
:component="WireUi::component('icon')"
|
||||
name="x-mark"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
</x-slot>
|
||||
<x-slot name="title">
|
||||
{{ $room->name ?? '未選擇' }}包廂設定
|
||||
</x-slot>
|
||||
<div class="grid grid-cols-3 gap-4 mb-4">
|
||||
<x-wireui:button wire:click="startNotify" >開機</x-wireui:button>
|
||||
<x-wireui:button wire:click="stopNotify" >關機</x-wireui:button>
|
||||
<x-wireui:button wire:click="fireNotify" >火災</x-wireui:button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<x-wireui:button wire:click="openAccountNotify" >包廂開帳</x-wireui:button>
|
||||
<x-wireui:button wire:click="closeAccountNotify" >包廂關帳</x-wireui:button>
|
||||
</div>
|
||||
|
||||
</x-wireui:card>
|
||||
</x-wireui:modal>
|
50
resources/views/livewire/admin/room-grid.blade.php
Normal file
50
resources/views/livewire/admin/room-grid.blade.php
Normal file
@ -0,0 +1,50 @@
|
||||
<x-wireui:card title="{{ $branchName }} - 包廂設定" shadow="none">
|
||||
<div x-data="{ floor: '{{ $floors[0] ?? 1 }}', type: 'all' }">
|
||||
{{-- 樓層 Tab --}}
|
||||
<div class="flex gap-2 mb-2">
|
||||
@foreach($floors as $fl)
|
||||
<button
|
||||
class="px-3 py-1 rounded border"
|
||||
:class="floor === '{{ $fl }}' ? 'bg-blue-500 text-white' : 'bg-white text-gray-700'"
|
||||
x-on:click="floor = '{{ $fl }}'"
|
||||
>
|
||||
{{ $fl }}F
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- 類別 Tab --}}
|
||||
<div class="flex gap-2 mb-4">
|
||||
@foreach(['all' => '全部', 'pc' => 'PC', 'svr' => 'SVR'] as $value => $label)
|
||||
<button
|
||||
class="px-3 py-1 rounded border"
|
||||
:class="type === '{{ $value }}' ? 'bg-green-500 text-white' : 'bg-white text-gray-700'"
|
||||
x-on:click="type = '{{ $value }}'"
|
||||
>
|
||||
{{ $label }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- 房間卡片列表 --}}
|
||||
<div wire:poll.5s>
|
||||
<div class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@forelse($rooms as $room)
|
||||
<template x-if="floor == '{{ $room->floor }}' && (type == 'all' || type == '{{ $room->type }}')">
|
||||
<div>
|
||||
@if($room->type->value === \App\Enums\RoomType::SVR->value)
|
||||
<x-room-card-svr :room="$room" />
|
||||
@else
|
||||
<x-room-card :room="$room" />
|
||||
@endif
|
||||
</div>
|
||||
</template>
|
||||
@empty
|
||||
<div class="col-span-full text-center text-gray-500">尚無包廂資料</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-wireui:notifications/>
|
||||
<livewire:admin.room-detail-modal />
|
||||
</x-wireui:card>
|
4
resources/views/livewire/admin/rooms.blade.php
Normal file
4
resources/views/livewire/admin/rooms.blade.php
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
<x-layouts.admin>
|
||||
<livewire:admin.room-grid />
|
||||
</x-layouts.admin>
|
@ -1,40 +0,0 @@
|
||||
<x-wireui:modal-card title="{{ $userId ? __('users.EditUser') : __('users.CreateNew') }}" blur wire:model.defer="showModal">
|
||||
<div class="space-y-4">
|
||||
<x-wireui:input label="{{__('users.name')}}" wire:model.defer="fields.name" required />
|
||||
<x-wireui:input label="Email" wire:model.defer="fields.email" required />
|
||||
<x-wireui:input label="Phone" wire:model.defer="fields.phone" />
|
||||
<x-wireui:select
|
||||
label="{{__('users.gender')}}"
|
||||
wire:model.defer="fields.gender"
|
||||
placeholder="{{__('users.select_gender')}}"
|
||||
:options="$genderOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
/>
|
||||
<x-wireui:select
|
||||
label="{{__('users.status')}}"
|
||||
wire:model.defer="fields.status"
|
||||
placeholder="{{__('users.select_status')}}"
|
||||
:options="$statusOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
/>
|
||||
|
||||
<x-wireui:select
|
||||
label="{{__('users.role')}}"
|
||||
wire:model.defer="selectedRoles"
|
||||
placeholder="{{__('users.select_role')}}"
|
||||
multiselect
|
||||
option-label="label"
|
||||
option-value="value"
|
||||
:options="$rolesOptions->map(fn($p) => ['value' => $p->id, 'label' => $p->name])->toArray()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex justify-between w-full">
|
||||
<x-wireui:button flat label="{{__('users.cancel')}}" wire:click="closeModal" />
|
||||
<x-wireui:button primary label="{{__('users.submit')}}" wire:click="save" />
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-wireui:modal-card>
|
@ -1,15 +1,2 @@
|
||||
<x-admin.section-header title="{{ __('users.list') }}">
|
||||
<x-wireui:button
|
||||
wire:click="$dispatchTo('admin.user-form', 'openModal')"
|
||||
icon="plus"
|
||||
label="{{ __('users.CreateNew') }}"
|
||||
class="bg-blue-600 text-white"
|
||||
/>
|
||||
|
||||
<x-wireui:button
|
||||
wire:click="$dispatchTo('admin.user-import-data','openModal')"
|
||||
icon="document-plus"
|
||||
label="{{ __('users.ImportData') }}"
|
||||
class="bg-green-600 text-white"
|
||||
/>
|
||||
</x-admin.section-header>
|
@ -1,63 +0,0 @@
|
||||
<x-wireui:modal-card title="{{ __('users.ImportData') }}" blur wire:model.defer="showModal" hide-close>
|
||||
|
||||
{{-- 說明區塊 --}}
|
||||
<div class="mb-4 p-4 bg-gray-100 border border-gray-300 rounded text-sm text-gray-700">
|
||||
<p class="font-semibold mb-2">匯入格式說明</p>
|
||||
<p class="mb-2">請依下列表格格式準備 Excel 或 CSV 檔案:</p>
|
||||
|
||||
<div class="overflow-x-auto mb-2">
|
||||
<table class="min-w-full text-sm text-left border border-collapse border-gray-300">
|
||||
<thead class="bg-gray-200">
|
||||
<tr>
|
||||
<th class="border border-gray-300 px-3 py-1">欄位名稱</th>
|
||||
<th class="border border-gray-300 px-3 py-1">說明</th>
|
||||
<th class="border border-gray-300 px-3 py-1">範例</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="border border-gray-300 px-3 py-1">???</td>
|
||||
<td class="border border-gray-300 px-3 py-1">???</td>
|
||||
<td class="border border-gray-300 px-3 py-1">???</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- 檔案上傳 --}}
|
||||
<div x-data="{
|
||||
fileName: '',
|
||||
updateFileInfo(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) this.fileName = file.name+ '('+(file.size / 1024 / 1024).toFixed(2) + ' MB'+')';
|
||||
}
|
||||
}"
|
||||
>
|
||||
<div x-show="$wire.file === null" >
|
||||
<input type="file" wire:model="file" accept=".csv, .xls, .xlsx" class="mb-2 w-full" @change="updateFileInfo" />
|
||||
<p class="text-xs text-gray-500 mb-2" >
|
||||
系統限制:最大上傳 {{ $maxUploadSize }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 檔案資訊顯示 -->
|
||||
<div wire:loading.remove wire:target="file" class="text-sm text-green-600 flex items-center space-x-1" x-show="$wire.file != null">
|
||||
<x-wireui:icon name="check-circle" class="w-5 h-5 text-green-500" />
|
||||
<strong x-text="fileName"></strong>
|
||||
</div>
|
||||
<!-- 上傳中提示 -->
|
||||
<div wire:loading wire:target="file" class="text-sm text-blue-500">
|
||||
檔案上傳中,請稍候...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex justify-between w-full">
|
||||
<x-wireui:button flat label="{{ __('users.cancel') }}" wire:click="closeModal" />
|
||||
<x-wireui:button primary label="{{ __('users.submit') }}" wire:click="import" />
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-wireui:modal-card>
|
@ -1,6 +1,3 @@
|
||||
<x-layouts.admin>
|
||||
<x-wireui:notifications/>
|
||||
<livewire:admin.user-table />
|
||||
<livewire:admin.user-form />
|
||||
<livewire:admin.user-import-data />
|
||||
</x-layouts.admin>
|
@ -8,8 +8,9 @@ new class extends Component
|
||||
public array $menus=[
|
||||
['label' => 'Dashboard', 'route' => 'admin.dashboard', 'icon' => 'home', 'permission' => null],
|
||||
['label' => 'ActivityLog', 'route' => 'admin.activity-log', 'icon' => 'clock', 'permission' => null],
|
||||
['label' => 'Role', 'route' => 'admin.roles', 'icon' => 'user-circle', 'permission' => 'role-list'],
|
||||
['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'],
|
||||
['label' => 'Branche', 'route' => 'admin.branches', 'icon' => 'building-library', 'permission' => 'room-list'],
|
||||
['label' => 'Room', 'route' => 'admin.rooms', 'icon' => 'film', 'permission' => 'room-list'],
|
||||
];
|
||||
|
||||
/**
|
||||
@ -29,25 +30,12 @@ new class extends Component
|
||||
<div class="p-4 border-b">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-bold text-lg">管理後台</div>
|
||||
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition">
|
||||
<div x-data="{{ json_encode(['name' => auth()->user()->name]) }}"
|
||||
x-text="name"
|
||||
x-on:profile-updated.window="name = $event.detail.name"></div>
|
||||
<svg class="ml-1 w-4 h-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link :href="route('profile')" wire:navigate>
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
<div
|
||||
class="inline-flex items-center px-2 py-1 text-sm font-medium text-gray-700"
|
||||
x-data="{ name: '{{ auth()->user()->name }}' }"
|
||||
x-text="name"
|
||||
x-on:profile-updated.window="name = $event.detail.name">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
16
routes/api.php
Normal file
16
routes/api.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\ArtistController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\RoomControlController;
|
||||
use App\Http\Controllers\SqliteUploadController;
|
||||
|
||||
Route::post('/room/receiveRegister', [RoomControlController::class, 'receiveRegister']);
|
||||
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/profile', [AuthController::class, 'profile']);
|
||||
Route::post('/room/sendSwitch', [RoomControlController::class, 'sendSwitch']);
|
||||
Route::post('/room/heartbeat', [RoomControlController::class, 'StatusReport']);
|
||||
Route::post('/upload-sqlite', [SqliteUploadController::class, 'upload']);
|
||||
});
|
@ -6,3 +6,9 @@ use Illuminate\Support\Facades\Artisan;
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::command('machine_statuses:clear')->dailyAt('12:00'); // 每天凌晨 12:10 執行
|
||||
//首次部署或有新增命令時)建立或更新任務排程 Crontab
|
||||
// 檢查是否已有下列 crontab 設定(crontab -e):
|
||||
//分鐘 小時 日 月 星期 指令
|
||||
// * * * * * cd /Users/allen.yan/work/KTV && php artisan schedule:run >> /dev/null 2>&1
|
@ -21,7 +21,7 @@ require __DIR__.'/auth.php';
|
||||
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/dashboard', AdminDashboard::class)->name('dashboard');
|
||||
Route::get('/activity-log', function () {return view('livewire.admin.activity-log');})->name('activity-log');
|
||||
Route::get('/roles', function () {return view('livewire.admin.roles');})->name('roles');
|
||||
Route::get('/users', function () {return view('livewire.admin.users');})->name('users');
|
||||
|
||||
Route::get('/branches', function () {return view('livewire.admin.branches');})->name('branches');
|
||||
Route::get('/rooms', function () {return view('livewire.admin.rooms');})->name('rooms');
|
||||
});
|
52
更新後部署流程(建議步驟).ini
Normal file
52
更新後部署流程(建議步驟).ini
Normal file
@ -0,0 +1,52 @@
|
||||
✅ Laravel 更新後部署流程(建議步驟)
|
||||
|
||||
1. 拉取新版程式碼
|
||||
git pull origin main
|
||||
|
||||
2. 安裝依賴套件
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
npm install && npm run build
|
||||
|
||||
3. 執行資料庫 migration(如有 schema 變更)
|
||||
php artisan migrate
|
||||
|
||||
4. 清除並重新快取設定與路由
|
||||
php artisan config:clear
|
||||
php artisan config:cache
|
||||
php artisan route:clear
|
||||
php artisan route:cache
|
||||
php artisan view:clear
|
||||
php artisan view:cache
|
||||
|
||||
5. (首次部署或有新增命令時)建立或更新任務排程 Crontab
|
||||
檢查是否已有下列 crontab 設定(crontab -e):
|
||||
分鐘 小時 日 月 星期 指令
|
||||
* * * * * cd /path/to/your/project && php artisan schedule:run >> /dev/null 2>&1
|
||||
這樣 Laravel 才能自動執行你在 routes/console.php 中定義的排程任務。
|
||||
|
||||
6. (選擇性)部署完立即執行某些 Artisan 指令
|
||||
例如你可能希望部署後立即重建一次機器狀態資料表,可以執行:
|
||||
php artisan machine_statuses:clear
|
||||
|
||||
7. 權限與快取設定(根據伺服器環境)
|
||||
確認 storage 和 bootstrap/cache 目錄權限正確:
|
||||
chmod -R 775 storage bootstrap/cache
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
|
||||
✅ 完整部署腳本範例(可寫成 deploy.sh)
|
||||
#!/bin/bash
|
||||
|
||||
cd /var/www/your-project
|
||||
|
||||
git pull origin main
|
||||
composer install --no-dev --optimize-autoloader
|
||||
php artisan migrate --force
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan machine_statuses:clear
|
||||
|
||||
echo "✅ Laravel 專案已更新並執行完成。"
|
Loading…
x
Reference in New Issue
Block a user