Clients CRUD

This commit is contained in:
PovilasKorop 2024-09-10 11:26:04 +03:00
parent 3f4dbbf5d4
commit 282ef63cf2
25 changed files with 1043 additions and 11 deletions

View File

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
enum ProjectStatus: string
{
case OPEN = 'open';
case IN_PROGRESS = 'in progress';
case BLOCKED = 'blocked';
case CANCELLED = 'cancelled';
case COMPLETED = 'completed';
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use App\Models\Client;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\StoreClientRequest;
use App\Http\Requests\UpdateClientRequest;
class ClientController extends Controller
{
public function index(): View
{
$clients = Client::paginate(20);
return view('clients.index', compact('clients'));
}
public function create(): View
{
return view('clients.create');
}
public function store(StoreClientRequest $request): RedirectResponse
{
Client::create($request->validated());
return redirect()->route('clients.index');
}
public function edit(Client $client): View
{
return view('clients.edit', compact('client'));
}
public function update(UpdateClientRequest $request, Client $client): RedirectResponse
{
$client->update($request->validated());
return redirect()->route('clients.index');
}
public function destroy(Client $client): RedirectResponse
{
$client->delete();
return redirect()->route('clients.index');
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Client;
use App\Models\Project;
use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;
use App\Http\Requests\StoreProjectRequest;
use App\Http\Requests\UpdateProjectRequest;
class ProjectController extends Controller
{
public function index(): View
{
$projects = Project::with(['user', 'client'])->paginate(10);
return view('projects.index', compact('projects'));
}
public function create(): View
{
$users = User::select(['id', 'first_name', 'last_name'])->get();
$clients = Client::select(['id', 'company_name'])->get();
return view('projects.create', compact('users', 'clients'));
}
public function store(StoreProjectRequest $request): RedirectResponse
{
Project::create($request->validated());
return redirect()->route('projects.index');
}
public function edit(Project $project): View
{
$users = User::select(['id', 'first_name', 'last_name'])->get();
$clients = Client::select(['id', 'company_name'])->get();
return view('projects.edit', compact('project', 'users', 'clients'));
}
public function update(UpdateProjectRequest $request, Project $project): RedirectResponse
{
$project->update($request->validated());
return redirect()->route('projects.index');
}
public function destroy(Project $project): RedirectResponse
{
$project->delete();
return redirect()->route('projects.index');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'contact_name' => ['required', 'string', 'max:255'],
'contact_email' => ['required', 'string', 'email', 'max:255', Rule::unique('clients')],
'contact_phone_number' => ['required'],
'company_name' => ['required'],
'company_address' => ['required'],
'company_city' => ['required', 'string'],
'company_zip' => ['required', 'integer'],
'company_vat' => ['required', 'numeric'],
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use App\Enums\ProjectStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => ['required'],
'description' => ['required'],
'user_id' => ['required', Rule::exists('users', 'id')],
'client_id' => ['required', Rule::exists('clients', 'id')],
'deadline_at' => ['required', 'date'],
'status' => ['required', Rule::enum(ProjectStatus::class)],
];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateClientRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'contact_name' => ['required', 'string', 'max:255'],
'contact_email' => ['required', 'string', 'email', 'max:255', Rule::unique('clients')->ignore($this->client)],
'contact_phone_number' => ['required'],
'company_name' => ['required'],
'company_address' => ['required'],
'company_city' => ['required', 'string'],
'company_zip' => ['required', 'integer'],
'company_vat' => ['required', 'numeric'],
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests;
use App\Enums\ProjectStatus;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateProjectRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'title' => ['required'],
'description' => ['required'],
'user_id' => ['required', Rule::exists('users', 'id')],
'client_id' => ['required', Rule::exists('clients', 'id')],
'deadline_at' => ['required', 'date'],
'status' => ['required', Rule::enum(ProjectStatus::class)],
];
}
}

23
app/Models/Client.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Client extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'contact_name',
'contact_email',
'contact_phone_number',
'company_name',
'company_address',
'company_city',
'company_zip',
'company_vat',
];
}

40
app/Models/Project.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use App\Enums\ProjectStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Project extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'title',
'description',
'user_id',
'client_id',
'deadline_at',
'status',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
protected function casts(): array
{
return [
'status' => ProjectStatus::class,
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Client>
*/
class ClientFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'contact_name' => fake()->name(),
'contact_email' => fake()->unique()->safeEmail(),
'contact_phone_number' => fake()->phoneNumber(),
'company_name' => fake()->company(),
'company_address' => fake()->address(),
'company_city' => fake()->city(),
'company_zip' => fake()->postcode(),
'company_vat' => fake()->unique()->numerify(),
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Database\Factories;
use App\Enums\ProjectStatus;
use App\Models\Client;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Project>
*/
class ProjectFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$users = User::pluck('id');
$clients = Client::pluck('id');
return [
'title' => fake()->sentence(3),
'description' => fake()->paragraph(),
'deadline_at' => now()->addDays(rand(1, 30))->format('Y-m-d'),
'status' => fake()->randomElement(ProjectStatus::cases())->value,
'user_id' => $users->random(),
'client_id' => $clients->random(),
];
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->string('contact_name');
$table->string('contact_email')->unique();
$table->string('contact_phone_number');
$table->string('company_name');
$table->string('company_address');
$table->string('company_city');
$table->string('company_zip');
$table->integer('company_vat');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('clients');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('projects', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description');
$table->foreignId('user_id')->constrained();
$table->foreignId('client_id')->constrained();
$table->date('deadline_at');
$table->string('status');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('projects');
}
};

View File

@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\Client;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ClientSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Client::factory(50)->create();
}
}

View File

@ -2,9 +2,7 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\RoleEnum;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
@ -14,14 +12,11 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
$this->call(RoleSeeder::class);
User::factory(10)->create();
User::factory()->create([
'first_name' => 'Admin',
'last_name' => 'Admin',
'email' => 'admin@admin.com',
'password' => 'secret',
])->syncRoles([RoleEnum::ADMIN]);
$this->call([
RoleSeeder::class,
UserSeeder::class,
ClientSeeder::class,
ProjectSeeder::class,
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use App\Models\Project;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ProjectSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Project::factory(20)->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use App\RoleEnum;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
User::factory(10)->create();
User::factory()->create([
'first_name' => 'Admin',
'last_name' => 'Admin',
'email' => 'admin@admin.com',
'password' => 'secret',
])->syncRoles([RoleEnum::ADMIN]);
}
}

View File

@ -0,0 +1,100 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create Client') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('clients.store') }}">
@csrf
<div class="divide-y-2 space-y-4">
<div>
<h3 class="text-xl font-semibold mb-4">Contact information</h3>
<!-- Name -->
<div>
<x-input-label for="contact_name" :value="__('Name')"/>
<x-text-input id="contact_name" class="block mt-1 w-full" type="text"
name="contact_name" :value="old('contact_name')" required/>
<x-input-error :messages="$errors->get('contact_name')" class="mt-2"/>
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="contact_email" :value="__('Email')"/>
<x-text-input id="contact_email" class="block mt-1 w-full" type="email"
name="contact_email" :value="old('contact_email')" required/>
<x-input-error :messages="$errors->get('contact_email')" class="mt-2"/>
</div>
<!-- Phone Number -->
<div class="mt-4">
<x-input-label for="contact_phone_number" :value="__('Phone number')"/>
<x-text-input id="contact_phone_number" class="block mt-1 w-full" type="text"
name="contact_phone_number" :value="old('contact_phone_number')"
required/>
<x-input-error :messages="$errors->get('contact_phone_number')" class="mt-2"/>
</div>
</div>
<div>
<h3 class="text-xl font-semibold my-4 mt-4">Company information</h3>
<!-- Company Name -->
<div>
<x-input-label for="company_name" :value="__('Company Name')"/>
<x-text-input id="company_name" class="block mt-1 w-full" type="text"
name="company_name" :value="old('company_name')" required/>
<x-input-error :messages="$errors->get('company_name')" class="mt-2"/>
</div>
<!-- Company VAT -->
<div class="mt-4">
<x-input-label for="company_vat" :value="__('Company VAT')"/>
<x-text-input id="company_vat" class="block mt-1 w-full" type="text"
name="company_vat" :value="old('company_vat')" required/>
<x-input-error :messages="$errors->get('company_vat')" class="mt-2"/>
</div>
<!-- Company Address -->
<div class="mt-4">
<x-input-label for="company_address" :value="__('Company address')"/>
<x-text-input id="company_address" class="block mt-1 w-full" type="text"
name="company_address" :value="old('company_address')" required/>
<x-input-error :messages="$errors->get('company_address')" class="mt-2"/>
</div>
<!-- Company City -->
<div class="mt-4">
<x-input-label for="company_city" :value="__('Company city')"/>
<x-text-input id="company_city" class="block mt-1 w-full" type="text"
name="company_city" :value="old('company_city')" required/>
<x-input-error :messages="$errors->get('company_city')" class="mt-2"/>
</div>
<!-- Company ZIP -->
<div class="mt-4">
<x-input-label for="company_zip" :value="__('Company zip')"/>
<x-text-input id="company_zip" class="block mt-1 w-full" type="text"
name="company_zip" :value="old('company_zip')" required/>
<x-input-error :messages="$errors->get('company_zip')" class="mt-2"/>
</div>
</div>
<div>
<x-primary-button class="mt-4">
{{ __('Save') }}
</x-primary-button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,92 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Client') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('clients.update', $client) }}">
@method('PUT')
@csrf
<div class="divide-y-2 space-y-4">
<div>
<h3 class="text-xl font-semibold mb-4">Contact information</h3>
<!-- Name -->
<div>
<x-input-label for="contact_name" :value="__('Name')" />
<x-text-input id="contact_name" class="block mt-1 w-full" type="text" name="contact_name" :value="old('contact_name', $client->contact_name)" required />
<x-input-error :messages="$errors->get('contact_name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="contact_email" :value="__('Email')" />
<x-text-input id="contact_email" class="block mt-1 w-full" type="email" name="contact_email" :value="old('contact_email', $client->contact_email)" required />
<x-input-error :messages="$errors->get('contact_email')" class="mt-2" />
</div>
<!-- Phone Number -->
<div class="mt-4">
<x-input-label for="contact_phone_number" :value="__('Phone number')" />
<x-text-input id="contact_phone_number" class="block mt-1 w-full" type="text" name="contact_phone_number" :value="old('contact_phone_number', $client->contact_phone_number)" required />
<x-input-error :messages="$errors->get('contact_phone_number')" class="mt-2" />
</div>
</div>
<div>
<h3 class="text-xl font-semibold my-4 mt-4">Company information</h3>
<!-- Company Name -->
<div>
<x-input-label for="company_name" :value="__('Company Name')" />
<x-text-input id="company_name" class="block mt-1 w-full" type="text" name="company_name" :value="old('company_name', $client->company_name)" required />
<x-input-error :messages="$errors->get('company_name')" class="mt-2" />
</div>
<!-- Company VAT -->
<div class="mt-4">
<x-input-label for="company_vat" :value="__('Company VAT')" />
<x-text-input id="company_vat" class="block mt-1 w-full" type="text" name="company_vat" :value="old('company_vat', $client->company_vat)" required />
<x-input-error :messages="$errors->get('company_vat')" class="mt-2" />
</div>
<!-- Company Address -->
<div class="mt-4">
<x-input-label for="company_address" :value="__('Company address')" />
<x-text-input id="company_address" class="block mt-1 w-full" type="text" name="company_address" :value="old('company_address', $client->company_address)" required />
<x-input-error :messages="$errors->get('company_address')" class="mt-2" />
</div>
<!-- Company City -->
<div class="mt-4">
<x-input-label for="company_city" :value="__('Company city')" />
<x-text-input id="company_city" class="block mt-1 w-full" type="text" name="company_city" :value="old('company_city', $client->company_city)" required />
<x-input-error :messages="$errors->get('company_city')" class="mt-2" />
</div>
<!-- Company ZIP -->
<div class="mt-4">
<x-input-label for="company_zip" :value="__('Company zip')" />
<x-text-input id="company_zip" class="block mt-1 w-full" type="text" name="company_zip" :value="old('company_zip', $client->company_zip)" required />
<x-input-error :messages="$errors->get('company_zip')" class="mt-2" />
</div>
</div>
<div>
<x-primary-button class="mt-4">
{{ __('Save') }}
</x-primary-button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,67 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Clients') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<a href="{{ route('clients.create') }}" class="underline">Add new client</a>
<table class="min-w-full divide-y divide-gray-200 border mt-4">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Company</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">VAT</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Address</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@foreach($clients as $client)
<tr class="bg-white">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $client->company_name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $client->company_vat }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $client->company_address }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
<a href="{{ route('clients.edit', $client) }}" class="underline">Edit</a>
|
<form action="{{ route('clients.destroy', $client) }}"
method="POST"
onsubmit="return confirm('Are you sure?');"
class="inline-block">
@method('DELETE')
@csrf
<button type="submit" class="text-red-500 underline">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="mt-4">
{{ $clients->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -20,6 +20,12 @@
{{ __('Users') }}
</x-nav-link>
@endrole
<x-nav-link :href="route('clients.index')" :active="request()->routeIs('clients.*')">
{{ __('Clients') }}
</x-nav-link>
<x-nav-link :href="route('projects.index')" :active="request()->routeIs('projects.*')">
{{ __('Projects') }}
</x-nav-link>
</div>
</div>
@ -80,6 +86,12 @@
{{ __('Users') }}
</x-responsive-nav-link>
@endrole
<x-responsive-nav-link :href="route('clients.index')" :active="request()->routeIs('clients.*')">
{{ __('Clients') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('projects.index')" :active="request()->routeIs('projects.*')">
{{ __('Projects') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->

View File

@ -0,0 +1,81 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create Project') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('projects.store') }}">
@csrf
<!-- Title -->
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" required />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<!-- Description -->
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="description" required>{{ old('description') }}</textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<!-- Deadline At -->
<div class="mt-4">
<x-input-label for="deadline_at" :value="__('Deadline')" />
<x-text-input id="deadline_at" class="block mt-1 w-full" type="date" name="deadline_at" min="{{ today()->format('Y-m-d') }}" :value="old('deadline_at')" required />
<x-input-error :messages="$errors->get('deadline_at')" class="mt-2" />
</div>
<!-- Assigned User -->
<div class="mt-4">
<x-input-label for="user_id" :value="__('Assigned user')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="user_id" id="user_id">
@foreach($users as $user)
<option value="{{ $user->id }}"
@selected(old('user_id') == $user->id)>{{ $user->first_name . ' ' . $user->last_name }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('user_id')" class="mt-2" />
</div>
<!-- Assigned Client -->
<div class="mt-4">
<x-input-label for="client_id" :value="__('Client')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="client_id" id="client_id">
@foreach($clients as $client)
<option value="{{ $client->id }}"
@selected(old('client_id') == $client->id)>{{ $client->company_name }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('client_id')" class="mt-2" />
</div>
<!-- Status -->
<div class="mt-4">
<x-input-label for="status" :value="__('Status')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="status" id="status">
@foreach(\App\Enums\ProjectStatus::cases() as $status)
<option value="{{ $status->value }}" @selected(old('status') == $status->value)>{{ $status->value }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('status')" class="mt-2" />
</div>
<x-primary-button class="mt-4">
{{ __('Save') }}
</x-primary-button>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,82 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit Project') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200">
<form method="POST" action="{{ route('projects.update', $project) }}">
@csrf
@method('PUT')
<!-- Title -->
<div>
<x-input-label for="title" :value="__('Title')" />
<x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title', $project->title)" required />
<x-input-error :messages="$errors->get('title')" class="mt-2" />
</div>
<!-- Description -->
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="description" required>{{ old('description', $project->description) }}</textarea>
<x-input-error :messages="$errors->get('description')" class="mt-2" />
</div>
<!-- Deadline At -->
<div class="mt-4">
<x-input-label for="deadline_at" :value="__('Deadline')" />
<x-text-input id="deadline_at" class="block mt-1 w-full" type="date" name="deadline_at" min="{{ today()->format('Y-m-d') }}" :value="old('deadline_at', $project->deadline_at)" required />
<x-input-error :messages="$errors->get('deadline_at')" class="mt-2" />
</div>
<!-- Assigned User -->
<div class="mt-4">
<x-input-label for="user_id" :value="__('Assigned user')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="user_id" id="user_id">
@foreach($users as $user)
<option value="{{ $user->id }}"
@selected(old('user_id', $project->user_id) == $user->id)>{{ $user->first_name . ' ' . $user->last_name }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('user_id')" class="mt-2" />
</div>
<!-- Assigned Client -->
<div class="mt-4">
<x-input-label for="client_id" :value="__('Client')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="client_id" id="client_id">
@foreach($clients as $client)
<option value="{{ $client->id }}"
@selected(old('client_id', $project->client_id) == $client->id)>{{ $client->company_name }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('client_id')" class="mt-2" />
</div>
<!-- Status -->
<div class="mt-4">
<x-input-label for="status" :value="__('Status')" />
<select class="block mt-1 w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" name="status" id="status">
@foreach(\App\Enums\ProjectStatus::cases() as $status)
<option value="{{ $status->value }}" @selected(old('status', $project->status->value) == $status->value)>{{ $status->value }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('status')" class="mt-2" />
</div>
<x-primary-button class="mt-4">
{{ __('Save') }}
</x-primary-button>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -0,0 +1,79 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Projects') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<a href="{{ route('projects.create') }}" class="underline">Add new project</a>
<table class="min-w-full divide-y divide-gray-200 border mt-4">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Title</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Assigned To</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Client</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Deadline</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Status</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@foreach($projects as $project)
<tr class="bg-white">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $project->title }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $project->user->first_name }} {{ $project->user->last_name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $project->client->company_name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $project->deadline_at }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $project->status }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
<a href="{{ route('projects.edit', $project) }}" class="underline">Edit</a>
|
<form action="{{ route('projects.destroy', $project) }}"
method="POST"
onsubmit="return confirm('Are you sure?');"
class="inline-block">
@method('DELETE')
@csrf
<button type="submit" class="text-red-500 underline">Delete</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="mt-4">
{{ $projects->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@ -1,6 +1,8 @@
<?php
use App\Http\Controllers\ClientController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ProjectController;
use App\Http\Controllers\UserController;
use App\RoleEnum;
use Illuminate\Support\Facades\Route;
@ -16,6 +18,8 @@
Route::middleware('auth')->group(function () {
Route::resource('users', UserController::class)
->middleware(['role:' . RoleEnum::ADMIN->value]);
Route::resource('clients', ClientController::class);
Route::resource('projects', ProjectController::class);
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');