feat: Implement Ledger alignment and correct corrective mutation authors in seeders
This commit is contained in:
parent
bf7da48a58
commit
3dd7c48b08
@ -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'],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -18,6 +18,7 @@ class Ledger extends Model
|
|||||||
'name',
|
'name',
|
||||||
'rules',
|
'rules',
|
||||||
'score',
|
'score',
|
||||||
|
'alignment',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function dynamic(): BelongsTo
|
public function dynamic(): BelongsTo
|
||||||
|
|||||||
@ -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',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user