add more ux and ui patches with extra login helper
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import { memberApi } from '$lib/client/api';
|
||||
import { formatDDMMYY, formatHumanDate } from '$lib/format';
|
||||
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
|
||||
import { checkCircleIcon, revokeIcon } from '$lib/components/icons';
|
||||
import type { AssignedChore, Completion, ChoreTemplate, BonusConfig, Reward } from '$lib/types';
|
||||
import { handleOf } from '@shared/slugify';
|
||||
import {
|
||||
@@ -64,10 +65,6 @@
|
||||
};
|
||||
});
|
||||
|
||||
function totalChoresFor(memberId: string): number {
|
||||
return parentAssigned.filter((a: any) => a.memberId === memberId).length;
|
||||
}
|
||||
|
||||
function todayCompletionsFor(memberId: string) {
|
||||
return parentCompletions.filter(
|
||||
(c: any) => c.memberId === memberId && (c.date?.slice(0, 10) || c.date) === today
|
||||
@@ -332,10 +329,17 @@
|
||||
);
|
||||
|
||||
let weeklyTotal = $derived.by(() => {
|
||||
const dailyChores = memberChores.filter((a) => a.frequency === 'daily');
|
||||
const weeklyChores = memberChores.filter((a) => a.frequency === 'weekly');
|
||||
const activeTodos = memberTodos.filter((t) => t.type !== 'emoji').length;
|
||||
return dailyChores.length * 7 + weeklyChores.length + activeTodos;
|
||||
// Must match the pocket-money % threshold definition: all assigned
|
||||
// non-todo chores for the member (no season filter), weighted by the
|
||||
// number of days in the displayed period — so the hero total agrees
|
||||
// with the Goals progress instead of showing a different denominator.
|
||||
const chores = assigned.filter((a) => a.memberId === memberId && !a.isTodo);
|
||||
const periodDays =
|
||||
Math.round(
|
||||
(new Date(weekEnd + 'T00:00:00').getTime() - new Date(weekStart + 'T00:00:00').getTime()) /
|
||||
86400000
|
||||
) + 1;
|
||||
return chores.reduce((sum, a) => sum + (a.frequency === 'daily' ? periodDays : 1), 0);
|
||||
});
|
||||
|
||||
let weekCompletions = $derived(
|
||||
@@ -349,6 +353,17 @@
|
||||
let weeklyDone = $derived(weekCompletions.length);
|
||||
let weeklyPct = $derived(weeklyTotal > 0 ? Math.round((weeklyDone / weeklyTotal) * 100) : 0);
|
||||
|
||||
// Pocket-money target as a % of chores, for the layered target bar under the
|
||||
// hero progress. Only meaningful for a percent threshold (points targets
|
||||
// can't be expressed as a fraction of chores).
|
||||
let pmTargetPct = $derived.by(() => {
|
||||
const pm = bonusConfigs.find(
|
||||
(c) => c.isPocketMoney && c.status === 'active' && (!c.memberId || c.memberId === memberId)
|
||||
);
|
||||
if (!pm || pm.thresholdType !== 'percent') return null;
|
||||
return Math.min(100, Math.max(0, Number(pm.criteriaValue) || 0));
|
||||
});
|
||||
|
||||
let weekRewards = $derived(
|
||||
rewards.filter((r) => r.memberId === memberId && (r.date?.slice(0, 10) || r.date) >= weekStart)
|
||||
);
|
||||
@@ -505,10 +520,15 @@
|
||||
? totalPotential > 0 && ratio * 100 >= configured
|
||||
: target > 0 && current >= target;
|
||||
|
||||
return {
|
||||
config: cfg,
|
||||
current,
|
||||
criteriaValue: configured,
|
||||
const unit = isPercent ? '% of chores' : isCount ? 'chores' : 'pts';
|
||||
const targetLabel = `${configured} ${unit}`;
|
||||
const targetPct = isPercent ? Math.min(100, Math.max(0, configured)) : 100;
|
||||
return {
|
||||
config: cfg,
|
||||
current,
|
||||
criteriaValue: configured,
|
||||
targetPct,
|
||||
targetLabel,
|
||||
target,
|
||||
totalPotential,
|
||||
isPercent,
|
||||
@@ -705,13 +725,11 @@
|
||||
|
||||
<div class="kanban-scroll">
|
||||
<div class="kanban-inner">
|
||||
<CardGrid>
|
||||
<CardGrid stackTablet>
|
||||
<Card cols={1} title="Members">
|
||||
{#each parentMembers as m}
|
||||
{@const total = totalChoresFor(m.id)}
|
||||
{@const todays = todayCompletionsFor(m.id)}
|
||||
{@const done = todays.length}
|
||||
{@const pct = total > 0 ? Math.round((done / total) * 100) : 0}
|
||||
{@const s = memberInSummary(m.id)}
|
||||
{@const pm = parentBonusConfigs.find(
|
||||
(c: any) => c.isPocketMoney && c.memberId === m.id
|
||||
@@ -722,7 +740,7 @@
|
||||
<span class="member-name">{m.name}</span>
|
||||
{#if !(pm && pm.rewardValue)}
|
||||
<Button
|
||||
href="/{famSlug}/{username}/bonuses?s=pocketmoney"
|
||||
href="/{famSlug}/{username}/rewards?s=pocketmoney"
|
||||
variant="secondary"
|
||||
size="sm">Pocket money</Button
|
||||
>
|
||||
@@ -742,18 +760,12 @@
|
||||
<span class="unset">Pocket money: amount not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="progress-row">
|
||||
<span class="label">Today:</span>
|
||||
<div class="bar-wrap">
|
||||
<div class="bar-fill" style="width:{pct}%"></div>
|
||||
</div>
|
||||
<span class="count">{done}/{total}</span>
|
||||
</div>
|
||||
{#if done > 0}
|
||||
<ul class="done-list">
|
||||
{#each todays as c}
|
||||
<li>
|
||||
✅ {choreNameFor(c.assignedChoreId)}
|
||||
{#each todays as c, i}
|
||||
<li class="done-item" class:odd={i % 2 === 1}>
|
||||
<span class="done-icon">{@html checkCircleIcon}</span>
|
||||
<span class="done-name">{choreNameFor(c.assignedChoreId)}</span>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/revoke"
|
||||
@@ -763,7 +775,7 @@
|
||||
class="revoke-form"
|
||||
>
|
||||
<input type="hidden" name="id" value={c.id} />
|
||||
<button type="submit" class="revoke-btn" title="Revoke">↩</button>
|
||||
<button type="submit" class="revoke-btn" title="Revoke">{@html revokeIcon}</button>
|
||||
</form>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -1145,8 +1157,18 @@ const res = await fetch('/api/members', {
|
||||
|
||||
<div class="hero-progress">
|
||||
<span class="hero-pct">{weeklyTotal > 0 ? weeklyPct : 0}%</span>
|
||||
<div class="bar-wrap hero-bar">
|
||||
<div class="bar-fill hero-fill" style="width:{weeklyPct}%"></div>
|
||||
<div class="hero-bars">
|
||||
<div class="bar-wrap hero-bar">
|
||||
<div class="bar-fill hero-fill" style="width:{weeklyPct}%"></div>
|
||||
</div>
|
||||
{#if pmTargetPct != null}
|
||||
<div class="hero-target">
|
||||
<div class="bar-wrap hero-target-bar">
|
||||
<div class="bar-fill hero-target-fill" style="width:{pmTargetPct}%"></div>
|
||||
</div>
|
||||
<span class="hero-target-lbl">🎯 {pmTargetPct}%</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="hero-count">
|
||||
{weeklyTotal > 0 ? `${weeklyDone}/${weeklyTotal} done` : 'no chores yet'}
|
||||
@@ -1209,6 +1231,9 @@ const res = await fetch('/api/members', {
|
||||
</div>
|
||||
<div class="goal-bar-wrap">
|
||||
<div class="goal-bar-fill" style="width:{pct}%"></div>
|
||||
{#if goal.targetPct < 100}
|
||||
<div class="goal-target" style="left:{goal.targetPct}%"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="goal-foot">
|
||||
{#if goal.isPercent && goal.totalPotential === 0}
|
||||
@@ -1222,11 +1247,12 @@ const res = await fetch('/api/members', {
|
||||
>{Math.min(goal.current, goal.target)} / {goal.target}</span
|
||||
>
|
||||
{/if}
|
||||
{#if goal.achieved}
|
||||
<span class="goal-badge">✅ earned</span>
|
||||
{:else}
|
||||
<span class="goal-pct">{pct}%</span>
|
||||
{/if}
|
||||
{#if goal.achieved}
|
||||
<span class="goal-badge">✅ earned</span>
|
||||
{:else}
|
||||
<span class="goal-target-pill">🎯 {goal.targetLabel}</span>
|
||||
<span class="goal-pct">{pct}%</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -1293,19 +1319,24 @@ const res = await fetch('/api/members', {
|
||||
{#each memberTodos.filter((t) => !isTodoDone(t.id)) as todo}
|
||||
{@const urgency = todoUrgency(todo)}
|
||||
{#if todo.type === 'emoji'}
|
||||
<div
|
||||
class="chore todo-chore todo-display"
|
||||
class:todo-blue={urgency === 'blue'}
|
||||
class:todo-green={urgency === 'green'}
|
||||
class:todo-orange={urgency === 'orange'}
|
||||
class:todo-red={urgency === 'red'}
|
||||
class:todo-black={urgency === 'black'}
|
||||
>
|
||||
<span class="checkbox">📋</span>
|
||||
<div
|
||||
class="chore todo-chore todo-display"
|
||||
class:todo-blue={urgency === 'blue'}
|
||||
class:todo-green={urgency === 'green'}
|
||||
class:todo-orange={urgency === 'orange'}
|
||||
class:todo-red={urgency === 'red'}
|
||||
class:todo-black={urgency === 'black'}
|
||||
>
|
||||
<span class="checkbox">📋</span>
|
||||
<span class="todo-main">
|
||||
<span class="chore-name">{todo.customName || 'Todo'}</span>
|
||||
<span class="todo-label">todo</span>
|
||||
<span class="chore-value">{todo.emoji || '🎯'}</span>
|
||||
</div>
|
||||
{#if todo.completeBy}
|
||||
<span class="todo-due">due {formatHumanDate(todo.completeBy)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="todo-label">todo</span>
|
||||
<span class="chore-value">{todo.emoji || '🎯'}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="chore todo-chore"
|
||||
@@ -1318,7 +1349,12 @@ const res = await fetch('/api/members', {
|
||||
disabled={!!togglingIds}
|
||||
>
|
||||
<span class="checkbox">📋</span>
|
||||
<span class="chore-name">{todo.customName || 'Todo'}</span>
|
||||
<span class="todo-main">
|
||||
<span class="chore-name">{todo.customName || 'Todo'}</span>
|
||||
{#if todo.completeBy}
|
||||
<span class="todo-due">due {formatHumanDate(todo.completeBy)}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="todo-label">todo</span>
|
||||
<span class="chore-value">
|
||||
{todo.type === 'money'
|
||||
@@ -1530,18 +1566,39 @@ const res = await fetch('/api/members', {
|
||||
.done-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0.4rem 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
.done-list li {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.05rem 0;
|
||||
color: #059669;
|
||||
.done-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: #059669;
|
||||
}
|
||||
.done-item.odd {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.done-icon {
|
||||
display: inline-flex;
|
||||
color: #22c55e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.done-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.revoke-form {
|
||||
display: inline;
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.revoke-btn {
|
||||
background: none;
|
||||
@@ -1792,6 +1849,41 @@ const res = await fetch('/api/members', {
|
||||
.hero-fill {
|
||||
background: #fff;
|
||||
}
|
||||
.hero-bars {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.hero-bars .hero-bar {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
.hero-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.hero-target-bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
.hero-target-fill {
|
||||
height: 100%;
|
||||
background: #fde047;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.hero-target-lbl {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
color: #fde047;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.hero-count {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
@@ -1944,17 +2036,40 @@ const res = await fetch('/api/members', {
|
||||
color: #059669;
|
||||
}
|
||||
.goal-bar-wrap {
|
||||
position: relative;
|
||||
height: 8px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.35rem;
|
||||
overflow: visible;
|
||||
margin: 0.35rem 0 0.5rem;
|
||||
}
|
||||
.goal-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
max-width: 100%;
|
||||
}
|
||||
.goal-target {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: -4px;
|
||||
width: 3px;
|
||||
background: #ef4444;
|
||||
border-radius: 2px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.goal-target-pill {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.goal-achieved .goal-bar-fill {
|
||||
background: linear-gradient(90deg, #10b981, #34d399);
|
||||
@@ -2104,6 +2219,20 @@ const res = await fetch('/api/members', {
|
||||
padding: 1px 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.todo-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 1px;
|
||||
}
|
||||
.todo-due {
|
||||
font-size: 0.68rem;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.todo-display {
|
||||
cursor: default;
|
||||
opacity: 0.85;
|
||||
|
||||
Reference in New Issue
Block a user