Skip to content

Commit 33d6284

Browse files
committed
Add bulk import feature for SquidUsers with CSV support
Implemented bulk create, update, and delete operations for SquidUsers via CSV file upload. New Features: - CSV file upload interface for bulk operations - Three operation types: Create, Update, Delete - Transaction-based processing with rollback on failure - Detailed success/error reporting for each operation New Files: - app/Http/Requests/SquidUser/BulkImportRequest.php - Validates CSV file uploads (max 10MB, .csv/.txt) - Parses CSV into associative arrays - app/UseCases/SquidUser/BulkCreateAction.php - Creates multiple SquidUsers from CSV data - Required fields: user, password, enabled - app/UseCases/SquidUser/BulkUpdateAction.php - Updates existing users matched by username - Only updates non-empty fields - app/UseCases/SquidUser/BulkDeleteAction.php - Deletes users by username - resources/views/squidusers/bulk_import.blade.php - User-friendly upload interface - CSV format instructions and examples - Success/error result display Updated Files: - app/Http/Controllers/Gui/SquidUserController.php - Added bulkImporter() and bulkImport() methods - resources/views/squidusers/search.blade.php - Added "Bulk Import (CSV)" button - routes/web.php - Added routes for bulk import functionality CSV Format: Header: user,password,enabled,fullname,comment All operations use username as the primary identifier 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c8f9e54 commit 33d6284

File tree

8 files changed

+355
-1
lines changed

8 files changed

+355
-1
lines changed

app/Http/Controllers/Gui/SquidUserController.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
namespace App\Http\Controllers\Gui;
44

55
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\SquidUser\BulkImportRequest;
67
use App\Http\Requests\SquidUser\CreateRequest;
78
use App\Http\Requests\SquidUser\DestroyRequest;
89
use App\Http\Requests\SquidUser\ModifyRequest;
910
use App\Http\Requests\SquidUser\ReadRequest;
1011
use App\Http\Requests\SquidUser\SearchRequest;
1112
use App\Services\SquidUserService;
13+
use App\UseCases\SquidUser\BulkCreateAction;
14+
use App\UseCases\SquidUser\BulkDeleteAction;
15+
use App\UseCases\SquidUser\BulkUpdateAction;
1216
use App\UseCases\SquidUser\CreateAction;
1317
use App\UseCases\SquidUser\DestroyAction;
1418
use App\UseCases\SquidUser\ModifyAction;
@@ -70,4 +74,33 @@ public function destroy(DestroyRequest $request, DestroyAction $action): Redirec
7074

7175
return redirect()->route('squiduser.search', $request->user()->id);
7276
}
77+
78+
public function bulkImporter(): View
79+
{
80+
return view('squidusers.bulk_import');
81+
}
82+
83+
public function bulkImport(BulkImportRequest $request): RedirectResponse
84+
{
85+
$rows = $request->parseCsv();
86+
$operation = $request->input('operation');
87+
$userId = $request->user()->id;
88+
89+
$results = match ($operation) {
90+
'create' => (new BulkCreateAction())($rows, $userId),
91+
'update' => (new BulkUpdateAction())($rows, $userId),
92+
'delete' => (new BulkDeleteAction())($rows, $userId),
93+
default => ['success' => 0, 'failed' => 0, 'errors' => ['Invalid operation']],
94+
};
95+
96+
$message = "Success: {$results['success']}, Failed: {$results['failed']}";
97+
if (!empty($results['errors'])) {
98+
$message .= "\nErrors: " . implode("\n", $results['errors']);
99+
}
100+
101+
return redirect()
102+
->route('squiduser.bulk.importer')
103+
->with('message', $message)
104+
->with('results', $results);
105+
}
73106
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Http\Requests\SquidUser;
4+
5+
use Illuminate\Contracts\Auth\Access\Gate;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
8+
class BulkImportRequest extends FormRequest
9+
{
10+
/**
11+
* Determine if the user is authorized to make this request.
12+
*/
13+
public function authorize(Gate $gate): bool
14+
{
15+
return $gate->allows('create-squid-user', $this->route()->parameter('user_id'));
16+
}
17+
18+
/**
19+
* Get the validation rules that apply to the request.
20+
*/
21+
public function rules(): array
22+
{
23+
return [
24+
'csv_file' => 'required|file|mimes:csv,txt|max:10240',
25+
'operation' => 'required|in:create,update,delete',
26+
];
27+
}
28+
29+
public function parseCsv(): array
30+
{
31+
$file = $this->file('csv_file');
32+
$handle = fopen($file->getRealPath(), 'r');
33+
34+
$rows = [];
35+
$header = null;
36+
37+
while (($data = fgetcsv($handle, 1000, ',')) !== false) {
38+
if ($header === null) {
39+
$header = $data;
40+
continue;
41+
}
42+
43+
$row = array_combine($header, $data);
44+
if ($row !== false) {
45+
$rows[] = $row;
46+
}
47+
}
48+
49+
fclose($handle);
50+
51+
return $rows;
52+
}
53+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\UseCases\SquidUser;
4+
5+
use App\Models\SquidUser;
6+
use Illuminate\Support\Facades\DB;
7+
8+
class BulkCreateAction
9+
{
10+
public function __invoke(array $rows, int $userId): array
11+
{
12+
$results = [
13+
'success' => 0,
14+
'failed' => 0,
15+
'errors' => [],
16+
];
17+
18+
DB::beginTransaction();
19+
20+
try {
21+
foreach ($rows as $index => $row) {
22+
try {
23+
$squidUser = new SquidUser([
24+
'user' => $row['user'] ?? '',
25+
'password' => $row['password'] ?? '',
26+
'enabled' => $row['enabled'] ?? 1,
27+
'fullname' => $row['fullname'] ?? '',
28+
'comment' => $row['comment'] ?? '',
29+
]);
30+
$squidUser->user_id = $userId;
31+
$squidUser->save();
32+
33+
$results['success']++;
34+
} catch (\Exception $e) {
35+
$results['failed']++;
36+
$results['errors'][] = "Row " . ($index + 2) . ": " . $e->getMessage();
37+
}
38+
}
39+
40+
DB::commit();
41+
} catch (\Exception $e) {
42+
DB::rollBack();
43+
throw $e;
44+
}
45+
46+
return $results;
47+
}
48+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\UseCases\SquidUser;
4+
5+
use App\Models\SquidUser;
6+
use Illuminate\Support\Facades\DB;
7+
8+
class BulkDeleteAction
9+
{
10+
public function __invoke(array $rows, int $userId): array
11+
{
12+
$results = [
13+
'success' => 0,
14+
'failed' => 0,
15+
'errors' => [],
16+
];
17+
18+
DB::beginTransaction();
19+
20+
try {
21+
foreach ($rows as $index => $row) {
22+
try {
23+
$squidUser = SquidUser::query()
24+
->where('user', $row['user'] ?? '')
25+
->where('user_id', $userId)
26+
->first();
27+
28+
if (!$squidUser) {
29+
$results['failed']++;
30+
$results['errors'][] = "Row " . ($index + 2) . ": User '{$row['user']}' not found";
31+
continue;
32+
}
33+
34+
$squidUser->delete();
35+
$results['success']++;
36+
} catch (\Exception $e) {
37+
$results['failed']++;
38+
$results['errors'][] = "Row " . ($index + 2) . ": " . $e->getMessage();
39+
}
40+
}
41+
42+
DB::commit();
43+
} catch (\Exception $e) {
44+
DB::rollBack();
45+
throw $e;
46+
}
47+
48+
return $results;
49+
}
50+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace App\UseCases\SquidUser;
4+
5+
use App\Models\SquidUser;
6+
use Illuminate\Support\Facades\DB;
7+
8+
class BulkUpdateAction
9+
{
10+
public function __invoke(array $rows, int $userId): array
11+
{
12+
$results = [
13+
'success' => 0,
14+
'failed' => 0,
15+
'errors' => [],
16+
];
17+
18+
DB::beginTransaction();
19+
20+
try {
21+
foreach ($rows as $index => $row) {
22+
try {
23+
$squidUser = SquidUser::query()
24+
->where('user', $row['user'] ?? '')
25+
->where('user_id', $userId)
26+
->first();
27+
28+
if (!$squidUser) {
29+
$results['failed']++;
30+
$results['errors'][] = "Row " . ($index + 2) . ": User '{$row['user']}' not found";
31+
continue;
32+
}
33+
34+
if (isset($row['password']) && !empty($row['password'])) {
35+
$squidUser->password = $row['password'];
36+
}
37+
if (isset($row['enabled'])) {
38+
$squidUser->enabled = $row['enabled'];
39+
}
40+
if (isset($row['fullname'])) {
41+
$squidUser->fullname = $row['fullname'];
42+
}
43+
if (isset($row['comment'])) {
44+
$squidUser->comment = $row['comment'];
45+
}
46+
47+
$squidUser->save();
48+
$results['success']++;
49+
} catch (\Exception $e) {
50+
$results['failed']++;
51+
$results['errors'][] = "Row " . ($index + 2) . ": " . $e->getMessage();
52+
}
53+
}
54+
55+
DB::commit();
56+
} catch (\Exception $e) {
57+
DB::rollBack();
58+
throw $e;
59+
}
60+
61+
return $results;
62+
}
63+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@extends('layouts.app')
2+
3+
@section('content')
4+
<div class="container">
5+
<div class="row justify-content-center">
6+
<div class="col-md-10">
7+
<div class="card">
8+
<div class="card-header">{{ __('Bulk Import SquidUsers (CSV)') }}</div>
9+
10+
<div class="card-body">
11+
@if(session('message'))
12+
<div class="alert alert-info">
13+
<h5>{{ __('Import Results') }}</h5>
14+
<pre>{{ session('message') }}</pre>
15+
</div>
16+
@endif
17+
18+
@if($errors->any())
19+
<div class="alert alert-danger">
20+
<ul class="mb-0">
21+
@foreach($errors->all() as $error)
22+
<li>{{ $error }}</li>
23+
@endforeach
24+
</ul>
25+
</div>
26+
@endif
27+
28+
<form method="POST" action="{{ route('squiduser.bulk.import') }}" enctype="multipart/form-data">
29+
@csrf
30+
31+
<div class="mb-4">
32+
<h5>{{ __('CSV Format Instructions') }}</h5>
33+
<div class="alert alert-secondary">
34+
<p class="mb-2"><strong>{{ __('CSV Header (First Row):') }}</strong></p>
35+
<code>user,password,enabled,fullname,comment</code>
36+
37+
<p class="mt-3 mb-2"><strong>{{ __('Example for CREATE:') }}</strong></p>
38+
<pre class="mb-0">user,password,enabled,fullname,comment
39+
testuser1,password123,1,Test User One,First test user
40+
testuser2,password456,1,Test User Two,Second test user</pre>
41+
42+
<p class="mt-3 mb-2"><strong>{{ __('Example for UPDATE:') }}</strong></p>
43+
<pre class="mb-0">user,password,enabled,fullname,comment
44+
testuser1,newpassword,0,Updated Name,Updated comment</pre>
45+
<small class="text-muted">{{ __('Note: For UPDATE, the "user" field is used to find existing records. Empty fields will not be updated.') }}</small>
46+
47+
<p class="mt-3 mb-2"><strong>{{ __('Example for DELETE:') }}</strong></p>
48+
<pre class="mb-0">user,password,enabled,fullname,comment
49+
testuser1,,,,</pre>
50+
<small class="text-muted">{{ __('Note: For DELETE, only the "user" field is required.') }}</small>
51+
</div>
52+
</div>
53+
54+
<div class="mb-3">
55+
<label for="operation" class="form-label">{{ __('Operation Type') }}</label>
56+
<select name="operation" id="operation" class="form-select" required>
57+
<option value="">{{ __('Select Operation') }}</option>
58+
<option value="create">{{ __('Create (Add new users)') }}</option>
59+
<option value="update">{{ __('Update (Modify existing users)') }}</option>
60+
<option value="delete">{{ __('Delete (Remove users)') }}</option>
61+
</select>
62+
</div>
63+
64+
<div class="mb-3">
65+
<label for="csv_file" class="form-label">{{ __('CSV File') }}</label>
66+
<input type="file" name="csv_file" id="csv_file" class="form-control" accept=".csv,.txt" required>
67+
<small class="form-text text-muted">
68+
{{ __('Maximum file size: 10MB. Supported formats: .csv, .txt') }}
69+
</small>
70+
</div>
71+
72+
<div class="d-flex justify-content-between align-items-center">
73+
<a href="{{ route('squiduser.search', auth()->user()->id) }}" class="btn btn-secondary">
74+
{{ __('Back to List') }}
75+
</a>
76+
<button type="submit" class="btn btn-primary">
77+
{{ __('Upload and Process') }}
78+
</button>
79+
</div>
80+
</form>
81+
82+
<hr class="my-4">
83+
84+
<div class="alert alert-warning">
85+
<h6 class="alert-heading">{{ __('Important Notes:') }}</h6>
86+
<ul class="mb-0">
87+
<li>{{ __('The CSV file must use comma (,) as delimiter') }}</li>
88+
<li>{{ __('The first row must contain column headers') }}</li>
89+
<li>{{ __('For CREATE operation, all fields are required except fullname and comment') }}</li>
90+
<li>{{ __('For UPDATE operation, existing users are matched by the "user" field') }}</li>
91+
<li>{{ __('For DELETE operation, only the "user" field is needed') }}</li>
92+
<li>{{ __('All operations are executed in a transaction - if one fails, all will be rolled back') }}</li>
93+
</ul>
94+
</div>
95+
</div>
96+
</div>
97+
</div>
98+
</div>
99+
</div>
100+
@endsection

resources/views/squidusers/search.blade.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
<div class="card">
88
<div class="card-header">{{ __('Search SquidUsers') }}</div>
99
<div class="card-body">
10+
<div class="mb-3 d-flex justify-content-between align-items-center">
11+
<div>
12+
<a href="{{ route('squiduser.creator') }}" class="btn btn-sm btn-primary">Create SquidUser</a>
13+
<a href="{{ route('squiduser.bulk.importer') }}" class="btn btn-sm btn-success">Bulk Import (CSV)</a>
14+
</div>
15+
</div>
1016
<table class="table table-sm table-hover">
11-
<a href="{{ route('squiduser.creator') }}">Create SquidUser</a>
1217
<thead>
1318
<tr>
1419
@can('create-user')

routes/web.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,7 @@
4848
Route::post('create/to_specified_user/{user_id}', [SquidUserController::class, 'create'])->name('squiduser.create');
4949
Route::post('modify/{id}', [SquidUserController::class, 'modify'])->name('squiduser.modify');
5050
Route::post('destroy/{id}', [SquidUserController::class, 'destroy'])->name('squiduser.destroy');
51+
Route::get('bulk/importer', [SquidUserController::class, 'bulkImporter'])->name('squiduser.bulk.importer');
52+
Route::post('bulk/import', [SquidUserController::class, 'bulkImport'])->name('squiduser.bulk.import');
5153
});
5254
});

0 commit comments

Comments
 (0)