over naar uuids, policies, predefined mutations aan kunnen passen
This commit is contained in:
parent
06e5600447
commit
1e0782385b
@ -11,10 +11,10 @@ class DashboardController extends Controller
|
||||
public function index(Request $request, ActivityService $activityService)
|
||||
{
|
||||
$user = $request->user();
|
||||
$unreadEntities = $activityService->getUnreadEntitiesGrouped($user);
|
||||
$unreadDynamics = $activityService->getUnreadDynamicsGrouped($user);
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'unreadEntities' => $unreadEntities,
|
||||
'unreadDynamics' => $unreadDynamics,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreDynamicRequest;
|
||||
use App\Http\Requests\UpdateDynamicRequest;
|
||||
use App\Http\Resources\DynamicResource;
|
||||
use App\Http\Resources\LedgerResource;
|
||||
use App\Http\Resources\MessageResource;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Dynamic;
|
||||
use App\Services\ActivityService;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@ -19,7 +21,7 @@ class DynamicController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
return Inertia::render('Dynamics/Index', [
|
||||
'dynamics' => $request->user()->dynamics()->get(),
|
||||
'dynamics' => DynamicResource::collection($request->user()->dynamics()->get()),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -54,15 +56,14 @@ class DynamicController extends Controller
|
||||
|
||||
$dynamic->load(['ledgers.media', 'participants', 'chat']);
|
||||
|
||||
$isOwner = $dynamic->participants()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('role', 'owner')
|
||||
->exists();
|
||||
|
||||
return Inertia::render('Dynamics/Show', [
|
||||
'dynamic' => $dynamic,
|
||||
'isOwner' => $isOwner,
|
||||
'messages' => $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20),
|
||||
'dynamic' => new DynamicResource($dynamic),
|
||||
'ledgers' => LedgerResource::collection($dynamic->ledgers),
|
||||
'participants' => UserResource::collection($dynamic->participants),
|
||||
'messages' => MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)),
|
||||
'can' => [
|
||||
'update' => $request->user()->can('update', $dynamic),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -70,7 +71,7 @@ class DynamicController extends Controller
|
||||
{
|
||||
$this->authorize('view', $dynamic);
|
||||
|
||||
return $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20);
|
||||
return MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,7 +82,7 @@ class DynamicController extends Controller
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
return Inertia::render('Dynamics/Settings', [
|
||||
'dynamic' => $dynamic,
|
||||
'dynamic' => new DynamicResource($dynamic),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -125,6 +125,6 @@ class DynamicInvitationController extends Controller
|
||||
$invitation->delete();
|
||||
});
|
||||
|
||||
return redirect()->route('dynamics.show', $invitation->dynamic_id)->with('success', 'Successfully joined the dynamic!');
|
||||
return redirect()->route('dynamics.show', $invitation->dynamic)->with('success', 'Successfully joined the dynamic!');
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreLedgerRequest;
|
||||
use App\Http\Resources\DynamicResource;
|
||||
use App\Http\Resources\LedgerResource;
|
||||
use App\Http\Resources\MessageResource;
|
||||
use App\Http\Resources\MutationResource;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\Dynamic;
|
||||
use App\Models\Ledger;
|
||||
use App\Services\ActivityService;
|
||||
@ -30,7 +35,7 @@ class LedgerController extends Controller
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
return Inertia::render('Ledgers/Create', [
|
||||
'dynamic' => $dynamic,
|
||||
'dynamic' => new DynamicResource($dynamic),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -39,6 +44,7 @@ class LedgerController extends Controller
|
||||
*/
|
||||
public function store(StoreLedgerRequest $request, Dynamic $dynamic)
|
||||
{
|
||||
$this->authorize('create', [Ledger::class, $dynamic]);
|
||||
$ledger = $dynamic->ledgers()->create($request->except('media'));
|
||||
|
||||
if ($request->hasFile('media')) {
|
||||
@ -76,16 +82,16 @@ class LedgerController extends Controller
|
||||
'mutations.chat',
|
||||
]);
|
||||
|
||||
$isOwner = $dynamic->participants()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('role', 'owner')
|
||||
->exists();
|
||||
|
||||
return Inertia::render('Ledgers/Show', [
|
||||
'dynamic' => $dynamic,
|
||||
'ledger' => $ledger,
|
||||
'isOwner' => $isOwner,
|
||||
'messages' => $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20),
|
||||
'dynamic' => new DynamicResource($dynamic),
|
||||
'ledger' => new LedgerResource($ledger),
|
||||
'mutations' => MutationResource::collection($ledger->mutations),
|
||||
'participants' => UserResource::collection($dynamic->participants),
|
||||
'messages' => MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20)),
|
||||
'can' => [
|
||||
'update' => $request->user()->can('update', $ledger),
|
||||
'close' => $request->user()->can('close', $ledger),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@ -93,23 +99,41 @@ class LedgerController extends Controller
|
||||
{
|
||||
$this->authorize('view', $ledger);
|
||||
|
||||
return $dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20);
|
||||
return MessageResource::collection($dynamic->chat->messages()->with(['user', 'media'])->latest()->paginate(20));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Ledger $ledger)
|
||||
public function edit(Dynamic $dynamic, Ledger $ledger)
|
||||
{
|
||||
//
|
||||
$this->authorize('update', $ledger);
|
||||
|
||||
return Inertia::render('Ledgers/Edit', [
|
||||
'dynamic' => new DynamicResource($dynamic),
|
||||
'ledger' => new LedgerResource($ledger),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Ledger $ledger)
|
||||
public function update(StoreLedgerRequest $request, Dynamic $dynamic, Ledger $ledger)
|
||||
{
|
||||
//
|
||||
$this->authorize('update', $ledger);
|
||||
|
||||
$ledger->update($request->validated());
|
||||
|
||||
return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]);
|
||||
}
|
||||
|
||||
public function close(Request $request, Dynamic $dynamic, Ledger $ledger)
|
||||
{
|
||||
$this->authorize('close', $ledger);
|
||||
|
||||
$ledger->update(['status' => 'closed']);
|
||||
|
||||
return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,15 +2,19 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\MutationResource;
|
||||
use App\Http\Requests\StoreMutationRequest;
|
||||
use App\Models\Dynamic;
|
||||
use App\Models\Ledger;
|
||||
use App\Models\Mutation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
|
||||
class MutationController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
@ -32,13 +36,10 @@ class MutationController extends Controller
|
||||
*/
|
||||
public function store(StoreMutationRequest $request, Dynamic $dynamic, Ledger $ledger)
|
||||
{
|
||||
$isOwner = $dynamic->participants()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('role', 'owner')
|
||||
->exists();
|
||||
$this->authorize('create', [Mutation::class, $ledger]);
|
||||
|
||||
// If the user is an owner, default status to 'approved'. Otherwise default to 'pending'.
|
||||
$status = $isOwner ? 'approved' : 'pending';
|
||||
$status = $request->user()->can('update', $ledger) ? 'approved' : 'pending';
|
||||
|
||||
$mutation = DB::transaction(function () use ($request, $ledger, $status) {
|
||||
$mutation = $ledger->mutations()->create([
|
||||
@ -78,7 +79,9 @@ class MutationController extends Controller
|
||||
*/
|
||||
public function show(Dynamic $dynamic, Ledger $ledger, Mutation $mutation)
|
||||
{
|
||||
//
|
||||
$this->authorize('view', $mutation);
|
||||
|
||||
return new MutationResource($mutation);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,15 +97,7 @@ class MutationController extends Controller
|
||||
*/
|
||||
public function update(Request $request, Dynamic $dynamic, Ledger $ledger, Mutation $mutation)
|
||||
{
|
||||
// 1. Authorize - only owners can update mutation status!
|
||||
$isOwner = $dynamic->participants()
|
||||
->where('user_id', $request->user()->id)
|
||||
->where('role', 'owner')
|
||||
->exists();
|
||||
|
||||
if (!$isOwner) {
|
||||
abort(403, 'Only dynamic owners can approve or reject mutations.');
|
||||
}
|
||||
$this->authorize('update', $mutation);
|
||||
|
||||
$request->validate([
|
||||
'status' => ['required', 'string', 'in:approved,rejected'],
|
||||
@ -157,6 +152,15 @@ class MutationController extends Controller
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function void(Request $request, Dynamic $dynamic, Ledger $ledger, Mutation $mutation)
|
||||
{
|
||||
$this->authorize('void', $mutation);
|
||||
|
||||
$mutation->update(['status' => 'voided']);
|
||||
|
||||
return redirect()->route('dynamics.ledgers.show', [$dynamic, $ledger]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
|
||||
@ -16,21 +16,20 @@ class PredefinedMutationController extends Controller
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Dynamic $dynamic, Ledger $ledger)
|
||||
public function index(Dynamic $dynamic)
|
||||
{
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
return Inertia::render('Ledgers/PredefinedMutations/Index', [
|
||||
return Inertia::render('Dynamics/PredefinedMutations/Index', [
|
||||
'dynamic' => $dynamic,
|
||||
'ledger' => $ledger,
|
||||
'predefined_mutations' => $ledger->predefinedMutations()->latest()->get(),
|
||||
'predefined_mutations' => $dynamic->predefinedMutations()->latest()->get(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request, Dynamic $dynamic, Ledger $ledger)
|
||||
public function store(Request $request, Dynamic $dynamic)
|
||||
{
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
@ -38,10 +37,55 @@ class PredefinedMutationController extends Controller
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'amount' => ['required', 'integer'],
|
||||
'type' => ['required', 'string', 'in:reward,penalty'],
|
||||
]);
|
||||
|
||||
$ledger->predefinedMutations()->create($request->all());
|
||||
$dynamic->predefinedMutations()->create($request->all());
|
||||
|
||||
return redirect()->route('dynamics.ledgers.predefined-mutations.index', [$dynamic, $ledger]);
|
||||
return redirect()->route('dynamics.predefined-mutations.index', $dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Dynamic $dynamic, PredefinedMutation $predefinedMutation)
|
||||
{
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
return Inertia::render('Dynamics/PredefinedMutations/Edit', [
|
||||
'dynamic' => $dynamic,
|
||||
'predefined_mutation' => $predefinedMutation,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Dynamic $dynamic, PredefinedMutation $predefinedMutation)
|
||||
{
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'amount' => ['required', 'integer'],
|
||||
'type' => ['required', 'string', 'in:reward,penalty'],
|
||||
]);
|
||||
|
||||
$predefinedMutation->update($request->all());
|
||||
|
||||
return redirect()->route('dynamics.predefined-mutations.index', $dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Dynamic $dynamic, PredefinedMutation $predefinedMutation)
|
||||
{
|
||||
$this->authorize('update', $dynamic);
|
||||
|
||||
$predefinedMutation->delete();
|
||||
|
||||
return redirect()->route('dynamics.predefined-mutations.index', $dynamic);
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ class HandleInertiaRequests extends Middleware
|
||||
}
|
||||
|
||||
$service = app(\App\Services\ActivityService::class);
|
||||
return count($service->getUnreadEntitiesGrouped($request->user()));
|
||||
return count($service->getUnreadDynamicsGrouped($request->user()));
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
26
app/Http/Resources/BaseResource.php
Normal file
26
app/Http/Resources/BaseResource.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BaseResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$data = parent::toArray($request);
|
||||
|
||||
if (isset($data['id']) && isset($this->uuid)) {
|
||||
$data['id'] = $this->uuid;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
19
app/Http/Resources/DynamicResource.php
Normal file
19
app/Http/Resources/DynamicResource.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class DynamicResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Resources/LedgerResource.php
Normal file
19
app/Http/Resources/LedgerResource.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class LedgerResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
18
app/Http/Resources/MessageResource.php
Normal file
18
app/Http/Resources/MessageResource.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MessageResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Resources/MutationResource.php
Normal file
19
app/Http/Resources/MutationResource.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class MutationResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Resources/PredefinedMutationResource.php
Normal file
19
app/Http/Resources/PredefinedMutationResource.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class PredefinedMutationResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Resources/UserResource.php
Normal file
19
app/Http/Resources/UserResource.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class UserResource extends BaseResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return parent::toArray($request);
|
||||
}
|
||||
}
|
||||
@ -34,6 +34,11 @@ class Dynamic extends Model
|
||||
return $this->hasMany(DynamicInvitation::class);
|
||||
}
|
||||
|
||||
public function predefinedMutations(): HasMany
|
||||
{
|
||||
return $this->hasMany(PredefinedMutation::class);
|
||||
}
|
||||
|
||||
public function chat(): MorphOne
|
||||
{
|
||||
return $this->morphOne(Chat::class, 'chatable');
|
||||
@ -41,8 +46,17 @@ class Dynamic extends Model
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
});
|
||||
|
||||
static::created(function (Dynamic $dynamic) {
|
||||
$dynamic->chat()->create([]);
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ class Ledger extends Model
|
||||
'rules',
|
||||
'score',
|
||||
'alignment',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function dynamic(): BelongsTo
|
||||
@ -40,4 +41,16 @@ class Ledger extends Model
|
||||
{
|
||||
return $this->morphMany(Media::class, 'mediable');
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,10 @@ class Mutation extends Model
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
});
|
||||
|
||||
static::created(function (Mutation $mutation) {
|
||||
$mutation->chat()->create([]);
|
||||
|
||||
@ -87,4 +91,9 @@ class Mutation extends Model
|
||||
broadcast(new \App\Events\MessageSent($dynamicMsg));
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,27 @@ class PredefinedMutation extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'ledger_id',
|
||||
'dynamic_id',
|
||||
'name',
|
||||
'description',
|
||||
'amount',
|
||||
'type',
|
||||
];
|
||||
|
||||
public function ledger(): BelongsTo
|
||||
public function dynamic(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Ledger::class);
|
||||
return $this->belongsTo(Dynamic::class);
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,4 +69,16 @@ class User extends Authenticatable implements PasskeyUser
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::creating(function ($model) {
|
||||
$model->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
});
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'uuid';
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,15 @@ class LedgerPolicy
|
||||
*/
|
||||
public function update(User $user, Ledger $ledger): bool
|
||||
{
|
||||
return false;
|
||||
return $user->can('update', $ledger->dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can close the model.
|
||||
*/
|
||||
public function close(User $user, Ledger $ledger): bool
|
||||
{
|
||||
return $user->can('update', $ledger->dynamic);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2,18 +2,39 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Ledger;
|
||||
use App\Models\Mutation;
|
||||
use App\Models\User;
|
||||
|
||||
class MutationPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view the mutation.
|
||||
* Determine whether the user can create mutations.
|
||||
*/
|
||||
public function view(User $user, Mutation $mutation): bool
|
||||
public function create(User $user, Ledger $ledger): bool
|
||||
{
|
||||
$dynamic = $mutation->ledger->dynamic;
|
||||
$dynamic = $ledger->dynamic;
|
||||
|
||||
return $dynamic->participants()->where('user_id', $user->id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the mutation.
|
||||
*/
|
||||
public function update(User $user, Mutation $mutation): bool
|
||||
{
|
||||
$dynamic = $mutation->ledger->dynamic;
|
||||
|
||||
return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can void the mutation.
|
||||
*/
|
||||
public function void(User $user, Mutation $mutation): bool
|
||||
{
|
||||
$dynamic = $mutation->ledger->dynamic;
|
||||
|
||||
return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ namespace App\Providers;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
@ -23,6 +24,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
JsonResource::withoutWrapping();
|
||||
$this->configureDefaults();
|
||||
}
|
||||
|
||||
|
||||
@ -73,25 +73,15 @@ class ActivityService
|
||||
/**
|
||||
* Retrieve all activities for a given entity.
|
||||
*/
|
||||
public function getActivitiesForEntity($entity): array
|
||||
public function getActivitiesForDynamic(Dynamic $dynamic): array
|
||||
{
|
||||
if ($entity instanceof Dynamic) {
|
||||
$chatId = $entity->chat->id;
|
||||
$dynamic = $entity;
|
||||
} elseif ($entity instanceof Ledger) {
|
||||
$dynamic = $entity->dynamic;
|
||||
$chatId = $dynamic->chat->id;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
$participants = $dynamic->participants()->withPivot('display_name')->get();
|
||||
$participantsMap = $participants->reduce(function ($acc, $p) {
|
||||
$acc[$p->id] = $p->pivot->display_name ?? $p->name;
|
||||
return $acc;
|
||||
}, []);
|
||||
|
||||
$messages = Message::where('chat_id', $chatId)
|
||||
$messages = Message::where('chat_id', $dynamic->chat->id)
|
||||
->with(['user', 'subject'])
|
||||
->latest()
|
||||
->get();
|
||||
@ -113,27 +103,25 @@ class ActivityService
|
||||
/**
|
||||
* Get unread activities grouped by active entities (Dynamics, Ledgers) for the given user.
|
||||
*/
|
||||
public function getUnreadEntitiesGrouped(User $user): array
|
||||
public function getUnreadDynamicsGrouped(User $user): array
|
||||
{
|
||||
$groupedEntities = [];
|
||||
$groupedDynamics = [];
|
||||
$participatingDynamics = $user->dynamics()->with('ledgers')->get();
|
||||
|
||||
$entities = $participatingDynamics->concat($participatingDynamics->flatMap(fn ($d) => $d->ledgers));
|
||||
foreach ($participatingDynamics as $dynamic) {
|
||||
$readAt = $this->getCursorReadAt($user, $dynamic);
|
||||
$activities = $this->getActivitiesForDynamic($dynamic);
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$readAt = $this->getCursorReadAt($user, $entity);
|
||||
$activities = $this->getActivitiesForEntity($entity);
|
||||
|
||||
$this->partitionActivities($activities, $readAt, $entity, get_class($entity), $this->getUrlForEntity($entity), $groupedEntities);
|
||||
$this->partitionAndGroupActivities($activities, $readAt, $dynamic, $groupedDynamics);
|
||||
}
|
||||
|
||||
return $groupedEntities;
|
||||
return $groupedDynamics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition activities into read and unread, and construct the grouped entity metadata.
|
||||
*/
|
||||
private function partitionActivities(array $activities, \Carbon\CarbonInterface $readAt, $entity, string $type, string $url, array &$groupedEntities): void
|
||||
private function partitionAndGroupActivities(array $activities, \Carbon\CarbonInterface $readAt, Dynamic $dynamic, array &$groupedDynamics): void
|
||||
{
|
||||
$alreadyRead = [];
|
||||
$unread = [];
|
||||
@ -149,11 +137,10 @@ class ActivityService
|
||||
if (!empty($unread)) {
|
||||
$context = array_slice($alreadyRead, 0, 2);
|
||||
|
||||
$groupedEntities[] = [
|
||||
'id' => $entity->id,
|
||||
'name' => $entity->name,
|
||||
'type' => Str::afterLast($type, '\\'),
|
||||
'url' => $url,
|
||||
$groupedDynamics[] = [
|
||||
'id' => $dynamic->id,
|
||||
'name' => $dynamic->name,
|
||||
'url' => route('dynamics.show', $dynamic->uuid),
|
||||
'unread_count' => count($unread),
|
||||
'context_activities' => $context,
|
||||
'new_activities' => array_reverse($unread),
|
||||
@ -164,11 +151,11 @@ class ActivityService
|
||||
private function getUrlForEntity($entity): string
|
||||
{
|
||||
if ($entity instanceof Dynamic) {
|
||||
return route('dynamics.show', $entity->id);
|
||||
return route('dynamics.show', $entity->uuid);
|
||||
}
|
||||
|
||||
if ($entity instanceof Ledger) {
|
||||
return route('dynamics.ledgers.show', [$entity->dynamic_id, $entity->id]);
|
||||
return route('dynamics.ledgers.show', [$entity->dynamic->uuid, $entity->uuid]);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@ -18,7 +18,7 @@ return new class extends Migration
|
||||
$table->string('type');
|
||||
$table->integer('amount');
|
||||
$table->text('description')->nullable();
|
||||
$table->string('status')->default('pending');
|
||||
$table->string('status')->default('pending'); // pending, approved, rejected, voided
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@ -13,10 +13,11 @@ return new class extends Migration
|
||||
{
|
||||
Schema::create('predefined_mutations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ledger_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('dynamic_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->text('description')->nullable();
|
||||
$table->integer('amount');
|
||||
$table->string('type')->default('reward');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
@ -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::table('ledgers', function (Blueprint $table) {
|
||||
$table->string('status')->default('open');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ledgers', function (Blueprint $table) {
|
||||
$table->dropColumn('status');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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::table('mutations', function (Blueprint $table) {
|
||||
$table->string('status')->default('pending')->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mutations', function (Blueprint $table) {
|
||||
$table->string('status')->default('pending')->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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::table('dynamics', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Populate existing rows with UUIDs
|
||||
\App\Models\Dynamic::all()->each(function ($model) {
|
||||
$model->uuid = \Illuminate\Support\Str::uuid();
|
||||
$model->save();
|
||||
});
|
||||
|
||||
Schema::table('dynamics', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('dynamics', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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::table('ledgers', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Populate existing rows with UUIDs
|
||||
\App\Models\Ledger::all()->each(function ($model) {
|
||||
$model->uuid = \Illuminate\Support\Str::uuid();
|
||||
$model->save();
|
||||
});
|
||||
|
||||
Schema::table('ledgers', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ledgers', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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::table('mutations', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Populate existing rows with UUIDs
|
||||
\App\Models\Mutation::all()->each(function ($model) {
|
||||
$model->uuid = \Illuminate\Support\Str::uuid();
|
||||
$model->save();
|
||||
});
|
||||
|
||||
Schema::table('mutations', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mutations', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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::table('predefined_mutations', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Populate existing rows with UUIDs
|
||||
\App\Models\PredefinedMutation::all()->each(function ($model) {
|
||||
$model->uuid = \Illuminate\Support\Str::uuid();
|
||||
$model->save();
|
||||
});
|
||||
|
||||
Schema::table('predefined_mutations', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('predefined_mutations', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,38 @@
|
||||
<?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::table('users', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->after('id')->nullable();
|
||||
});
|
||||
|
||||
// Populate existing rows with UUIDs
|
||||
\App\Models\User::all()->each(function ($model) {
|
||||
$model->uuid = \Illuminate\Support\Str::uuid();
|
||||
$model->save();
|
||||
});
|
||||
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->uuid('uuid')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('uuid');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -40,6 +40,16 @@ function updateStatus(mutationId: number, status: 'approved' | 'rejected') {
|
||||
);
|
||||
}
|
||||
|
||||
function voidMutation(mutationId: number) {
|
||||
useForm({}).put(
|
||||
route('dynamics.ledgers.mutations.void', {
|
||||
dynamic: props.dynamicId,
|
||||
ledger: props.ledgerId,
|
||||
mutation: mutationId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function isOwnerUser(userId: number): boolean {
|
||||
const participant = props.participants?.find((p) => p.id === userId);
|
||||
|
||||
@ -157,21 +167,30 @@ function getAmountClass(amount: number): string {
|
||||
|
||||
<!-- Owner Approve/Reject Actions -->
|
||||
<div
|
||||
v-if="isOwner && mutation.status === 'pending'"
|
||||
v-if="isOwner && (mutation.status === 'pending' || mutation.status === 'approved')"
|
||||
class="c-mutation-list__actions"
|
||||
>
|
||||
<button
|
||||
v-if="mutation.status === 'pending'"
|
||||
@click="updateStatus(mutation.id, 'approved')"
|
||||
class="c-mutation-list__approve-btn"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
v-if="mutation.status === 'pending'"
|
||||
@click="updateStatus(mutation.id, 'rejected')"
|
||||
class="c-mutation-list__reject-btn"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
v-if="mutation.status !== 'voided'"
|
||||
@click="voidMutation(mutation.id)"
|
||||
class="c-mutation-list__void-btn"
|
||||
>
|
||||
Void
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Chat :chat="mutation.chat" :dynamic-id="dynamicId" :participants="participants" />
|
||||
@ -290,6 +309,10 @@ function getAmountClass(amount: number): string {
|
||||
@apply inline-flex cursor-pointer items-center rounded bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-500;
|
||||
}
|
||||
|
||||
.c-mutation-list__void-btn {
|
||||
@apply inline-flex cursor-pointer items-center rounded bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-gray-500;
|
||||
}
|
||||
|
||||
.c-mutation-list__empty {
|
||||
@apply mt-4 text-gray-500;
|
||||
}
|
||||
|
||||
@ -14,10 +14,9 @@ defineOptions({
|
||||
});
|
||||
|
||||
defineProps<{
|
||||
unreadEntities: Array<{
|
||||
unreadDynamics: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'Dynamic' | 'Ledger';
|
||||
url: string;
|
||||
unread_count: number;
|
||||
context_activities: Array<{
|
||||
@ -51,34 +50,27 @@ function formatTime(isoString: string): string {
|
||||
<div class="c-dashboard__container">
|
||||
<h2 class="c-dashboard__title">Recent Activity</h2>
|
||||
|
||||
<div v-if="unreadEntities.length > 0" class="c-dashboard__grid">
|
||||
<div v-if="unreadDynamics.length > 0" class="c-dashboard__grid">
|
||||
<div
|
||||
v-for="entity in unreadEntities"
|
||||
:key="`${entity.type}_${entity.id}`"
|
||||
v-for="dynamic in unreadDynamics"
|
||||
:key="dynamic.id"
|
||||
class="c-dashboard__card"
|
||||
>
|
||||
<div class="c-dashboard__card-header">
|
||||
<div class="c-dashboard__entity-meta">
|
||||
<span
|
||||
:class="[
|
||||
'c-dashboard__badge-type',
|
||||
entity.type === 'Dynamic'
|
||||
? 'c-dashboard__badge-type--dynamic'
|
||||
: 'c-dashboard__badge-type--ledger',
|
||||
]"
|
||||
>
|
||||
{{ entity.type }}
|
||||
<span class="c-dashboard__badge-type c-dashboard__badge-type--dynamic">
|
||||
Dynamic
|
||||
</span>
|
||||
<span class="c-dashboard__unread-count">
|
||||
{{ entity.unread_count }} New
|
||||
{{ dynamic.unread_count }} New
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
:href="entity.url"
|
||||
:href="dynamic.url"
|
||||
class="c-dashboard__entity-link"
|
||||
>
|
||||
<h3 class="c-dashboard__entity-title">
|
||||
{{ entity.name }}
|
||||
{{ dynamic.name }}
|
||||
</h3>
|
||||
</Link>
|
||||
</div>
|
||||
@ -86,7 +78,7 @@ function formatTime(isoString: string): string {
|
||||
<div class="c-dashboard__activity-list">
|
||||
<!-- Context / Read Activities -->
|
||||
<div
|
||||
v-for="activity in entity.context_activities"
|
||||
v-for="activity in dynamic.context_activities"
|
||||
:key="activity.id"
|
||||
class="c-dashboard__activity-item c-dashboard__activity-item--read"
|
||||
>
|
||||
@ -107,7 +99,7 @@ function formatTime(isoString: string): string {
|
||||
|
||||
<!-- Unread Separator Line -->
|
||||
<div
|
||||
v-if="entity.new_activities.length > 0"
|
||||
v-if="dynamic.new_activities.length > 0"
|
||||
class="c-dashboard__divider"
|
||||
>
|
||||
<span class="c-dashboard__divider-text"
|
||||
@ -117,7 +109,7 @@ function formatTime(isoString: string): string {
|
||||
|
||||
<!-- New / Unread Activities -->
|
||||
<div
|
||||
v-for="activity in entity.new_activities"
|
||||
v-for="activity in dynamic.new_activities"
|
||||
:key="activity.id"
|
||||
class="c-dashboard__activity-item c-dashboard__activity-item--unread"
|
||||
>
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
defineProps({
|
||||
dynamics: Array,
|
||||
});
|
||||
defineProps<{
|
||||
dynamics: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
rules: string;
|
||||
}>;
|
||||
}>();
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
|
||||
154
resources/js/pages/Dynamics/PredefinedMutations/Edit.vue
Normal file
154
resources/js/pages/Dynamics/PredefinedMutations/Edit.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
dynamic: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
predefined_mutation: {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
type: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
name: props.predefined_mutation.name,
|
||||
description: props.predefined_mutation.description,
|
||||
amount: props.predefined_mutation.amount,
|
||||
type: props.predefined_mutation.type,
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
name: 'Dynamics',
|
||||
href: route('dynamics.index'),
|
||||
},
|
||||
{
|
||||
name: props.dynamic.name,
|
||||
href: route('dynamics.show', props.dynamic.id),
|
||||
},
|
||||
{
|
||||
name: 'Predefined Mutations',
|
||||
href: route('dynamics.predefined-mutations.index', props.dynamic.id),
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
href: route('dynamics.predefined-mutations.edit', [props.dynamic.id, props.predefined_mutation.id]),
|
||||
},
|
||||
];
|
||||
|
||||
function submit() {
|
||||
form.put(route('dynamics.predefined-mutations.update', [props.dynamic.id, props.predefined_mutation.id]));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Edit Predefined Mutation" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="c-predefined-mutation-edit">
|
||||
<div class="c-predefined-mutation-edit__container">
|
||||
<div class="c-predefined-mutation-edit__card">
|
||||
<div class="c-predefined-mutation-edit__body">
|
||||
<h3 class="c-predefined-mutation-edit__title">
|
||||
Edit {{ predefined_mutation.name }}
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="submit" class="c-predefined-mutation-edit__form">
|
||||
<div class="c-predefined-mutation-edit__field">
|
||||
<label for="name" class="c-predefined-mutation-edit__label">Name</label>
|
||||
<input v-model="form.name" id="name" type="text" class="c-predefined-mutation-edit__input" />
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutation-edit__field">
|
||||
<label for="description" class="c-predefined-mutation-edit__label">Description</label>
|
||||
<textarea v-model="form.description" id="description" rows="4" class="c-predefined-mutation-edit__textarea"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutation-edit__field">
|
||||
<label for="amount" class="c-predefined-mutation-edit__label">Amount</label>
|
||||
<input v-model="form.amount" id="amount" type="number" class="c-predefined-mutation-edit__input" />
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutation-edit__field">
|
||||
<label for="type" class="c-predefined-mutation-edit__label">Type</label>
|
||||
<select v-model="form.type" id="type" class="c-predefined-mutation-edit__select">
|
||||
<option value="reward">Reward</option>
|
||||
<option value="penalty">Penalty</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutation-edit__actions">
|
||||
<button type="submit" :disabled="form.processing" class="c-predefined-mutation-edit__submit-btn">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "../../../../css/app.css";
|
||||
|
||||
.c-predefined-mutation-edit {
|
||||
@apply py-12;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__container {
|
||||
@apply mx-auto max-w-7xl sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__card {
|
||||
@apply overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__body {
|
||||
@apply p-6 text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__title {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__form {
|
||||
@apply mt-6 space-y-6;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__field {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__input {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__textarea {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__select {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 bg-white p-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__actions {
|
||||
@apply flex items-center gap-4;
|
||||
}
|
||||
|
||||
.c-predefined-mutation-edit__submit-btn {
|
||||
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
|
||||
}
|
||||
</style>
|
||||
266
resources/js/pages/Dynamics/PredefinedMutations/Index.vue
Normal file
266
resources/js/pages/Dynamics/PredefinedMutations/Index.vue
Normal file
@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
dynamic: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
predefined_mutations: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
type: string;
|
||||
}>;
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
description: '',
|
||||
amount: 0,
|
||||
type: 'reward',
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
name: 'Dynamics',
|
||||
href: route('dynamics.index'),
|
||||
},
|
||||
{
|
||||
name: props.dynamic.name,
|
||||
href: route('dynamics.show', props.dynamic.id),
|
||||
},
|
||||
{
|
||||
name: 'Predefined Mutations',
|
||||
href: route('dynamics.predefined-mutations.index', props.dynamic.id),
|
||||
},
|
||||
];
|
||||
|
||||
function submit() {
|
||||
form.post(route('dynamics.predefined-mutations.store', props.dynamic.id), {
|
||||
onSuccess: () => form.reset(),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Predefined Mutations" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="c-predefined-mutations">
|
||||
<div class="c-predefined-mutations__container">
|
||||
<div class="c-predefined-mutations__card">
|
||||
<div class="c-predefined-mutations__body">
|
||||
<h3 class="c-predefined-mutations__title">
|
||||
Predefined Mutations for {{ dynamic.name }}
|
||||
</h3>
|
||||
|
||||
<div class="c-predefined-mutations__list">
|
||||
<div
|
||||
v-for="mutation in predefined_mutations"
|
||||
:key="mutation.id"
|
||||
class="c-predefined-mutations__item"
|
||||
>
|
||||
<div class="c-predefined-mutations__item-details">
|
||||
<h4 class="c-predefined-mutations__item-name">
|
||||
{{ mutation.name }}
|
||||
</h4>
|
||||
<p class="c-predefined-mutations__item-description">
|
||||
{{ mutation.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="c-predefined-mutations__item-amount">
|
||||
{{ mutation.amount }}
|
||||
</div>
|
||||
<div class="c-predefined-mutations__item-actions">
|
||||
<Link :href="route('dynamics.predefined-mutations.edit', [dynamic.id, mutation.id])" class="c-predefined-mutations__item-action-btn">
|
||||
Edit
|
||||
</Link>
|
||||
<Link :href="route('dynamics.predefined-mutations.destroy', [dynamic.id, mutation.id])" method="delete" as="button" class="c-predefined-mutations__item-action-btn c-predefined-mutations__item-action-btn--danger">
|
||||
Delete
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutations__card mt-8">
|
||||
<div class="c-predefined-mutations__body">
|
||||
<h3 class="c-predefined-mutations__title">
|
||||
Create New Predefined Mutation
|
||||
</h3>
|
||||
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="c-predefined-mutations__form"
|
||||
>
|
||||
<div class="c-predefined-mutations__field">
|
||||
<label
|
||||
for="name"
|
||||
class="c-predefined-mutations__label"
|
||||
>Name</label
|
||||
>
|
||||
<input
|
||||
v-model="form.name"
|
||||
id="name"
|
||||
type="text"
|
||||
class="c-predefined-mutations__input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutations__field">
|
||||
<label
|
||||
for="description"
|
||||
class="c-predefined-mutations__label"
|
||||
>Description</label
|
||||
>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
id="description"
|
||||
rows="4"
|
||||
class="c-predefined-mutations__textarea"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutations__field">
|
||||
<label
|
||||
for="amount"
|
||||
class="c-predefined-mutations__label"
|
||||
>Amount</label
|
||||
>
|
||||
<input
|
||||
v-model="form.amount"
|
||||
id="amount"
|
||||
type="number"
|
||||
class="c-predefined-mutations__input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutations__field">
|
||||
<label
|
||||
for="type"
|
||||
class="c-predefined-mutations__label"
|
||||
>Type</label
|
||||
>
|
||||
<select
|
||||
v-model="form.type"
|
||||
id="type"
|
||||
class="c-predefined-mutations__select"
|
||||
>
|
||||
<option value="reward">Reward</option>
|
||||
<option value="penalty">Penalty</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="c-predefined-mutations__actions">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="c-predefined-mutations__submit-btn"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "../../../../css/app.css";
|
||||
|
||||
.c-predefined-mutations {
|
||||
@apply py-12;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__container {
|
||||
@apply mx-auto max-w-7xl sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__card {
|
||||
@apply overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__body {
|
||||
@apply p-6 text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__title {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__list {
|
||||
@apply mt-6 space-y-4;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item {
|
||||
@apply flex items-center justify-between rounded-lg border p-4 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-details {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-name {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-description {
|
||||
@apply text-sm text-gray-600 dark:text-gray-400;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-amount {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-actions {
|
||||
@apply flex gap-2;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-action-btn {
|
||||
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-3 py-1.5 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__item-action-btn--danger {
|
||||
@apply bg-red-600 hover:bg-red-500 focus:bg-red-500 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:focus:bg-red-400 dark:active:bg-red-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__form {
|
||||
@apply mt-6 space-y-6;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__field {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__input {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__textarea {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__select {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 bg-white p-2 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__actions {
|
||||
@apply flex items-center gap-4;
|
||||
}
|
||||
|
||||
.c-predefined-mutations__submit-btn {
|
||||
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
|
||||
}
|
||||
</style>
|
||||
145
resources/js/pages/Ledgers/Edit.vue
Normal file
145
resources/js/pages/Ledgers/Edit.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
dynamic: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
ledger: {
|
||||
id: number;
|
||||
name: string;
|
||||
rules: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const form = useForm({
|
||||
name: props.ledger.name,
|
||||
rules: props.ledger.rules,
|
||||
});
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
name: 'Dynamics',
|
||||
href: route('dynamics.index'),
|
||||
},
|
||||
{
|
||||
name: props.dynamic.name,
|
||||
href: route('dynamics.show', props.dynamic.id),
|
||||
},
|
||||
{
|
||||
name: props.ledger.name,
|
||||
href: route('dynamics.ledgers.show', [props.dynamic.id, props.ledger.id]),
|
||||
},
|
||||
{
|
||||
name: 'Edit',
|
||||
href: route('dynamics.ledgers.edit', [props.dynamic.id, props.ledger.id]),
|
||||
},
|
||||
];
|
||||
|
||||
function submit() {
|
||||
form.put(route('dynamics.ledgers.update', [props.dynamic.id, props.ledger.id]));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Edit Ledger" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="c-ledger-edit">
|
||||
<div class="c-ledger-edit__container">
|
||||
<div class="c-ledger-edit__card">
|
||||
<div class="c-ledger-edit__body">
|
||||
<h3 class="c-ledger-edit__title">
|
||||
Edit {{ ledger.name }}
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="submit" class="c-ledger-edit__form">
|
||||
<div class="c-ledger-edit__field">
|
||||
<label for="name" class="c-ledger-edit__label">Name</label>
|
||||
<input v-model="form.name" id="name" type="text" class="c-ledger-edit__input" />
|
||||
</div>
|
||||
|
||||
<div class="c-ledger-edit__field">
|
||||
<label for="rules" class="c-ledger-edit__label">Rules</label>
|
||||
<textarea v-model="form.rules" id="rules" rows="4" class="c-ledger-edit__textarea"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="c-ledger-edit__actions">
|
||||
<button type="submit" :disabled="form.processing" class="c-ledger-edit__submit-btn">
|
||||
Save
|
||||
</button>
|
||||
<Link
|
||||
:href="route('dynamics.ledgers.close', [dynamic.id, ledger.id])"
|
||||
method="put"
|
||||
as="button"
|
||||
class="c-ledger-edit__submit-btn c-ledger-edit__submit-btn--danger"
|
||||
>
|
||||
Close Ledger
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "../../../css/app.css";
|
||||
|
||||
.c-ledger-edit {
|
||||
@apply py-12;
|
||||
}
|
||||
|
||||
.c-ledger-edit__container {
|
||||
@apply mx-auto max-w-7xl sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.c-ledger-edit__card {
|
||||
@apply overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800;
|
||||
}
|
||||
|
||||
.c-ledger-edit__body {
|
||||
@apply p-6 text-gray-900 dark:text-gray-100;
|
||||
}
|
||||
|
||||
.c-ledger-edit__title {
|
||||
@apply text-lg font-medium;
|
||||
}
|
||||
|
||||
.c-ledger-edit__form {
|
||||
@apply mt-6 space-y-6;
|
||||
}
|
||||
|
||||
.c-ledger-edit__field {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.c-ledger-edit__label {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.c-ledger-edit__input {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-ledger-edit__textarea {
|
||||
@apply mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-ledger-edit__actions {
|
||||
@apply flex items-center gap-4;
|
||||
}
|
||||
|
||||
.c-ledger-edit__submit-btn {
|
||||
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
|
||||
}
|
||||
|
||||
.c-ledger-edit__submit-btn--danger {
|
||||
@apply bg-red-600 hover:bg-red-500 focus:bg-red-500 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:focus:bg-red-400 dark:active:bg-red-600;
|
||||
}
|
||||
</style>
|
||||
@ -212,6 +212,12 @@ function isOwnerUser(userId: number): boolean {
|
||||
>
|
||||
Predefined Mutations
|
||||
</InertiaLink>
|
||||
<InertiaLink
|
||||
:href="route('dynamics.ledgers.edit', [dynamic.id, ledger.id])"
|
||||
class="c-ledger-show__manage-btn"
|
||||
>
|
||||
Edit Ledger
|
||||
</InertiaLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -19,10 +19,12 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
||||
Route::get('/dynamics/{dynamic}/ledgers/create', [LedgerController::class, 'create'])->name('dynamics.ledgers.create');
|
||||
Route::get('/dynamics/{dynamic}/ledgers/{ledger}/messages', [LedgerController::class, 'messages'])->name('dynamics.ledgers.messages');
|
||||
Route::put('/dynamics/{dynamic}/ledgers/{ledger}/close', [LedgerController::class, 'close'])->name('dynamics.ledgers.close');
|
||||
Route::resource('dynamics.ledgers', LedgerController::class)->scoped()->except(['create']);
|
||||
|
||||
Route::resource('dynamics.ledgers.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped();
|
||||
Route::resource('dynamics.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped();
|
||||
|
||||
Route::put('/dynamics/{dynamic}/ledgers/{ledger}/mutations/{mutation}/void', [\App\Http\Controllers\MutationController::class, 'void'])->name('dynamics.ledgers.mutations.void');
|
||||
Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped();
|
||||
|
||||
Route::get('/dynamics/{dynamic}/invitations/create', [\App\Http\Controllers\DynamicInvitationController::class, 'create'])->name('dynamics.invitations.create');
|
||||
|
||||
@ -19,7 +19,7 @@ test('authenticated users can visit the dashboard', function () {
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page->component('Dashboard')->has('unreadEntities'));
|
||||
$response->assertInertia(fn ($page) => $page->component('Dashboard')->has('unreadDynamics'));
|
||||
});
|
||||
|
||||
test('visiting dynamic updates the read cursor', function () {
|
||||
@ -34,7 +34,7 @@ test('visiting dynamic updates the read cursor', function () {
|
||||
expect($initialCursor->toIso8601String())->toBe('1970-01-01T00:00:00+00:00');
|
||||
|
||||
// Visit Dynamic Show
|
||||
$this->get(route('dynamics.show', $dynamic->id))->assertOk();
|
||||
$this->get(route('dynamics.show', $dynamic->uuid))->assertOk();
|
||||
|
||||
// Re-check cursor is updated to near now
|
||||
$updatedCursor = $service->getCursorReadAt($user, $dynamic);
|
||||
@ -56,7 +56,7 @@ test('visiting ledger updates the read cursor', function () {
|
||||
expect($initialCursor->toIso8601String())->toBe('1970-01-01T00:00:00+00:00');
|
||||
|
||||
// Visit Ledger Show
|
||||
$this->get(route('dynamics.ledgers.show', [$dynamic->id, $ledger->id]))->assertOk();
|
||||
$this->get(route('dynamics.ledgers.show', [$dynamic->uuid, $ledger->uuid]))->assertOk();
|
||||
|
||||
// Re-check cursor is updated to near now
|
||||
$updatedCursor = $service->getCursorReadAt($user, $ledger);
|
||||
@ -101,23 +101,23 @@ test('dashboard groups and filters unread entities correctly based on cursor', f
|
||||
// Verify unread grouping structure
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->where('unreadEntities.0.name', 'Testing Dynamic')
|
||||
->where('unreadEntities.0.unread_count', 1)
|
||||
->has('unreadEntities.0.context_activities', 1) // Should have old message as context
|
||||
->where('unreadEntities.0.context_activities.0.content', 'Old message context')
|
||||
->has('unreadEntities.0.new_activities', 1) // Should have unread message
|
||||
->where('unreadEntities.0.new_activities.0.content', 'New unread message alert')
|
||||
->where('unreadDynamics.0.name', 'Testing Dynamic')
|
||||
->where('unreadDynamics.0.unread_count', 1)
|
||||
->has('unreadDynamics.0.context_activities', 1) // Should have old message as context
|
||||
->where('unreadDynamics.0.context_activities.0.content', 'Old message context')
|
||||
->has('unreadDynamics.0.new_activities', 1) // Should have unread message
|
||||
->where('unreadDynamics.0.new_activities.0.content', 'New unread message alert')
|
||||
);
|
||||
|
||||
// Now visit the Dynamic, which clears the unread count
|
||||
$this->get(route('dynamics.show', $dynamic->id))->assertOk();
|
||||
$this->get(route('dynamics.show', $dynamic->uuid))->assertOk();
|
||||
|
||||
// Dashboard should now show 0 unread groups (caught up)
|
||||
$response2 = $this->get(route('dashboard'));
|
||||
$response2->assertOk();
|
||||
$response2->assertInertia(fn ($page) => $page
|
||||
->component('Dashboard')
|
||||
->has('unreadEntities', 0)
|
||||
->has('unreadDynamics', 0)
|
||||
);
|
||||
|
||||
Carbon::setTestNow(); // Reset test time
|
||||
|
||||
@ -19,7 +19,7 @@ test('only owners can invite other users to a dynamic', function () {
|
||||
|
||||
// 1. Participant tries to send an invite (forbidden)
|
||||
$response = $this->actingAs($participant)
|
||||
->post(route('dynamics.invitations.store', $dynamic), [
|
||||
->post(route('dynamics.invitations.store', $dynamic->uuid), [
|
||||
'email' => 'invitee@example.com',
|
||||
'role' => 'participant',
|
||||
]);
|
||||
@ -29,7 +29,7 @@ test('only owners can invite other users to a dynamic', function () {
|
||||
|
||||
// 2. Owner sends a valid invite (allowed)
|
||||
$response = $this->actingAs($owner)
|
||||
->post(route('dynamics.invitations.store', $dynamic), [
|
||||
->post(route('dynamics.invitations.store', $dynamic->uuid), [
|
||||
'email' => 'invitee@example.com',
|
||||
'role' => 'participant',
|
||||
]);
|
||||
@ -83,7 +83,7 @@ test('only the user with the specified email address can accept the link', funct
|
||||
|
||||
// 3. Intended user accepts the signed invitation link (success)
|
||||
$response = $this->actingAs($invitee)->get($signedUrl);
|
||||
$response->assertRedirect(route('dynamics.show', $dynamic));
|
||||
$response->assertRedirect(route('dynamics.show', $dynamic->uuid));
|
||||
|
||||
// Verify invitee is joined as a participant with the specified role
|
||||
$isJoined = $dynamic->participants()
|
||||
|
||||
@ -12,17 +12,17 @@ test('dynamic owners can view ledger creation form and create ledgers', function
|
||||
$this->actingAs($owner);
|
||||
|
||||
// Can view form
|
||||
$this->get(route('dynamics.ledgers.create', $dynamic->id))->assertOk();
|
||||
$this->get(route('dynamics.ledgers.create', $dynamic->uuid))->assertOk();
|
||||
|
||||
// Can store ledger
|
||||
$response = $this->post(route('dynamics.ledgers.store', $dynamic->id), [
|
||||
$response = $this->post(route('dynamics.ledgers.store', $dynamic->uuid), [
|
||||
'name' => 'Chores Ledger',
|
||||
'rules' => 'Do the tasks.',
|
||||
'alignment' => 'positive',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors();
|
||||
$response->assertRedirect(route('dynamics.show', $dynamic->id));
|
||||
$response->assertRedirect(route('dynamics.show', $dynamic->uuid));
|
||||
|
||||
$this->assertDatabaseHas('ledgers', [
|
||||
'dynamic_id' => $dynamic->id,
|
||||
@ -41,10 +41,10 @@ test('non-owners cannot view ledger creation form or store ledgers', function ()
|
||||
$this->actingAs($participant);
|
||||
|
||||
// Cannot view form
|
||||
$this->get(route('dynamics.ledgers.create', $dynamic->id))->assertStatus(403);
|
||||
$this->get(route('dynamics.ledgers.create', $dynamic->uuid))->assertStatus(403);
|
||||
|
||||
// Cannot store ledger
|
||||
$response = $this->post(route('dynamics.ledgers.store', $dynamic->id), [
|
||||
$response = $this->post(route('dynamics.ledgers.store', $dynamic->uuid), [
|
||||
'name' => 'Illegal Ledger',
|
||||
'rules' => 'This should fail.',
|
||||
'alignment' => 'positive',
|
||||
|
||||
@ -15,7 +15,7 @@ test('authenticated participant can view another participant detail page in dyna
|
||||
|
||||
$this->actingAs($owner);
|
||||
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id]));
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
@ -40,7 +40,7 @@ test('non-participant cannot view participant detail page in dynamic', function
|
||||
|
||||
$this->actingAs($outsider);
|
||||
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id]));
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid]));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
@ -82,7 +82,7 @@ test('participant detail page displays their recent mutations in dynamic', funct
|
||||
|
||||
$this->actingAs($owner);
|
||||
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->id, $participant->id]));
|
||||
$response = $this->get(route('dynamics.users.show', [$dynamic->uuid, $participant->uuid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
|
||||
@ -29,7 +29,7 @@ test('participant can update their display name', function () {
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [
|
||||
'display_name' => 'Ally',
|
||||
]);
|
||||
|
||||
@ -53,7 +53,7 @@ test('display name update requires display_name parameter', function () {
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [
|
||||
'display_name' => '',
|
||||
]);
|
||||
|
||||
@ -68,7 +68,7 @@ test('non participant cannot update display name', function () {
|
||||
|
||||
$this->actingAs($nonParticipant);
|
||||
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->id), [
|
||||
$response = $this->put(route('dynamics.participant.update', $dynamic->uuid), [
|
||||
'display_name' => 'Bobby',
|
||||
]);
|
||||
|
||||
|
||||
@ -5,14 +5,13 @@ use App\Models\Dynamic;
|
||||
use App\Models\Ledger;
|
||||
use App\Models\PredefinedMutation;
|
||||
|
||||
test('owner can view predefined mutations for ledger', function () {
|
||||
test('owner can view predefined mutations for dynamic', function () {
|
||||
$owner = User::factory()->create();
|
||||
$dynamic = Dynamic::factory()->create();
|
||||
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
|
||||
|
||||
$predefined = PredefinedMutation::create([
|
||||
'ledger_id' => $ledger->id,
|
||||
'dynamic_id' => $dynamic->id,
|
||||
'name' => 'Weekly Room Cleaning',
|
||||
'description' => 'Cleaned up the master bedroom',
|
||||
'amount' => 20,
|
||||
@ -20,73 +19,71 @@ test('owner can view predefined mutations for ledger', function () {
|
||||
|
||||
$this->actingAs($owner);
|
||||
|
||||
$response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id]));
|
||||
$response = $this->get(route('dynamics.predefined-mutations.index', $dynamic->uuid));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('Ledgers/PredefinedMutations/Index')
|
||||
->where('ledger.id', $ledger->id)
|
||||
->component('Dynamics/PredefinedMutations/Index')
|
||||
->has('predefined_mutations', 1)
|
||||
->where('predefined_mutations.0.name', 'Weekly Room Cleaning')
|
||||
);
|
||||
});
|
||||
|
||||
test('non-owner cannot view predefined mutations for ledger', function () {
|
||||
test('non-owner cannot view predefined mutations for dynamic', function () {
|
||||
$owner = User::factory()->create();
|
||||
$participant = User::factory()->create();
|
||||
$dynamic = Dynamic::factory()->create();
|
||||
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
|
||||
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
|
||||
|
||||
$this->actingAs($participant);
|
||||
|
||||
$response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id]));
|
||||
$response = $this->get(route('dynamics.predefined-mutations.index', $dynamic->uuid));
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
test('owner can create predefined mutations for ledger', function () {
|
||||
test('owner can create predefined mutations for dynamic', function () {
|
||||
$owner = User::factory()->create();
|
||||
$dynamic = Dynamic::factory()->create();
|
||||
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
|
||||
|
||||
$this->actingAs($owner);
|
||||
|
||||
$response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [
|
||||
$response = $this->post(route('dynamics.predefined-mutations.store', $dynamic->uuid), [
|
||||
'name' => 'Polished mirrors',
|
||||
'description' => 'Mirror polishing in dungeon',
|
||||
'amount' => 15,
|
||||
'type' => 'reward',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('predefined_mutations', [
|
||||
'ledger_id' => $ledger->id,
|
||||
'dynamic_id' => $dynamic->id,
|
||||
'name' => 'Polished mirrors',
|
||||
'amount' => 15,
|
||||
]);
|
||||
});
|
||||
|
||||
test('non-owner cannot create predefined mutations for ledger', function () {
|
||||
test('non-owner cannot create predefined mutations for dynamic', function () {
|
||||
$owner = User::factory()->create();
|
||||
$participant = User::factory()->create();
|
||||
$dynamic = Dynamic::factory()->create();
|
||||
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
|
||||
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
|
||||
|
||||
$this->actingAs($participant);
|
||||
|
||||
$response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [
|
||||
$response = $this->post(route('dynamics.predefined-mutations.store', $dynamic->uuid), [
|
||||
'name' => 'Polished mirrors',
|
||||
'description' => 'Mirror polishing in dungeon',
|
||||
'amount' => 15,
|
||||
'type' => 'reward',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$this->assertDatabaseMissing('predefined_mutations', [
|
||||
'ledger_id' => $ledger->id,
|
||||
'dynamic_id' => $dynamic->id,
|
||||
'name' => 'Polished mirrors',
|
||||
]);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user