diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 0138c0d..c4b1c82 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -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, ]); } } diff --git a/app/Http/Controllers/DynamicController.php b/app/Http/Controllers/DynamicController.php index 00d581f..3fef526 100644 --- a/app/Http/Controllers/DynamicController.php +++ b/app/Http/Controllers/DynamicController.php @@ -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), ]); } diff --git a/app/Http/Controllers/DynamicInvitationController.php b/app/Http/Controllers/DynamicInvitationController.php index a39714f..6d35a74 100644 --- a/app/Http/Controllers/DynamicInvitationController.php +++ b/app/Http/Controllers/DynamicInvitationController.php @@ -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!'); } } diff --git a/app/Http/Controllers/LedgerController.php b/app/Http/Controllers/LedgerController.php index b428dab..c9ac0cb 100644 --- a/app/Http/Controllers/LedgerController.php +++ b/app/Http/Controllers/LedgerController.php @@ -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]); } /** diff --git a/app/Http/Controllers/MutationController.php b/app/Http/Controllers/MutationController.php index 5d31afd..65da010 100644 --- a/app/Http/Controllers/MutationController.php +++ b/app/Http/Controllers/MutationController.php @@ -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. */ diff --git a/app/Http/Controllers/PredefinedMutationController.php b/app/Http/Controllers/PredefinedMutationController.php index e669fcd..ae02836 100644 --- a/app/Http/Controllers/PredefinedMutationController.php +++ b/app/Http/Controllers/PredefinedMutationController.php @@ -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); } } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 987e593..0043228 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -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())); }, ]; } diff --git a/app/Http/Resources/BaseResource.php b/app/Http/Resources/BaseResource.php new file mode 100644 index 0000000..1db8906 --- /dev/null +++ b/app/Http/Resources/BaseResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + $data = parent::toArray($request); + + if (isset($data['id']) && isset($this->uuid)) { + $data['id'] = $this->uuid; + } + + return $data; + } +} diff --git a/app/Http/Resources/DynamicResource.php b/app/Http/Resources/DynamicResource.php new file mode 100644 index 0000000..69b9069 --- /dev/null +++ b/app/Http/Resources/DynamicResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/LedgerResource.php b/app/Http/Resources/LedgerResource.php new file mode 100644 index 0000000..1bc420f --- /dev/null +++ b/app/Http/Resources/LedgerResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/MessageResource.php b/app/Http/Resources/MessageResource.php new file mode 100644 index 0000000..5e11358 --- /dev/null +++ b/app/Http/Resources/MessageResource.php @@ -0,0 +1,18 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/MutationResource.php b/app/Http/Resources/MutationResource.php new file mode 100644 index 0000000..2ccdcbf --- /dev/null +++ b/app/Http/Resources/MutationResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/PredefinedMutationResource.php b/app/Http/Resources/PredefinedMutationResource.php new file mode 100644 index 0000000..6bb4e96 --- /dev/null +++ b/app/Http/Resources/PredefinedMutationResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..45b2b35 --- /dev/null +++ b/app/Http/Resources/UserResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/app/Models/Dynamic.php b/app/Models/Dynamic.php index 75f41e4..7cb68f3 100644 --- a/app/Models/Dynamic.php +++ b/app/Models/Dynamic.php @@ -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'; + } } diff --git a/app/Models/Ledger.php b/app/Models/Ledger.php index 22ce39b..97d9b34 100644 --- a/app/Models/Ledger.php +++ b/app/Models/Ledger.php @@ -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'; + } } diff --git a/app/Models/Mutation.php b/app/Models/Mutation.php index 67431c9..0c512de 100644 --- a/app/Models/Mutation.php +++ b/app/Models/Mutation.php @@ -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'; + } } diff --git a/app/Models/PredefinedMutation.php b/app/Models/PredefinedMutation.php index a3eb740..0ab9ae4 100644 --- a/app/Models/PredefinedMutation.php +++ b/app/Models/PredefinedMutation.php @@ -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'; } } diff --git a/app/Models/User.php b/app/Models/User.php index afcd538..73840d5 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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'; + } } diff --git a/app/Policies/LedgerPolicy.php b/app/Policies/LedgerPolicy.php index 8b1f66e..05bc736 100644 --- a/app/Policies/LedgerPolicy.php +++ b/app/Policies/LedgerPolicy.php @@ -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); } /** diff --git a/app/Policies/MutationPolicy.php b/app/Policies/MutationPolicy.php index e6288e8..dd29e07 100644 --- a/app/Policies/MutationPolicy.php +++ b/app/Policies/MutationPolicy.php @@ -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(); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f1525e9..7d7b56a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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(); } diff --git a/app/Services/ActivityService.php b/app/Services/ActivityService.php index 7b099a3..5a94246 100644 --- a/app/Services/ActivityService.php +++ b/app/Services/ActivityService.php @@ -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 ''; diff --git a/database/migrations/2026_06_14_222648_create_mutations_table.php b/database/migrations/2026_06_14_222648_create_mutations_table.php index 366e87d..a273833 100644 --- a/database/migrations/2026_06_14_222648_create_mutations_table.php +++ b/database/migrations/2026_06_14_222648_create_mutations_table.php @@ -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(); }); } diff --git a/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php b/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php index 787c51d..1f157e0 100644 --- a/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php +++ b/database/migrations/2026_06_16_225928_create_predefined_mutations_table.php @@ -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(); }); } diff --git a/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php b/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php new file mode 100644 index 0000000..28426b3 --- /dev/null +++ b/database/migrations/2026_06_21_202735_add_status_to_ledgers_table.php @@ -0,0 +1,28 @@ +string('status')->default('open'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ledgers', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +}; diff --git a/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php b/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php new file mode 100644 index 0000000..029ac02 --- /dev/null +++ b/database/migrations/2026_06_21_202842_add_voided_status_to_mutations_table.php @@ -0,0 +1,28 @@ +string('status')->default('pending')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('mutations', function (Blueprint $table) { + $table->string('status')->default('pending')->change(); + }); + } +}; diff --git a/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php b/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php new file mode 100644 index 0000000..d6b5f33 --- /dev/null +++ b/database/migrations/2026_06_21_203737_add_uuid_to_dynamics_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php b/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php new file mode 100644 index 0000000..c1b76b2 --- /dev/null +++ b/database/migrations/2026_06_21_203807_add_uuid_to_ledgers_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php b/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php new file mode 100644 index 0000000..c1baf5c --- /dev/null +++ b/database/migrations/2026_06_21_203835_add_uuid_to_mutations_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php b/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php new file mode 100644 index 0000000..5c4e347 --- /dev/null +++ b/database/migrations/2026_06_21_203907_add_uuid_to_predefined_mutations_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}; diff --git a/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php b/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php new file mode 100644 index 0000000..b657b76 --- /dev/null +++ b/database/migrations/2026_06_21_204007_add_uuid_to_users_table.php @@ -0,0 +1,38 @@ +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'); + }); + } +}; diff --git a/resources/js/components/MutationList.vue b/resources/js/components/MutationList.vue index 561eb64..49d0252 100644 --- a/resources/js/components/MutationList.vue +++ b/resources/js/components/MutationList.vue @@ -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 {
+ {{ mutation.description }} +
+