@@ -1,10 +1,10 @@
< script lang = "ts" >
import { page } from '$app/state' ;
import { enhance } from '$app/forms' ;
import { famStore } from '$lib/stores/fam.svelte' ;
import { ViewHeader , CardGrid , Card , Button } from '$lib/components' ;
import { famStore } from '$lib/stores/fam.svelte' ;
import { ViewHeader , CardGrid , Card , Button } from '$lib/components' ;
import type {
import type {
BonusConfig ,
BonusConfigWithProgress ,
BonusProgress ,
@@ -17,7 +17,7 @@ import type {
let showCreateModal = $state ( false );
let showEditModal = $state ( false );
let editingConfig = $state < BonusConfig | null >( null );
let editFromTarget = $state < 'member' | 'competition' | 'collaboration' | '' > ( '' );
let creatingFromTemplate = $state < BonusConfig | null >( null );
let createVals = $state ({
name : '' ,
@@ -53,20 +53,28 @@ import type {
let showPeriod = $derived ( editVals . occurrence !== 'once' );
let showCriteria = $derived ( editVals . type !== 'manual' );
let templateConfigs = $derived ( configs . filter (( c ) => c . phase === 'template' || c . phase === 'active' ) as BonusConfig []);
let readyConfigs = $derived ( configs . filter (( c ) => c . phase === 'ready' ) as BonusConfig []);
let templateConfigs = $derived ( configs . filter (( c ) => c . phase === 'template' ) as BonusConfig []);
let activeConfigs = $derived ( configs . filter (( c ) => c . phase === 'active' ) as BonusConfig []);
let completedConfigs = $derived ( configs . filter (( c ) => c . phase === 'completed' ) as BonusConfig []);
let completedRewards = $derived ( allRewards . filter (( r ) => r . status === 'claimed' ) as Reward []);
let unclaimedRewards = $derived (
allRewards . filter (( r ) => r . status === 'unclaimed' ) as Reward []
);
let progressByConfigId = $derived . by (() => {
const map = new Map < string , BonusConfigWithProgress >();
for ( const p of progressData ) map . set ( p . config . id , p );
return map ;
});
$effect (() => {
const len = famStore . completions . length ;
if ( len > 0 && famStore . initialized ) {
const famId = page . params . fam ;
const username = page . params . username ;
fetch ( `/ ${ famId } / ${ username } /bonuses?/evaluate` , { method : 'POST' })
. then (() => fetch ( `/ ${ famId } / ${ username } /bonuses/progress.json` ))
. then (( r ) => r . ok && r . json ())
. then (( d ) => { if ( d ) progressData = d ; })
. catch (() => {});
}
});
function memberName ( memberId : string ) : string {
return (
famStore . memberMap (). get ( memberId ) ? . name ||
@@ -75,15 +83,6 @@ import type {
);
}
function configName ( bonusConfigId : string | undefined ) : string {
if ( ! bonusConfigId ) return 'Manual' ;
return (
famStore . bonusConfigMap (). get ( bonusConfigId ) ? . name ||
configs . find (( c ) => c . id === bonusConfigId ) ? . name ||
'?'
);
}
function badgeClass ( cfg : BonusConfig ) : string {
const classes : string [] = [];
if ( cfg . target === 'individual' ) classes . push ( 'indiv' );
@@ -108,7 +107,7 @@ import type {
if ( e . dataTransfer ) e . dataTransfer . dropEffect = 'move' ;
}
function openDropEdit (
function openCreateFromTemplate (
raw : string ,
targetType : string ,
source : 'member' | 'competition' | 'collaboration'
@@ -117,7 +116,7 @@ import type {
if ( target === targetType ) return ;
const cfg = configs . find (( c ) => c . id === id );
if ( ! cfg ) return ;
editingConfig = cfg ;
creatingFromTemplate = cfg ;
editFromTarget = source ;
editVals = {
name : cfg.name ,
@@ -137,37 +136,19 @@ import type {
async function handleDropToMember ( e : DragEvent ) {
e . preventDefault ();
const raw = e . dataTransfer ? . getData ( 'text/plain' );
if ( raw ) openDropEdit ( raw , 'individual' , 'member' );
if ( raw ) openCreateFromTemplate ( raw , 'individual' , 'member' );
}
async function handleDropToCompetition ( e : DragEvent ) {
e . preventDefault ();
const raw = e . dataTransfer ? . getData ( 'text/plain' );
if ( raw ) openDropEdit ( raw , 'competitive' , 'competition' );
if ( raw ) openCreateFromTemplate ( raw , 'competitive' , 'competition' );
}
async function handleDropToCollaboration ( e : DragEvent ) {
e . preventDefault ();
const raw = e . dataTransfer ? . getData ( 'text/plain' );
if ( raw ) openDropEdit ( raw , 'collaborative' , 'collaboration' );
}
function openEdit ( cfg : BonusConfig ) {
editingConfig = cfg ;
editFromTarget = '' ;
editVals = {
name : cfg.name ,
description : cfg.description || '' ,
target : cfg.target ,
memberId : cfg.memberId || '' ,
type : cfg . type ,
occurrence : cfg.occurrence ,
rewardType : cfg.rewardType ,
rewardValue : cfg.rewardValue ,
criteriaValue : cfg.criteriaValue || 10 ,
period : cfg.period || 'weekly'
};
showEditModal = true ;
if ( raw ) openCreateFromTemplate ( raw , 'collaborative' , 'collaboration' );
}
function handleCreate() {
@@ -176,9 +157,6 @@ import type {
const rec = result . data ? . record ;
if ( rec ) {
configs = [ rec as BonusConfig , ... configs ];
if ( famStore . fam ? . featureFlags ? . optimisticUpdates ) {
famStore . applyRecord ( 'bonus_configs' , rec , 'create' );
}
}
showCreateModal = false ;
}
@@ -187,14 +165,9 @@ import type {
function handleUpdate() {
return async ({ result } : any ) => {
if ( result . type === 'success' && editingConfig ) {
const updated = { ... editingConfig , ... editVals , rewardValue : editVals.rewardValue };
const idx = configs . findIndex (( c ) => c . id === editingConfig ! . id );
if ( idx !== - 1 ) configs [ idx ] = updated as BonusConfig ;
if ( famStore . fam ? . featureFlags ? . optimisticUpdates ) {
famStore . applyRecord ( 'bonus_configs' , updated , 'update' );
}
if ( result . type === 'success' ) {
showEditModal = false ;
creatingFromTemplate = null ;
editingConfig = null ;
}
};
@@ -207,17 +180,26 @@ import type {
fire ();
}
const showPeriodForCreate = $derived ( createVals . occurrence !== 'once' );
const showCriteriaForCreate = $derived ( createVals . type !== 'manual' );
async function evaluateAll() {
const fd = new FormData ( );
fd . set ( 'configId' , '' );
const res = await fetch ( `/ ${ page . params . fam } / ${ page . params . username } /bonuses?/evaluate` , { method : 'POST' , body : fd });
const data = await res . json ( );
if ( ! data ? . error ) fire ();
let outstandingConfigs = $derived ( activeConfigs . filter (( c ) => isOutstanding ( c )));
let doneConfigs = $derived ( activeConfigs . filter (( c ) => isCompleted ( c ) && ! isOutstanding ( c )) );
function isCompleted ( cfg : BonusConfig ) : boolean {
const prog = progressByConfigId . get ( cfg . id );
if ( cfg . occurrence === 'once' ) {
const rewards = allRewards . filter (( r ) => r . bonusConfigId === cfg . id );
return rewards . length > 0 ;
}
if ( ! prog ) return false ;
return prog . progress . every (( p ) => p . achieved );
}
function isOutstanding ( cfg : BonusConfig ) : boolean {
if ( ! isCompleted ( cfg )) return false ;
const rewards = allRewards . filter (( r ) => r . bonusConfigId === cfg . id );
return rewards . some (( r ) => r . status === 'unclaimed' );
}
async function fire() {
@@ -230,56 +212,12 @@ import type {
< CardGrid >
< Card cols = { 3 } >
< div class = "a ction-bar " >
< Button onclick = { evaluateAll } size="sm" > Evaluate All</ Button >
</ div >
< div class = "kanban" >
<!-- Templates Column -->
< div class = "column templates-col" >
< div class = "template-se ction" >
< h3 > Templates</ h3 >
<!-- Ready section (above CTA) -->
{ #if readyConfigs . length }
{ #each readyConfigs as cfg }
< div class = "card ready" >
< div class = "card-head" >
< strong > { cfg . name } </ strong >
< form method = "POST" action = "?/deleteConfig" use:enhance >
< input type = "hidden" name = "id" value = { cfg . id } / >
< button type = "submit" class = "del-btn" title = "Remove" > × </ button >
</ form >
</ div >
< div class = "badges" >
< span class = "badge { badgeClass ( cfg )} " > { cfg . target } </ span >
< span class = "badge" > { cfg . type } </ span >
</ div >
< div class = "reward-preview" > { formatReward ( cfg )} </ div >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit</ Button >
< form method = "POST" action = "?/assignConfig" use:enhance class = "assign-form" >
< input type = "hidden" name = "id" value = { cfg . id } / >
< select name = "target" class = "asm-select" >
< option value = "individual" > Individual</ option >
< option value = "competitive" > Competitive</ option >
< option value = "collaborative" > Collaborative</ option >
</ select >
< select name = "memberId" class = "asm-select" >
< option value = "" > All</ option >
{ #each members as m }
< option value = { m . id } > { m . name } </option >
{ /each }
</ select >
< button type = "submit" class = "assign-btn" > Assign</ button >
</ form >
</ div >
{ /each }
{ /if }
< Button onclick = {() => ( showCreateModal = true )} > + New Bonus </ Button >
< div class = "template-list" >
{ #each templateConfigs as cfg }
< div
class = "card"
class = "template- card"
draggable = "true"
ondragstart = {( e ) => handleDragStart ( e , cfg )}
>
@@ -297,16 +235,23 @@ import type {
{ #if cfg . period } < span class = "badge period" > { cfg . period } </ span > { /if }
</ div >
< div class = "reward-preview" > { formatReward ( cfg )} </ div >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit</ Button >
</ div >
{ /each }
</ div >
{ #if templateConfigs . length === 0 }
< p class = "empty" > No templates yet. Create one below.</ p >
{ /if }
</ div >
< Button onclick = {() => ( showCreateModal = true )} > + New Template </ Button >
</ div >
< div class = "kanban" >
<!-- Member Column -->
< div class = "column member-col" ondragover = { handleDragOver } ondrop= { handleDropToMember } >
< h3 > Member</ h3 >
{ #each activeConfigs . filter (( c ) => c . target === 'individual' ) as cfg }
< div class = "col-card" >
{ @const done = isCompleted ( cfg )}
{ @const outstanding = isOutstanding ( cfg )}
< div class = "col-card" class:done class:outstanding >
< div class = "col-card-head" >
< strong > { cfg . name } </ strong >
< div class = "badges" >
@@ -321,6 +266,7 @@ import type {
{ #if p }
< div class = "member-progress" >
< span class = "mname" style = "color: { p . memberColor } " > { p . memberName } </ span >
{ #if p . criteriaValue > 0 }
< div class = "bar-wrap" >
< div
class = "bar-fill"
@@ -328,13 +274,18 @@ import type {
class:met = { p . achieved }
> </div >
</ div >
< span class = "stat { p . state } " > { p . state } </ span >
{ /if }
< span class = "stat { p . state } " >
{ outstanding ? 'Outstanding' : p . state }
</ span >
</ div >
{ /if }
{ /if }
{ /each }
< div class = "col-card-foot" >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit</ Button >
{ #if ! done }
< Button onclick = {() => { editingConfig = cfg ; editFromTarget = '' ; editVals = { name : cfg.name , description : cfg.description || '' , target : cfg.target , memberId : cfg.memberId || '' , type : cfg . type , occurrence : cfg.occurrence , rewardType : cfg.rewardType , rewardValue : cfg.rewardValue , criteriaValue : cfg.criteriaValue || 10 , period : cfg.period || 'weekly' }; showEditModal = true ; }} variant="secondary" size = "sm" > Edit</ Button >
{ /if }
</ div >
</ div >
{ /each }
@@ -347,8 +298,10 @@ import type {
< div class = "column comp-col" ondragover = { handleDragOver } ondrop= { handleDropToCompetition } >
< h3 > Competition</ h3 >
{ #each activeConfigs . filter (( c ) => c . target === 'competitive' ) as cfg }
{ @const done = isCompleted ( cfg )}
{ @const outstanding = isOutstanding ( cfg )}
{ @const prog = progressByConfigId . get ( cfg . id )}
< div class = "col-card" >
< div class = "col-card" class:done class:outstanding >
< div class = "col-card-head" >
< strong > { cfg . name } </ strong >
< div class = "badges" >
@@ -363,6 +316,7 @@ import type {
< div class = "lb-row" class:winner = { i === 0 && p . current > 0 } >
< span class = "lb-rank" > #{ i + 1 } </ span >
< span class = "mname" style = "color: { p . memberColor } " > { p . memberName } </ span >
{ #if p . criteriaValue > 0 }
< div class = "bar-wrap sm" >
< div
class = "bar-fill"
@@ -370,13 +324,18 @@ import type {
class:met = { p . achieved }
> </div >
</ div >
< span class = "stat { p . state } " > { p . state } </ span >
{ /if }
< span class = "stat { p . state } " >
{ outstanding ? 'Outstanding' : p . state }
</ span >
</ div >
{ /each }
</ div >
{ /if }
< div class = "col-card-foot" >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit</ Button >
{ #if ! done }
< Button onclick = {() => { editingConfig = cfg ; editFromTarget = '' ; editVals = { name : cfg.name , description : cfg.description || '' , target : cfg.target , memberId : cfg.memberId || '' , type : cfg . type , occurrence : cfg.occurrence , rewardType : cfg.rewardType , rewardValue : cfg.rewardValue , criteriaValue : cfg.criteriaValue || 10 , period : cfg.period || 'weekly' }; showEditModal = true ; }} variant="secondary" size = "sm" > Edit</ Button >
{ /if }
</ div >
</ div >
{ /each }
@@ -389,8 +348,10 @@ import type {
< div class = "column collab-col" ondragover = { handleDragOver } ondrop= { handleDropToCollaboration } >
< h3 > Collaboration</ h3 >
{ #each activeConfigs . filter (( c ) => c . target === 'collaborative' ) as cfg }
{ @const done = isCompleted ( cfg )}
{ @const outstanding = isOutstanding ( cfg )}
{ @const prog = progressByConfigId . get ( cfg . id )}
< div class = "col-card" >
< div class = "col-card" class:done class:outstanding >
< div class = "col-card-head" >
< strong > { cfg . name } </ strong >
< div class = "badges" >
@@ -404,6 +365,7 @@ import type {
{ #if team }
< div class = "team-progress" >
< span class = "mname" style = "color: { team . memberColor } " > { team . memberName } </ span >
{ #if team . criteriaValue > 0 }
< div class = "bar-wrap" >
< div
class = "bar-fill"
@@ -414,16 +376,20 @@ import type {
class:met = { team . achieved }
> </div >
</ div >
< span class = "stat"
> { team . current } /{ team . criteriaValue }
{ cfg . type === 'threshold' ? 'pts' : 'chores' } </ span
>
< span class = "stat { team . state } " > { team . state } </ span >
{ /if }
< span class = "stat" >
{ team . current } / { team . criteriaValue }
{ cfg . type === 'threshold' ? 'pts' : 'chores' } </ span >
< span class = "stat { team . state } " >
{ outstanding ? 'Outstanding' : team . state }
</ span >
</ div >
{ /if }
{ /if }
< div class = "col-card-foot" >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit</ Button >
{ #if ! done }
< Button onclick = {() => { editingConfig = cfg ; editFromTarget = '' ; editVals = { name : cfg.name , description : cfg.description || '' , target : cfg.target , memberId : cfg.memberId || '' , type : cfg . type , occurrence : cfg.occurrence , rewardType : cfg.rewardType , rewardValue : cfg.rewardValue , criteriaValue : cfg.criteriaValue || 10 , period : cfg.period || 'weekly' }; showEditModal = true ; }} variant="secondary" size = "sm" > Edit</ Button >
{ /if }
</ div >
</ div >
{ /each }
@@ -431,52 +397,59 @@ import type {
< p class = "empty" > Drag a template here</ p >
{ /if }
</ div >
</ div >
</ div >
<!-- Completed Section -->
{ #if completedConfigs . length }
< div class = "completed -section" >
< h3 > C ompleted </ h3 >
< div class = "completed -grid" >
{ #each completedConfigs as cfg }
< div class = "completed-card" >
<!-- Outstanding section (completed configs with unclaimed rewards) -->
{ #if outstandingConfigs . length > 0 }
< div class = "outstanding -section" >
< h3 > Outstanding — Issue to c omplete</ h3 >
< div class = "outstanding -grid" >
{ #each outstandingConfigs as cfg }
{ @const rewards = allRewards . filter (( r ) => r . bonusConfigId === cfg . id && r . status === 'unclaimed' )}
< div class = "outstanding-card" >
< div class = "card-head" >
< strong > { cfg . name } </ strong >
< div class = "badges" >
< span class = "badge { badgeClass ( cfg )} " > { cfg . target } </ span >
< span class = "badge" > { cfg . type } </ span >
</ div >
</ div >
< div class = "reward-preview" > { formatReward ( cfg )} </ div >
< div class = "card-actions" >
< Button onclick = {() => openEdit ( cfg )} variant="secondary" size = "sm" > Edit </ Button >
< form method = "POST" action = "?/completeConfig" use:enhance >
< input type = "hidden" name = "id" value = { cfg . id } / >
< input type = "hidden" name = "phase" value = "active" />
< button type = "submit" class = "reactivate-btn" > Reactivate</ button >
</ form >
< form method = "POST" action = "?/destroyConfig" use:enhance >
< input type = "hidden" name = "id" value = { cfg . id } / >
< button type = "submit" class = "del-btn" title = "Permanently delete" > × </ button >
</ form >
{ #each rewards as r }
< div class = "claim-row" >
< span > { memberName ( r . memberId )} </ span >
< button onclick = {() => claimReward ( r . id ) } class="claim-btn" > Issue </ button >
</ div >
{ /each }
</ div >
{ /each }
</ div >
</ div >
{ /if }
{ /if }
<!-- Create Modal -->
{ #if showCreateModal }
<!-- Completed section (all rewards claimed) -->
{ #if doneConfigs . length > 0 }
< div class = "completed-section" >
< h3 > Completed</ h3 >
< div class = "completed-grid" >
{ #each doneConfigs as cfg }
< div class = "completed-card" >
< div class = "card-head" >
< strong > { cfg . name } </ strong >
</ div >
< div class = "reward-preview" > { formatReward ( cfg )} </ div >
</ div >
{ /each }
</ div >
</ div >
{ /if }
<!-- Create Modal -->
{ #if showCreateModal }
< div class = "overlay" onclick = {() => ( showCreateModal = false )} role="presentation" >
< div class = "modal" onclick = {( e ) => e . stopPropagation ()} role="dialog" >
< h3 > New Bonus</ h3 >
< h3 > New Bonus Template </ h3 >
< form method = "POST" action = "?/createConfig" use:enhance = { handleCreate } >
< input type = "hidden" name = "phase" value = "template" />
< label > Name < input name = "name" bind:value = { createVals . name } required /></ label >
< label > Description < input name = "description" bind:value = { createVals . description } / ></ label >
< label
> Target
< label > Target
< select name = "target" bind:value = { createVals . target } >
< option value = "individual" > Individual</ option >
< option value = "competitive" > Competitive</ option >
@@ -484,8 +457,7 @@ import type {
</ select >
</ label >
{ #if createVals . target === 'individual' }
< label
> Member
< label > Member
< select name = "memberId" bind:value = { createVals . memberId } >
< option value = "" > All (everyone)</ option >
{ #each members as m }
@@ -494,8 +466,7 @@ import type {
</ select >
</ label >
{ /if }
< label
> Type
< label > Type
< select name = "type" bind:value = { createVals . type } >
< option value = "threshold" > Threshold (points)</ option >
< option value = "count" > Count (completions)</ option >
@@ -503,31 +474,19 @@ import type {
</ select >
</ label >
{ #if showCriteriaForCreate }
< label
> C riteria Value
< input
type = "number"
name = "criteriaValue"
bind:value = { createVals . criteriaValue }
required
/>
< span class = "hint"
> { createVals . type === 'threshold'
? 'Points needed'
: 'Chore completions needed' } </ span
>
< label > Criteria Value
< input type = "number" name = "c riteriaValue" bind:value = { createVals . criteriaValue } required />
< span class = "hint" > { createVals . type === 'threshold' ? 'Points needed' : 'Chore completions needed' } </ span >
</ label >
{ /if }
< label
> Occurrence
< label > Occurrence
< select name = "occurrence" bind:value = { createVals . occurrence } >
< option value = "recurring" > Recurring</ option >
< option value = "once" > Once</ option >
</ select >
</ label >
{ #if showPeriodForCreate }
< label
> Period
< label > Period
< select name = "period" bind:value = { createVals . period } >
< option value = "" > Anytime</ option >
< option value = "weekly" > Weekly</ option >
@@ -535,24 +494,16 @@ import type {
</ select >
</ label >
{ /if }
< label
> Reward Type
< label > Reward Type
< select name = "rewardType" bind:value = { createVals . rewardType } >
< option value = "points" > Points</ option >
< option value = "cash" > Cash</ option >
< option value = "prize" > Prize</ option >
</ select >
</ label >
< label
> Reward Value
< label > Reward Value
< input name = "rewardValue" bind:value = { createVals . rewardValue } required />
< span class = "hint"
> { createVals . rewardType === 'cash'
? 'Amount in pence (e.g. 200 = £2)'
: createVals . rewardType === 'points'
? 'Points to award'
: 'Prize description' } </ span
>
< span class = "hint" > { createVals . rewardType === 'cash' ? 'Amount in pence (e.g. 200 = £2)' : createVals . rewardType === 'points' ? 'Points to award' : 'Prize description' } </ span >
</ label >
< div class = "modal-actions" >
< button type = "button" onclick = {() => ( showCreateModal = false )} > Cancel</button >
@@ -561,27 +512,21 @@ import type {
</ form >
</ div >
</ div >
{ /if }
{ /if }
<!-- Edit Modal -->
{ #if showEditModal && editingConfig }
< div
class = "overlay"
onclick = {() => {
showEditModal = false ;
editingConfig = null ;
}}
role="presentation"
>
<!-- Edit / Create-from-Template Modal -->
{ #if showEditModal && ( editingConfig || creatingFromTemplate ) }
{ @const targetCfg = creatingFromTemplate || editingConfig ! }
< div class = "overlay" onclick = {() => { showEditModal = false ; creatingFromTemplate = null ; editingConfig = null ; }} role="presentation" >
< div class = "modal" onclick = {( e ) => e . stopPropagation ()} role="dialog" >
< h3 > Edit: { editingConfig . name } </ h3 >
< form method = "POST" action = " ?/updateConfig" use:enhance = { handleUpdate } >
< h3 > { creatingFromTemplate ? 'Create from: ' : ' Edit: ' } {targetCfg . name } </ h3 >
< form method = "POST" action = { creatingFromTemplate ? '?/createFromTemplate' : ' ?/updateConfig' } use:enhance= { handleUpdate } >
{ #if editingConfig }
< input type = "hidden" name = "id" value = { editingConfig . id } / >
< input type = "hidden" name = "templateId" value = { editingConfig . id } / >
{ /if }
< label > Name < input name = "name" bind:value = { editVals . name } required /></ label >
< label > Description < input name = "description" bind:value = { editVals . description } / ></ label >
< label
> Target
< label > Target
< select name = "target" bind:value = { editVals . target } >
< option value = "individual" > Individual</ option >
< option value = "competitive" > Competitive</ option >
@@ -589,8 +534,7 @@ import type {
</ select >
</ label >
{ #if editVals . target === 'individual' }
< label
> Member
< label > Member
< select name = "memberId" bind:value = { editVals . memberId } >
< option value = "" > All (everyone)</ option >
{ #each members as m }
@@ -599,8 +543,7 @@ import type {
</ select >
</ label >
{ /if }
< label
> Type
< label > Type
< select name = "type" bind:value = { editVals . type } >
< option value = "threshold" > Threshold (points)</ option >
< option value = "count" > Count (completions)</ option >
@@ -608,29 +551,19 @@ import type {
</ select >
</ label >
{ #if showCriteria }
< label
> C riteria Value
< input
type = "number"
name = "criteriaValue"
bind:value = { editVals . criteriaValue }
required
/>
< span class = "hint"
> { editVals . type === 'threshold' ? 'Points needed' : 'Chore completions needed' } </ span
>
< label > Criteria Value
< input type = "number" name = "c riteriaValue" bind:value = { editVals . criteriaValue } required />
< span class = "hint" > { editVals . type === 'threshold' ? 'Points needed' : 'Chore completions needed' } </ span >
</ label >
{ /if }
< label
> Occurrence
< label > Occurrence
< select name = "occurrence" bind:value = { editVals . occurrence } >
< option value = "recurring" > Recurring</ option >
< option value = "once" > Once</ option >
</ select >
</ label >
{ #if showPeriod }
< label
> Period
< label > Period
< select name = "period" bind:value = { editVals . period } >
< option value = "" > Anytime</ option >
< option value = "weekly" > Weekly</ option >
@@ -638,52 +571,54 @@ import type {
</ select >
</ label >
{ /if }
< label
> Reward Type
< label > Reward Type
< select name = "rewardType" bind:value = { editVals . rewardType } >
< option value = "points" > Points</ option >
< option value = "cash" > Cash</ option >
< option value = "prize" > Prize</ option >
</ select >
</ label >
< label
> Reward Value
< label > Reward Value
< input name = "rewardValue" bind:value = { editVals . rewardValue } required />
< span class = "hint"
> { editVals . rewardType === 'cash'
? 'Amount in pence'
: editVals . rewardType === 'points'
? 'Points to award'
: 'Prize description' } </ span
>
< span class = "hint" > { editVals . rewardType === 'cash' ? 'Amount in pence' : editVals . rewardType === 'points' ? 'Points to award' : 'Prize description' } </ span >
</ label >
< div class = "modal-actions" >
< button
type = "button"
onclick = {() => {
showEditModal = false ;
editingConfig = null ;
}} > Cancel</button
>
< button type = "submit" > Save</ button >
{ #if editingConfig . phase === 'template' }
< button type = "submit" formaction = "?/createFromTemplate" class = "create-from-btn" > Create Instance</ button >
{ :else if editingConfig . phase === 'ready' || editingConfig . phase === 'active' }
< input type = "hidden" name = "phase" value = "completed" />
< button type = "submit" formaction = "?/completeConfig" class = "complete-btn" > Mark Complete</ button >
{ :else if editingConfig . phase === 'completed' }
< input type = "hidden" name = "phase" value = "active" />
< button type = "submit" formaction = "?/completeConfig" class = "reactivate-btn" > Reactivate</ button >
{ /if }
< button type = "button" onclick = {() => { showEditModal = false ; creatingFromTemplate = null ; editingConfig = null ; }} > Discard</button >
< button type = "submit" > { creatingFromTemplate ? 'Confirm' : 'Save' } </ button >
</ div >
</ form >
</ div >
</ div >
{ /if }
</ Card >
{ /if }
</ Card >
</ CardGrid >
< style >
. template-section {
margin-bottom : 1.5 rem ;
}
. template-section h3 {
margin : 0 0 0.5 rem ;
font-size : 1 rem ;
}
. template-list {
display : flex ;
gap : 0.5 rem ;
flex-wrap : wrap ;
margin-bottom : 0.75 rem ;
}
. template-card {
background : white ;
border : 1 px solid #e5e7eb ;
border-radius : 6 px ;
padding : 0.5 rem 0.75 rem ;
min-width : 160 px ;
cursor : grab ;
}
. template-card : active {
cursor : grabbing ;
opacity : 0.8 ;
}
. kanban {
display : flex ;
gap : 1 rem ;
@@ -705,43 +640,6 @@ import type {
margin : 0 0 0.75 rem ;
font-size : 1 rem ;
}
. action-bar { display : flex ; gap : 0.5 rem ; margin-bottom : 0.75 rem ; }
. card-actions { display : flex ; gap : 0.3 rem ; margin-top : 0.3 rem ; }
. add-btn {
width : 100 % ;
padding : 0.5 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : 0.9 rem ;
margin-bottom : 0.5 rem ;
}
. empty {
color : #9ca3af ;
font-size : 0.85 rem ;
text-align : center ;
padding : 1 rem ;
border : 1 px dashed #d1d5db ;
border-radius : 6 px ;
}
. card {
background : white ;
border : 1 px solid #e5e7eb ;
border-radius : 6 px ;
padding : 0.5 rem 0.75 rem ;
margin-bottom : 0.5 rem ;
cursor : grab ;
}
. card : active {
cursor : grabbing ;
opacity : 0.8 ;
}
. card . archived {
opacity : 0.5 ;
}
. card-head {
display : flex ;
justify-content : space-between ;
@@ -772,7 +670,6 @@ import type {
background : #fce7f3 ;
color : #be185d ;
}
. badge . period {
background : #d1fae5 ;
color : #059669 ;
@@ -786,15 +683,6 @@ import type {
color : #6b7280 ;
font-weight : 600 ;
}
. edit-btn {
font-size : 0.7 rem ;
padding : 2 px 8 px ;
background : #f3f4f6 ;
border : 1 px solid #d1d5db ;
border-radius : 4 px ;
cursor : pointer ;
margin-top : 0.3 rem ;
}
. del-btn {
background : none ;
border : none ;
@@ -807,12 +695,14 @@ import type {
. del-btn : hover {
color : #dc2626 ;
}
. archived-hdr {
font-size : 0.8 rem ;
. empty {
color : #9ca3af ;
margin : 0.75 rem 0 0.25 rem ;
font-size : 0.85 rem ;
text-align : center ;
padding : 1 rem ;
border : 1 px dashed #d1d5db ;
border-radius : 6 px ;
}
. col-card {
background : white ;
border : 1 px solid #e5e7eb ;
@@ -823,6 +713,10 @@ import type {
. col-card . done {
opacity : 0.75 ;
}
. col-card . outstanding {
border-color : #f59e0b ;
background : #fffbeb ;
}
. col-card-head {
display : flex ;
justify-content : space-between ;
@@ -837,7 +731,6 @@ import type {
display : flex ;
gap : 0.3 rem ;
}
. member-progress {
display : flex ;
align-items : center ;
@@ -888,29 +781,6 @@ import type {
background : #d1fae5 ;
color : #059669 ;
}
. trigger-btn ,
. claim-btn ,
. eval-btn {
font-size : 0.65 rem ;
padding : 2 px 8 px ;
border : none ;
border-radius : 4 px ;
cursor : pointer ;
white-space : nowrap ;
}
. trigger-btn {
background : #f59e0b ;
color : white ;
}
. claim-btn {
background : #10b981 ;
color : white ;
}
. eval-btn {
background : #6366f1 ;
color : white ;
}
. leaderboard {
margin-bottom : 0.3 rem ;
}
@@ -931,20 +801,15 @@ import type {
color : #9ca3af ;
min-width : 20 px ;
}
. done-body {
font-size : 0.8 rem ;
color : #6b7280 ;
. claim-btn {
background : #10b981 ;
color : white ;
border : none ;
border-radius : 4 px ;
padding : 2 px 8 px ;
font-size : 0.65 rem ;
cursor : pointer ;
}
. done-body . reward-val {
font-weight : 600 ;
color : #059669 ;
display : block ;
}
. ts {
font-size : 0.7 rem ;
color : #9ca3af ;
}
. overlay {
position : fixed ;
inset : 0 ;
@@ -1011,44 +876,35 @@ import type {
background : #6366f1 ;
color : white ;
}
/* ── Lifecycle styles ── */
. card . ready {
border-color : #f59e0b ;
background : #fffbeb ;
cursor : default ;
. outstanding-section {
margin-top : 2 rem ;
border-top : 2 px solid #f59e0b ;
padding-top : 1 rem ;
}
. assign-form {
. outstanding-section h3 {
font-size : 1 rem ;
margin : 0 0 0.75 rem ;
color : #b45309 ;
}
. outstanding-grid {
display : flex ;
gap : 4 px ;
margin-top : 0.4 rem ;
align-items : center ;
gap : 0.5 rem ;
flex-wrap : wrap ;
}
. asm-select {
font-size : 0.65 rem ;
padding : 2 px 4 px ;
border : 1 px solid #d1d5db ;
border-radius : 4 px ;
flex : 1 ;
min-width : 70 px ;
. outstanding-card {
background : #fffbeb ;
border : 1 px solid #f59e0b ;
border-radius : 6 px ;
padding : 0.5 rem 0.75 rem ;
min-width : 180 px ;
}
. assign-btn ,
. create-from-btn ,
. complete-btn ,
. reactivate-btn {
font-size : 0.65 rem ;
padding : 2 px 8 px ;
border : none ;
border-radius : 4 px ;
cursor : pointer ;
white-space : nowrap ;
. claim-row {
display : flex ;
justify-content : space-between ;
align-items : center ;
font-size : 0.8 rem ;
margin-top : 0.3 rem ;
}
. assign-btn { background : #f59e0b ; color : white ; }
. create-from-btn { background : #6366f1 ; color : white ; }
. complete-btn { background : #10b981 ; color : white ; }
. reactivate-btn { background : #8b5cf6 ; color : white ; }
. completed-section {
margin-top : 2 rem ;
border-top : 2 px solid #e5e7eb ;