complete story 2.4 with qa test and added a feature disallowing admin from deactivating his own account
This commit is contained in:
@@ -28,7 +28,7 @@ new class extends Component {
|
||||
{{ __('clients.back_to_companies') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button variant="ghost" class="border border-amber-500 text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-900/20" x-data x-on:click="$flux.modal('convert-to-individual').show()" icon="user">
|
||||
{{ __('clients.convert_to_individual') }}
|
||||
</flux:button>
|
||||
@@ -182,5 +182,11 @@ new class extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lifecycle Actions Section --}}
|
||||
<div class="mt-6 rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('clients.account_actions') }}</flux:heading>
|
||||
<livewire:admin.clients.lifecycle-actions :client="$client" />
|
||||
</div>
|
||||
|
||||
<livewire:admin.clients.convert-to-individual-modal :client="$client" />
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ new class extends Component {
|
||||
{{ __('clients.back_to_clients') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<flux:button variant="ghost" class="border border-amber-500 text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-900/20" x-data x-on:click="$flux.modal('convert-to-company').show()" icon="building-office">
|
||||
{{ __('clients.convert_to_company') }}
|
||||
</flux:button>
|
||||
@@ -174,5 +174,11 @@ new class extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lifecycle Actions Section --}}
|
||||
<div class="mt-6 rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-700 dark:bg-zinc-800">
|
||||
<flux:heading size="lg" class="mb-4">{{ __('clients.account_actions') }}</flux:heading>
|
||||
<livewire:admin.clients.lifecycle-actions :client="$client" />
|
||||
</div>
|
||||
|
||||
<livewire:admin.clients.convert-to-company-modal :client="$client" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\User;
|
||||
use App\Notifications\AccountReactivatedNotification;
|
||||
use App\Notifications\PasswordResetByAdminNotification;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public User $client;
|
||||
|
||||
public string $deleteConfirmation = '';
|
||||
public bool $showDeleteModal = false;
|
||||
public bool $showDeactivateModal = false;
|
||||
public bool $showReactivateModal = false;
|
||||
public bool $showPasswordResetModal = false;
|
||||
|
||||
public function mount(User $client): void
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
public function openDeactivateModal(): void
|
||||
{
|
||||
$this->showDeactivateModal = true;
|
||||
}
|
||||
|
||||
public function closeDeactivateModal(): void
|
||||
{
|
||||
$this->showDeactivateModal = false;
|
||||
}
|
||||
|
||||
public function deactivate(): void
|
||||
{
|
||||
// Prevent admin from deactivating their own account
|
||||
if ($this->client->id === auth()->id()) {
|
||||
$this->showDeactivateModal = false;
|
||||
session()->flash('error', __('clients.cannot_deactivate_self'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$oldStatus = $this->client->status->value;
|
||||
|
||||
DB::transaction(function () use ($oldStatus) {
|
||||
$this->client->deactivate();
|
||||
|
||||
// Invalidate all active sessions for this user
|
||||
DB::table('sessions')
|
||||
->where('user_id', $this->client->id)
|
||||
->delete();
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'deactivate',
|
||||
'target_type' => 'user',
|
||||
'target_id' => $this->client->id,
|
||||
'old_values' => ['status' => $oldStatus],
|
||||
'new_values' => ['status' => 'deactivated'],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->showDeactivateModal = false;
|
||||
$this->client->refresh();
|
||||
session()->flash('success', __('clients.user_deactivated'));
|
||||
}
|
||||
|
||||
public function openReactivateModal(): void
|
||||
{
|
||||
$this->showReactivateModal = true;
|
||||
}
|
||||
|
||||
public function closeReactivateModal(): void
|
||||
{
|
||||
$this->showReactivateModal = false;
|
||||
}
|
||||
|
||||
public function reactivate(): void
|
||||
{
|
||||
$oldStatus = $this->client->status->value;
|
||||
|
||||
DB::transaction(function () use ($oldStatus) {
|
||||
$this->client->reactivate();
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'reactivate',
|
||||
'target_type' => 'user',
|
||||
'target_id' => $this->client->id,
|
||||
'old_values' => ['status' => $oldStatus],
|
||||
'new_values' => ['status' => 'active'],
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->client->notify(new AccountReactivatedNotification);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
});
|
||||
|
||||
$this->showReactivateModal = false;
|
||||
$this->client->refresh();
|
||||
session()->flash('success', __('clients.user_reactivated'));
|
||||
}
|
||||
|
||||
public function openDeleteModal(): void
|
||||
{
|
||||
$this->deleteConfirmation = '';
|
||||
$this->showDeleteModal = true;
|
||||
}
|
||||
|
||||
public function closeDeleteModal(): void
|
||||
{
|
||||
$this->showDeleteModal = false;
|
||||
$this->deleteConfirmation = '';
|
||||
}
|
||||
|
||||
public function delete(): void
|
||||
{
|
||||
// Prevent admin from deleting their own account
|
||||
if ($this->client->id === auth()->id()) {
|
||||
$this->addError('deleteConfirmation', __('clients.cannot_delete_self'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->deleteConfirmation !== $this->client->email) {
|
||||
$this->addError('deleteConfirmation', __('clients.email_confirmation_mismatch'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DB::transaction(function () {
|
||||
// Log before deletion (so we have the record before cascade delete)
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'delete',
|
||||
'target_type' => 'user',
|
||||
'target_id' => $this->client->id,
|
||||
'old_values' => $this->client->only([
|
||||
'id', 'user_type', 'full_name', 'email',
|
||||
'national_id', 'company_name', 'company_cert_number',
|
||||
'contact_person_name', 'contact_person_id', 'phone', 'status',
|
||||
]),
|
||||
'new_values' => null,
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$this->client->delete();
|
||||
});
|
||||
|
||||
session()->flash('success', __('clients.user_deleted'));
|
||||
|
||||
// Redirect to appropriate index based on user type
|
||||
$redirectRoute = $this->client->isIndividual()
|
||||
? route('admin.clients.individual.index')
|
||||
: route('admin.clients.company.index');
|
||||
|
||||
$this->redirect($redirectRoute, navigate: true);
|
||||
}
|
||||
|
||||
public function openPasswordResetModal(): void
|
||||
{
|
||||
$this->showPasswordResetModal = true;
|
||||
}
|
||||
|
||||
public function closePasswordResetModal(): void
|
||||
{
|
||||
$this->showPasswordResetModal = false;
|
||||
}
|
||||
|
||||
public function resetPassword(): void
|
||||
{
|
||||
$newPassword = Str::random(12);
|
||||
|
||||
DB::transaction(function () use ($newPassword) {
|
||||
$this->client->update([
|
||||
'password' => Hash::make($newPassword),
|
||||
]);
|
||||
|
||||
AdminLog::create([
|
||||
'admin_id' => auth()->id(),
|
||||
'action' => 'password_reset',
|
||||
'target_type' => 'user',
|
||||
'target_id' => $this->client->id,
|
||||
'old_values' => null,
|
||||
'new_values' => null,
|
||||
'ip_address' => request()->ip(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->client->notify(new PasswordResetByAdminNotification($newPassword));
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
});
|
||||
|
||||
$this->showPasswordResetModal = false;
|
||||
session()->flash('success', __('clients.password_reset_sent'));
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
{{-- Action Buttons --}}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{-- Password Reset Button --}}
|
||||
<flux:button variant="ghost" icon="key" wire:click="openPasswordResetModal">
|
||||
{{ __('clients.reset_password') }}
|
||||
</flux:button>
|
||||
|
||||
{{-- Deactivate/Reactivate Button --}}
|
||||
@if ($client->isActive())
|
||||
<flux:button variant="ghost" class="text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-900/20" icon="pause-circle" wire:click="openDeactivateModal">
|
||||
{{ __('clients.deactivate') }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button variant="ghost" class="text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20" icon="play-circle" wire:click="openReactivateModal">
|
||||
{{ __('clients.reactivate') }}
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
{{-- Delete Button --}}
|
||||
<flux:button variant="danger" icon="trash" wire:click="openDeleteModal">
|
||||
{{ __('clients.delete') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Deactivate Confirmation Modal --}}
|
||||
<flux:modal wire:model="showDeactivateModal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('clients.confirm_deactivate') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:callout variant="warning" icon="exclamation-triangle">
|
||||
<flux:callout.text>{{ __('clients.deactivate_warning') }}</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p class="mb-2">{{ __('clients.deactivate_effects') }}:</p>
|
||||
<ul class="list-disc ps-5 space-y-1">
|
||||
<li>{{ __('clients.deactivate_effect_login') }}</li>
|
||||
<li>{{ __('clients.deactivate_effect_sessions') }}</li>
|
||||
<li>{{ __('clients.deactivate_effect_data') }}</li>
|
||||
<li>{{ __('clients.deactivate_effect_reversible') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<flux:button type="button" variant="ghost" wire:click="closeDeactivateModal">
|
||||
{{ __('clients.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" variant="primary" class="bg-amber-600 hover:bg-amber-700" wire:click="deactivate">
|
||||
{{ __('clients.confirm_deactivate_action') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Reactivate Confirmation Modal --}}
|
||||
<flux:modal wire:model="showReactivateModal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('clients.confirm_reactivate') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:callout variant="success" icon="check-circle">
|
||||
<flux:callout.text>{{ __('clients.reactivate_message') }}</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p class="mb-2">{{ __('clients.reactivate_effects') }}:</p>
|
||||
<ul class="list-disc ps-5 space-y-1">
|
||||
<li>{{ __('clients.reactivate_effect_login') }}</li>
|
||||
<li>{{ __('clients.reactivate_effect_data') }}</li>
|
||||
<li>{{ __('clients.reactivate_effect_email') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<flux:button type="button" variant="ghost" wire:click="closeReactivateModal">
|
||||
{{ __('clients.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" variant="primary" wire:click="reactivate">
|
||||
{{ __('clients.confirm_reactivate_action') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Delete Confirmation Modal --}}
|
||||
<flux:modal wire:model="showDeleteModal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('clients.confirm_delete') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:callout variant="danger" icon="exclamation-triangle">
|
||||
<flux:callout.heading>{{ __('clients.delete_warning_title') }}</flux:callout.heading>
|
||||
<flux:callout.text>{{ __('clients.delete_warning') }}</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p class="mb-2">{{ __('clients.will_be_deleted') }}:</p>
|
||||
<ul class="list-disc ps-5 space-y-1">
|
||||
<li>{{ __('clients.delete_item_consultations') }}</li>
|
||||
<li>{{ __('clients.delete_item_timelines') }}</li>
|
||||
<li>{{ __('clients.delete_item_notifications') }}</li>
|
||||
<li>{{ __('clients.delete_item_account') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>{{ __('clients.type_email_to_confirm', ['email' => $client->email]) }}</flux:label>
|
||||
<flux:input wire:model="deleteConfirmation" placeholder="{{ $client->email }}" />
|
||||
<flux:error name="deleteConfirmation" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<flux:button type="button" variant="ghost" wire:click="closeDeleteModal">
|
||||
{{ __('clients.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" variant="danger" wire:click="delete">
|
||||
{{ __('clients.delete_permanently') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Password Reset Modal --}}
|
||||
<flux:modal wire:model="showPasswordResetModal" class="md:w-96">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<flux:heading size="lg">{{ __('clients.reset_password_title') }}</flux:heading>
|
||||
</div>
|
||||
|
||||
<flux:callout variant="info" icon="information-circle">
|
||||
<flux:callout.text>{{ __('clients.reset_password_message') }}</flux:callout.text>
|
||||
</flux:callout>
|
||||
|
||||
<div class="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<p class="mb-2">{{ __('clients.reset_password_effects') }}:</p>
|
||||
<ul class="list-disc ps-5 space-y-1">
|
||||
<li>{{ __('clients.reset_password_effect_generate') }}</li>
|
||||
<li>{{ __('clients.reset_password_effect_email') }}</li>
|
||||
<li>{{ __('clients.reset_password_effect_old') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 pt-4">
|
||||
<flux:button type="button" variant="ghost" wire:click="closePasswordResetModal">
|
||||
{{ __('clients.cancel') }}
|
||||
</flux:button>
|
||||
<flux:button type="button" variant="primary" wire:click="resetPassword">
|
||||
{{ __('clients.confirm_reset_password') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
Reference in New Issue
Block a user