Inicjazlizacja zaliczek
This commit is contained in:
parent
97adda56d3
commit
2f9780a9b5
105
app/Http/Controllers/AdvanceController.php
Normal file
105
app/Http/Controllers/AdvanceController.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreAdvanceRequest;
|
||||||
|
use App\Models\Advance;
|
||||||
|
use App\Models\AdvanceType;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class AdvanceController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Advance::with(['user', 'type']);
|
||||||
|
|
||||||
|
if ($request->user()->hasRole('pracownik')) {
|
||||||
|
$query->where('user_id', $request->user()->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = trim((string) $request->query('search', ''));
|
||||||
|
if ($search !== '') {
|
||||||
|
$query->where(function ($query) use ($search) {
|
||||||
|
$query->whereHas('user', function ($userQuery) use ($search) {
|
||||||
|
$userQuery->where('name', 'like', "%{$search}%")
|
||||||
|
->orWhere('surname', 'like', "%{$search}%")
|
||||||
|
->orWhere('email', 'like', "%{$search}%");
|
||||||
|
})->orWhereHas('type', function ($typeQuery) use ($search) {
|
||||||
|
$typeQuery->where('name', 'like', "%{$search}%");
|
||||||
|
})->orWhere('amount', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('type')) {
|
||||||
|
$query->where('advance_type_id', $request->integer('type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('date_from')) {
|
||||||
|
$query->whereDate('date', '>=', $request->query('date_from'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('date_to')) {
|
||||||
|
$query->whereDate('date', '<=', $request->query('date_to'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$advances = $query->orderByDesc('date')->orderByDesc('id')->paginate(10)->withQueryString();
|
||||||
|
$types = AdvanceType::orderBy('name')->get();
|
||||||
|
|
||||||
|
return view('zaliczki', compact('advances', 'types'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$types = AdvanceType::orderBy('name')->get();
|
||||||
|
$users = request()->user()->hasRole('pracownik')
|
||||||
|
? collect()
|
||||||
|
: User::orderBy('name')->orderBy('surname')->get();
|
||||||
|
|
||||||
|
return view('zaliczki.create', compact('types', 'users'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreAdvanceRequest $request)
|
||||||
|
{
|
||||||
|
Advance::create([
|
||||||
|
'date' => $request->validated('date'),
|
||||||
|
'amount' => $request->validated('amount'),
|
||||||
|
'advance_type_id' => $request->validated('advance_type_id'),
|
||||||
|
'user_id' => $request->validated('user_id'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('zaliczki')->with('status', 'Zaliczka została dodana.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Request $request, Advance $advance)
|
||||||
|
{
|
||||||
|
$this->authorizeAccess($request, $advance);
|
||||||
|
$types = AdvanceType::orderBy('name')->get();
|
||||||
|
$users = $request->user()->hasRole('pracownik')
|
||||||
|
? collect()
|
||||||
|
: User::orderBy('name')->orderBy('surname')->get();
|
||||||
|
|
||||||
|
return view('zaliczki.edit', compact('advance', 'types', 'users'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(StoreAdvanceRequest $request, Advance $advance)
|
||||||
|
{
|
||||||
|
$this->authorizeAccess($request, $advance);
|
||||||
|
$advance->update($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('zaliczki')->with('status', 'Zaliczka została zaktualizowana.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, Advance $advance)
|
||||||
|
{
|
||||||
|
$this->authorizeAccess($request, $advance);
|
||||||
|
$advance->delete();
|
||||||
|
|
||||||
|
return redirect()->route('zaliczki')->with('status', 'Zaliczka została usunięta.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function authorizeAccess(Request $request, Advance $advance): void
|
||||||
|
{
|
||||||
|
abort_if($request->user()->hasRole('pracownik') && $advance->user_id !== $request->user()->id, 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Http/Requests/StoreAdvanceRequest.php
Normal file
40
app/Http/Requests/StoreAdvanceRequest.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreAdvanceRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()?->can('zaliczki_access') ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => ['required', 'date'],
|
||||||
|
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||||
|
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||||
|
'advance_type_id' => ['required', 'integer', 'exists:advance_types,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->user()?->hasRole('pracownik')) {
|
||||||
|
$this->merge(['user_id' => $this->user()->id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => 'data',
|
||||||
|
'user_id' => 'użytkownik',
|
||||||
|
'amount' => 'kwota',
|
||||||
|
'advance_type_id' => 'rodzaj',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Models/Advance.php
Normal file
47
app/Models/Advance.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Advance extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'date',
|
||||||
|
'user_id',
|
||||||
|
'amount',
|
||||||
|
'advance_type_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => 'date',
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::saving(function (Advance $advance) {
|
||||||
|
$typeName = AdvanceType::whereKey($advance->advance_type_id)->value('name');
|
||||||
|
$amount = abs((float) $advance->amount);
|
||||||
|
|
||||||
|
$advance->amount = $typeName === 'Wpłata' ? $amount : -$amount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function type(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AdvanceType::class, 'advance_type_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Models/AdvanceType.php
Normal file
19
app/Models/AdvanceType.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class AdvanceType extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['name'];
|
||||||
|
|
||||||
|
public function advances(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Advance::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Permission\Traits\HasRoles; // 1. Import traitu Spatie
|
use Spatie\Permission\Traits\HasRoles; // 1. Import traitu Spatie
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
@ -53,4 +54,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function advances(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Advance::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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::create('advance_types', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('advance_types');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?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::create('advances', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->date('date');
|
||||||
|
$table->foreignId('user_id')->constrained()->restrictOnDelete();
|
||||||
|
$table->decimal('amount', 12, 2);
|
||||||
|
$table->foreignId('advance_type_id')->constrained('advance_types')->restrictOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['date', 'advance_type_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('advances');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$depositTypeIds = DB::table('advance_types')
|
||||||
|
->where('name', 'Wpłata')
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
if ($depositTypeIds->isNotEmpty()) {
|
||||||
|
DB::table('advances')
|
||||||
|
->whereIn('advance_type_id', $depositTypeIds)
|
||||||
|
->update(['amount' => DB::raw('ABS(amount)')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('advances')
|
||||||
|
->whereNotIn('advance_type_id', $depositTypeIds)
|
||||||
|
->update(['amount' => DB::raw('-ABS(amount)')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
16
database/seeders/AdvanceTypeSeeder.php
Normal file
16
database/seeders/AdvanceTypeSeeder.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\AdvanceType;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class AdvanceTypeSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
foreach (['Paliwo', 'Hotel', 'Wpłata'] as $name) {
|
||||||
|
AdvanceType::firstOrCreate(['name' => $name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,10 @@ class DatabaseSeeder extends Seeder
|
|||||||
Permission::firstOrCreate(['name' => $permission]);
|
Permission::firstOrCreate(['name' => $permission]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->call([
|
||||||
|
AdvanceTypeSeeder::class,
|
||||||
|
]);
|
||||||
|
|
||||||
// 2. Tworzenie ról (Roles)
|
// 2. Tworzenie ról (Roles)
|
||||||
$adminRole = Role::firstOrCreate(['name' => 'admin']);
|
$adminRole = Role::firstOrCreate(['name' => 'admin']);
|
||||||
$kierownikRole = Role::firstOrCreate(['name' => 'kierownik']);
|
$kierownikRole = Role::firstOrCreate(['name' => 'kierownik']);
|
||||||
@ -52,8 +56,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 4. Wywołanie seedera użytkowników
|
// 4. Wywołanie seedera użytkowników
|
||||||
$this->call([
|
$this->call([UserSeeder::class]);
|
||||||
UserSeeder::class,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,14 +1,98 @@
|
|||||||
<x-app-layout>
|
<x-app-layout>
|
||||||
<x-slot name="header">
|
<x-slot name="header">
|
||||||
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
<div class="flex items-center justify-between gap-4">
|
||||||
{{ __('Zaliczki') }}
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-white leading-tight">{{ __('Zaliczki') }}</h2>
|
||||||
</h2>
|
<a href="{{ route('zaliczki.create') }}" class="inline-flex items-center px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-md shadow-sm transition">Dodaj zaliczkę</a>
|
||||||
|
</div>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.advance-table-row:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .advance-table-row:hover {
|
||||||
|
background-color: #374151 !important;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg p-6 text-gray-900 dark:text-gray-100">
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6 text-gray-900 dark:text-white">
|
||||||
Tu będą Zaliczki
|
@if(session('status'))
|
||||||
|
<div class="mb-5 rounded-md bg-green-50 dark:bg-green-900/30 px-4 py-3 text-sm text-green-700 dark:text-green-300">{{ session('status') }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<form method="GET" action="{{ route('zaliczki') }}" class="mb-6 flex flex-wrap items-center gap-2">
|
||||||
|
<div class="w-full sm:w-64">
|
||||||
|
<x-input-label for="search" value="Szukaj" class="sr-only" />
|
||||||
|
<x-text-input id="search" type="text" name="search" :value="request('search')" placeholder="Użytkownik, e-mail lub kwota" class="block w-full px-3 py-2 text-sm" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-40">
|
||||||
|
<x-input-label for="type" value="Rodzaj" class="sr-only" />
|
||||||
|
<select id="type" name="type" class="block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm py-2 text-sm">
|
||||||
|
<option value="">Wszystkie rodzaje</option>
|
||||||
|
@foreach($types as $type)
|
||||||
|
<option value="{{ $type->id }}" {{ (string) request('type') === (string) $type->id ? 'selected' : '' }}>{{ $type->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-36">
|
||||||
|
<x-input-label for="date_from" value="Od" class="sr-only" />
|
||||||
|
<x-text-input id="date_from" type="date" name="date_from" :value="request('date_from')" class="block w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full sm:w-36">
|
||||||
|
<x-input-label for="date_to" value="Do" class="sr-only" />
|
||||||
|
<x-text-input id="date_to" type="date" name="date_to" :value="request('date_to')" class="block w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<x-primary-button type="submit">Szukaj</x-primary-button>
|
||||||
|
@if(request()->hasAny(['search', 'type', 'date_from', 'date_to']))
|
||||||
|
<a href="{{ route('zaliczki') }}" class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition">Wyczyść</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-gray-700 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||||||
|
<th class="py-3.5 px-4 text-left">Data</th>
|
||||||
|
<th class="py-3.5 px-4 text-left">Użytkownik</th>
|
||||||
|
<th class="py-3.5 px-4 text-left">Kwota</th>
|
||||||
|
<th class="py-3.5 px-4 text-left">Rodzaj</th>
|
||||||
|
<th class="py-3.5 px-4 text-center">Akcje</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700/60 text-sm">
|
||||||
|
@forelse($advances as $advance)
|
||||||
|
<tr class="advance-table-row hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<td class="py-4 px-4">{{ $advance->date->format('d.m.Y') }}</td>
|
||||||
|
<td class="py-4 px-4 font-medium">{{ $advance->user->name }} {{ $advance->user->surname }}</td>
|
||||||
|
<td class="py-4 px-4">{{ number_format((float) $advance->amount, 2, ',', ' ') }} zł</td>
|
||||||
|
<td class="py-4 px-4">{{ $advance->type->name }}</td>
|
||||||
|
<td class="py-4 px-4">
|
||||||
|
<div class="flex items-center justify-center gap-3">
|
||||||
|
<a href="{{ route('zaliczki.edit', $advance) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300 font-medium text-sm transition">Edytuj</a>
|
||||||
|
<form method="POST" action="{{ route('zaliczki.destroy', $advance) }}" onsubmit="return confirm('Czy na pewno chcesz usunąć tę zaliczkę?');">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 font-medium text-sm transition">Usuń</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="py-8 text-center text-gray-500 dark:text-gray-400">Brak zaliczek spełniających podane kryteria.</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($advances->hasPages())
|
||||||
|
<div class="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">{{ $advances->links() }}</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
63
resources/views/zaliczki/create.blade.php
Normal file
63
resources/views/zaliczki/create.blade.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-white leading-tight">
|
||||||
|
{{ __('Dodaj zaliczkę') }}
|
||||||
|
</h2>
|
||||||
|
<a href="{{ route('zaliczki') }}" class="inline-flex items-center px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white text-sm font-medium rounded-md shadow-sm transition">
|
||||||
|
Powrót do listy
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6 text-gray-900 dark:text-white">
|
||||||
|
<form method="POST" action="{{ route('zaliczki.store') }}">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="date" :value="__('Data')" />
|
||||||
|
<x-text-input id="date" class="block mt-1 w-full" type="date" name="date" :value="old('date', now()->format('Y-m-d'))" required autofocus />
|
||||||
|
<x-input-error :messages="$errors->get('date')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
@if($users->isNotEmpty())
|
||||||
|
<x-input-label for="user_id" :value="__('Użytkownik')" />
|
||||||
|
<select id="user_id" name="user_id" class="block mt-1 w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" required>
|
||||||
|
<option value="" disabled {{ old('user_id') ? '' : 'selected' }}>-- Wybierz użytkownika --</option>
|
||||||
|
@foreach($users as $user)
|
||||||
|
<option value="{{ $user->id }}" {{ (string) old('user_id') === (string) $user->id ? 'selected' : '' }}>{{ $user->name }} {{ $user->surname }} ({{ $user->email }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<x-input-error :messages="$errors->get('user_id')" class="mt-2" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="amount" :value="__('Kwota')" />
|
||||||
|
<x-text-input id="amount" class="block mt-1 w-full" type="number" name="amount" :value="old('amount')" min="0.01" step="0.01" required />
|
||||||
|
<x-input-error :messages="$errors->get('amount')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="advance_type_id" :value="__('Rodzaj')" />
|
||||||
|
<select id="advance_type_id" name="advance_type_id" class="block mt-1 w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" required>
|
||||||
|
<option value="" disabled {{ old('advance_type_id') ? '' : 'selected' }}>-- Wybierz rodzaj --</option>
|
||||||
|
@foreach($types as $type)
|
||||||
|
<option value="{{ $type->id }}" {{ (string) old('advance_type_id') === (string) $type->id ? 'selected' : '' }}>{{ $type->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<x-input-error :messages="$errors->get('advance_type_id')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-6 gap-4">
|
||||||
|
<a href="{{ route('zaliczki') }}" class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition">Anuluj</a>
|
||||||
|
<x-primary-button>Zapisz</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
58
resources/views/zaliczki/edit.blade.php
Normal file
58
resources/views/zaliczki/edit.blade.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<x-app-layout>
|
||||||
|
<x-slot name="header">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<h2 class="font-semibold text-xl text-gray-800 dark:text-white leading-tight">{{ __('Edytuj zaliczkę') }}</h2>
|
||||||
|
<a href="{{ route('zaliczki') }}" class="inline-flex items-center px-4 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white text-sm font-medium rounded-md shadow-sm transition">Powrót do listy</a>
|
||||||
|
</div>
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="py-12">
|
||||||
|
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg border border-gray-200 dark:border-gray-700/50 p-6 text-gray-900 dark:text-white">
|
||||||
|
<form method="POST" action="{{ route('zaliczki.update', $advance) }}">
|
||||||
|
@csrf
|
||||||
|
@method('PUT')
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<x-input-label for="date" :value="__('Data')" />
|
||||||
|
<x-text-input id="date" class="block mt-1 w-full" type="date" name="date" :value="old('date', $advance->date->format('Y-m-d'))" required autofocus />
|
||||||
|
<x-input-error :messages="$errors->get('date')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
@if($users->isNotEmpty())
|
||||||
|
<x-input-label for="user_id" :value="__('Użytkownik')" />
|
||||||
|
<select id="user_id" name="user_id" class="block mt-1 w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" required>
|
||||||
|
@foreach($users as $user)
|
||||||
|
<option value="{{ $user->id }}" {{ (string) old('user_id', $advance->user_id) === (string) $user->id ? 'selected' : '' }}>{{ $user->name }} {{ $user->surname }} ({{ $user->email }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<x-input-error :messages="$errors->get('user_id')" class="mt-2" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="amount" :value="__('Kwota')" />
|
||||||
|
<x-text-input id="amount" class="block mt-1 w-full" type="number" name="amount" :value="old('amount', abs((float) $advance->amount))" min="0.01" step="0.01" required />
|
||||||
|
<x-input-error :messages="$errors->get('amount')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<x-input-label for="advance_type_id" :value="__('Rodzaj')" />
|
||||||
|
<select id="advance_type_id" name="advance_type_id" class="block mt-1 w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" required>
|
||||||
|
@foreach($types as $type)
|
||||||
|
<option value="{{ $type->id }}" {{ (string) old('advance_type_id', $advance->advance_type_id) === (string) $type->id ? 'selected' : '' }}>{{ $type->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
<x-input-error :messages="$errors->get('advance_type_id')" class="mt-2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end mt-6 gap-4">
|
||||||
|
<a href="{{ route('zaliczki') }}" class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 transition">Anuluj</a>
|
||||||
|
<x-primary-button>Zapisz</x-primary-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\DashboardController;
|
use App\Http\Controllers\DashboardController;
|
||||||
|
use App\Http\Controllers\AdvanceController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\UserController;
|
use App\Http\Controllers\UserController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@ -46,7 +47,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
|
|
||||||
// Moduł Zaliczki
|
// Moduł Zaliczki
|
||||||
Route::middleware(['can:zaliczki_access'])->group(function () {
|
Route::middleware(['can:zaliczki_access'])->group(function () {
|
||||||
Route::view('/zaliczki', 'zaliczki')->name('zaliczki');
|
Route::get('/zaliczki', [AdvanceController::class, 'index'])->name('zaliczki');
|
||||||
|
Route::get('/zaliczki/create', [AdvanceController::class, 'create'])->name('zaliczki.create');
|
||||||
|
Route::post('/zaliczki', [AdvanceController::class, 'store'])->name('zaliczki.store');
|
||||||
|
Route::get('/zaliczki/{advance}/edit', [AdvanceController::class, 'edit'])->name('zaliczki.edit');
|
||||||
|
Route::put('/zaliczki/{advance}', [AdvanceController::class, 'update'])->name('zaliczki.update');
|
||||||
|
Route::delete('/zaliczki/{advance}', [AdvanceController::class, 'destroy'])->name('zaliczki.destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
140
tests/Feature/AdvanceTest.php
Normal file
140
tests/Feature/AdvanceTest.php
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Advance;
|
||||||
|
use App\Models\AdvanceType;
|
||||||
|
use App\Models\User;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Permission::firstOrCreate(['name' => 'zaliczki_access']);
|
||||||
|
});
|
||||||
|
|
||||||
|
function userWithAdvanceAccess(string $roleName = 'pracownik'): User
|
||||||
|
{
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$role = Role::firstOrCreate(['name' => $roleName]);
|
||||||
|
$role->givePermissionTo('zaliczki_access');
|
||||||
|
$user->assignRole($role);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('employee sees only own advances', function () {
|
||||||
|
$employee = userWithAdvanceAccess();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$type = AdvanceType::create(['name' => 'Paliwo']);
|
||||||
|
|
||||||
|
Advance::create(['date' => '2026-08-01', 'user_id' => $employee->id, 'amount' => 100, 'advance_type_id' => $type->id]);
|
||||||
|
Advance::create(['date' => '2026-08-02', 'user_id' => $otherUser->id, 'amount' => 200, 'advance_type_id' => $type->id]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($employee)->get(route('zaliczki'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('100,00');
|
||||||
|
$response->assertDontSee('200,00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('advance is stored for authenticated user', function () {
|
||||||
|
$employee = userWithAdvanceAccess();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$type = AdvanceType::create(['name' => 'Hotel']);
|
||||||
|
|
||||||
|
$response = $this->actingAs($employee)->post(route('zaliczki.store'), [
|
||||||
|
'date' => '2026-08-28',
|
||||||
|
'user_id' => $otherUser->id,
|
||||||
|
'amount' => '250.50',
|
||||||
|
'advance_type_id' => $type->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect(route('zaliczki'));
|
||||||
|
$this->assertDatabaseHas('advances', [
|
||||||
|
'user_id' => $employee->id,
|
||||||
|
'advance_type_id' => $type->id,
|
||||||
|
'amount' => -250.50,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manager can create an advance for another user', function () {
|
||||||
|
$manager = userWithAdvanceAccess('kierownik');
|
||||||
|
$employee = User::factory()->create();
|
||||||
|
$type = AdvanceType::create(['name' => 'Wpłata']);
|
||||||
|
|
||||||
|
$response = $this->actingAs($manager)->post(route('zaliczki.store'), [
|
||||||
|
'date' => '2026-08-28',
|
||||||
|
'user_id' => $employee->id,
|
||||||
|
'amount' => '300.00',
|
||||||
|
'advance_type_id' => $type->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect(route('zaliczki'));
|
||||||
|
$this->assertDatabaseHas('advances', [
|
||||||
|
'user_id' => $employee->id,
|
||||||
|
'advance_type_id' => $type->id,
|
||||||
|
'amount' => 300.00,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('employee can edit own advance but cannot access another users advance', function () {
|
||||||
|
$employee = userWithAdvanceAccess();
|
||||||
|
$otherUser = User::factory()->create();
|
||||||
|
$type = AdvanceType::create(['name' => 'Hotel']);
|
||||||
|
$advance = Advance::create(['date' => '2026-08-28', 'user_id' => $employee->id, 'amount' => 100, 'advance_type_id' => $type->id]);
|
||||||
|
$otherAdvance = Advance::create(['date' => '2026-08-28', 'user_id' => $otherUser->id, 'amount' => 200, 'advance_type_id' => $type->id]);
|
||||||
|
|
||||||
|
$this->actingAs($employee)->get(route('zaliczki.edit', $otherAdvance))->assertForbidden();
|
||||||
|
|
||||||
|
$response = $this->actingAs($employee)->put(route('zaliczki.update', $advance), [
|
||||||
|
'date' => '2026-08-27',
|
||||||
|
'amount' => '150.00',
|
||||||
|
'advance_type_id' => $type->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertRedirect(route('zaliczki'));
|
||||||
|
$this->assertDatabaseHas('advances', ['id' => $advance->id, 'amount' => -150, 'date' => '2026-08-27 00:00:00']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manager can delete an advance', function () {
|
||||||
|
$manager = userWithAdvanceAccess('kierownik');
|
||||||
|
$employee = User::factory()->create();
|
||||||
|
$type = AdvanceType::create(['name' => 'Paliwo']);
|
||||||
|
$advance = Advance::create(['date' => '2026-08-28', 'user_id' => $employee->id, 'amount' => 100, 'advance_type_id' => $type->id]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($manager)->delete(route('zaliczki.destroy', $advance));
|
||||||
|
|
||||||
|
$response->assertRedirect(route('zaliczki'));
|
||||||
|
$this->assertDatabaseMissing('advances', ['id' => $advance->id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expenses are negative and deposits are positive regardless of input sign', function () {
|
||||||
|
$employee = userWithAdvanceAccess();
|
||||||
|
$fuel = AdvanceType::create(['name' => 'Paliwo']);
|
||||||
|
$deposit = AdvanceType::create(['name' => 'Wpłata']);
|
||||||
|
|
||||||
|
Advance::create(['date' => '2026-08-28', 'user_id' => $employee->id, 'amount' => 100, 'advance_type_id' => $fuel->id]);
|
||||||
|
Advance::create(['date' => '2026-08-28', 'user_id' => $employee->id, 'amount' => -200, 'advance_type_id' => $deposit->id]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('advances', ['advance_type_id' => $fuel->id, 'amount' => -100]);
|
||||||
|
$this->assertDatabaseHas('advances', ['advance_type_id' => $deposit->id, 'amount' => 200]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('manager can filter advances by text type and date range', function () {
|
||||||
|
$manager = userWithAdvanceAccess('kierownik');
|
||||||
|
$employee = User::factory()->create(['name' => 'Anna', 'surname' => 'Nowak']);
|
||||||
|
$fuel = AdvanceType::create(['name' => 'Paliwo']);
|
||||||
|
$hotel = AdvanceType::create(['name' => 'Hotel']);
|
||||||
|
|
||||||
|
Advance::create(['date' => '2026-08-10', 'user_id' => $employee->id, 'amount' => 100, 'advance_type_id' => $fuel->id]);
|
||||||
|
Advance::create(['date' => '2026-08-20', 'user_id' => $employee->id, 'amount' => 200, 'advance_type_id' => $hotel->id]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($manager)->get(route('zaliczki', [
|
||||||
|
'search' => 'Anna',
|
||||||
|
'type' => $fuel->id,
|
||||||
|
'date_from' => '2026-08-01',
|
||||||
|
'date_to' => '2026-08-15',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('100,00');
|
||||||
|
$response->assertDontSee('200,00');
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user