feat: Implement Ledger alignment and correct corrective mutation authors in seeders

This commit is contained in:
Daan Meijer 2026-06-15 23:22:08 +02:00
parent bf7da48a58
commit 3dd7c48b08
9 changed files with 177 additions and 8 deletions

View File

@ -27,6 +27,7 @@ class StoreLedgerRequest extends FormRequest
return [ return [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'rules' => ['nullable', 'string'], 'rules' => ['nullable', 'string'],
'alignment' => ['required', 'string', 'in:positive,neutral,negative'],
'media' => ['nullable', 'array'], 'media' => ['nullable', 'array'],
'media.*' => ['file', 'mimes:jpg,jpeg,png,gif,mp4,mov,avi,webm', 'max:20480'], 'media.*' => ['file', 'mimes:jpg,jpeg,png,gif,mp4,mov,avi,webm', 'max:20480'],
]; ];

View File

@ -18,6 +18,7 @@ class Ledger extends Model
'name', 'name',
'rules', 'rules',
'score', 'score',
'alignment',
]; ];
public function dynamic(): BelongsTo public function dynamic(): BelongsTo

View File

@ -30,6 +30,7 @@ class LedgerFactory extends Factory
]), ]),
'rules' => 'Scores are added by Doms and subtracted for protocol breaches.', 'rules' => 'Scores are added by Doms and subtracted for protocol breaches.',
'score' => 0, 'score' => 0,
'alignment' => 'neutral',
]; ];
} }
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('ledgers', function (Blueprint $table) {
$table->string('alignment')->default('neutral')->after('rules');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('ledgers', function (Blueprint $table) {
$table->dropColumn('alignment');
});
}
};

View File

@ -94,6 +94,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Curfew Compliance', 'name' => 'Curfew Compliance',
'rules' => 'Must be in bed and checked in by 11:00 PM.', 'rules' => 'Must be in bed and checked in by 11:00 PM.',
'score' => 35, 'score' => 35,
'alignment' => 'positive',
]); ]);
$cleaningLedger = Ledger::create([ $cleaningLedger = Ledger::create([
@ -101,6 +102,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Dungeon Cleaning', 'name' => 'Dungeon Cleaning',
'rules' => 'Earn +10 to +20 points for cleaning/setup. Penalty -10 for disorganization.', 'rules' => 'Earn +10 to +20 points for cleaning/setup. Penalty -10 for disorganization.',
'score' => 45, 'score' => 45,
'alignment' => 'neutral',
]); ]);
$etiquetteLedger = Ledger::create([ $etiquetteLedger = Ledger::create([
@ -108,6 +110,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Protocol & Etiquette', 'name' => 'Protocol & Etiquette',
'rules' => 'Address others by correct titles. -5 per infraction.', 'rules' => 'Address others by correct titles. -5 per infraction.',
'score' => -15, 'score' => -15,
'alignment' => 'negative',
]); ]);
// Seed Curfew Mutations // Seed Curfew Mutations
@ -168,7 +171,7 @@ class DatabaseSeeder extends Seeder
Mutation::create([ Mutation::create([
'ledger_id' => $cleaningLedger->id, 'ledger_id' => $cleaningLedger->id,
'user_id' => $bob->id, 'user_id' => $alice->id,
'type' => 'penalty', 'type' => 'penalty',
'amount' => -10, 'amount' => -10,
'description' => 'Left keys in the locks unmonitored', 'description' => 'Left keys in the locks unmonitored',
@ -209,7 +212,7 @@ class DatabaseSeeder extends Seeder
// Seed Etiquette Mutations // Seed Etiquette Mutations
Mutation::create([ Mutation::create([
'ledger_id' => $etiquetteLedger->id, 'ledger_id' => $etiquetteLedger->id,
'user_id' => $bob->id, 'user_id' => $alice->id,
'type' => 'penalty', 'type' => 'penalty',
'amount' => -5, 'amount' => -5,
'description' => 'Interrupted Domina Alice during daily instructions', 'description' => 'Interrupted Domina Alice during daily instructions',
@ -218,7 +221,7 @@ class DatabaseSeeder extends Seeder
Mutation::create([ Mutation::create([
'ledger_id' => $etiquetteLedger->id, 'ledger_id' => $etiquetteLedger->id,
'user_id' => $bob->id, 'user_id' => $alice->id,
'type' => 'penalty', 'type' => 'penalty',
'amount' => -10, 'amount' => -10,
'description' => 'Forgot correct posture during morning roll call', 'description' => 'Forgot correct posture during morning roll call',
@ -227,7 +230,7 @@ class DatabaseSeeder extends Seeder
Mutation::create([ Mutation::create([
'ledger_id' => $etiquetteLedger->id, 'ledger_id' => $etiquetteLedger->id,
'user_id' => $bob->id, 'user_id' => $alice->id,
'type' => 'penalty', 'type' => 'penalty',
'amount' => -5, 'amount' => -5,
'description' => 'Spoke out of turn in general chat', 'description' => 'Spoke out of turn in general chat',
@ -288,6 +291,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Kitchen Chores', 'name' => 'Kitchen Chores',
'rules' => 'Scores for dishwashing, trash duty, and deep oven cleaning.', 'rules' => 'Scores for dishwashing, trash duty, and deep oven cleaning.',
'score' => 40, 'score' => 40,
'alignment' => 'positive',
]); ]);
$coffeeLedger = Ledger::create([ $coffeeLedger = Ledger::create([
@ -295,6 +299,7 @@ class DatabaseSeeder extends Seeder
'name' => 'Coffee Machine Maintenance', 'name' => 'Coffee Machine Maintenance',
'rules' => ' refill beans and descale monthly.', 'rules' => ' refill beans and descale monthly.',
'score' => 10, 'score' => 10,
'alignment' => 'neutral',
]); ]);
// Seed Chores Mutations // Seed Chores Mutations

View File

@ -9,6 +9,7 @@ const props = defineProps<{
const form = useForm({ const form = useForm({
name: '', name: '',
rules: '', rules: '',
alignment: 'neutral',
media: [] as File[], media: [] as File[],
}); });
@ -80,6 +81,29 @@ function submit() {
</div> </div>
</div> </div>
<div class="c-create-ledger-form__field">
<label
for="alignment"
class="c-create-ledger-form__label"
>Alignment</label
>
<select
v-model="form.alignment"
id="alignment"
class="c-create-ledger-form__select"
>
<option value="positive">Positive (Higher Score is Better)</option>
<option value="neutral">Neutral (Frictionless / Standard)</option>
<option value="negative">Negative (Lower Score is Better / Demerits)</option>
</select>
<div
v-if="form.errors.alignment"
class="c-create-ledger-form__error"
>
{{ form.errors.alignment }}
</div>
</div>
<!-- Media Uploads for Ledgers --> <!-- Media Uploads for Ledgers -->
<div class="c-create-ledger-form__field"> <div class="c-create-ledger-form__field">
<label <label
@ -170,6 +194,10 @@ function submit() {
@apply mt-1 block w-full rounded-md border 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; @apply mt-1 block w-full rounded-md border 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-create-ledger-form__select {
@apply mt-1 block w-full rounded-md border 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-create-ledger-form__file-input { .c-create-ledger-form__file-input {
@apply mt-1 block w-full text-sm text-neutral-500 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-xs file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100; @apply mt-1 block w-full text-sm text-neutral-500 file:mr-4 file:rounded-md file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-xs file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100;
} }

View File

@ -8,6 +8,7 @@ defineProps<{
id: number; id: number;
name: string; name: string;
score: number; score: number;
alignment: string;
media?: Array<{ id: number; url: string; mime_type: string }>; media?: Array<{ id: number; url: string; mime_type: string }>;
}>; }>;
}>(); }>();
@ -41,6 +42,28 @@ defineProps<{
Score: {{ ledger.score }} Score: {{ ledger.score }}
</p> </p>
<!-- Ledger Alignment Badge -->
<div class="c-ledger-list__alignment-wrapper">
<span
v-if="ledger.alignment === 'positive'"
class="c-ledger-list__alignment-badge c-ledger-list__alignment-badge--positive"
>
Higher is Better
</span>
<span
v-else-if="ledger.alignment === 'negative'"
class="c-ledger-list__alignment-badge c-ledger-list__alignment-badge--negative"
>
Lower is Better
</span>
<span
v-else
class="c-ledger-list__alignment-badge c-ledger-list__alignment-badge--neutral"
>
Neutral
</span>
</div>
<!-- Ledger Media Thumbnails --> <!-- Ledger Media Thumbnails -->
<div <div
v-if="ledger.media && ledger.media.length > 0" v-if="ledger.media && ledger.media.length > 0"
@ -103,6 +126,26 @@ defineProps<{
@apply mt-2 text-sm text-gray-600 dark:text-gray-400; @apply mt-2 text-sm text-gray-600 dark:text-gray-400;
} }
.c-ledger-list__alignment-wrapper {
@apply mt-2 flex items-center;
}
.c-ledger-list__alignment-badge {
@apply text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded;
}
.c-ledger-list__alignment-badge--positive {
@apply text-green-600 bg-green-50/50 dark:text-green-400 dark:bg-green-950/20;
}
.c-ledger-list__alignment-badge--negative {
@apply text-red-600 bg-red-50/50 dark:text-red-400 dark:bg-red-950/20;
}
.c-ledger-list__alignment-badge--neutral {
@apply text-gray-500 bg-gray-50/50 dark:text-gray-400 dark:bg-neutral-800/20;
}
.c-ledger-list__media-list { .c-ledger-list__media-list {
@apply mt-2 flex flex-wrap gap-1; @apply mt-2 flex flex-wrap gap-1;
} }

View File

@ -6,6 +6,7 @@ import Chat from '@/components/Chat.vue';
const props = defineProps<{ const props = defineProps<{
dynamicId: number; dynamicId: number;
ledgerId: number; ledgerId: number;
ledgerAlignment?: string;
mutations: Array<{ mutations: Array<{
id: number; id: number;
user_id: number; user_id: number;
@ -44,6 +45,22 @@ function isOwnerUser(userId: number): boolean {
return participant?.pivot?.role === 'owner'; return participant?.pivot?.role === 'owner';
} }
function getAmountClass(amount: number): string {
const alignment = props.ledgerAlignment || 'neutral';
if (alignment === 'positive') {
return amount > 0 ? 'c-mutation-list__item-amount--positive' : 'c-mutation-list__item-amount--negative';
}
if (alignment === 'negative') {
// Lower is better: negative amount is positive/favorable, positive amount is negative/unfavorable
return amount < 0 ? 'c-mutation-list__item-amount--positive' : 'c-mutation-list__item-amount--negative';
}
// Neutral alignment
return 'c-mutation-list__item-amount--neutral';
}
</script> </script>
<template> <template>
@ -69,10 +86,7 @@ function isOwnerUser(userId: number): boolean {
</span> </span>
<div class="c-mutation-list__item-meta"> <div class="c-mutation-list__item-meta">
<span <span
:class="{ :class="getAmountClass(mutation.amount)"
'c-mutation-list__item-amount--positive': mutation.amount > 0,
'c-mutation-list__item-amount--negative': mutation.amount < 0,
}"
class="c-mutation-list__item-amount" class="c-mutation-list__item-amount"
> >
{{ mutation.amount > 0 ? '+' : '' {{ mutation.amount > 0 ? '+' : ''
@ -210,6 +224,10 @@ function isOwnerUser(userId: number): boolean {
@apply text-red-500; @apply text-red-500;
} }
.c-mutation-list__item-amount--neutral {
@apply text-gray-500 dark:text-gray-400;
}
.c-mutation-list__item-status { .c-mutation-list__item-status {
@apply rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase; @apply rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wider uppercase;
} }

View File

@ -22,6 +22,7 @@ const props = defineProps<{
name: string; name: string;
score: number; score: number;
rules: string; rules: string;
alignment: string;
media?: Array<{ id: number; url: string; mime_type: string }>; media?: Array<{ id: number; url: string; mime_type: string }>;
mutations: Array<{ mutations: Array<{
id: number; id: number;
@ -175,6 +176,28 @@ function isOwnerUser(userId: number): boolean {
{{ ledger.rules }} {{ ledger.rules }}
</p> </p>
<!-- Ledger Alignment Badge / Subtitle -->
<div class="c-ledger-show__alignment-wrapper">
<span
v-if="ledger.alignment === 'positive'"
class="c-ledger-show__alignment-badge c-ledger-show__alignment-badge--positive"
>
Positive Alignment A higher score is better.
</span>
<span
v-else-if="ledger.alignment === 'negative'"
class="c-ledger-show__alignment-badge c-ledger-show__alignment-badge--negative"
>
Negative Alignment A lower score is better (demerits / infractions).
</span>
<span
v-else
class="c-ledger-show__alignment-badge c-ledger-show__alignment-badge--neutral"
>
Neutral Alignment Scorekeeping neutral.
</span>
</div>
<!-- Ledger Descriptive Media --> <!-- Ledger Descriptive Media -->
<div <div
v-if="ledger.media && ledger.media.length > 0" v-if="ledger.media && ledger.media.length > 0"
@ -219,6 +242,7 @@ function isOwnerUser(userId: number): boolean {
:mutations="ledger.mutations" :mutations="ledger.mutations"
:participants="dynamic.participants" :participants="dynamic.participants"
:is-owner="isOwner" :is-owner="isOwner"
:ledger-alignment="ledger.alignment"
@open-lightbox="openLightbox" @open-lightbox="openLightbox"
/> />
</div> </div>
@ -296,6 +320,26 @@ function isOwnerUser(userId: number): boolean {
@apply mt-2 text-sm text-gray-600 dark:text-gray-400; @apply mt-2 text-sm text-gray-600 dark:text-gray-400;
} }
.c-ledger-show__alignment-wrapper {
@apply mt-3 flex items-center;
}
.c-ledger-show__alignment-badge {
@apply text-xs font-semibold uppercase tracking-wide px-2.5 py-1 rounded;
}
.c-ledger-show__alignment-badge--positive {
@apply text-green-600 bg-green-50/50 dark:text-green-400 dark:bg-green-950/20;
}
.c-ledger-show__alignment-badge--negative {
@apply text-red-600 bg-red-50/50 dark:text-red-400 dark:bg-red-950/20;
}
.c-ledger-show__alignment-badge--neutral {
@apply text-gray-500 bg-gray-50/50 dark:text-gray-400 dark:bg-neutral-800/20;
}
.c-ledger-show__media-list { .c-ledger-show__media-list {
@apply mt-4 flex flex-wrap gap-3; @apply mt-4 flex flex-wrap gap-3;
} }