Add personal application fields to members

This commit is contained in:
2026-01-25 05:52:40 +08:00
parent 65de7d9019
commit c2f0047ed9
10 changed files with 729 additions and 27 deletions

View File

@@ -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,

View File

@@ -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