DONE: use full name for date in letter, include password in email after membership is done, block ic_number to exclude - (#9)
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local> Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
@@ -54,6 +54,7 @@ class User extends Authenticatable
|
||||
'member_type',
|
||||
'public_profile_token',
|
||||
'join_date',
|
||||
'leave_date',
|
||||
'birth_date',
|
||||
'birth_place',
|
||||
'onboarding_completed_at',
|
||||
@@ -136,6 +137,7 @@ class User extends Authenticatable
|
||||
'member_type' => 'string',
|
||||
'public_profile_token_expires_at' => 'datetime',
|
||||
'join_date' => 'date',
|
||||
'leave_date' => 'date',
|
||||
'birth_date' => 'date',
|
||||
'birth_place' => 'string',
|
||||
'onboarding_completed_at' => 'datetime',
|
||||
|
||||
@@ -38,6 +38,7 @@ class UserResource extends JsonResource
|
||||
'member_type' => $this->member_type,
|
||||
'public_profile_token' => $this->public_profile_token,
|
||||
'join_date' => $this->join_date,
|
||||
'leave_date' => $this->leave_date,
|
||||
'birth_date' => $this->birth_date,
|
||||
'birth_place' => $this->birth_place,
|
||||
'onboarding_completed_at' => $this->onboarding_completed_at,
|
||||
|
||||
@@ -19,6 +19,7 @@ class MembershipApplicationPassedNotification extends Notification implements Sh
|
||||
public function __construct(
|
||||
public MembershipApplication $application,
|
||||
public Document $resultLetter,
|
||||
public ?string $plainPassword = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -40,6 +41,8 @@ class MembershipApplicationPassedNotification extends Notification implements Sh
|
||||
data: [
|
||||
'application' => $this->application,
|
||||
'applicant' => $applicant,
|
||||
'plainPassword' => $this->plainPassword,
|
||||
'loginUrl' => rtrim(config('user.frontend_url'), '/'),
|
||||
],
|
||||
)->attach(
|
||||
Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
|
||||
|
||||
@@ -9,6 +9,15 @@ Permohonan keahlian anda (**{{ $application->application_number }}**) **dilulusk
|
||||
|
||||
Surat keputusan rasmi dilampirkan dalam e-mel ini.
|
||||
|
||||
@if ($plainPassword)
|
||||
Akaun portal anda telah didaftarkan. Maklumat log masuk adalah seperti berikut:
|
||||
|
||||
- **E-mel:** {{ $applicant->email }}
|
||||
- **Kata laluan:** {{ $plainPassword }}
|
||||
|
||||
Sila log masuk di [{{ $loginUrl }}]({{ $loginUrl }}) dan tukar kata laluan anda selepas log masuk kali pertama.
|
||||
@endif
|
||||
|
||||
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
|
||||
|
||||
@include('emails.partials.footer')
|
||||
|
||||
@@ -274,15 +274,18 @@ class MembershipApplicationService
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) {
|
||||
$plainPassword = null;
|
||||
|
||||
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
|
||||
$user = $this->createMemberFromApplication($application);
|
||||
$application->update(['user_id' => $user->id]);
|
||||
$member = $this->createMemberFromApplication($application);
|
||||
$application->update(['user_id' => $member['user']->id]);
|
||||
$plainPassword = $member['plainPassword'];
|
||||
$application->refresh();
|
||||
}
|
||||
|
||||
$resultLetter = $this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate);
|
||||
|
||||
$this->sendResultNotification($application, $resultLetter);
|
||||
$this->sendResultNotification($application, $resultLetter, $plainPassword);
|
||||
|
||||
$application->update([
|
||||
'status' => ApplicationStatus::Completed,
|
||||
@@ -438,7 +441,7 @@ class MembershipApplicationService
|
||||
'application' => $application,
|
||||
'applicant' => $applicant,
|
||||
'boardMeetingReference' => $boardMeetingReference,
|
||||
'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d M Y'),
|
||||
'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d F Y'),
|
||||
'isPassed' => $isPassed,
|
||||
'letterSubject' => $isPassed
|
||||
? 'KELULUSAN PERMOHONAN MENJADI ANGGOTA KOPERASI PERMODALAN KELANTAN BERHAD (KoPKB)'
|
||||
@@ -447,7 +450,10 @@ class MembershipApplicationService
|
||||
];
|
||||
}
|
||||
|
||||
protected function createMemberFromApplication(MembershipApplication $application): User
|
||||
/**
|
||||
* @return array{user: User, plainPassword: string}
|
||||
*/
|
||||
protected function createMemberFromApplication(MembershipApplication $application): array
|
||||
{
|
||||
$application->loadMissing(['applicant', 'heirs']);
|
||||
$applicant = $application->applicant;
|
||||
@@ -458,10 +464,12 @@ class MembershipApplicationService
|
||||
]);
|
||||
}
|
||||
|
||||
$plainPassword = Str::password(16);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $applicant->name,
|
||||
'email' => $applicant->email,
|
||||
'password' => Hash::make(Str::password(16)),
|
||||
'password' => Hash::make($plainPassword),
|
||||
'ic_number' => $applicant->ic_number,
|
||||
'phone_number' => $applicant->phone_number,
|
||||
'position' => $applicant->current_position,
|
||||
@@ -507,7 +515,10 @@ class MembershipApplicationService
|
||||
|
||||
app(EmailVerificationOtpService::class)->send($user);
|
||||
|
||||
return $user;
|
||||
return [
|
||||
'user' => $user,
|
||||
'plainPassword' => $plainPassword,
|
||||
];
|
||||
}
|
||||
|
||||
public function lookupMemberByIcNumber(string $icNumber): ?User
|
||||
@@ -532,12 +543,15 @@ class MembershipApplicationService
|
||||
->notify(new MembershipApplicationSubmittedNotification($application));
|
||||
}
|
||||
|
||||
protected function sendResultNotification(MembershipApplication $application, Document $resultLetter): void
|
||||
{
|
||||
protected function sendResultNotification(
|
||||
MembershipApplication $application,
|
||||
Document $resultLetter,
|
||||
?string $plainPassword = null,
|
||||
): void {
|
||||
$application->loadMissing('applicant');
|
||||
|
||||
$notification = $application->board_result === BoardResult::Pass->value
|
||||
? new MembershipApplicationPassedNotification($application, $resultLetter)
|
||||
? new MembershipApplicationPassedNotification($application, $resultLetter, $plainPassword)
|
||||
: new MembershipApplicationFailedNotification($application, $resultLetter);
|
||||
|
||||
Notification::route('mail', $application->applicant->email)->notify($notification);
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->date('leave_date')->nullable()->after('join_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('leave_date');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -44,7 +44,7 @@ class UserController extends BaseCrudController
|
||||
$validated['uuid'] = Str::uuid();
|
||||
$validated['password'] = Hash::make('suteraselamanya');
|
||||
|
||||
return $validated;
|
||||
return $this->applyLeaveDateForStatus($validated);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +58,25 @@ class UserController extends BaseCrudController
|
||||
unset($validated['email']);
|
||||
}
|
||||
|
||||
return $this->applyLeaveDateForStatus($validated, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or clear leave_date based on membership status transitions.
|
||||
*/
|
||||
protected function applyLeaveDateForStatus(array $validated, ?User $user = null): array
|
||||
{
|
||||
$newStatus = $validated['status'] ?? $user?->status;
|
||||
$currentStatus = $user?->status;
|
||||
|
||||
if ($newStatus === 'inactive') {
|
||||
if ($currentStatus !== 'inactive') {
|
||||
$validated['leave_date'] = $validated['leave_date'] ?? now()->toDateString();
|
||||
}
|
||||
} elseif (in_array($newStatus, ['active', 'pending'], true)) {
|
||||
$validated['leave_date'] = null;
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
@@ -171,13 +190,27 @@ class UserController extends BaseCrudController
|
||||
$this->authorize('viewAny', $this->modelClass);
|
||||
|
||||
try {
|
||||
$request->validate([
|
||||
'join_date_from' => 'nullable|date',
|
||||
'join_date_to' => 'nullable|date',
|
||||
'leave_date_from' => 'nullable|date',
|
||||
'leave_date_to' => 'nullable|date',
|
||||
]);
|
||||
|
||||
$perPage = min((int) $request->get('per_page', 10), 500);
|
||||
$dateFilters = array_filter([
|
||||
'join_date_from' => $request->get('join_date_from'),
|
||||
'join_date_to' => $request->get('join_date_to'),
|
||||
'leave_date_from' => $request->get('leave_date_from'),
|
||||
'leave_date_to' => $request->get('leave_date_to'),
|
||||
]);
|
||||
$items = $this->userService->getPaginatedList(
|
||||
$perPage,
|
||||
$request->get('search', ''),
|
||||
$request->get('status', ''),
|
||||
$request->get('sort_by', 'id'),
|
||||
$request->get('sort_order', 'asc')
|
||||
$request->get('sort_order', 'asc'),
|
||||
$dateFilters
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -44,6 +44,7 @@ class UserRequest extends FormRequest
|
||||
'member_number' => 'required|integer',
|
||||
'member_type' => 'required|string|max:255',
|
||||
'join_date' => 'nullable|date',
|
||||
'leave_date' => 'nullable|date',
|
||||
'birth_date' => 'nullable|date',
|
||||
'birth_place' => 'nullable|string|max:255',
|
||||
];
|
||||
@@ -74,6 +75,7 @@ class UserRequest extends FormRequest
|
||||
'member_number' => 'required|integer',
|
||||
'member_type' => 'required|string|max:255',
|
||||
'join_date' => 'nullable|date',
|
||||
'leave_date' => 'nullable|date',
|
||||
'birth_date' => 'nullable|date',
|
||||
'birth_place' => 'nullable|string|max:255',
|
||||
];
|
||||
@@ -100,6 +102,7 @@ class UserRequest extends FormRequest
|
||||
'member_number' => 'required|integer',
|
||||
'member_type' => 'required|string|max:255',
|
||||
'join_date' => 'required|date',
|
||||
'leave_date' => 'nullable|date',
|
||||
'birth_date' => 'required|date',
|
||||
'birth_place' => 'required|string|max:255',
|
||||
];
|
||||
@@ -136,6 +139,7 @@ class UserRequest extends FormRequest
|
||||
'member_type.max' => 'Jenis anggota tidak boleh melebihi 255 aksara.',
|
||||
'join_date.required' => 'Tarikh join diperlukan.',
|
||||
'join_date.date' => 'Tarikh join tidak sah.',
|
||||
'leave_date.date' => 'Tarikh berhenti tidak sah.',
|
||||
'birth_date.required' => 'Tarikh lahir diperlukan.',
|
||||
'birth_date.date' => 'Tarikh lahir tidak sah.',
|
||||
'birth_place.required' => 'Tempat lahir diperlukan.',
|
||||
|
||||
@@ -20,7 +20,8 @@ interface UserRepositoryInterface
|
||||
string $search = '',
|
||||
string $status = '',
|
||||
string $sortBy = 'name',
|
||||
string $sortOrder = 'asc'
|
||||
string $sortOrder = 'asc',
|
||||
array $dateFilters = []
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,25 @@ class UserRepository implements UserRepositoryInterface
|
||||
});
|
||||
}
|
||||
|
||||
private function applyDateRangeFilters($query, array $dateFilters): void
|
||||
{
|
||||
if (! empty($dateFilters['join_date_from'])) {
|
||||
$query->whereDate('join_date', '>=', $dateFilters['join_date_from']);
|
||||
}
|
||||
|
||||
if (! empty($dateFilters['join_date_to'])) {
|
||||
$query->whereDate('join_date', '<=', $dateFilters['join_date_to']);
|
||||
}
|
||||
|
||||
if (! empty($dateFilters['leave_date_from'])) {
|
||||
$query->whereDate('leave_date', '>=', $dateFilters['leave_date_from']);
|
||||
}
|
||||
|
||||
if (! empty($dateFilters['leave_date_to'])) {
|
||||
$query->whereDate('leave_date', '<=', $dateFilters['leave_date_to']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Users with pagination and search
|
||||
*/
|
||||
@@ -46,7 +65,8 @@ class UserRepository implements UserRepositoryInterface
|
||||
string $search = '',
|
||||
string $status = '',
|
||||
string $sortBy = 'name',
|
||||
string $sortOrder = 'asc'
|
||||
string $sortOrder = 'asc',
|
||||
array $dateFilters = []
|
||||
) {
|
||||
$allowedSortColumns = [
|
||||
'id',
|
||||
@@ -55,6 +75,8 @@ class UserRepository implements UserRepositoryInterface
|
||||
'position',
|
||||
'status',
|
||||
'member_number',
|
||||
'join_date',
|
||||
'leave_date',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
];
|
||||
@@ -73,6 +95,8 @@ class UserRepository implements UserRepositoryInterface
|
||||
$query->where('status', $status);
|
||||
}
|
||||
|
||||
$this->applyDateRangeFilters($query, $dateFilters);
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
@@ -93,6 +117,8 @@ class UserRepository implements UserRepositoryInterface
|
||||
'position',
|
||||
'status',
|
||||
'member_number',
|
||||
'join_date',
|
||||
'leave_date',
|
||||
'created_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
@@ -21,14 +21,16 @@ class UserService
|
||||
string $search,
|
||||
string $status,
|
||||
string $sortBy,
|
||||
string $sortOrder
|
||||
string $sortOrder,
|
||||
array $dateFilters = []
|
||||
): LengthAwarePaginator {
|
||||
return $this->repository->getAllWithRelationsPaginated(
|
||||
$perPage,
|
||||
$search,
|
||||
$status,
|
||||
$sortBy,
|
||||
$sortOrder
|
||||
$sortOrder,
|
||||
$dateFilters
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ class UserListResource extends JsonResource
|
||||
'member_number' => $this->member_number,
|
||||
'member_type' => $this->member_type,
|
||||
'join_date' => $this->join_date,
|
||||
'leave_date' => $this->leave_date,
|
||||
'birth_date' => $this->birth_date,
|
||||
'birth_place' => $this->birth_place,
|
||||
'created_at' => $this->created_at,
|
||||
|
||||
Reference in New Issue
Block a user