Initial commit
This commit is contained in:
88
app/Console/Commands/ArchiveExpiredDocuments.php
Normal file
88
app/Console/Commands/ArchiveExpiredDocuments.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ArchiveExpiredDocuments extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'documents:archive-expired
|
||||
{--dry-run : Preview which documents would be archived}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Automatically archive documents that have passed their expiration date';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
// Check if auto-archive is enabled in system settings
|
||||
$settings = app(\App\Services\SettingsService::class);
|
||||
if (!$settings->isAutoArchiveEnabled()) {
|
||||
$this->info('Auto-archive is disabled in system settings.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find expired documents that should be auto-archived
|
||||
$expiredDocuments = Document::where('status', 'active')
|
||||
->where('auto_archive_on_expiry', true)
|
||||
->whereNotNull('expires_at')
|
||||
->whereDate('expires_at', '<', now())
|
||||
->get();
|
||||
|
||||
if ($expiredDocuments->isEmpty()) {
|
||||
$this->info('No expired documents found.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Found {$expiredDocuments->count()} expired document(s)");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY RUN - No changes will be made');
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
foreach ($expiredDocuments as $document) {
|
||||
$this->line("- {$document->title} (expired: {$document->expires_at->format('Y-m-d')})");
|
||||
|
||||
if (!$dryRun) {
|
||||
$document->archive();
|
||||
|
||||
AuditLog::create([
|
||||
'user_id' => null,
|
||||
'action' => 'document.auto_archived',
|
||||
'auditable_type' => Document::class,
|
||||
'auditable_id' => $document->id,
|
||||
'old_values' => ['status' => 'active'],
|
||||
'new_values' => ['status' => 'archived'],
|
||||
'description' => "Document auto-archived due to expiration on {$document->expires_at->format('Y-m-d')}",
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'CLI Auto-Archive',
|
||||
]);
|
||||
|
||||
$this->info(" ✓ Archived");
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
if (!$dryRun) {
|
||||
$this->info("Successfully archived {$expiredDocuments->count()} document(s)");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
37
app/Console/Commands/AssignRole.php
Normal file
37
app/Console/Commands/AssignRole.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class AssignRole extends Command
|
||||
{
|
||||
protected $signature = 'roles:assign {email} {role}';
|
||||
|
||||
protected $description = 'Assign a role to a user by email';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$email = $this->argument('email');
|
||||
$roleName = $this->argument('role');
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User not found for email {$email}");
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => 'web']);
|
||||
|
||||
$user->assignRole($role);
|
||||
|
||||
$this->info("Assigned role {$roleName} to {$email}");
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
189
app/Console/Commands/ImportDocuments.php
Normal file
189
app/Console/Commands/ImportDocuments.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Document;
|
||||
use App\Models\DocumentCategory;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportDocuments extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'documents:import
|
||||
{path : Path to directory containing documents and manifest.json}
|
||||
{--user-id=1 : User ID to attribute uploads to}
|
||||
{--dry-run : Preview import without making changes}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Bulk import documents from a directory with manifest.json';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
$userId = $this->option('user-id');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
// Validate path
|
||||
if (!File::isDirectory($path)) {
|
||||
$this->error("Directory not found: {$path}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check for manifest.json
|
||||
$manifestPath = $path . '/manifest.json';
|
||||
if (!File::exists($manifestPath)) {
|
||||
$this->error("manifest.json not found in {$path}");
|
||||
$this->info("Expected format:");
|
||||
$this->line($this->getManifestExample());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load manifest
|
||||
$manifest = json_decode(File::get($manifestPath), true);
|
||||
if (!$manifest || !isset($manifest['documents'])) {
|
||||
$this->error("Invalid manifest.json format");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate user
|
||||
$user = User::find($userId);
|
||||
if (!$user) {
|
||||
$this->error("User not found: {$userId}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info("Importing documents from: {$path}");
|
||||
$this->info("Attributed to: {$user->name}");
|
||||
if ($dryRun) {
|
||||
$this->warn("DRY RUN - No changes will be made");
|
||||
}
|
||||
$this->newLine();
|
||||
|
||||
$successCount = 0;
|
||||
$errorCount = 0;
|
||||
|
||||
foreach ($manifest['documents'] as $item) {
|
||||
try {
|
||||
$this->processDocument($path, $item, $user, $dryRun);
|
||||
$successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Error processing {$item['file']}: {$e->getMessage()}");
|
||||
$errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Import complete!");
|
||||
$this->info("Success: {$successCount}");
|
||||
if ($errorCount > 0) {
|
||||
$this->error("Errors: {$errorCount}");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function processDocument(string $basePath, array $item, User $user, bool $dryRun): void
|
||||
{
|
||||
$filePath = $basePath . '/' . $item['file'];
|
||||
|
||||
// Validate file exists
|
||||
if (!File::exists($filePath)) {
|
||||
throw new \Exception("File not found: {$filePath}");
|
||||
}
|
||||
|
||||
// Find or create category
|
||||
$category = DocumentCategory::where('slug', $item['category'])->first();
|
||||
if (!$category) {
|
||||
throw new \Exception("Category not found: {$item['category']}");
|
||||
}
|
||||
|
||||
$this->line("Processing: {$item['title']}");
|
||||
$this->line(" Category: {$category->name}");
|
||||
$this->line(" File: {$item['file']}");
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(" [DRY RUN] Would create document");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy file to storage
|
||||
$fileInfo = pathinfo($filePath);
|
||||
$storagePath = 'documents/' . uniqid() . '.' . $fileInfo['extension'];
|
||||
Storage::disk('private')->put($storagePath, File::get($filePath));
|
||||
|
||||
// Create document
|
||||
$document = Document::create([
|
||||
'document_category_id' => $category->id,
|
||||
'title' => $item['title'],
|
||||
'document_number' => $item['document_number'] ?? null,
|
||||
'description' => $item['description'] ?? null,
|
||||
'access_level' => $item['access_level'] ?? $category->default_access_level,
|
||||
'status' => 'active',
|
||||
'created_by' => $user->id,
|
||||
'updated_by' => $user->id,
|
||||
]);
|
||||
|
||||
// Add first version
|
||||
$document->addVersion(
|
||||
filePath: $storagePath,
|
||||
originalFilename: $fileInfo['basename'],
|
||||
mimeType: File::mimeType($filePath),
|
||||
fileSize: File::size($filePath),
|
||||
uploadedBy: $user,
|
||||
versionNotes: $item['version_notes'] ?? 'Initial import'
|
||||
);
|
||||
|
||||
AuditLog::create([
|
||||
'user_id' => $user->id,
|
||||
'action' => 'document.imported',
|
||||
'auditable_type' => Document::class,
|
||||
'auditable_id' => $document->id,
|
||||
'old_values' => null,
|
||||
'new_values' => ['title' => $item['title']],
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'CLI Import',
|
||||
]);
|
||||
|
||||
$this->info(" ✓ Created document ID: {$document->id}");
|
||||
}
|
||||
|
||||
protected function getManifestExample(): string
|
||||
{
|
||||
return <<<'JSON'
|
||||
{
|
||||
"documents": [
|
||||
{
|
||||
"file": "bylaws.pdf",
|
||||
"title": "協會章程",
|
||||
"category": "association-bylaws",
|
||||
"document_number": "2024-001",
|
||||
"description": "協會章程修正版",
|
||||
"access_level": "members",
|
||||
"version_notes": "Initial import"
|
||||
},
|
||||
{
|
||||
"file": "meeting-2024-01.pdf",
|
||||
"title": "2024年1月會議記錄",
|
||||
"category": "meeting-minutes",
|
||||
"access_level": "members"
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON;
|
||||
}
|
||||
}
|
||||
146
app/Console/Commands/ImportMembers.php
Normal file
146
app/Console/Commands/ImportMembers.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\MemberActivationMail;
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use App\Support\AuditLogger;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImportMembers extends Command
|
||||
{
|
||||
protected $signature = 'members:import {path : CSV file path}';
|
||||
|
||||
protected $description = 'Import members from a CSV file';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
|
||||
if (! is_file($path)) {
|
||||
$this->error("File not found: {$path}");
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'r');
|
||||
|
||||
if (! $handle) {
|
||||
$this->error("Unable to open file: {$path}");
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$header = fgetcsv($handle);
|
||||
|
||||
if (! $header) {
|
||||
$this->error('CSV file is empty.');
|
||||
fclose($handle);
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
|
||||
$expected = [
|
||||
'full_name',
|
||||
'email',
|
||||
'phone',
|
||||
'address_line_1',
|
||||
'address_line_2',
|
||||
'city',
|
||||
'postal_code',
|
||||
'emergency_contact_name',
|
||||
'emergency_contact_phone',
|
||||
'membership_started_at',
|
||||
'membership_expires_at',
|
||||
];
|
||||
|
||||
foreach ($expected as $column) {
|
||||
if (! in_array($column, $header, true)) {
|
||||
$this->error("Missing required column: {$column}");
|
||||
fclose($handle);
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$indexes = array_flip($header);
|
||||
|
||||
$createdUsers = 0;
|
||||
$updatedMembers = 0;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
$email = trim($row[$indexes['email']] ?? '');
|
||||
|
||||
if ($email === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullName = trim($row[$indexes['full_name']] ?? '');
|
||||
$nationalId = trim($row[$indexes['national_id']] ?? '');
|
||||
$phone = trim($row[$indexes['phone']] ?? '');
|
||||
$started = trim($row[$indexes['membership_started_at']] ?? '');
|
||||
$expires = trim($row[$indexes['membership_expires_at']] ?? '');
|
||||
$address1 = trim($row[$indexes['address_line_1']] ?? '');
|
||||
$address2 = trim($row[$indexes['address_line_2']] ?? '');
|
||||
$city = trim($row[$indexes['city']] ?? '');
|
||||
$postal = trim($row[$indexes['postal_code']] ?? '');
|
||||
$emergencyName = trim($row[$indexes['emergency_contact_name']] ?? '');
|
||||
$emergencyPhone = trim($row[$indexes['emergency_contact_phone']] ?? '');
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
$isNewUser = false;
|
||||
|
||||
if (! $user) {
|
||||
$user = User::create([
|
||||
'name' => $fullName !== '' ? $fullName : $email,
|
||||
'email' => $email,
|
||||
'password' => Str::random(32),
|
||||
]);
|
||||
$isNewUser = true;
|
||||
$createdUsers++;
|
||||
}
|
||||
|
||||
$member = Member::updateOrCreate(
|
||||
['user_id' => $user->id],
|
||||
[
|
||||
'full_name' => $fullName !== '' ? $fullName : $user->name,
|
||||
'email' => $email,
|
||||
'national_id' => $nationalId !== '' ? $nationalId : null,
|
||||
'phone' => $phone !== '' ? $phone : null,
|
||||
'address_line_1' => $address1 ?: null,
|
||||
'address_line_2' => $address2 ?: null,
|
||||
'city' => $city ?: null,
|
||||
'postal_code' => $postal ?: null,
|
||||
'emergency_contact_name' => $emergencyName ?: null,
|
||||
'emergency_contact_phone' => $emergencyPhone ?: null,
|
||||
'membership_started_at' => $started !== '' ? $started : null,
|
||||
'membership_expires_at' => $expires !== '' ? $expires : null,
|
||||
],
|
||||
);
|
||||
|
||||
$updatedMembers++;
|
||||
|
||||
if ($isNewUser) {
|
||||
$token = Password::createToken($user);
|
||||
|
||||
Mail::to($user)->queue(new MemberActivationMail($user, $token));
|
||||
AuditLogger::log('user.activation_link_sent', $user, [
|
||||
'email' => $user->email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$this->info("Users created: {$createdUsers}");
|
||||
$this->info("Members imported/updated: {$updatedMembers}");
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
49
app/Console/Commands/SendMembershipExpiryReminders.php
Normal file
49
app/Console/Commands/SendMembershipExpiryReminders.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Mail\MembershipExpiryReminderMail;
|
||||
use App\Models\Member;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class SendMembershipExpiryReminders extends Command
|
||||
{
|
||||
protected $signature = 'members:send-expiry-reminders {--days=30 : Number of days before expiry to send reminders}';
|
||||
|
||||
protected $description = 'Send membership expiry reminder emails';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$days = (int) $this->option('days');
|
||||
$targetDate = now()->addDays($days)->toDateString();
|
||||
|
||||
$members = Member::whereDate('membership_expires_at', $targetDate)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('last_expiry_reminder_sent_at')
|
||||
->orWhere('last_expiry_reminder_sent_at', '<', now()->subDays(1));
|
||||
})
|
||||
->get();
|
||||
|
||||
if ($members->isEmpty()) {
|
||||
$this->info('No members to remind.');
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ($members as $member) {
|
||||
if (! $member->email) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Mail::to($member->email)->queue(new MembershipExpiryReminderMail($member));
|
||||
$member->last_expiry_reminder_sent_at = now();
|
||||
$member->save();
|
||||
}
|
||||
|
||||
$this->info('Reminders sent to '.$members->count().' member(s).');
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
27
app/Console/Kernel.php
Normal file
27
app/Console/Kernel.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
/**
|
||||
* Define the application's command schedule.
|
||||
*/
|
||||
protected function schedule(Schedule $schedule): void
|
||||
{
|
||||
$schedule->command('members:send-expiry-reminders --days=30')->daily();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the commands for the application.
|
||||
*/
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user