forked from yvv/decision
Fusion du lot F : objections redit / contredit, coquilles validées par leur auteur, messagerie, délai de 3 jours
Conflits avec B2 : docs.rs garde shown_tally / shown_evaluation et votable ; proposals.rs sert les totaux montrés et « votable » ; CONTROLES réunit C24 (« faire une objection », le panier par son adresse), C28–C33 et les deux passages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,16 +3,20 @@
|
||||
// vote et les autres. À combien il se rapproche du passage en vigueur. Façon
|
||||
// toggle, pas une fenêtre qui s'ouvre »): the level of the water, the totals —
|
||||
// never who voted what —, how far the threshold is, your vote and where it
|
||||
// stands, then the inertia and the formula. The rest of a version (typos,
|
||||
// translations, flags, history) lives in the panel, tab « Détails ».
|
||||
// stands, then the inertia and the formula. A version held back by an
|
||||
// objection says so, and tells each one what is theirs to do (ADR 0010). The
|
||||
// rest of a version (typos, translations, objections, history) lives in the
|
||||
// panel, tab « Détails ».
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import type { DocState } from '$lib/doc.svelte';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte';
|
||||
import { inertiaBadge, titleOf } from '$lib/labels';
|
||||
import { holding } from '$lib/objections';
|
||||
import { session } from '$lib/session.svelte';
|
||||
import { toasts } from '$lib/toast.svelte';
|
||||
import type { LevelInfo, Proposal, Unit } from '$lib/types';
|
||||
import { errorText, g1Amount, voteDirect, type Line } from '$lib/vote';
|
||||
import type { Entered, LevelInfo, Objection, Proposal, Unit } from '$lib/types';
|
||||
import { announce, errorText, g1Amount, voteDirect, type Line } from '$lib/vote';
|
||||
import Formula from './Formula.svelte';
|
||||
import InertiaPicker from './InertiaPicker.svelte';
|
||||
import WaterLevel from './WaterLevel.svelte';
|
||||
@@ -24,6 +28,7 @@
|
||||
onopen,
|
||||
onsettings,
|
||||
ondetails,
|
||||
onanswer,
|
||||
onsign,
|
||||
}: {
|
||||
doc: DocState;
|
||||
@@ -33,6 +38,8 @@
|
||||
onopen: (proposalId: string) => void;
|
||||
onsettings: (proposalId: string) => void;
|
||||
ondetails: (proposalId: string) => void;
|
||||
/** The author of a version held back answers its objection: modify the article repeated, or reword. */
|
||||
onanswer: (objection: Objection) => void;
|
||||
/** On the Ğ1 chain: the page has the lines signed by the person's wallet. */
|
||||
onsign: (lines: Line[]) => void;
|
||||
} = $props();
|
||||
@@ -41,6 +48,7 @@
|
||||
electorate: number;
|
||||
voteFloor: number;
|
||||
levels: LevelInfo[];
|
||||
objections: Objection[];
|
||||
}
|
||||
|
||||
let detail = $state<Detail | null>(null);
|
||||
@@ -51,7 +59,9 @@
|
||||
const view = $derived(doc.view);
|
||||
const access = $derived(view?.vote);
|
||||
const chain = $derived(access?.way === 'chain');
|
||||
const votable = $derived(p.status === 'open' || p.status === 'in_force');
|
||||
const live = $derived(p.status === 'open' || p.status === 'in_force');
|
||||
/** The objections holding this version back, and what each one asks of whom. */
|
||||
const held = $derived(p.blocked ? holding(detail?.objections ?? []) : []);
|
||||
const title = $derived(p.kind === 'text' ? p.title : titleOf(p, unit));
|
||||
const voted = $derived(p.myVote !== null && !p.myVoteStale);
|
||||
const ballot = $derived(p.myBallot);
|
||||
@@ -60,6 +70,9 @@
|
||||
const id = p.id;
|
||||
void p.tally.for;
|
||||
void p.tally.against;
|
||||
void p.blocked;
|
||||
void p.objections;
|
||||
void p.revision;
|
||||
api
|
||||
.get<Detail>(`/proposals/${id}`)
|
||||
.then((d) => {
|
||||
@@ -74,6 +87,22 @@
|
||||
root?.scrollIntoView({ block: 'nearest', behavior: reduce ? 'auto' : 'smooth' });
|
||||
});
|
||||
|
||||
async function settle(o: Objection, gesture: 'lift' | 'reiterate') {
|
||||
error = '';
|
||||
busy = true;
|
||||
try {
|
||||
const r = await api.post<{ entered?: Entered[] }>(`/objections/${o.id}/${gesture}`);
|
||||
toasts.show(t(gesture === 'lift' ? 'objection.done.lifted' : 'objection.done.reiterated'));
|
||||
await doc.reload();
|
||||
announce(doc, r.entered ?? []);
|
||||
void session.refreshUnread();
|
||||
} catch (e) {
|
||||
error = errorText(e);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cast(choice: 'for' | 'against' | null) {
|
||||
error = '';
|
||||
const lines: Line[] = [{ proposalId: p.id, choice }];
|
||||
@@ -105,7 +134,38 @@
|
||||
<p class="faint small">{inertiaBadge(p.votedAtInertia)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if p.blocked}<p class="warn">{t('vote.blocked')}</p>{/if}
|
||||
{#if p.blocked}
|
||||
<div class="held" role="status">
|
||||
<p class="lead">{t('vote.held')}</p>
|
||||
{#each held as o (o.id)}
|
||||
<div class="objection">
|
||||
<p>
|
||||
<b>{t(o.kind === 'redit' ? 'vote.held.redit' : 'vote.held.contredit', { other: o.other.title ?? '' })}</b>
|
||||
<span class="faint">· {t('objection.by', { name: o.objector ?? '' })}</span>
|
||||
</p>
|
||||
<p class="reason">{o.reason}</p>
|
||||
{#if o.mine}
|
||||
<p>{o.phase === 'objector' ? t('vote.held.mine.asked', { date: i18n.date(o.dueAt) }) : t('vote.held.mine')}</p>
|
||||
{:else if o.phase === 'objector'}
|
||||
<p>{t('vote.held.waiting', { name: o.objector ?? '', date: i18n.date(o.dueAt) })}</p>
|
||||
{:else if o.iAnswer}
|
||||
<p>{t(o.kind === 'redit' ? 'vote.held.author.redit' : 'vote.held.author.contredit')}</p>
|
||||
{/if}
|
||||
<div class="gestures">
|
||||
{#if o.iAnswer && !o.mine && o.phase === 'author'}
|
||||
<button class="btn small primary" type="button" onclick={() => onanswer(o)}>{t(o.kind === 'redit' ? 'objection.respond.redit' : 'objection.respond.contredit')}</button>
|
||||
{/if}
|
||||
{#if o.canLift}
|
||||
<button class="btn small" type="button" disabled={busy} onclick={() => settle(o, 'lift')}>{t(o.mine ? 'objection.lift' : 'objection.lift.admin')}</button>
|
||||
{/if}
|
||||
{#if o.canReiterate}
|
||||
<button class="btn small quiet" type="button" disabled={busy} onclick={() => settle(o, 'reiterate')}>{t('objection.reiterate')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mine" aria-live="polite">
|
||||
{#if ballot?.status === 'pending'}
|
||||
@@ -126,8 +186,10 @@
|
||||
{#if p.myVoteChain || ballot}<a class="faint small" href="/bulletins">{t('vote.mine.receipts')}</a>{/if}
|
||||
</div>
|
||||
|
||||
{#if !votable}
|
||||
{#if !live}
|
||||
<p class="muted">{t('vote.closed')}</p>
|
||||
{:else if p.blocked}
|
||||
<!-- Held back: no new vote, nor a vote taken back, until the objection is lifted. -->
|
||||
{:else if access && !access.open}
|
||||
<p class="closed">{t(access.reason === 'chain_off' ? 'vote.chain.off' : 'vote.members')}</p>
|
||||
{:else if access}
|
||||
@@ -248,6 +310,34 @@
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.held {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-radius: var(--r-input);
|
||||
background: var(--ochre-wash);
|
||||
}
|
||||
.lead {
|
||||
font-weight: 750;
|
||||
}
|
||||
.objection {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.reason {
|
||||
font-family: var(--font-text);
|
||||
font-style: italic;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.gestures {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.gestures:empty {
|
||||
display: none;
|
||||
}
|
||||
.more {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
|
||||
Reference in New Issue
Block a user