From c2f0047ed98614a5221380fb59c9d205920bd67b Mon Sep 17 00:00:00 2001
From: gbanyan
Date: Sun, 25 Jan 2026 05:52:40 +0800
Subject: [PATCH] Add personal application fields to members
---
app/Console/Commands/ImportMembers.php | 28 ++-
app/Console/Commands/ImportMembersCommand.php | 129 +++++++++--
app/Http/Requests/StoreMemberRequest.php | 11 +
app/Http/Requests/UpdateMemberRequest.php | 17 ++
app/Models/Member.php | 17 ++
...10_add_profile_fields_to_members_table.php | 48 +++++
.../views/admin/members/create.blade.php | 204 +++++++++++++++++-
resources/views/admin/members/edit.blade.php | 204 +++++++++++++++++-
.../views/admin/members/import.blade.php | 12 ++
resources/views/admin/members/show.blade.php | 86 ++++++++
10 files changed, 729 insertions(+), 27 deletions(-)
create mode 100644 database/migrations/2026_01_25_054310_add_profile_fields_to_members_table.php
diff --git a/app/Console/Commands/ImportMembers.php b/app/Console/Commands/ImportMembers.php
index 9c2ab31..818e48d 100644
--- a/app/Console/Commands/ImportMembers.php
+++ b/app/Console/Commands/ImportMembers.php
@@ -46,7 +46,7 @@ class ImportMembers extends Command
$header = array_map('trim', $header);
- $expected = [
+ $required = [
'full_name',
'email',
'phone',
@@ -60,7 +60,7 @@ class ImportMembers extends Command
'membership_expires_at',
];
- foreach ($expected as $column) {
+ foreach ($required as $column) {
if (! in_array($column, $header, true)) {
$this->error("Missing required column: {$column}");
fclose($handle);
@@ -82,8 +82,19 @@ class ImportMembers extends Command
}
$fullName = trim($row[$indexes['full_name']] ?? '');
- $nationalId = trim($row[$indexes['national_id']] ?? '');
+ $memberNumber = isset($indexes['member_number']) ? trim($row[$indexes['member_number']] ?? '') : '';
+ $nationalId = isset($indexes['national_id']) ? trim($row[$indexes['national_id']] ?? '') : '';
$phone = trim($row[$indexes['phone']] ?? '');
+ $phoneHome = isset($indexes['phone_home']) ? trim($row[$indexes['phone_home']] ?? '') : '';
+ $phoneFax = isset($indexes['phone_fax']) ? trim($row[$indexes['phone_fax']] ?? '') : '';
+ $birthDate = isset($indexes['birth_date']) ? trim($row[$indexes['birth_date']] ?? '') : '';
+ $gender = isset($indexes['gender']) ? trim($row[$indexes['gender']] ?? '') : '';
+ $identityType = isset($indexes['identity_type']) ? trim($row[$indexes['identity_type']] ?? '') : '';
+ $identityOtherText = isset($indexes['identity_other_text']) ? trim($row[$indexes['identity_other_text']] ?? '') : '';
+ $occupation = isset($indexes['occupation']) ? trim($row[$indexes['occupation']] ?? '') : '';
+ $employer = isset($indexes['employer']) ? trim($row[$indexes['employer']] ?? '') : '';
+ $jobTitle = isset($indexes['job_title']) ? trim($row[$indexes['job_title']] ?? '') : '';
+ $appliedAt = isset($indexes['applied_at']) ? trim($row[$indexes['applied_at']] ?? '') : '';
$started = trim($row[$indexes['membership_started_at']] ?? '');
$expires = trim($row[$indexes['membership_expires_at']] ?? '');
$address1 = trim($row[$indexes['address_line_1']] ?? '');
@@ -109,10 +120,21 @@ class ImportMembers extends Command
$member = Member::updateOrCreate(
['user_id' => $user->id],
[
+ 'member_number' => $memberNumber !== '' ? $memberNumber : null,
'full_name' => $fullName !== '' ? $fullName : $user->name,
'email' => $email,
'national_id' => $nationalId !== '' ? $nationalId : null,
'phone' => $phone !== '' ? $phone : null,
+ 'phone_home' => $phoneHome !== '' ? $phoneHome : null,
+ 'phone_fax' => $phoneFax !== '' ? $phoneFax : null,
+ 'birth_date' => $birthDate !== '' ? $birthDate : null,
+ 'gender' => $gender !== '' ? $gender : null,
+ 'identity_type' => $identityType !== '' ? $identityType : null,
+ 'identity_other_text' => $identityOtherText !== '' ? $identityOtherText : null,
+ 'occupation' => $occupation !== '' ? $occupation : null,
+ 'employer' => $employer !== '' ? $employer : null,
+ 'job_title' => $jobTitle !== '' ? $jobTitle : null,
+ 'applied_at' => $appliedAt !== '' ? $appliedAt : null,
'address_line_1' => $address1 ?: null,
'address_line_2' => $address2 ?: null,
'city' => $city ?: null,
diff --git a/app/Console/Commands/ImportMembersCommand.php b/app/Console/Commands/ImportMembersCommand.php
index a480763..15fc2b3 100644
--- a/app/Console/Commands/ImportMembersCommand.php
+++ b/app/Console/Commands/ImportMembersCommand.php
@@ -9,6 +9,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
class ImportMembersCommand extends Command
{
@@ -78,11 +79,17 @@ class ImportMembersCommand extends Command
continue;
}
- $phone = $this->normalizePhone($row[6] ?? '');
- $email = $this->resolveEmail($name, $row[10] ?? '');
- $nationalId = trim($row[7] ?? '');
+ $memberNumber = $this->normalizeMemberNumber($row[0] ?? '');
+ $birthDate = $this->normalizeRocDate($row[2] ?? '');
+ $gender = $this->normalizeGender($row[3] ?? '');
+ $occupation = trim($row[4] ?? '');
$address = trim($row[5] ?? '');
- $memberType = $this->mapMemberType($row[8] ?? '');
+ $phone = $this->normalizePhone($row[6] ?? '');
+ $nationalId = trim($row[7] ?? '');
+ [$identityType, $identityOtherText] = $this->parseIdentityType($row[8] ?? '');
+ $applyDate = $this->normalizeRocDate($row[9] ?? '');
+ $email = $this->resolveEmail($name, $row[10] ?? '');
+ $memberType = Member::TYPE_INDIVIDUAL;
// Validate phone for password generation
if (strlen($phone) < 4) {
@@ -102,6 +109,15 @@ class ImportMembersCommand extends Command
continue;
}
+ if ($memberNumber) {
+ $existingMemberNumber = Member::where('member_number', $memberNumber)->first();
+ if ($existingMemberNumber) {
+ $this->warn("Row {$rowNum}: {$name} - Member number already exists: {$memberNumber}");
+ $skipped++;
+ continue;
+ }
+ }
+
// Check for existing member by national ID
if ($nationalId) {
$existingMember = Member::where('national_id_hash', hash('sha256', $nationalId))->first();
@@ -128,13 +144,24 @@ class ImportMembersCommand extends Command
// Create Member
$member = Member::create([
'user_id' => $user->id,
+ 'member_number' => $memberNumber ?: null,
'full_name' => $name,
'email' => $email,
'phone' => $phone,
+ 'phone_home' => null,
+ 'phone_fax' => null,
'address_line_1' => $address,
'national_id' => $nationalId ?: null,
'membership_status' => Member::STATUS_ACTIVE,
'membership_type' => $memberType,
+ 'identity_type' => $identityType,
+ 'identity_other_text' => $identityOtherText ?: null,
+ 'birth_date' => $birthDate,
+ 'gender' => $gender,
+ 'occupation' => $occupation ?: null,
+ 'employer' => null,
+ 'job_title' => null,
+ 'applied_at' => $applyDate,
'membership_started_at' => now(),
'membership_expires_at' => now()->endOfYear(), // 2026-12-31
]);
@@ -277,18 +304,96 @@ class ImportMembersCommand extends Command
return $phone;
}
- protected function mapMemberType(string $type): string
+ protected function normalizeMemberNumber(mixed $value): ?string
{
- // Map Chinese member types to constants
- $type = trim($type);
- return match ($type) {
- '榮譽會員' => Member::TYPE_HONORARY,
- '終身會員' => Member::TYPE_LIFETIME,
- '學生會員' => Member::TYPE_STUDENT,
- default => Member::TYPE_REGULAR, // 患者, 家屬, etc.
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ $raw = is_numeric($value) ? (string) (int) $value : trim((string) $value);
+ return $raw !== '' ? $raw : null;
+ }
+
+ protected function normalizeGender(mixed $value): ?string
+ {
+ $value = trim((string) $value);
+ return match ($value) {
+ '男' => 'male',
+ '女' => 'female',
+ '其他' => 'other',
+ default => null,
};
}
+ protected function normalizeRocDate(mixed $value): ?string
+ {
+ if ($value === null || $value === '') {
+ return null;
+ }
+
+ if ($value instanceof \DateTimeInterface) {
+ return $value->format('Y-m-d');
+ }
+
+ if (is_numeric($value)) {
+ $numeric = (float) $value;
+ if ($numeric > 10000) {
+ return ExcelDate::excelToDateTimeObject($numeric)->format('Y-m-d');
+ }
+ }
+
+ $raw = trim((string) $value);
+ if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $raw)) {
+ return $raw;
+ }
+
+ if (preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $raw)) {
+ return str_replace('/', '-', $raw);
+ }
+
+ $digits = preg_replace('/\D/', '', $raw);
+ if (strlen($digits) === 6 || strlen($digits) === 7) {
+ $yearLength = strlen($digits) - 4;
+ $rocYear = (int) substr($digits, 0, $yearLength);
+ $month = (int) substr($digits, $yearLength, 2);
+ $day = (int) substr($digits, $yearLength + 2, 2);
+ $year = $rocYear + 1911;
+
+ if (checkdate($month, $day, $year)) {
+ return sprintf('%04d-%02d-%02d', $year, $month, $day);
+ }
+ }
+
+ return null;
+ }
+
+ protected function parseIdentityType(mixed $value): array
+ {
+ $raw = trim((string) $value);
+
+ if ($raw === '') {
+ return [null, null];
+ }
+
+ if (str_contains($raw, '病友')) {
+ return [Member::IDENTITY_PATIENT, null];
+ }
+
+ if (str_contains($raw, '父母') || str_contains($raw, '家長')) {
+ return [Member::IDENTITY_PARENT, null];
+ }
+
+ if (str_contains($raw, '社會') || str_contains($raw, '學者') || str_contains($raw, '醫師')) {
+ return [Member::IDENTITY_SOCIAL, null];
+ }
+
+ if (str_contains($raw, '其他')) {
+ return [Member::IDENTITY_OTHER, $raw];
+ }
+
+ return [Member::IDENTITY_OTHER, $raw];
+ }
+
protected function toPinyin(string $name): string
{
// Simple romanization for email generation
diff --git a/app/Http/Requests/StoreMemberRequest.php b/app/Http/Requests/StoreMemberRequest.php
index 5e70426..6dfc775 100644
--- a/app/Http/Requests/StoreMemberRequest.php
+++ b/app/Http/Requests/StoreMemberRequest.php
@@ -22,10 +22,21 @@ class StoreMemberRequest extends FormRequest
public function rules(): array
{
return [
+ 'member_number' => ['nullable', 'string', 'max:50', 'unique:members,member_number'],
'full_name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'national_id' => ['nullable', 'string', 'max:50'],
'phone' => ['nullable', 'string', 'max:50'],
+ 'phone_home' => ['nullable', 'string', 'max:50'],
+ 'phone_fax' => ['nullable', 'string', 'max:50'],
+ 'birth_date' => ['nullable', 'date'],
+ 'gender' => ['nullable', 'in:male,female,other'],
+ 'identity_type' => ['nullable', 'in:patient,parent,social,other'],
+ 'identity_other_text' => ['nullable', 'string', 'max:255', 'required_if:identity_type,other'],
+ 'occupation' => ['nullable', 'string', 'max:120'],
+ 'employer' => ['nullable', 'string', 'max:255'],
+ 'job_title' => ['nullable', 'string', 'max:120'],
+ 'applied_at' => ['nullable', 'date'],
'address_line_1' => ['nullable', 'string', 'max:255'],
'address_line_2' => ['nullable', 'string', 'max:255'],
'city' => ['nullable', 'string', 'max:120'],
diff --git a/app/Http/Requests/UpdateMemberRequest.php b/app/Http/Requests/UpdateMemberRequest.php
index 320bc0a..bd9c3e5 100644
--- a/app/Http/Requests/UpdateMemberRequest.php
+++ b/app/Http/Requests/UpdateMemberRequest.php
@@ -3,6 +3,7 @@
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Validation\Rule;
class UpdateMemberRequest extends FormRequest
{
@@ -22,10 +23,26 @@ class UpdateMemberRequest extends FormRequest
public function rules(): array
{
return [
+ 'member_number' => [
+ 'nullable',
+ 'string',
+ 'max:50',
+ Rule::unique('members', 'member_number')->ignore($this->member),
+ ],
'full_name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'national_id' => ['nullable', 'string', 'max:50'],
'phone' => ['nullable', 'string', 'max:50'],
+ 'phone_home' => ['nullable', 'string', 'max:50'],
+ 'phone_fax' => ['nullable', 'string', 'max:50'],
+ 'birth_date' => ['nullable', 'date'],
+ 'gender' => ['nullable', 'in:male,female,other'],
+ 'identity_type' => ['nullable', 'in:patient,parent,social,other'],
+ 'identity_other_text' => ['nullable', 'string', 'max:255', 'required_if:identity_type,other'],
+ 'occupation' => ['nullable', 'string', 'max:120'],
+ 'employer' => ['nullable', 'string', 'max:255'],
+ 'job_title' => ['nullable', 'string', 'max:120'],
+ 'applied_at' => ['nullable', 'date'],
'address_line_1' => ['nullable', 'string', 'max:255'],
'address_line_2' => ['nullable', 'string', 'max:255'],
'city' => ['nullable', 'string', 'max:120'],
diff --git a/app/Models/Member.php b/app/Models/Member.php
index a81e678..9e0cb4e 100644
--- a/app/Models/Member.php
+++ b/app/Models/Member.php
@@ -33,18 +33,30 @@ class Member extends Model
// Identity type constants (病友/家長)
const IDENTITY_PATIENT = 'patient'; // 病友
const IDENTITY_PARENT = 'parent'; // 家長/父母
+ const IDENTITY_SOCIAL = 'social'; // 社會人士
+ const IDENTITY_OTHER = 'other'; // 其他
protected $fillable = [
'user_id',
+ 'member_number',
'full_name',
'email',
'phone',
+ 'phone_home',
+ 'phone_fax',
'address_line_1',
'address_line_2',
'city',
'postal_code',
+ 'birth_date',
+ 'gender',
+ 'occupation',
+ 'employer',
+ 'job_title',
+ 'applied_at',
'emergency_contact_name',
'emergency_contact_phone',
+ 'national_id',
'national_id_encrypted',
'national_id_hash',
'membership_started_at',
@@ -52,6 +64,7 @@ class Member extends Model
'membership_status',
'membership_type',
'identity_type',
+ 'identity_other_text',
'guardian_member_id',
'disability_certificate_path',
'disability_certificate_status',
@@ -63,6 +76,8 @@ class Member extends Model
protected $casts = [
'membership_started_at' => 'date',
'membership_expires_at' => 'date',
+ 'birth_date' => 'date',
+ 'applied_at' => 'date',
'disability_verified_at' => 'datetime',
];
@@ -118,6 +133,8 @@ class Member extends Model
return match ($this->identity_type) {
self::IDENTITY_PATIENT => '病友',
self::IDENTITY_PARENT => '家長/父母',
+ self::IDENTITY_SOCIAL => '社會人士',
+ self::IDENTITY_OTHER => $this->identity_other_text ? '其他(' . $this->identity_other_text . ')' : '其他',
default => '未設定',
};
}
diff --git a/database/migrations/2026_01_25_054310_add_profile_fields_to_members_table.php b/database/migrations/2026_01_25_054310_add_profile_fields_to_members_table.php
new file mode 100644
index 0000000..a46aca7
--- /dev/null
+++ b/database/migrations/2026_01_25_054310_add_profile_fields_to_members_table.php
@@ -0,0 +1,48 @@
+string('member_number', 50)->unique()->nullable();
+ $table->date('birth_date')->nullable();
+ $table->string('gender', 20)->nullable();
+ $table->string('occupation', 120)->nullable();
+ $table->string('employer', 255)->nullable();
+ $table->string('job_title', 120)->nullable();
+ $table->date('applied_at')->nullable();
+ $table->string('phone_home', 50)->nullable();
+ $table->string('phone_fax', 50)->nullable();
+ $table->string('identity_other_text', 255)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('members', function (Blueprint $table) {
+ $table->dropColumn([
+ 'member_number',
+ 'birth_date',
+ 'gender',
+ 'occupation',
+ 'employer',
+ 'job_title',
+ 'applied_at',
+ 'phone_home',
+ 'phone_fax',
+ 'identity_other_text',
+ ]);
+ });
+ }
+};
diff --git a/resources/views/admin/members/create.blade.php b/resources/views/admin/members/create.blade.php
index bdb64a1..994d274 100644
--- a/resources/views/admin/members/create.blade.php
+++ b/resources/views/admin/members/create.blade.php
@@ -37,6 +37,22 @@
@enderror
+
+
+
+ @error('member_number')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+ @error('birth_date')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('gender')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+ @error('identity_type')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('identity_other_text')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+ @error('phone')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('phone_home')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+
+ @error('occupation')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('job_title')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+ @error('employer')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('applied_at')
{{ $message }}
@enderror
diff --git a/resources/views/admin/members/edit.blade.php b/resources/views/admin/members/edit.blade.php
index 12eabb6..8567a46 100644
--- a/resources/views/admin/members/edit.blade.php
+++ b/resources/views/admin/members/edit.blade.php
@@ -38,6 +38,22 @@
@enderror
+
+
+
+ @error('member_number')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+ @error('birth_date')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('gender')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+ @error('identity_type')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('identity_other_text')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+ @error('phone')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('phone_home')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+
+
+ @error('occupation')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('job_title')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+
+ @error('employer')
+
{{ $message }}
+ @enderror
+
+
+
+
+
+ @error('applied_at')
{{ $message }}
@enderror
diff --git a/resources/views/admin/members/import.blade.php b/resources/views/admin/members/import.blade.php
index 36c6273..67c19b8 100644
--- a/resources/views/admin/members/import.blade.php
+++ b/resources/views/admin/members/import.blade.php
@@ -14,8 +14,20 @@
full_name
+ member_number (optional)
email
phone
+ phone_home (optional)
+ phone_fax (optional)
+ birth_date (YYYY-MM-DD, optional)
+ gender (male/female/other, optional)
+ identity_type (patient/parent/social/other, optional)
+ identity_other_text (optional)
+ occupation (optional)
+ employer (optional)
+ job_title (optional)
+ applied_at (YYYY-MM-DD, optional)
+ national_id (optional)
address_line_1 (optional)
address_line_2 (optional)
city (optional)
diff --git a/resources/views/admin/members/show.blade.php b/resources/views/admin/members/show.blade.php
index 978c79f..c90247b 100644
--- a/resources/views/admin/members/show.blade.php
+++ b/resources/views/admin/members/show.blade.php
@@ -85,6 +85,15 @@
+
+
-
+ 會員編號
+
+ -
+ {{ $member->member_number ?? __('Not set') }}
+
+
+
-
電話
@@ -94,6 +103,16 @@
+
+
-
+ 室內電話/傳真
+
+
-
+
{{ $member->phone_home ?? __('Not set') }}
+ {{ $member->phone_fax ?? '' }}
+
+
+
-
會員資格狀態
@@ -112,6 +131,45 @@
+
+
-
+ 出生年月日
+
+ -
+ @if ($member->birth_date)
+ {{ $member->birth_date->toDateString() }}
+ @else
+ 未設定
+ @endif
+
+
+
+
+
-
+ 性別
+
+ -
+ @php
+ $genderLabel = match($member->gender) {
+ 'male' => '男',
+ 'female' => '女',
+ 'other' => '其他',
+ default => '未設定',
+ };
+ @endphp
+ {{ $genderLabel }}
+
+
+
+
+
-
+ 身份別
+
+ -
+ {{ $member->identity_type_label }}
+
+
+
-
會員資格開始
@@ -125,6 +183,19 @@
+
+
-
+ 申請日期
+
+ -
+ @if ($member->applied_at)
+ {{ $member->applied_at->toDateString() }}
+ @else
+ 未設定
+ @endif
+
+
+
-
會員資格到期
@@ -138,6 +209,21 @@
+
+
-
+ 現職/服務單位
+
+
-
+
{{ $member->occupation ?? __('Not set') }}
+ @if ($member->employer)
+ {{ $member->employer }}
+ @endif
+ @if ($member->job_title)
+ {{ $member->job_title }}
+ @endif
+
+
+
-
地址