feat: Implement chat functionality
This commit is contained in:
parent
8f2cf8e642
commit
f8ee8165ff
@ -46,7 +46,7 @@ class DynamicController extends Controller
|
||||
{
|
||||
$this->authorize('view', $dynamic);
|
||||
|
||||
$dynamic->load('ledgers', 'participants');
|
||||
$dynamic->load('ledgers', 'participants', 'chat.messages.user');
|
||||
|
||||
return Inertia::render('Dynamics/Show', [
|
||||
'dynamic' => $dynamic,
|
||||
|
||||
@ -43,7 +43,7 @@ class LedgerController extends Controller
|
||||
{
|
||||
$this->authorize('view', $ledger);
|
||||
|
||||
$ledger->load('mutations.user');
|
||||
$ledger->load('mutations.user', 'mutations.chat.messages.user');
|
||||
|
||||
return Inertia::render('Ledgers/Show', [
|
||||
'dynamic' => $dynamic,
|
||||
|
||||
72
app/Http/Controllers/MessageController.php
Normal file
72
app/Http/Controllers/MessageController.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreMessageRequest;
|
||||
use App\Models\Chat;
|
||||
use App\Models\Message;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MessageController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(StoreMessageRequest $request, Chat $chat)
|
||||
{
|
||||
$chat->messages()->create([
|
||||
...$request->validated(),
|
||||
'user_id' => $request->user()->id,
|
||||
]);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Message $message)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Message $message)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Message $message)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Message $message)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/StoreMessageRequest.php
Normal file
34
app/Http/Requests/StoreMessageRequest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
|
||||
use App\Models\Chat;
|
||||
|
||||
class StoreMessageRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$chat = $this->route('chat');
|
||||
|
||||
return $chat && $this->user()->can('view', $chat->chatable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'content' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
24
app/Models/Chat.php
Normal file
24
app/Models/Chat.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Chat extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\ChatFactory> */
|
||||
use HasFactory;
|
||||
|
||||
public function chatable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(Message::class);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
class Dynamic extends Model
|
||||
{
|
||||
@ -27,4 +28,16 @@ class Dynamic extends Model
|
||||
{
|
||||
return $this->hasMany(Ledger::class);
|
||||
}
|
||||
|
||||
public function chat(): MorphOne
|
||||
{
|
||||
return $this->morphOne(Chat::class, 'chatable');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (Dynamic $dynamic) {
|
||||
$dynamic->chat()->create([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
29
app/Models/Message.php
Normal file
29
app/Models/Message.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Message extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\MessageFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'chat_id',
|
||||
'user_id',
|
||||
'content',
|
||||
];
|
||||
|
||||
public function chat(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Chat::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
class Mutation extends Model
|
||||
{
|
||||
@ -30,4 +31,16 @@ class Mutation extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function chat(): MorphOne
|
||||
{
|
||||
return $this->morphOne(Chat::class, 'chatable');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function (Mutation $mutation) {
|
||||
$mutation->chat()->create([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
24
database/factories/ChatFactory.php
Normal file
24
database/factories/ChatFactory.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Chat;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Chat>
|
||||
*/
|
||||
class ChatFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
24
database/factories/MessageFactory.php
Normal file
24
database/factories/MessageFactory.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Message;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Message>
|
||||
*/
|
||||
class MessageFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
<?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('messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('chat_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->text('content');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('messages');
|
||||
}
|
||||
};
|
||||
28
database/migrations/2026_06_14_223624_create_chats_table.php
Normal file
28
database/migrations/2026_06_14_223624_create_chats_table.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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('chats', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('chatable');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('chats');
|
||||
}
|
||||
};
|
||||
48
resources/js/components/Chat.vue
Normal file
48
resources/js/components/Chat.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
const props = defineProps({
|
||||
chat: Object,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
content: '',
|
||||
});
|
||||
|
||||
function submit() {
|
||||
form.post(route('chats.messages.store', props.chat.id), {
|
||||
onSuccess: () => form.reset(),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-8">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100">Chat</h4>
|
||||
<div class="mt-4 space-y-4">
|
||||
<div v-for="message in chat.messages" :key="message.id" class="p-4 bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-semibold">{{ message.user.name }}</span>
|
||||
<span class="text-xs text-gray-500">{{ new Date(message.created_at).toLocaleString() }}</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ message.content }}</p>
|
||||
</div>
|
||||
<div v-if="chat.messages.length === 0" class="text-gray-500">
|
||||
No messages yet.
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Message</label>
|
||||
<textarea v-model="form.content" id="content" rows="3" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"></textarea>
|
||||
<div v-if="form.errors.content" class="text-sm text-red-600">{{ form.errors.content }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" :disabled="form.processing" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import Chat from '@/components/Chat.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
@ -43,6 +44,8 @@ function submit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chat :chat="dynamic.chat" />
|
||||
|
||||
<div class="mt-8">
|
||||
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100">Participants</h4>
|
||||
<ul class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import Chat from '@/components/Chat.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
@ -85,6 +86,7 @@ function submit() {
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ mutation.description }}</p>
|
||||
<div class="mt-2 text-xs text-gray-500">{{ new Date(mutation.created_at).toLocaleString() }}</div>
|
||||
<Chat :chat="mutation.chat" />
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="ledger.mutations.length === 0" class="mt-4 text-gray-500">
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\MessageController;
|
||||
use App\Http\Controllers\MutationController;
|
||||
use App\Http\Controllers\DynamicController;
|
||||
use App\Http\Controllers\LedgerController;
|
||||
@ -13,6 +14,8 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::resource('dynamics', DynamicController::class);
|
||||
Route::resource('dynamics.ledgers', LedgerController::class)->scoped();
|
||||
Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped();
|
||||
|
||||
Route::post('/chats/{chat}/messages', [MessageController::class, 'store'])->name('chats.messages.store');
|
||||
});
|
||||
|
||||
require __DIR__.'/settings.php';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user