mutations hebben single
linter / quality (push) Failing after 1m6s
tests / ci (8.3) (push) Failing after 50s
tests / ci (8.4) (push) Failing after 1m6s
tests / ci (8.5) (push) Failing after 1m6s

This commit is contained in:
Daan Meijer
2026-07-06 17:47:14 +02:00
parent 3d6a5362e6
commit 2ab1acf040
4 changed files with 282 additions and 196 deletions
+23 -190
View File
@@ -1,64 +1,22 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { Link } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
import Chat from '@/components/Chat.vue';
const props = defineProps<{
dynamicId: string;
ledgerId: string;
ledgerAlignment?: string;
mutations: Array<{
id: number;
id: string; // Now a UUID
user_id: number;
user: { name: string };
amount: number;
description: string;
status: string;
created_at: string;
chat: any;
media?: Array<{ id: number; url: string; mime_type: string }>;
can: {
update: boolean;
void: boolean;
};
}>;
participants?: Array<{
id: number;
name: string;
pivot?: { role: string };
}>;
}>();
const emit = defineEmits<{
(e: 'open-lightbox', url: string, mimeType: string): void;
}>();
function updateStatus(mutationId: number, status: 'approved' | 'rejected') {
useForm({ status }).put(
route('dynamics.ledgers.mutations.update', {
dynamic: props.dynamicId,
ledger: props.ledgerId,
mutation: mutationId,
}),
);
}
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);
return participant?.pivot?.role === 'owner';
}
function getAmountClass(amount: number): string {
const alignment = props.ledgerAlignment || 'neutral';
@@ -87,18 +45,15 @@ function getAmountClass(amount: number): string {
<li
v-for="mutation in mutations"
:key="mutation.id"
class="c-mutation-list__item"
>
<div class="c-mutation-list__item-header">
<div>
<span class="c-mutation-list__item-author">
{{
isOwnerUser(mutation.user_id)
? 'Added by'
: 'Suggested by'
}}
{{ mutation.user.name }}
</span>
<Link
:href="route('dynamics.ledgers.mutations.show', { dynamic: dynamicId, ledger: ledgerId, mutation: mutation.id })"
class="c-mutation-list__item-link"
>
<div class="c-mutation-list__item-content">
<p class="c-mutation-list__item-desc">
{{ mutation.description }}
</p>
<div class="c-mutation-list__item-meta">
<span
:class="getAmountClass(mutation.amount)"
@@ -107,10 +62,8 @@ function getAmountClass(amount: number): string {
{{ mutation.amount > 0 ? '+' : ''
}}{{ mutation.amount }}
</span>
<!-- Only show status badge if mutation was NOT auto-approved by an owner -->
<span
v-if="!isOwnerUser(mutation.user_id)"
:class="{
<span
:class="{
'c-mutation-list__item-status--pending':
mutation.status === 'pending',
'c-mutation-list__item-status--approved':
@@ -118,85 +71,13 @@ function getAmountClass(amount: number): string {
'c-mutation-list__item-status--rejected':
mutation.status === 'rejected',
}"
class="c-mutation-list__item-status"
>
class="c-mutation-list__item-status"
>
{{ mutation.status }}
</span>
</div>
</div>
<div class="c-mutation-list__item-time">
{{ new Date(mutation.created_at).toLocaleString() }}
</div>
</div>
<p class="c-mutation-list__item-desc">
{{ mutation.description }}
</p>
<!-- Attached Mutation Proof Media -->
<div
v-if="mutation.media && mutation.media.length > 0"
class="c-mutation-list__media-list"
>
<div
v-for="item in mutation.media"
:key="item.id"
class="c-mutation-list__media-item"
>
<img
v-if="item.mime_type.startsWith('image/')"
:src="item.url"
class="c-mutation-list__media-img"
@click="
emit('open-lightbox', item.url, item.mime_type)
"
/>
<div
v-else-if="item.mime_type.startsWith('video/')"
class="c-mutation-list__media-video-wrapper"
@click="
emit('open-lightbox', item.url, item.mime_type)
"
>
<video
:src="item.url"
class="c-mutation-list__media-video"
></video>
<div class="c-mutation-list__media-video-overlay">
</div>
</div>
</div>
</div>
<!-- Owner Approve/Reject Actions -->
<div
v-if="mutation.can?.update || mutation.can?.void"
class="c-mutation-list__actions"
>
<button
v-if="mutation.can?.update"
@click="updateStatus(mutation.id, 'approved')"
class="c-mutation-list__approve-btn"
>
Approve
</button>
<button
v-if="mutation.can?.update"
@click="updateStatus(mutation.id, 'rejected')"
class="c-mutation-list__reject-btn"
>
Reject
</button>
<button
v-if="mutation.can?.void"
@click="voidMutation(mutation.id)"
class="c-mutation-list__void-btn"
>
Void
</button>
</div>
<Chat v-if="mutation.chat" :chat="mutation.chat" :dynamic-id="dynamicId" :participants="participants" />
</Link>
</li>
</ul>
<div v-if="mutations.length === 0" class="c-mutation-list__empty">
@@ -217,23 +98,23 @@ function getAmountClass(amount: number): string {
}
.c-mutation-list__list {
@apply mt-4 space-y-4;
@apply mt-4 space-y-2;
}
.c-mutation-list__item {
@apply overflow-hidden bg-white p-4 shadow-sm sm:rounded-lg dark:bg-gray-800;
.c-mutation-list__item-link {
@apply block overflow-hidden bg-white p-4 shadow-sm sm:rounded-lg dark:bg-gray-800 transition-all duration-200 hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700;
}
.c-mutation-list__item-header {
@apply flex items-start justify-between;
.c-mutation-list__item-content {
@apply flex items-center justify-between;
}
.c-mutation-list__item-author {
@apply font-semibold;
.c-mutation-list__item-desc {
@apply text-sm text-gray-600 dark:text-gray-400;
}
.c-mutation-list__item-meta {
@apply mt-1 flex items-center gap-2;
@apply flex items-center gap-2;
}
.c-mutation-list__item-amount {
@@ -268,54 +149,6 @@ function getAmountClass(amount: number): string {
@apply bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400;
}
.c-mutation-list__item-time {
@apply text-xs text-gray-500;
}
.c-mutation-list__item-desc {
@apply mt-3 text-sm text-gray-600 dark:text-gray-400;
}
.c-mutation-list__media-list {
@apply mt-3 flex flex-wrap gap-2;
}
.c-mutation-list__media-item {
@apply max-w-[200px] overflow-hidden rounded-md border border-neutral-200 bg-black dark:border-neutral-700;
}
.c-mutation-list__media-img {
@apply h-auto max-h-[150px] w-full cursor-pointer object-cover transition-opacity hover:opacity-90;
}
.c-mutation-list__media-video-wrapper {
@apply relative cursor-pointer transition-opacity hover:opacity-90;
}
.c-mutation-list__media-video {
@apply h-auto max-h-[150px] w-full;
}
.c-mutation-list__media-video-overlay {
@apply absolute inset-0 flex items-center justify-center bg-black/40 text-2xl font-bold text-white;
}
.c-mutation-list__actions {
@apply mt-3 flex gap-2 border-t border-neutral-100 pt-3 dark:border-neutral-700;
}
.c-mutation-list__approve-btn {
@apply inline-flex cursor-pointer items-center rounded bg-green-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-green-500;
}
.c-mutation-list__reject-btn {
@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;
}