| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396 |
- const { form, button, div, h2, p, section, input, label, br, a, span, table, thead, tbody, tr, th, td, textarea, select, option, ul, li, img } = require('../server/node_modules/hyperaxe');
- const moment = require('../server/node_modules/moment');
- const { template, i18n } = require('./main_views');
- const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss');
- const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || []));
- const methodKey = (m) => String(m || '').toUpperCase();
- const methodLabel = (m) => {
- const key = `courtsMethod${methodKey(m)}`;
- const raw = i18n[key] || '';
- return String(raw).toUpperCase();
- };
- const showVoteMetrics = (m) => {
- const k = methodKey(m);
- return k === 'POPULAR' || k === 'KARMATOCRACY';
- };
- const CASE_TITLE_PRESETS = [
- 'Minor conflict',
- 'Moderate conflict',
- 'Severe conflict',
- 'Harassment or abuse',
- 'Content moderation',
- 'Ban or restriction',
- 'Governance dispute'
- ];
- const FILTERS = [
- { value: 'cases', key: 'courtsFilterCases' },
- { value: 'mycases', key: 'courtsFilterMyCases' },
- { value: 'actions', key: 'courtsFilterActions' },
- { value: 'judges', key: 'courtsFilterJudges' },
- { value: 'history', key: 'courtsFilterHistory' },
- { value: 'rules', key: 'courtsFilterRules' },
- { value: 'open', key: 'courtsFilterOpenCase' }
- ];
- const Tabs = (active) =>
- div(
- { class: 'filters' },
- form(
- { method: 'GET', action: '/courts' },
- FILTERS.map((f) => {
- const isOpen = f.value === 'open';
- const cls =
- isOpen
- ? 'create-button'
- : active === f.value
- ? 'filter-btn active'
- : 'filter-btn';
- return button(
- {
- type: 'submit',
- name: 'filter',
- value: f.value,
- class: cls
- },
- i18n[f.key]
- );
- })
- )
- );
- const CaseForm = () =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsCaseFormTitle),
- form(
- {
- method: 'POST',
- action: '/courts/cases/create'
- },
- label(i18n.courtsCaseTitle),
- br(),
- input({
- type: 'text',
- name: 'titleSuffix',
- required: true,
- placeholder: 'Subject or short description'
- }),
- br(),
- label('Case type'),
- br(),
- select(
- { name: 'titlePreset', required: true },
- CASE_TITLE_PRESETS.map((t) => option({ value: t }, t))
- ),
- br(),
- br(),
- label(i18n.courtsCaseRespondent),
- br(),
- input({
- type: 'text',
- name: 'respondentId',
- placeholder: i18n.courtsCaseRespondentPh,
- required: true,
- pattern: '^@[A-Za-z0-9+/]+=*\\.ed25519$',
- title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)'
- }),
- br(),
- label(i18n.courtsCaseMethod),
- br(),
- select(
- { name: 'method', required: true },
- ['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY'].map((m) =>
- option({ value: m }, i18n[`courtsMethod${m}`])
- )
- ),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsCaseSubmit)
- )
- );
- const NominateJudgeForm = () =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsNominateJudge),
- form(
- { method: 'POST', action: '/courts/judges/nominate' },
- label(i18n.courtsJudgeId),
- br(),
- input({
- type: 'text',
- name: 'judgeId',
- placeholder: i18n.courtsJudgeIdPh,
- required: true
- }),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsNominateBtn)
- )
- );
- const EvidenceForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsAddEvidence),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/evidence/add`,
- enctype: 'multipart/form-data'
- },
- label(i18n.courtsEvidenceText),
- br(),
- textarea({ name: 'text', rows: 3, maxlength: 1000 }),
- br(),
- br(),
- label(i18n.courtsEvidenceLink),
- br(),
- input({
- type: 'url',
- name: 'link',
- placeholder: i18n.courtsEvidenceLinkPh
- }),
- br(),
- br(),
- label(i18n.uploadMedia || 'Upload media (max-size: 50MB)'),
- br(),
- input({ type: 'file', name: 'image' }),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsEvidenceSubmit)
- )
- );
- const AnswerForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsAnswerTitle),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/answer`
- },
- label(i18n.courtsAnswerText),
- br(),
- textarea({ name: 'answer', rows: 4, maxlength: 2000 }),
- br(),
- br(),
- select(
- { name: 'stance' },
- ['DENY', 'ADMIT', 'PARTIAL'].map((s) =>
- option({ value: s }, i18n[`courtsStance${s}`])
- )
- ),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsAnswerSubmit)
- )
- );
- const VerdictForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsVerdictTitle),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/decide`
- },
- label(i18n.courtsVerdictResult),
- br(),
- input({ type: 'text', name: 'outcome', required: true }),
- br(),
- br(),
- label(i18n.courtsVerdictOrders),
- br(),
- textarea({
- name: 'orders',
- rows: 4,
- maxlength: 2000,
- placeholder: i18n.courtsVerdictOrdersPh
- }),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsIssueVerdict)
- )
- );
- const SettlementForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsMediationPropose),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/settlements/propose`
- },
- label(i18n.courtsSettlementText),
- br(),
- textarea({ name: 'terms', rows: 3, maxlength: 2000 }),
- br(),
- br(),
- button(
- { type: 'submit', class: 'create-button' },
- i18n.courtsSettlementProposeBtn
- )
- )
- );
- const RespondentMediatorsForm = (c) => {
- if (
- !c.isRespondent ||
- c.status === 'SOLVED' ||
- c.status === 'UNSOLVED' ||
- c.status === 'DISCARDED'
- )
- return null;
- return div(
- { class: 'div-center' },
- h2(i18n.courtsCaseMediatorsRespondentTitle),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(c.id)}/mediators/respondent`
- },
- label(i18n.courtsCaseMediatorsRespondent),
- br(),
- input({
- type: 'text',
- name: 'mediators',
- placeholder: i18n.courtsCaseMediatorsPh
- }),
- br(),
- br(),
- button({ type: 'submit', class: 'create-button' }, i18n.courtsMediatorsSubmit)
- )
- );
- };
- const JudgeAssignForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsAssignJudgeTitle),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/judge`
- },
- label(i18n.courtsJudgeId),
- br(),
- input({
- type: 'text',
- name: 'judgeId',
- placeholder: i18n.courtsJudgeIdPh,
- required: true
- }),
- br(),
- br(),
- button(
- { type: 'submit', class: 'create-button' },
- i18n.courtsAssignJudgeBtn
- )
- )
- );
- const SupportCaseForm = (caseId) =>
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/support`
- },
- button({ type: 'submit', class: 'vote-btn' }, i18n.courtsSupportCase)
- );
- const VerdictVoteForm = (caseId) =>
- div(
- { class: 'div-center' },
- h2(i18n.courtsVerdictVoteTitle),
- form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(caseId)}/verdict/vote`
- },
- label(i18n.courtsVerdictVoteLabel),
- br(),
- select(
- { name: 'decision' },
- option({ value: 'ACCEPT' }, i18n.courtsVerdictVoteAccept),
- option({ value: 'REJECT' }, i18n.courtsVerdictVoteReject)
- ),
- br(),
- br(),
- button(
- { type: 'submit', class: 'create-button' },
- i18n.courtsVerdictVoteSubmit
- )
- )
- );
- const shortId = (id) => {
- const s = String(id || '');
- if (!s) return '';
- if (s.length <= 16) return s;
- return `${s.slice(0, 6)}…${s.slice(-4)}`;
- };
- const UserLinkCompact = (id) =>
- id
- ? a(
- { class: 'user-link', href: `/author/${encodeURIComponent(id)}` },
- shortId(id)
- )
- : span('');
- const UserLinkFull = (id) =>
- id
- ? a(
- { class: 'user-link', href: `/author/${encodeURIComponent(id)}` },
- id
- )
- : span('');
- const renderRichTextNodes = (raw) => {
- const text = String(raw || '');
- if (!text) return [];
- const nodes = [];
- let remaining = text;
- const imgRegex = /!\[[^\]]*]\(([^)]+)\)/;
- const linkRegex = /\[([^\]]+)]\((https?:\/\/[^)]+)\)/;
- while (remaining.length) {
- const imgMatch = imgRegex.exec(remaining);
- const linkMatch = linkRegex.exec(remaining);
- let next = null;
- let type = null;
- if (imgMatch && (!linkMatch || imgMatch.index < linkMatch.index)) {
- next = imgMatch;
- type = 'img';
- } else if (linkMatch) {
- next = linkMatch;
- type = 'link';
- }
- if (!next) {
- if (remaining.trim()) nodes.push(p(remaining));
- break;
- }
- const idx = next.index;
- if (idx > 0) {
- const before = remaining.slice(0, idx);
- if (before.trim()) nodes.push(p(before));
- }
- if (type === 'img') {
- const ref = next[1];
- nodes.push(
- img({
- class: 'evidence-image',
- src: `/blob/${encodeURIComponent(ref)}`,
- alt: 'evidence'
- })
- );
- } else {
- const labelText = next[1];
- const url = next[2];
- nodes.push(
- a(
- {
- class: 'evidence-link',
- href: url,
- target: '_blank',
- rel: 'noopener noreferrer'
- },
- labelText
- )
- );
- }
- remaining = remaining.slice(idx + next[0].length);
- }
- return nodes;
- };
- const RichTextBlock = (raw) => {
- const nodes = renderRichTextNodes(raw);
- if (!nodes.length) return null;
- return nodes;
- };
- const CaseCard = (c) => {
- const mid = methodKey(c.method);
- const yes = Number(c.yes || 0);
- const total = Number(c.total || 0);
- const needed = Number(c.needed || 0);
- const showMetricsFlag = showVoteMetrics(mid) && (total > 0 || needed > 0);
- const accLink = UserLinkCompact(c.accuser);
- const resLink = UserLinkCompact(c.respondent);
- const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
- ? c.mediatorsAccuser
- : [];
- const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
- ? c.mediatorsRespondent
- : [];
- const canShowDetails = c.mine || c.publicDetails;
- const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
- span(
- { class: 'mediator' },
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- shortId(mId)
- ),
- idx < mediatorsAccuser.length - 1 ? span(', ') : null
- )
- );
- const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
- span(
- { class: 'mediator' },
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- shortId(mId)
- ),
- idx < mediatorsRespondent.length - 1 ? span(', ') : null
- )
- );
- const showPublicPrefForm =
- (c.status === 'SOLVED' ||
- c.status === 'UNSOLVED' ||
- c.status === 'DISCARDED') &&
- (c.isAccuser || c.isRespondent);
- const publicPreferenceForm = showPublicPrefForm
- ? form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(c.id)}/public`
- },
- label(i18n.courtsPublicPrefLabel),
- br(),
- select(
- { name: 'preference' },
- option(
- { value: 'YES', selected: c.myPublicPreference === true },
- i18n.courtsPublicPrefYes
- ),
- option(
- { value: 'NO', selected: c.myPublicPreference === false },
- i18n.courtsPublicPrefNo
- )
- ),
- br(),
- br(),
- button(
- { type: 'submit', class: 'create-button' },
- i18n.courtsPublicPrefSubmit
- )
- )
- : null;
- const respondentMediatorsForm = RespondentMediatorsForm(c);
- const canAddEvidence =
- c.isAccuser || c.isRespondent || c.isMediator || c.isJudge || c.isDictator;
- const canAnswer = c.isRespondent;
- const canIssueVerdict =
- (c.isJudge || c.isDictator || c.isMediator) &&
- c.status === 'OPEN' &&
- !c.hasVerdict;
- const canProposeSettlement =
- (c.isAccuser || c.isRespondent || c.isMediator) &&
- methodKey(c.method) === 'MEDIATION' &&
- c.status === 'OPEN';
- const canVoteVerdict =
- c.hasVerdict &&
- (c.isAccuser || c.isRespondent) &&
- c.status === 'OPEN';
- const isNormalUser =
- !c.isAccuser &&
- !c.isRespondent &&
- !c.isMediator &&
- !c.isJudge &&
- !c.isDictator;
- const canSupport =
- isNormalUser &&
- (c.status === 'IN_PROGRESS' || c.status === 'OPEN');
- const canAssignJudge =
- methodKey(c.method) === 'JUDGE' &&
- !c.judgeId &&
- (c.isAccuser || c.isRespondent);
- return div(
- { class: 'card' },
- div(
- { class: 'card-header' },
- div(
- { class: 'card-header__meta' },
- h2(c.title || ''),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
- span({ class: 'card-value' }, methodLabel(c.method))
- ),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
- span({ class: 'card-value' }, String(c.status || ''))
- ),
- c.answerBy
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '),
- span({ class: 'card-value' }, fmt(c.answerBy))
- )
- : null,
- c.evidenceBy
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '),
- span({ class: 'card-value' }, fmt(c.evidenceBy))
- )
- : null
- )
- ),
- div(
- { class: 'table-wrap mt-2' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(tr(th(i18n.courtsAccuser), th(i18n.courtsRespondent))),
- tbody(tr(td(accLink), td(resLink)))
- ])
- ),
- mediatorsAccuser.length
- ? div(
- { class: 'card-field' },
- span(
- { class: 'card-label' },
- i18n.courtsMediatorsAccuserLabel + ': '
- ),
- span({ class: 'card-value' }, ...mediatorLinksAccuser)
- )
- : null,
- mediatorsRespondent.length
- ? div(
- { class: 'card-field' },
- span(
- { class: 'card-label' },
- i18n.courtsMediatorsRespondentLabel + ': '
- ),
- span({ class: 'card-value' }, ...mediatorLinksRespondent)
- )
- : null,
- c.supportCount
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsSupportCount + ': '),
- span({ class: 'card-value' }, String(c.supportCount))
- )
- : null,
- canShowDetails ? RichTextBlock(c.description || '') : null,
- showMetricsFlag
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsVotesNeeded + ': '),
- span({ class: 'card-value' }, String(needed))
- )
- : null,
- showMetricsFlag
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ': '),
- span({ class: 'card-value' }, `${yes}/${total}`)
- )
- : null,
- c.voteId
- ? form(
- { method: 'GET', action: `/votes/${encodeURIComponent(c.voteId)}` },
- button({ type: 'submit', class: 'vote-btn' }, i18n.courtsOpenVote)
- )
- : null,
- canSupport ? SupportCaseForm(c.id || '') : null,
- publicPreferenceForm,
- respondentMediatorsForm,
- canAssignJudge ? JudgeAssignForm(c.id || '') : null,
- canAddEvidence ? EvidenceForm(c.id || '') : null,
- canAnswer ? AnswerForm(c.id || '') : null,
- canIssueVerdict ? VerdictForm(c.id || '') : null,
- canVoteVerdict ? VerdictVoteForm(c.id || '') : null,
- canProposeSettlement ? SettlementForm(c.id || '') : null
- );
- };
- const MyCaseCard = (c) => {
- const mid = methodKey(c.method);
- const yes = Number(c.yes || 0);
- const total = Number(c.total || 0);
- const needed = Number(c.needed || 0);
- const showMetricsFlag = showVoteMetrics(mid) && (total > 0 || needed > 0);
- const accLinkFull = UserLinkFull(c.accuser);
- const respondentId = c.respondentId || c.respondent;
- const resLinkFull = UserLinkFull(respondentId);
- const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
- ? c.mediatorsAccuser
- : [];
- const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
- ? c.mediatorsRespondent
- : [];
- const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
- span(
- {},
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- mId
- ),
- idx < mediatorsAccuser.length - 1 ? span(', ') : null
- )
- );
- const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
- span(
- {},
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- mId
- ),
- idx < mediatorsRespondent.length - 1 ? span(', ') : null
- )
- );
- const judgeLinkFull = c.judgeId ? UserLinkFull(c.judgeId) : span('');
- const showMediatorsTable =
- !!c.method &&
- (mediatorsAccuser.length > 0 ||
- mediatorsRespondent.length > 0 ||
- !!c.judgeId);
- const showPublicPrefForm =
- (c.status === 'SOLVED' ||
- c.status === 'UNSOLVED' ||
- c.status === 'DISCARDED') &&
- (c.isAccuser || c.isRespondent);
- const publicPreferenceForm = showPublicPrefForm
- ? form(
- {
- method: 'POST',
- action: `/courts/cases/${encodeURIComponent(c.id)}/public`
- },
- label(i18n.courtsPublicPrefLabel),
- br(),
- select(
- { name: 'preference' },
- option(
- { value: 'YES', selected: c.myPublicPreference === true },
- i18n.courtsPublicPrefYes
- ),
- option(
- { value: 'NO', selected: c.myPublicPreference === false },
- i18n.courtsPublicPrefNo
- )
- ),
- br(),
- br(),
- button(
- { type: 'submit', class: 'create-button' },
- i18n.courtsPublicPrefSubmit
- )
- )
- : null;
- const respondentMediatorsForm = RespondentMediatorsForm(c);
- const canAddEvidence =
- c.isAccuser || c.isRespondent || c.isMediator || c.isJudge || c.isDictator;
- const canAnswer = c.isRespondent;
- const canIssueVerdict =
- (c.isJudge || c.isDictator || c.isMediator) &&
- c.status === 'OPEN' &&
- !c.hasVerdict;
- const canProposeSettlement =
- (c.isAccuser || c.isRespondent || c.isMediator) &&
- methodKey(c.method) === 'MEDIATION' &&
- c.status === 'OPEN';
- const canVoteVerdict =
- c.hasVerdict &&
- (c.isAccuser || c.isRespondent) &&
- c.status === 'OPEN';
- const isNormalUser =
- !c.isAccuser &&
- !c.isRespondent &&
- !c.isMediator &&
- !c.isJudge &&
- !c.isDictator;
- const canSupport =
- isNormalUser &&
- (c.status === 'IN_PROGRESS' || c.status === 'OPEN');
- const canAssignJudge =
- methodKey(c.method) === 'JUDGE' &&
- !c.judgeId &&
- (c.isAccuser || c.isRespondent);
- return div(
- { class: 'card' },
- br(),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThCase + ': '),
- span({ class: 'card-value' }, c.title || '')
- ),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
- span({ class: 'card-value' }, String(c.status || ''))
- ),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsMethod + ': '),
- span({ class: 'card-value' }, methodLabel(c.method))
- ),
- c.answerBy
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '),
- span({ class: 'card-value' }, fmt(c.answerBy))
- )
- : null,
- c.evidenceBy
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '),
- span({ class: 'card-value' }, fmt(c.evidenceBy))
- )
- : null,
- c.decisionBy
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
- span({ class: 'card-value' }, fmt(c.decisionBy))
- )
- : null,
- div(
- { class: 'table-wrap mt-2' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(i18n.courtsAccuser),
- th(i18n.courtsRespondent)
- )
- ),
- tbody(
- tr(
- td(accLinkFull),
- td(resLinkFull)
- )
- )
- ])
- ),
- showMediatorsTable
- ? div(
- { class: 'table-wrap mt-2 mediators-table' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(i18n.courtsThJudge),
- th(
- i18n.courtsMediatorsAccuserLabel ||
- i18n.courtsMediatorsLabel ||
- 'Accuser mediators'
- ),
- th(
- i18n.courtsMediatorsRespondentLabel ||
- i18n.courtsMediatorsLabel ||
- 'Respondent mediators'
- )
- )
- ),
- tbody(
- tr(
- td(judgeLinkFull),
- td(
- mediatorLinksAccuser.length
- ? mediatorLinksAccuser
- : span('')
- ),
- td(
- mediatorLinksRespondent.length
- ? mediatorLinksRespondent
- : span('')
- )
- )
- )
- ])
- )
- : null,
- c.supportCount
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsSupportCount + ': '),
- span({ class: 'card-value' }, String(c.supportCount))
- )
- : null,
- showMetricsFlag
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsVotesNeeded + ': '),
- span({ class: 'card-value' }, String(needed))
- )
- : null,
- showMetricsFlag
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ': '),
- span({ class: 'card-value' }, `${yes}/${total}`)
- )
- : null,
- c.voteId
- ? form(
- { method: 'GET', action: `/votes/${encodeURIComponent(c.voteId)}` },
- button({ type: 'submit', class: 'vote-btn' }, i18n.courtsOpenVote)
- )
- : null,
- canSupport ? SupportCaseForm(c.id || '') : null,
- publicPreferenceForm,
- respondentMediatorsForm,
- canAssignJudge ? JudgeAssignForm(c.id || '') : null,
- canAddEvidence ? EvidenceForm(c.id || '') : null,
- canAnswer ? AnswerForm(c.id || '') : null,
- canIssueVerdict ? VerdictForm(c.id || '') : null,
- canVoteVerdict ? VerdictVoteForm(c.id || '') : null,
- canProposeSettlement ? SettlementForm(c.id || '') : null
- );
- };
- const CasesList = (rows = []) => {
- const cards = rows.map(CaseCard);
- return div({ class: 'cards' }, ...cards);
- };
- const MyCasesList = (rows = []) => {
- const cards = rows.map(MyCaseCard);
- return div({ class: 'cards' }, ...cards);
- };
- const roleTextForCase = (c) => {
- if (c.isAccuser) return i18n.courtsRoleAccuser || 'Accuser';
- if (c.isRespondent) return i18n.courtsRoleDefence || 'Defence';
- if (c.isMediator) return i18n.courtsRoleMediator || 'Mediator';
- if (c.isJudge) return i18n.courtsRoleJudge || 'Judge';
- if (c.isDictator) return i18n.courtsRoleDictator || 'Dictator';
- return '';
- };
- const CasesTable = (rows = [], opts = {}) => {
- if (!rows.length) return div({ class: 'empty' }, p(i18n.courtsNoCases));
- const showRole = !!opts.showRole;
- const bodyRows = rows.map((c) => {
- const role = showRole ? roleTextForCase(c) : '';
- return tr(
- td(c.title || ''),
- td(
- c.accuser
- ? a(
- {
- class: 'user-link',
- href: `/author/${encodeURIComponent(c.accuser)}`
- },
- c.accuser
- )
- : ''
- ),
- td(methodLabel(c.method)),
- td(c.createdAt ? fmt(c.createdAt) : ''),
- showRole ? td(role) : null,
- td(
- c.id
- ? form(
- {
- method: 'GET',
- action: `/courts/cases/${encodeURIComponent(c.id)}`
- },
- button(
- { type: 'submit', class: 'link-button' },
- i18n.courtsViewDetailsShort ||
- i18n.courtsViewDetails ||
- 'View'
- )
- )
- : ''
- )
- );
- });
- return div(
- { class: 'table-wrap' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(i18n.courtsThCase),
- th(i18n.courtsAccuser),
- th(i18n.courtsCaseMethod),
- th(i18n.courtsThCreatedAt),
- showRole ? th(i18n.courtsThRole || 'Role') : null,
- th(i18n.courtsThDetails || '')
- )
- ),
- applyEl(tbody, null, bodyRows)
- ])
- );
- };
- const NominationsTable = (nominations = [], currentUserId = '') => {
- if (!nominations || !nominations.length)
- return div({ class: 'empty' }, p(i18n.courtsNoNominations));
- const rows = nominations.map((n) => {
- const isSelf =
- currentUserId &&
- String(n.judgeId || '') === String(currentUserId || '');
- return tr(
- td(
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(n.judgeId)}` },
- n.judgeId
- )
- ),
- td(String(n.supports || 0)),
- td(fmt(n.createdAt)),
- td(
- isSelf
- ? span('')
- : form(
- {
- method: 'POST',
- action: `/courts/judges/${encodeURIComponent(n.id)}/vote`
- },
- button({ class: 'vote-btn' }, i18n.courtsThVote)
- )
- )
- );
- });
- return div(
- { class: 'table-wrap' },
- h2(i18n.courtsNominationsTitle),
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(i18n.courtsThJudge),
- th(i18n.courtsThSupports),
- th(i18n.courtsThDate),
- th(i18n.courtsThVote)
- )
- ),
- applyEl(tbody, null, rows)
- ])
- );
- };
- const JudgesSection = (nominations = [], currentUserId = '') => {
- const nomBlock = NominationsTable(nominations, currentUserId);
- return div(NominateJudgeForm(), nomBlock);
- };
- const HistoryList = (rows = []) => {
- if (!rows.length) return div({ class: 'empty' }, p(i18n.courtsNoHistory));
- const cards = rows.map((hh) => {
- const canShowDescription = hh.mine || hh.publicDetails;
- return div(
- { class: 'card' },
- br(),
- hh.method
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
- span({ class: 'card-value' }, methodLabel(hh.method))
- )
- : null,
- hh.decidedAt
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
- span({ class: 'card-value' }, fmt(hh.decidedAt))
- )
- : null,
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
- span({ class: 'card-value' }, String(hh.status || ''))
- ),
- h2(hh.title || ''),
- canShowDescription ? RichTextBlock(hh.description || '') : null,
- form(
- {
- method: 'GET',
- action: `/courts/cases/${encodeURIComponent(hh.id)}`
- },
- button({ type: 'submit', class: 'create-button' }, i18n.courtsViewDetails)
- )
- );
- });
- return div({ class: 'cards' }, ...cards);
- };
- const RulesContent = () =>
- div(
- { class: 'card' },
- h2(i18n.courtsRulesTitle),
- ul(
- li(i18n.courtsRulesIntro),
- li(i18n.courtsRulesLifecycle),
- li(i18n.courtsRulesRoles),
- li(i18n.courtsRulesEvidence),
- li(i18n.courtsRulesDeliberation),
- li(i18n.courtsRulesVerdict),
- li(i18n.courtsRulesAppeals),
- li(i18n.courtsRulesPrivacy),
- li(i18n.courtsRulesMisconduct),
- li(i18n.courtsRulesGlossary)
- )
- );
- const CaseSearch = (filter, search = '') =>
- div(
- { class: 'filters' },
- form(
- { method: 'GET', action: '/courts' },
- input({ type: 'hidden', name: 'filter', value: filter }),
- input({
- type: 'text',
- name: 'search',
- placeholder: i18n.searchCasesPlaceholder,
- value: search || ''
- }),
- br(),
- button({ type: 'submit' }, i18n.applyFilters),
- br()
- )
- );
- const CaseDetailsBlock = (c) => {
- if (!c) return div({ class: 'empty' }, p(i18n.courtsNoCases));
- const canShowFull = c.mine || c.publicDetails;
- const accLink = UserLinkFull(c.accuser);
- const resLink = UserLinkFull(c.respondent || c.respondentId);
- const judgeValue = c.judgeId
- ? UserLinkFull(c.judgeId)
- : span(i18n.courtsJudgeNotAssigned || '-');
- const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
- ? c.mediatorsAccuser
- : [];
- const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
- ? c.mediatorsRespondent
- : [];
- const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
- span(
- { class: 'mediator' },
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- shortId(mId)
- ),
- idx < mediatorsAccuser.length - 1 ? span(', ') : null
- )
- );
- const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
- span(
- { class: 'mediator' },
- a(
- { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
- shortId(mId)
- ),
- idx < mediatorsRespondent.length - 1 ? span(', ') : null
- )
- );
- const showMediatorsTable =
- c.method && (mediatorsAccuser.length || mediatorsRespondent.length);
- const evidences = Array.isArray(c.evidences) ? c.evidences : [];
- const answers = Array.isArray(c.answers) ? c.answers : [];
- const settlements = Array.isArray(c.settlements) ? c.settlements : [];
- const evidenceSideLabel = (e) => {
- const side = String((e && e.side) || '').toUpperCase();
- if (side === 'ACCUSER') {
- return i18n.courtsEvidenceSideAccuser || 'Accuser evidence';
- }
- if (side === 'RESPONDENT' || side === 'DEFENCE' || side === 'DEFENSE') {
- return i18n.courtsEvidenceSideRespondent || 'Defence evidence';
- }
- return i18n.courtsEvidenceSideUnknown || '';
- };
- return div(
- { class: 'card case-details-card' },
- br(),
- div(
- { class: 'card-header' },
- div(
- { class: 'card-header__meta' },
- h2(c.title || ''),
- c.method
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
- span({ class: 'card-value' }, methodLabel(c.method))
- )
- : null,
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThJudge + ': '),
- span({ class: 'card-value' }, judgeValue)
- ),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
- span({ class: 'card-value' }, String(c.status || ''))
- ),
- c.createdAt
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThCreatedAt + ': '),
- span({ class: 'card-value' }, fmt(c.createdAt))
- )
- : null,
- c.decidedAt
- ? div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
- span({ class: 'card-value' }, fmt(c.decidedAt))
- )
- : null
- )
- ),
- div(
- { class: 'table-wrap mt-2' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(i18n.courtsAccuser),
- th(i18n.courtsRespondent)
- )
- ),
- tbody(
- tr(
- td(accLink),
- td(resLink)
- )
- )
- ])
- ),
- showMediatorsTable
- ? div(
- { class: 'table-wrap mt-2 mediators-table' },
- applyEl(table, { class: 'table table--centered' }, [
- thead(
- tr(
- th(
- i18n.courtsMediatorsAccuserLabel ||
- i18n.courtsMediatorsLabel ||
- 'Accuser mediators'
- ),
- th(
- i18n.courtsMediatorsRespondentLabel ||
- i18n.courtsMediatorsLabel ||
- 'Respondent mediators'
- )
- )
- ),
- tbody(
- tr(
- td(
- mediatorLinksAccuser.length
- ? mediatorLinksAccuser
- : span('')
- ),
- td(
- mediatorLinksRespondent.length
- ? mediatorLinksRespondent
- : span('')
- )
- )
- )
- ])
- )
- : null,
- canShowFull ? RichTextBlock(c.description || '') : null,
- !canShowFull
- ? div(
- { class: 'card-field' },
- p(i18n.courtsDetailsNotPublic)
- )
- : null,
- canShowFull && evidences.length
- ? div(
- { class: 'card-section evidences-section' },
- h2(i18n.courtsDetailsEvidenceTitle),
- ...evidences.map((e) => {
- const bodyChildren = [];
- if (e.text && String(e.text).trim()) {
- bodyChildren.push(RichTextBlock(e.text));
- }
- if (e.link && String(e.link).trim()) {
- bodyChildren.push(
- a(
- {
- class: 'evidence-link',
- href: e.link,
- target: '_blank',
- rel: 'noopener noreferrer'
- },
- e.link
- )
- );
- }
- if (e.imageUrl && String(e.imageUrl).trim()) {
- bodyChildren.push(
- br(),br(),
- img({
- class: 'evidence-image',
- src: `/blob/${encodeURIComponent(e.imageUrl)}`,
- alt: 'evidence'
- })
- );
- }
- if (!bodyChildren.length) {
- bodyChildren.push(span(''));
- }
- const sideLabel = evidenceSideLabel(e);
- const dateText = fmt(e.createdAt);
- const dateWithSide = sideLabel
- ? dateText + ' · ' + sideLabel
- : dateText;
- return div(
- { class: 'evidence-item' },
- div(
- { class: 'evidence-date' },
- dateWithSide
- ),
- div(
- { class: 'evidence-body' },
- ...bodyChildren
- )
- );
- })
- )
- : null,
- canShowFull && answers.length
- ? div(
- { class: 'card-section answers-section' },
- h2(i18n.courtsDetailsAnswersTitle),
- ...answers.map((aItem) => {
- const stanceKey = methodKey(aItem.stance);
- const stanceLabel =
- stanceKey && i18n[`courtsStance${stanceKey}`]
- ? i18n[`courtsStance${stanceKey}`]
- : aItem.stance || '';
- const dateText = fmt(aItem.createdAt);
- const metaText = stanceLabel
- ? dateText + ' · ' + stanceLabel
- : dateText;
- return div(
- { class: 'answer-item' },
- div(
- { class: 'answer-meta' },
- metaText
- ),
- RichTextBlock(aItem.text || '')
- );
- })
- )
- : null,
- canShowFull && c.verdict
- ? div(
- { class: 'card-section verdict-section' },
- h2(i18n.courtsDetailsVerdictTitle),
- div(
- { class: 'card-field' },
- span({ class: 'card-label' }, i18n.courtsVerdictResult + ': '),
- span({ class: 'card-value' }, c.verdict.result || '')
- ),
- c.verdict.orders
- ? div(
- { class: 'card-field' },
- span(
- { class: 'card-label' },
- i18n.courtsVerdictOrders + ': '
- ),
- RichTextBlock(c.verdict.orders || '')
- )
- : null
- )
- : null,
- canShowFull && settlements.length
- ? div(
- { class: 'card-section settlements-section' },
- h2(i18n.courtsDetailsSettlementsTitle),
- ...settlements.map((sItem) =>
- div(
- { class: 'settlement-item' },
- div(
- { class: 'settlement-date' },
- fmt(sItem.createdAt)
- ),
- RichTextBlock(sItem.terms || '')
- )
- )
- )
- : null
- );
- };
- const courtsView = async (state) => {
- const {
- filter = 'cases',
- cases = [],
- myCases = [],
- history = [],
- nominations = [],
- search = '',
- userId = ''
- } = state;
- return template(
- i18n.courtsTitle,
- section(
- div(
- { class: 'tags-header' },
- h2(i18n.courtsTitle),
- p(i18n.courtsDescription)
- ),
- Tabs(filter),
- filter === 'cases' ? CaseSearch(filter, search) : null
- ),
- section(
- filter === 'cases' ? CasesTable(cases) : null,
- filter === 'mycases'
- ? myCases.length
- ? CasesTable(myCases, { showRole: true })
- : div({ class: 'empty' }, p(i18n.courtsNoMyCases))
- : null,
- filter === 'actions'
- ? myCases.length
- ? MyCasesList(myCases)
- : div({ class: 'empty' }, p(i18n.courtsNoMyCases))
- : null,
- filter === 'judges' ? JudgesSection(nominations, userId) : null,
- filter === 'history' ? HistoryList(history) : null,
- filter === 'rules' ? RulesContent() : null,
- filter === 'open' ? CaseForm() : null
- )
- );
- };
- const courtsCaseView = async (state) => {
- const { caseData } = state;
- return template(
- i18n.courtsTitle,
- section(
- div(
- { class: 'tags-header' },
- h2(i18n.courtsTitle),
- p(i18n.courtsDescription)
- ),
- Tabs('cases')
- ),
- section(CaseDetailsBlock(caseData))
- );
- };
- module.exports = { courtsView, courtsCaseView };
|