diff --git a/app/Http/Controllers/Admin/StaffCardController.php b/app/Http/Controllers/Admin/StaffCardController.php
index 60a7ec9..a80169c 100644
--- a/app/Http/Controllers/Admin/StaffCardController.php
+++ b/app/Http/Controllers/Admin/StaffCardController.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\StaffCard;
use App\Models\StaffCardLog;
+use App\Services\StaffCardImportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -141,4 +142,34 @@ class StaffCardController extends Controller
'message' => __('Status updated successfully'),
]);
}
+
+ public function downloadTemplate(StaffCardImportService $importService)
+ {
+ return $importService->downloadTemplate();
+ }
+
+ public function import(Request $request, StaffCardImportService $importService)
+ {
+ $request->validate([
+ 'file' => 'required|file|mimes:xlsx,xls,csv|max:5120',
+ 'company_id' => 'nullable|exists:companies,id',
+ ]);
+
+ $companyId = $request->get('company_id') ?: auth()->user()->company_id;
+
+ if (auth()->user()->isSystemAdmin() && !$companyId) {
+ return response()->json([
+ 'success' => false,
+ 'message' => __('Please select a company'),
+ ], 422);
+ }
+
+ $results = $importService->import($request->file('file')->getRealPath(), $companyId);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => __(':count staff cards imported successfully', ['count' => $results['success_count']]),
+ 'results' => $results,
+ ]);
+ }
}
diff --git a/app/Services/StaffCardImportService.php b/app/Services/StaffCardImportService.php
new file mode 100644
index 0000000..a4fd12c
--- /dev/null
+++ b/app/Services/StaffCardImportService.php
@@ -0,0 +1,136 @@
+open($filePath);
+
+ $results = [
+ 'success_count' => 0,
+ 'error_count' => 0,
+ 'errors' => [],
+ ];
+
+ $rowCount = 0;
+ foreach ($reader->getSheetIterator() as $sheet) {
+ foreach ($sheet->getRowIterator() as $row) {
+ $rowCount++;
+
+ // Skip heading row (Row 1)
+ if ($rowCount === 1) {
+ continue;
+ }
+
+ $cells = $row->toArray();
+
+ // Expecting: 0:employee_id, 1:name, 2:card_uid, 3:notes
+ $data = [
+ 'employee_id' => isset($cells[0]) ? (string)$cells[0] : null,
+ 'name' => isset($cells[1]) ? (string)$cells[1] : null,
+ 'card_uid' => isset($cells[2]) ? (string)$cells[2] : null,
+ 'notes' => isset($cells[3]) ? (string)$cells[3] : null,
+ 'company_id' => $companyId,
+ ];
+
+ // Basic validation
+ if (empty($data['employee_id']) && empty($data['name']) && empty($data['card_uid'])) {
+ continue; // Skip empty rows
+ }
+
+ $validator = Validator::make($data, [
+ 'employee_id' => 'required|string|max:50',
+ 'name' => 'required|string|max:100',
+ 'card_uid' => 'required|string|max:100',
+ ]);
+
+ if ($validator->fails()) {
+ $results['error_count']++;
+ $results['errors'][] = [
+ 'row' => $rowCount,
+ 'messages' => $validator->errors()->all(),
+ ];
+ continue;
+ }
+
+ try {
+ // Update or create based on card_uid and company_id
+ StaffCard::updateOrCreate(
+ [
+ 'company_id' => $data['company_id'],
+ 'card_uid' => $data['card_uid'],
+ ],
+ [
+ 'employee_id' => $data['employee_id'],
+ 'name' => $data['name'],
+ 'notes' => $data['notes'],
+ 'status' => 'active',
+ ]
+ );
+ $results['success_count']++;
+ } catch (\Exception $e) {
+ $results['error_count']++;
+ $results['errors'][] = [
+ 'row' => $rowCount,
+ 'messages' => [$e->getMessage()],
+ ];
+ }
+ }
+ }
+
+ $reader->close();
+
+ return $results;
+ }
+
+ /**
+ * Download Excel template for staff cards
+ *
+ * @return void
+ */
+ public function downloadTemplate()
+ {
+ $writer = new Writer();
+ $fileName = 'staff_cards_template.xlsx';
+
+ $writer->openToBrowser($fileName);
+
+ // Header Row
+ $headerRow = Row::fromValues([
+ 'Employee ID (必填)',
+ 'Staff Name (必填)',
+ 'IC Card UID (必填)',
+ 'Notes (選填)'
+ ]);
+ $writer->addRow($headerRow);
+
+ // Example Row
+ $exampleRow = Row::fromValues([
+ 'S001',
+ '王小明',
+ '12345678',
+ '這是備註範例'
+ ]);
+ $writer->addRow($exampleRow);
+
+ $writer->close();
+ }
+}
diff --git a/composer.json b/composer.json
index 537e081..532cf84 100644
--- a/composer.json
+++ b/composer.json
@@ -14,6 +14,7 @@
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1",
+ "openspout/openspout": "^5.7",
"simplesoftwareio/simple-qrcode": "^4.2",
"spatie/laravel-permission": "^7.2"
},
diff --git a/composer.lock b/composer.lock
index 39b5199..f2eac5f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "2889e194212440faeb9f8f3dd7513795",
+ "content-hash": "0df2fc3ffb40fcff1794055a9ec63017",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -2894,6 +2894,99 @@
],
"time": "2026-02-16T23:10:27+00:00"
},
+ {
+ "name": "openspout/openspout",
+ "version": "v5.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/openspout/openspout.git",
+ "reference": "b82aa46191802c187f67e9639ddc604b9e7a73e9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/openspout/openspout/zipball/b82aa46191802c187f67e9639ddc604b9e7a73e9",
+ "reference": "b82aa46191802c187f67e9639ddc604b9e7a73e9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-libxml": "*",
+ "ext-xmlreader": "*",
+ "ext-zip": "*",
+ "php": "~8.4.0 || ~8.5.0"
+ },
+ "require-dev": {
+ "ext-fileinfo": "*",
+ "ext-zlib": "*",
+ "friendsofphp/php-cs-fixer": "^3.95.1",
+ "infection/infection": "^0.32.7",
+ "phpbench/phpbench": "^1.6.1",
+ "phpstan/phpstan": "^2.1.53",
+ "phpstan/phpstan-phpunit": "^2.0.16",
+ "phpstan/phpstan-strict-rules": "^2.0.10",
+ "phpunit/phpunit": "^13.1.7"
+ },
+ "suggest": {
+ "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
+ "ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenSpout\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Adrien Loison",
+ "email": "adrien@box.com"
+ }
+ ],
+ "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
+ "homepage": "https://github.com/openspout/openspout",
+ "keywords": [
+ "OOXML",
+ "csv",
+ "excel",
+ "memory",
+ "odf",
+ "ods",
+ "office",
+ "open",
+ "php",
+ "read",
+ "scale",
+ "spreadsheet",
+ "stream",
+ "write",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/openspout/openspout/issues",
+ "source": "https://github.com/openspout/openspout/tree/v5.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/filippotessarotto",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/Slamdunk",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-29T07:42:36+00:00"
+ },
{
"name": "phpoption/phpoption",
"version": "1.9.5",
diff --git a/lang/en.json b/lang/en.json
index 02af7dc..2fe7faa 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -1964,5 +1964,14 @@
"visit_gift": "Visit Gift",
"vs Yesterday": "vs Yesterday",
"warehouses": "Warehouse Management",
- "待填寫": "Pending"
+ "待填寫": "Pending",
+ "Import Excel": "Import Excel",
+ "Import Staff Cards": "Import Staff Cards",
+ "Excel File": "Excel File",
+ "Start Import": "Start Import",
+ "Only .xlsx, .xls, .csv files are supported (Max 5MB)": "Only .xlsx, .xls, .csv files are supported (Max 5MB)",
+ "Download Template": "Download Template",
+ "Please use our standard template to ensure data compatibility.": "Please use our standard template to ensure data compatibility.",
+ ":count staff cards imported successfully": ":count staff cards imported successfully",
+ "Import failed": "Import failed"
}
\ No newline at end of file
diff --git a/lang/ja.json b/lang/ja.json
index ec6fdc3..c341428 100644
--- a/lang/ja.json
+++ b/lang/ja.json
@@ -1964,5 +1964,14 @@
"visit_gift": "来店ギフト",
"vs Yesterday": "昨日比",
"warehouses": "倉庫管理",
- "待填寫": "未記入"
+ "待填寫": "未記入",
+ "Import Excel": "Excel インポート",
+ "Import Staff Cards": "スタッフカードのインポート",
+ "Excel File": "Excel ファイル",
+ "Start Import": "インポート開始",
+ "Only .xlsx, .xls, .csv files are supported (Max 5MB)": ".xlsx, .xls, .csv ファイルのみサポートされています (最大 5MB)",
+ "Download Template": "テンプレートのダウンロード",
+ "Please use our standard template to ensure data compatibility.": "データの互換性を確保するために、標準テンプレートを使用してください。",
+ ":count staff cards imported successfully": ":count 枚のスタッフカードが正常にインポートされました",
+ "Import failed": "インポートに失敗しました"
}
\ No newline at end of file
diff --git a/lang/zh_TW.json b/lang/zh_TW.json
index 090d0b0..6a92804 100644
--- a/lang/zh_TW.json
+++ b/lang/zh_TW.json
@@ -2012,5 +2012,14 @@
"visit_gift": "來店禮",
"vs Yesterday": "較昨日",
"warehouses": "倉庫管理",
- "待填寫": "待填寫"
+ "待填寫": "待填寫",
+ "Import Excel": "Excel 匯入",
+ "Import Staff Cards": "匯入員工識別卡",
+ "Excel File": "Excel 檔案",
+ "Start Import": "開始匯入",
+ "Only .xlsx, .xls, .csv files are supported (Max 5MB)": "僅支援 .xlsx, .xls, .csv 檔案 (最大 5MB)",
+ "Download Template": "下載範例檔",
+ "Please use our standard template to ensure data compatibility.": "請使用標準範例檔以確保資料相容性。",
+ ":count staff cards imported successfully": "成功匯入 :count 張員工識別卡",
+ "Import failed": "匯入失敗"
}
\ No newline at end of file
diff --git a/resources/views/admin/data-config/staff-cards/index.blade.php b/resources/views/admin/data-config/staff-cards/index.blade.php
index 2401b26..77ac1ee 100644
--- a/resources/views/admin/data-config/staff-cards/index.blade.php
+++ b/resources/views/admin/data-config/staff-cards/index.blade.php
@@ -8,13 +8,22 @@