courts_view.js 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396
  1. 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');
  2. const moment = require('../server/node_modules/moment');
  3. const { template, i18n } = require('./main_views');
  4. const fmt = (d) => moment(d).format('YYYY-MM-DD HH:mm:ss');
  5. const applyEl = (fn, attrs, kids) => fn.apply(null, [attrs || {}].concat(kids || []));
  6. const methodKey = (m) => String(m || '').toUpperCase();
  7. const methodLabel = (m) => {
  8. const key = `courtsMethod${methodKey(m)}`;
  9. const raw = i18n[key] || '';
  10. return String(raw).toUpperCase();
  11. };
  12. const showVoteMetrics = (m) => {
  13. const k = methodKey(m);
  14. return k === 'POPULAR' || k === 'KARMATOCRACY';
  15. };
  16. const CASE_TITLE_PRESETS = [
  17. 'Minor conflict',
  18. 'Moderate conflict',
  19. 'Severe conflict',
  20. 'Harassment or abuse',
  21. 'Content moderation',
  22. 'Ban or restriction',
  23. 'Governance dispute'
  24. ];
  25. const FILTERS = [
  26. { value: 'cases', key: 'courtsFilterCases' },
  27. { value: 'mycases', key: 'courtsFilterMyCases' },
  28. { value: 'actions', key: 'courtsFilterActions' },
  29. { value: 'judges', key: 'courtsFilterJudges' },
  30. { value: 'history', key: 'courtsFilterHistory' },
  31. { value: 'rules', key: 'courtsFilterRules' },
  32. { value: 'open', key: 'courtsFilterOpenCase' }
  33. ];
  34. const Tabs = (active) =>
  35. div(
  36. { class: 'filters' },
  37. form(
  38. { method: 'GET', action: '/courts' },
  39. FILTERS.map((f) => {
  40. const isOpen = f.value === 'open';
  41. const cls =
  42. isOpen
  43. ? 'create-button'
  44. : active === f.value
  45. ? 'filter-btn active'
  46. : 'filter-btn';
  47. return button(
  48. {
  49. type: 'submit',
  50. name: 'filter',
  51. value: f.value,
  52. class: cls
  53. },
  54. i18n[f.key]
  55. );
  56. })
  57. )
  58. );
  59. const CaseForm = () =>
  60. div(
  61. { class: 'div-center' },
  62. h2(i18n.courtsCaseFormTitle),
  63. form(
  64. {
  65. method: 'POST',
  66. action: '/courts/cases/create'
  67. },
  68. label(i18n.courtsCaseTitle),
  69. br(),
  70. input({
  71. type: 'text',
  72. name: 'titleSuffix',
  73. required: true,
  74. placeholder: 'Subject or short description'
  75. }),
  76. br(),
  77. label('Case type'),
  78. br(),
  79. select(
  80. { name: 'titlePreset', required: true },
  81. CASE_TITLE_PRESETS.map((t) => option({ value: t }, t))
  82. ),
  83. br(),
  84. br(),
  85. label(i18n.courtsCaseRespondent),
  86. br(),
  87. input({
  88. type: 'text',
  89. name: 'respondentId',
  90. placeholder: i18n.courtsCaseRespondentPh,
  91. required: true,
  92. pattern: '^@[A-Za-z0-9+/]+=*\\.ed25519$',
  93. title: i18n.courtsRespondentInvalid || 'Must be a valid SSB ID (@...ed25519)'
  94. }),
  95. br(),
  96. label(i18n.courtsCaseMethod),
  97. br(),
  98. select(
  99. { name: 'method', required: true },
  100. ['JUDGE', 'DICTATOR', 'POPULAR', 'MEDIATION', 'KARMATOCRACY'].map((m) =>
  101. option({ value: m }, i18n[`courtsMethod${m}`])
  102. )
  103. ),
  104. br(),
  105. br(),
  106. button({ type: 'submit', class: 'create-button' }, i18n.courtsCaseSubmit)
  107. )
  108. );
  109. const NominateJudgeForm = () =>
  110. div(
  111. { class: 'div-center' },
  112. h2(i18n.courtsNominateJudge),
  113. form(
  114. { method: 'POST', action: '/courts/judges/nominate' },
  115. label(i18n.courtsJudgeId),
  116. br(),
  117. input({
  118. type: 'text',
  119. name: 'judgeId',
  120. placeholder: i18n.courtsJudgeIdPh,
  121. required: true
  122. }),
  123. br(),
  124. br(),
  125. button({ type: 'submit', class: 'create-button' }, i18n.courtsNominateBtn)
  126. )
  127. );
  128. const EvidenceForm = (caseId) =>
  129. div(
  130. { class: 'div-center' },
  131. h2(i18n.courtsAddEvidence),
  132. form(
  133. {
  134. method: 'POST',
  135. action: `/courts/cases/${encodeURIComponent(caseId)}/evidence/add`,
  136. enctype: 'multipart/form-data'
  137. },
  138. label(i18n.courtsEvidenceText),
  139. br(),
  140. textarea({ name: 'text', rows: 3, maxlength: 1000 }),
  141. br(),
  142. br(),
  143. label(i18n.courtsEvidenceLink),
  144. br(),
  145. input({
  146. type: 'url',
  147. name: 'link',
  148. placeholder: i18n.courtsEvidenceLinkPh
  149. }),
  150. br(),
  151. br(),
  152. label(i18n.uploadMedia || 'Upload media (max-size: 50MB)'),
  153. br(),
  154. input({ type: 'file', name: 'image' }),
  155. br(),
  156. br(),
  157. button({ type: 'submit', class: 'create-button' }, i18n.courtsEvidenceSubmit)
  158. )
  159. );
  160. const AnswerForm = (caseId) =>
  161. div(
  162. { class: 'div-center' },
  163. h2(i18n.courtsAnswerTitle),
  164. form(
  165. {
  166. method: 'POST',
  167. action: `/courts/cases/${encodeURIComponent(caseId)}/answer`
  168. },
  169. label(i18n.courtsAnswerText),
  170. br(),
  171. textarea({ name: 'answer', rows: 4, maxlength: 2000 }),
  172. br(),
  173. br(),
  174. select(
  175. { name: 'stance' },
  176. ['DENY', 'ADMIT', 'PARTIAL'].map((s) =>
  177. option({ value: s }, i18n[`courtsStance${s}`])
  178. )
  179. ),
  180. br(),
  181. br(),
  182. button({ type: 'submit', class: 'create-button' }, i18n.courtsAnswerSubmit)
  183. )
  184. );
  185. const VerdictForm = (caseId) =>
  186. div(
  187. { class: 'div-center' },
  188. h2(i18n.courtsVerdictTitle),
  189. form(
  190. {
  191. method: 'POST',
  192. action: `/courts/cases/${encodeURIComponent(caseId)}/decide`
  193. },
  194. label(i18n.courtsVerdictResult),
  195. br(),
  196. input({ type: 'text', name: 'outcome', required: true }),
  197. br(),
  198. br(),
  199. label(i18n.courtsVerdictOrders),
  200. br(),
  201. textarea({
  202. name: 'orders',
  203. rows: 4,
  204. maxlength: 2000,
  205. placeholder: i18n.courtsVerdictOrdersPh
  206. }),
  207. br(),
  208. br(),
  209. button({ type: 'submit', class: 'create-button' }, i18n.courtsIssueVerdict)
  210. )
  211. );
  212. const SettlementForm = (caseId) =>
  213. div(
  214. { class: 'div-center' },
  215. h2(i18n.courtsMediationPropose),
  216. form(
  217. {
  218. method: 'POST',
  219. action: `/courts/cases/${encodeURIComponent(caseId)}/settlements/propose`
  220. },
  221. label(i18n.courtsSettlementText),
  222. br(),
  223. textarea({ name: 'terms', rows: 3, maxlength: 2000 }),
  224. br(),
  225. br(),
  226. button(
  227. { type: 'submit', class: 'create-button' },
  228. i18n.courtsSettlementProposeBtn
  229. )
  230. )
  231. );
  232. const RespondentMediatorsForm = (c) => {
  233. if (
  234. !c.isRespondent ||
  235. c.status === 'SOLVED' ||
  236. c.status === 'UNSOLVED' ||
  237. c.status === 'DISCARDED'
  238. )
  239. return null;
  240. return div(
  241. { class: 'div-center' },
  242. h2(i18n.courtsCaseMediatorsRespondentTitle),
  243. form(
  244. {
  245. method: 'POST',
  246. action: `/courts/cases/${encodeURIComponent(c.id)}/mediators/respondent`
  247. },
  248. label(i18n.courtsCaseMediatorsRespondent),
  249. br(),
  250. input({
  251. type: 'text',
  252. name: 'mediators',
  253. placeholder: i18n.courtsCaseMediatorsPh
  254. }),
  255. br(),
  256. br(),
  257. button({ type: 'submit', class: 'create-button' }, i18n.courtsMediatorsSubmit)
  258. )
  259. );
  260. };
  261. const JudgeAssignForm = (caseId) =>
  262. div(
  263. { class: 'div-center' },
  264. h2(i18n.courtsAssignJudgeTitle),
  265. form(
  266. {
  267. method: 'POST',
  268. action: `/courts/cases/${encodeURIComponent(caseId)}/judge`
  269. },
  270. label(i18n.courtsJudgeId),
  271. br(),
  272. input({
  273. type: 'text',
  274. name: 'judgeId',
  275. placeholder: i18n.courtsJudgeIdPh,
  276. required: true
  277. }),
  278. br(),
  279. br(),
  280. button(
  281. { type: 'submit', class: 'create-button' },
  282. i18n.courtsAssignJudgeBtn
  283. )
  284. )
  285. );
  286. const SupportCaseForm = (caseId) =>
  287. form(
  288. {
  289. method: 'POST',
  290. action: `/courts/cases/${encodeURIComponent(caseId)}/support`
  291. },
  292. button({ type: 'submit', class: 'vote-btn' }, i18n.courtsSupportCase)
  293. );
  294. const VerdictVoteForm = (caseId) =>
  295. div(
  296. { class: 'div-center' },
  297. h2(i18n.courtsVerdictVoteTitle),
  298. form(
  299. {
  300. method: 'POST',
  301. action: `/courts/cases/${encodeURIComponent(caseId)}/verdict/vote`
  302. },
  303. label(i18n.courtsVerdictVoteLabel),
  304. br(),
  305. select(
  306. { name: 'decision' },
  307. option({ value: 'ACCEPT' }, i18n.courtsVerdictVoteAccept),
  308. option({ value: 'REJECT' }, i18n.courtsVerdictVoteReject)
  309. ),
  310. br(),
  311. br(),
  312. button(
  313. { type: 'submit', class: 'create-button' },
  314. i18n.courtsVerdictVoteSubmit
  315. )
  316. )
  317. );
  318. const shortId = (id) => {
  319. const s = String(id || '');
  320. if (!s) return '';
  321. if (s.length <= 16) return s;
  322. return `${s.slice(0, 6)}…${s.slice(-4)}`;
  323. };
  324. const UserLinkCompact = (id) =>
  325. id
  326. ? a(
  327. { class: 'user-link', href: `/author/${encodeURIComponent(id)}` },
  328. shortId(id)
  329. )
  330. : span('');
  331. const UserLinkFull = (id) =>
  332. id
  333. ? a(
  334. { class: 'user-link', href: `/author/${encodeURIComponent(id)}` },
  335. id
  336. )
  337. : span('');
  338. const renderRichTextNodes = (raw) => {
  339. const text = String(raw || '');
  340. if (!text) return [];
  341. const nodes = [];
  342. let remaining = text;
  343. const imgRegex = /!\[[^\]]*]\(([^)]+)\)/;
  344. const linkRegex = /\[([^\]]+)]\((https?:\/\/[^)]+)\)/;
  345. while (remaining.length) {
  346. const imgMatch = imgRegex.exec(remaining);
  347. const linkMatch = linkRegex.exec(remaining);
  348. let next = null;
  349. let type = null;
  350. if (imgMatch && (!linkMatch || imgMatch.index < linkMatch.index)) {
  351. next = imgMatch;
  352. type = 'img';
  353. } else if (linkMatch) {
  354. next = linkMatch;
  355. type = 'link';
  356. }
  357. if (!next) {
  358. if (remaining.trim()) nodes.push(p(remaining));
  359. break;
  360. }
  361. const idx = next.index;
  362. if (idx > 0) {
  363. const before = remaining.slice(0, idx);
  364. if (before.trim()) nodes.push(p(before));
  365. }
  366. if (type === 'img') {
  367. const ref = next[1];
  368. nodes.push(
  369. img({
  370. class: 'evidence-image',
  371. src: `/blob/${encodeURIComponent(ref)}`,
  372. alt: 'evidence'
  373. })
  374. );
  375. } else {
  376. const labelText = next[1];
  377. const url = next[2];
  378. nodes.push(
  379. a(
  380. {
  381. class: 'evidence-link',
  382. href: url,
  383. target: '_blank',
  384. rel: 'noopener noreferrer'
  385. },
  386. labelText
  387. )
  388. );
  389. }
  390. remaining = remaining.slice(idx + next[0].length);
  391. }
  392. return nodes;
  393. };
  394. const RichTextBlock = (raw) => {
  395. const nodes = renderRichTextNodes(raw);
  396. if (!nodes.length) return null;
  397. return nodes;
  398. };
  399. const CaseCard = (c) => {
  400. const mid = methodKey(c.method);
  401. const yes = Number(c.yes || 0);
  402. const total = Number(c.total || 0);
  403. const needed = Number(c.needed || 0);
  404. const showMetricsFlag = showVoteMetrics(mid) && (total > 0 || needed > 0);
  405. const accLink = UserLinkCompact(c.accuser);
  406. const resLink = UserLinkCompact(c.respondent);
  407. const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
  408. ? c.mediatorsAccuser
  409. : [];
  410. const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
  411. ? c.mediatorsRespondent
  412. : [];
  413. const canShowDetails = c.mine || c.publicDetails;
  414. const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
  415. span(
  416. { class: 'mediator' },
  417. a(
  418. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  419. shortId(mId)
  420. ),
  421. idx < mediatorsAccuser.length - 1 ? span(', ') : null
  422. )
  423. );
  424. const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
  425. span(
  426. { class: 'mediator' },
  427. a(
  428. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  429. shortId(mId)
  430. ),
  431. idx < mediatorsRespondent.length - 1 ? span(', ') : null
  432. )
  433. );
  434. const showPublicPrefForm =
  435. (c.status === 'SOLVED' ||
  436. c.status === 'UNSOLVED' ||
  437. c.status === 'DISCARDED') &&
  438. (c.isAccuser || c.isRespondent);
  439. const publicPreferenceForm = showPublicPrefForm
  440. ? form(
  441. {
  442. method: 'POST',
  443. action: `/courts/cases/${encodeURIComponent(c.id)}/public`
  444. },
  445. label(i18n.courtsPublicPrefLabel),
  446. br(),
  447. select(
  448. { name: 'preference' },
  449. option(
  450. { value: 'YES', selected: c.myPublicPreference === true },
  451. i18n.courtsPublicPrefYes
  452. ),
  453. option(
  454. { value: 'NO', selected: c.myPublicPreference === false },
  455. i18n.courtsPublicPrefNo
  456. )
  457. ),
  458. br(),
  459. br(),
  460. button(
  461. { type: 'submit', class: 'create-button' },
  462. i18n.courtsPublicPrefSubmit
  463. )
  464. )
  465. : null;
  466. const respondentMediatorsForm = RespondentMediatorsForm(c);
  467. const canAddEvidence =
  468. c.isAccuser || c.isRespondent || c.isMediator || c.isJudge || c.isDictator;
  469. const canAnswer = c.isRespondent;
  470. const canIssueVerdict =
  471. (c.isJudge || c.isDictator || c.isMediator) &&
  472. c.status === 'OPEN' &&
  473. !c.hasVerdict;
  474. const canProposeSettlement =
  475. (c.isAccuser || c.isRespondent || c.isMediator) &&
  476. methodKey(c.method) === 'MEDIATION' &&
  477. c.status === 'OPEN';
  478. const canVoteVerdict =
  479. c.hasVerdict &&
  480. (c.isAccuser || c.isRespondent) &&
  481. c.status === 'OPEN';
  482. const isNormalUser =
  483. !c.isAccuser &&
  484. !c.isRespondent &&
  485. !c.isMediator &&
  486. !c.isJudge &&
  487. !c.isDictator;
  488. const canSupport =
  489. isNormalUser &&
  490. (c.status === 'IN_PROGRESS' || c.status === 'OPEN');
  491. const canAssignJudge =
  492. methodKey(c.method) === 'JUDGE' &&
  493. !c.judgeId &&
  494. (c.isAccuser || c.isRespondent);
  495. return div(
  496. { class: 'card' },
  497. div(
  498. { class: 'card-header' },
  499. div(
  500. { class: 'card-header__meta' },
  501. h2(c.title || ''),
  502. div(
  503. { class: 'card-field' },
  504. span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
  505. span({ class: 'card-value' }, methodLabel(c.method))
  506. ),
  507. div(
  508. { class: 'card-field' },
  509. span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
  510. span({ class: 'card-value' }, String(c.status || ''))
  511. ),
  512. c.answerBy
  513. ? div(
  514. { class: 'card-field' },
  515. span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '),
  516. span({ class: 'card-value' }, fmt(c.answerBy))
  517. )
  518. : null,
  519. c.evidenceBy
  520. ? div(
  521. { class: 'card-field' },
  522. span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '),
  523. span({ class: 'card-value' }, fmt(c.evidenceBy))
  524. )
  525. : null
  526. )
  527. ),
  528. div(
  529. { class: 'table-wrap mt-2' },
  530. applyEl(table, { class: 'table table--centered' }, [
  531. thead(tr(th(i18n.courtsAccuser), th(i18n.courtsRespondent))),
  532. tbody(tr(td(accLink), td(resLink)))
  533. ])
  534. ),
  535. mediatorsAccuser.length
  536. ? div(
  537. { class: 'card-field' },
  538. span(
  539. { class: 'card-label' },
  540. i18n.courtsMediatorsAccuserLabel + ': '
  541. ),
  542. span({ class: 'card-value' }, ...mediatorLinksAccuser)
  543. )
  544. : null,
  545. mediatorsRespondent.length
  546. ? div(
  547. { class: 'card-field' },
  548. span(
  549. { class: 'card-label' },
  550. i18n.courtsMediatorsRespondentLabel + ': '
  551. ),
  552. span({ class: 'card-value' }, ...mediatorLinksRespondent)
  553. )
  554. : null,
  555. c.supportCount
  556. ? div(
  557. { class: 'card-field' },
  558. span({ class: 'card-label' }, i18n.courtsSupportCount + ': '),
  559. span({ class: 'card-value' }, String(c.supportCount))
  560. )
  561. : null,
  562. canShowDetails ? RichTextBlock(c.description || '') : null,
  563. showMetricsFlag
  564. ? div(
  565. { class: 'card-field' },
  566. span({ class: 'card-label' }, i18n.courtsVotesNeeded + ': '),
  567. span({ class: 'card-value' }, String(needed))
  568. )
  569. : null,
  570. showMetricsFlag
  571. ? div(
  572. { class: 'card-field' },
  573. span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ': '),
  574. span({ class: 'card-value' }, `${yes}/${total}`)
  575. )
  576. : null,
  577. c.voteId
  578. ? form(
  579. { method: 'GET', action: `/votes/${encodeURIComponent(c.voteId)}` },
  580. button({ type: 'submit', class: 'vote-btn' }, i18n.courtsOpenVote)
  581. )
  582. : null,
  583. canSupport ? SupportCaseForm(c.id || '') : null,
  584. publicPreferenceForm,
  585. respondentMediatorsForm,
  586. canAssignJudge ? JudgeAssignForm(c.id || '') : null,
  587. canAddEvidence ? EvidenceForm(c.id || '') : null,
  588. canAnswer ? AnswerForm(c.id || '') : null,
  589. canIssueVerdict ? VerdictForm(c.id || '') : null,
  590. canVoteVerdict ? VerdictVoteForm(c.id || '') : null,
  591. canProposeSettlement ? SettlementForm(c.id || '') : null
  592. );
  593. };
  594. const MyCaseCard = (c) => {
  595. const mid = methodKey(c.method);
  596. const yes = Number(c.yes || 0);
  597. const total = Number(c.total || 0);
  598. const needed = Number(c.needed || 0);
  599. const showMetricsFlag = showVoteMetrics(mid) && (total > 0 || needed > 0);
  600. const accLinkFull = UserLinkFull(c.accuser);
  601. const respondentId = c.respondentId || c.respondent;
  602. const resLinkFull = UserLinkFull(respondentId);
  603. const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
  604. ? c.mediatorsAccuser
  605. : [];
  606. const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
  607. ? c.mediatorsRespondent
  608. : [];
  609. const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
  610. span(
  611. {},
  612. a(
  613. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  614. mId
  615. ),
  616. idx < mediatorsAccuser.length - 1 ? span(', ') : null
  617. )
  618. );
  619. const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
  620. span(
  621. {},
  622. a(
  623. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  624. mId
  625. ),
  626. idx < mediatorsRespondent.length - 1 ? span(', ') : null
  627. )
  628. );
  629. const judgeLinkFull = c.judgeId ? UserLinkFull(c.judgeId) : span('');
  630. const showMediatorsTable =
  631. !!c.method &&
  632. (mediatorsAccuser.length > 0 ||
  633. mediatorsRespondent.length > 0 ||
  634. !!c.judgeId);
  635. const showPublicPrefForm =
  636. (c.status === 'SOLVED' ||
  637. c.status === 'UNSOLVED' ||
  638. c.status === 'DISCARDED') &&
  639. (c.isAccuser || c.isRespondent);
  640. const publicPreferenceForm = showPublicPrefForm
  641. ? form(
  642. {
  643. method: 'POST',
  644. action: `/courts/cases/${encodeURIComponent(c.id)}/public`
  645. },
  646. label(i18n.courtsPublicPrefLabel),
  647. br(),
  648. select(
  649. { name: 'preference' },
  650. option(
  651. { value: 'YES', selected: c.myPublicPreference === true },
  652. i18n.courtsPublicPrefYes
  653. ),
  654. option(
  655. { value: 'NO', selected: c.myPublicPreference === false },
  656. i18n.courtsPublicPrefNo
  657. )
  658. ),
  659. br(),
  660. br(),
  661. button(
  662. { type: 'submit', class: 'create-button' },
  663. i18n.courtsPublicPrefSubmit
  664. )
  665. )
  666. : null;
  667. const respondentMediatorsForm = RespondentMediatorsForm(c);
  668. const canAddEvidence =
  669. c.isAccuser || c.isRespondent || c.isMediator || c.isJudge || c.isDictator;
  670. const canAnswer = c.isRespondent;
  671. const canIssueVerdict =
  672. (c.isJudge || c.isDictator || c.isMediator) &&
  673. c.status === 'OPEN' &&
  674. !c.hasVerdict;
  675. const canProposeSettlement =
  676. (c.isAccuser || c.isRespondent || c.isMediator) &&
  677. methodKey(c.method) === 'MEDIATION' &&
  678. c.status === 'OPEN';
  679. const canVoteVerdict =
  680. c.hasVerdict &&
  681. (c.isAccuser || c.isRespondent) &&
  682. c.status === 'OPEN';
  683. const isNormalUser =
  684. !c.isAccuser &&
  685. !c.isRespondent &&
  686. !c.isMediator &&
  687. !c.isJudge &&
  688. !c.isDictator;
  689. const canSupport =
  690. isNormalUser &&
  691. (c.status === 'IN_PROGRESS' || c.status === 'OPEN');
  692. const canAssignJudge =
  693. methodKey(c.method) === 'JUDGE' &&
  694. !c.judgeId &&
  695. (c.isAccuser || c.isRespondent);
  696. return div(
  697. { class: 'card' },
  698. br(),
  699. div(
  700. { class: 'card-field' },
  701. span({ class: 'card-label' }, i18n.courtsThCase + ': '),
  702. span({ class: 'card-value' }, c.title || '')
  703. ),
  704. div(
  705. { class: 'card-field' },
  706. span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
  707. span({ class: 'card-value' }, String(c.status || ''))
  708. ),
  709. div(
  710. { class: 'card-field' },
  711. span({ class: 'card-label' }, i18n.courtsMethod + ': '),
  712. span({ class: 'card-value' }, methodLabel(c.method))
  713. ),
  714. c.answerBy
  715. ? div(
  716. { class: 'card-field' },
  717. span({ class: 'card-label' }, i18n.courtsThAnswerBy + ': '),
  718. span({ class: 'card-value' }, fmt(c.answerBy))
  719. )
  720. : null,
  721. c.evidenceBy
  722. ? div(
  723. { class: 'card-field' },
  724. span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ': '),
  725. span({ class: 'card-value' }, fmt(c.evidenceBy))
  726. )
  727. : null,
  728. c.decisionBy
  729. ? div(
  730. { class: 'card-field' },
  731. span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
  732. span({ class: 'card-value' }, fmt(c.decisionBy))
  733. )
  734. : null,
  735. div(
  736. { class: 'table-wrap mt-2' },
  737. applyEl(table, { class: 'table table--centered' }, [
  738. thead(
  739. tr(
  740. th(i18n.courtsAccuser),
  741. th(i18n.courtsRespondent)
  742. )
  743. ),
  744. tbody(
  745. tr(
  746. td(accLinkFull),
  747. td(resLinkFull)
  748. )
  749. )
  750. ])
  751. ),
  752. showMediatorsTable
  753. ? div(
  754. { class: 'table-wrap mt-2 mediators-table' },
  755. applyEl(table, { class: 'table table--centered' }, [
  756. thead(
  757. tr(
  758. th(i18n.courtsThJudge),
  759. th(
  760. i18n.courtsMediatorsAccuserLabel ||
  761. i18n.courtsMediatorsLabel ||
  762. 'Accuser mediators'
  763. ),
  764. th(
  765. i18n.courtsMediatorsRespondentLabel ||
  766. i18n.courtsMediatorsLabel ||
  767. 'Respondent mediators'
  768. )
  769. )
  770. ),
  771. tbody(
  772. tr(
  773. td(judgeLinkFull),
  774. td(
  775. mediatorLinksAccuser.length
  776. ? mediatorLinksAccuser
  777. : span('')
  778. ),
  779. td(
  780. mediatorLinksRespondent.length
  781. ? mediatorLinksRespondent
  782. : span('')
  783. )
  784. )
  785. )
  786. ])
  787. )
  788. : null,
  789. c.supportCount
  790. ? div(
  791. { class: 'card-field' },
  792. span({ class: 'card-label' }, i18n.courtsSupportCount + ': '),
  793. span({ class: 'card-value' }, String(c.supportCount))
  794. )
  795. : null,
  796. showMetricsFlag
  797. ? div(
  798. { class: 'card-field' },
  799. span({ class: 'card-label' }, i18n.courtsVotesNeeded + ': '),
  800. span({ class: 'card-value' }, String(needed))
  801. )
  802. : null,
  803. showMetricsFlag
  804. ? div(
  805. { class: 'card-field' },
  806. span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ': '),
  807. span({ class: 'card-value' }, `${yes}/${total}`)
  808. )
  809. : null,
  810. c.voteId
  811. ? form(
  812. { method: 'GET', action: `/votes/${encodeURIComponent(c.voteId)}` },
  813. button({ type: 'submit', class: 'vote-btn' }, i18n.courtsOpenVote)
  814. )
  815. : null,
  816. canSupport ? SupportCaseForm(c.id || '') : null,
  817. publicPreferenceForm,
  818. respondentMediatorsForm,
  819. canAssignJudge ? JudgeAssignForm(c.id || '') : null,
  820. canAddEvidence ? EvidenceForm(c.id || '') : null,
  821. canAnswer ? AnswerForm(c.id || '') : null,
  822. canIssueVerdict ? VerdictForm(c.id || '') : null,
  823. canVoteVerdict ? VerdictVoteForm(c.id || '') : null,
  824. canProposeSettlement ? SettlementForm(c.id || '') : null
  825. );
  826. };
  827. const CasesList = (rows = []) => {
  828. const cards = rows.map(CaseCard);
  829. return div({ class: 'cards' }, ...cards);
  830. };
  831. const MyCasesList = (rows = []) => {
  832. const cards = rows.map(MyCaseCard);
  833. return div({ class: 'cards' }, ...cards);
  834. };
  835. const roleTextForCase = (c) => {
  836. if (c.isAccuser) return i18n.courtsRoleAccuser || 'Accuser';
  837. if (c.isRespondent) return i18n.courtsRoleDefence || 'Defence';
  838. if (c.isMediator) return i18n.courtsRoleMediator || 'Mediator';
  839. if (c.isJudge) return i18n.courtsRoleJudge || 'Judge';
  840. if (c.isDictator) return i18n.courtsRoleDictator || 'Dictator';
  841. return '';
  842. };
  843. const CasesTable = (rows = [], opts = {}) => {
  844. if (!rows.length) return div({ class: 'empty' }, p(i18n.courtsNoCases));
  845. const showRole = !!opts.showRole;
  846. const bodyRows = rows.map((c) => {
  847. const role = showRole ? roleTextForCase(c) : '';
  848. return tr(
  849. td(c.title || ''),
  850. td(
  851. c.accuser
  852. ? a(
  853. {
  854. class: 'user-link',
  855. href: `/author/${encodeURIComponent(c.accuser)}`
  856. },
  857. c.accuser
  858. )
  859. : ''
  860. ),
  861. td(methodLabel(c.method)),
  862. td(c.createdAt ? fmt(c.createdAt) : ''),
  863. showRole ? td(role) : null,
  864. td(
  865. c.id
  866. ? form(
  867. {
  868. method: 'GET',
  869. action: `/courts/cases/${encodeURIComponent(c.id)}`
  870. },
  871. button(
  872. { type: 'submit', class: 'link-button' },
  873. i18n.courtsViewDetailsShort ||
  874. i18n.courtsViewDetails ||
  875. 'View'
  876. )
  877. )
  878. : ''
  879. )
  880. );
  881. });
  882. return div(
  883. { class: 'table-wrap' },
  884. applyEl(table, { class: 'table table--centered' }, [
  885. thead(
  886. tr(
  887. th(i18n.courtsThCase),
  888. th(i18n.courtsAccuser),
  889. th(i18n.courtsCaseMethod),
  890. th(i18n.courtsThCreatedAt),
  891. showRole ? th(i18n.courtsThRole || 'Role') : null,
  892. th(i18n.courtsThDetails || '')
  893. )
  894. ),
  895. applyEl(tbody, null, bodyRows)
  896. ])
  897. );
  898. };
  899. const NominationsTable = (nominations = [], currentUserId = '') => {
  900. if (!nominations || !nominations.length)
  901. return div({ class: 'empty' }, p(i18n.courtsNoNominations));
  902. const rows = nominations.map((n) => {
  903. const isSelf =
  904. currentUserId &&
  905. String(n.judgeId || '') === String(currentUserId || '');
  906. return tr(
  907. td(
  908. a(
  909. { class: 'user-link', href: `/author/${encodeURIComponent(n.judgeId)}` },
  910. n.judgeId
  911. )
  912. ),
  913. td(String(n.supports || 0)),
  914. td(fmt(n.createdAt)),
  915. td(
  916. isSelf
  917. ? span('')
  918. : form(
  919. {
  920. method: 'POST',
  921. action: `/courts/judges/${encodeURIComponent(n.id)}/vote`
  922. },
  923. button({ class: 'vote-btn' }, i18n.courtsThVote)
  924. )
  925. )
  926. );
  927. });
  928. return div(
  929. { class: 'table-wrap' },
  930. h2(i18n.courtsNominationsTitle),
  931. applyEl(table, { class: 'table table--centered' }, [
  932. thead(
  933. tr(
  934. th(i18n.courtsThJudge),
  935. th(i18n.courtsThSupports),
  936. th(i18n.courtsThDate),
  937. th(i18n.courtsThVote)
  938. )
  939. ),
  940. applyEl(tbody, null, rows)
  941. ])
  942. );
  943. };
  944. const JudgesSection = (nominations = [], currentUserId = '') => {
  945. const nomBlock = NominationsTable(nominations, currentUserId);
  946. return div(NominateJudgeForm(), nomBlock);
  947. };
  948. const HistoryList = (rows = []) => {
  949. if (!rows.length) return div({ class: 'empty' }, p(i18n.courtsNoHistory));
  950. const cards = rows.map((hh) => {
  951. const canShowDescription = hh.mine || hh.publicDetails;
  952. return div(
  953. { class: 'card' },
  954. br(),
  955. hh.method
  956. ? div(
  957. { class: 'card-field' },
  958. span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
  959. span({ class: 'card-value' }, methodLabel(hh.method))
  960. )
  961. : null,
  962. hh.decidedAt
  963. ? div(
  964. { class: 'card-field' },
  965. span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
  966. span({ class: 'card-value' }, fmt(hh.decidedAt))
  967. )
  968. : null,
  969. div(
  970. { class: 'card-field' },
  971. span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
  972. span({ class: 'card-value' }, String(hh.status || ''))
  973. ),
  974. h2(hh.title || ''),
  975. canShowDescription ? RichTextBlock(hh.description || '') : null,
  976. form(
  977. {
  978. method: 'GET',
  979. action: `/courts/cases/${encodeURIComponent(hh.id)}`
  980. },
  981. button({ type: 'submit', class: 'create-button' }, i18n.courtsViewDetails)
  982. )
  983. );
  984. });
  985. return div({ class: 'cards' }, ...cards);
  986. };
  987. const RulesContent = () =>
  988. div(
  989. { class: 'card' },
  990. h2(i18n.courtsRulesTitle),
  991. ul(
  992. li(i18n.courtsRulesIntro),
  993. li(i18n.courtsRulesLifecycle),
  994. li(i18n.courtsRulesRoles),
  995. li(i18n.courtsRulesEvidence),
  996. li(i18n.courtsRulesDeliberation),
  997. li(i18n.courtsRulesVerdict),
  998. li(i18n.courtsRulesAppeals),
  999. li(i18n.courtsRulesPrivacy),
  1000. li(i18n.courtsRulesMisconduct),
  1001. li(i18n.courtsRulesGlossary)
  1002. )
  1003. );
  1004. const CaseSearch = (filter, search = '') =>
  1005. div(
  1006. { class: 'filters' },
  1007. form(
  1008. { method: 'GET', action: '/courts' },
  1009. input({ type: 'hidden', name: 'filter', value: filter }),
  1010. input({
  1011. type: 'text',
  1012. name: 'search',
  1013. placeholder: i18n.searchCasesPlaceholder,
  1014. value: search || ''
  1015. }),
  1016. br(),
  1017. button({ type: 'submit' }, i18n.applyFilters),
  1018. br()
  1019. )
  1020. );
  1021. const CaseDetailsBlock = (c) => {
  1022. if (!c) return div({ class: 'empty' }, p(i18n.courtsNoCases));
  1023. const canShowFull = c.mine || c.publicDetails;
  1024. const accLink = UserLinkFull(c.accuser);
  1025. const resLink = UserLinkFull(c.respondent || c.respondentId);
  1026. const judgeValue = c.judgeId
  1027. ? UserLinkFull(c.judgeId)
  1028. : span(i18n.courtsJudgeNotAssigned || '-');
  1029. const mediatorsAccuser = Array.isArray(c.mediatorsAccuser)
  1030. ? c.mediatorsAccuser
  1031. : [];
  1032. const mediatorsRespondent = Array.isArray(c.mediatorsRespondent)
  1033. ? c.mediatorsRespondent
  1034. : [];
  1035. const mediatorLinksAccuser = mediatorsAccuser.map((mId, idx) =>
  1036. span(
  1037. { class: 'mediator' },
  1038. a(
  1039. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  1040. shortId(mId)
  1041. ),
  1042. idx < mediatorsAccuser.length - 1 ? span(', ') : null
  1043. )
  1044. );
  1045. const mediatorLinksRespondent = mediatorsRespondent.map((mId, idx) =>
  1046. span(
  1047. { class: 'mediator' },
  1048. a(
  1049. { class: 'user-link', href: `/author/${encodeURIComponent(mId)}` },
  1050. shortId(mId)
  1051. ),
  1052. idx < mediatorsRespondent.length - 1 ? span(', ') : null
  1053. )
  1054. );
  1055. const showMediatorsTable =
  1056. c.method && (mediatorsAccuser.length || mediatorsRespondent.length);
  1057. const evidences = Array.isArray(c.evidences) ? c.evidences : [];
  1058. const answers = Array.isArray(c.answers) ? c.answers : [];
  1059. const settlements = Array.isArray(c.settlements) ? c.settlements : [];
  1060. const evidenceSideLabel = (e) => {
  1061. const side = String((e && e.side) || '').toUpperCase();
  1062. if (side === 'ACCUSER') {
  1063. return i18n.courtsEvidenceSideAccuser || 'Accuser evidence';
  1064. }
  1065. if (side === 'RESPONDENT' || side === 'DEFENCE' || side === 'DEFENSE') {
  1066. return i18n.courtsEvidenceSideRespondent || 'Defence evidence';
  1067. }
  1068. return i18n.courtsEvidenceSideUnknown || '';
  1069. };
  1070. return div(
  1071. { class: 'card case-details-card' },
  1072. br(),
  1073. div(
  1074. { class: 'card-header' },
  1075. div(
  1076. { class: 'card-header__meta' },
  1077. h2(c.title || ''),
  1078. c.method
  1079. ? div(
  1080. { class: 'card-field' },
  1081. span({ class: 'card-label' }, i18n.courtsCaseMethod + ': '),
  1082. span({ class: 'card-value' }, methodLabel(c.method))
  1083. )
  1084. : null,
  1085. div(
  1086. { class: 'card-field' },
  1087. span({ class: 'card-label' }, i18n.courtsThJudge + ': '),
  1088. span({ class: 'card-value' }, judgeValue)
  1089. ),
  1090. div(
  1091. { class: 'card-field' },
  1092. span({ class: 'card-label' }, i18n.courtsThStatus + ': '),
  1093. span({ class: 'card-value' }, String(c.status || ''))
  1094. ),
  1095. c.createdAt
  1096. ? div(
  1097. { class: 'card-field' },
  1098. span({ class: 'card-label' }, i18n.courtsThCreatedAt + ': '),
  1099. span({ class: 'card-value' }, fmt(c.createdAt))
  1100. )
  1101. : null,
  1102. c.decidedAt
  1103. ? div(
  1104. { class: 'card-field' },
  1105. span({ class: 'card-label' }, i18n.courtsThDecisionBy + ': '),
  1106. span({ class: 'card-value' }, fmt(c.decidedAt))
  1107. )
  1108. : null
  1109. )
  1110. ),
  1111. div(
  1112. { class: 'table-wrap mt-2' },
  1113. applyEl(table, { class: 'table table--centered' }, [
  1114. thead(
  1115. tr(
  1116. th(i18n.courtsAccuser),
  1117. th(i18n.courtsRespondent)
  1118. )
  1119. ),
  1120. tbody(
  1121. tr(
  1122. td(accLink),
  1123. td(resLink)
  1124. )
  1125. )
  1126. ])
  1127. ),
  1128. showMediatorsTable
  1129. ? div(
  1130. { class: 'table-wrap mt-2 mediators-table' },
  1131. applyEl(table, { class: 'table table--centered' }, [
  1132. thead(
  1133. tr(
  1134. th(
  1135. i18n.courtsMediatorsAccuserLabel ||
  1136. i18n.courtsMediatorsLabel ||
  1137. 'Accuser mediators'
  1138. ),
  1139. th(
  1140. i18n.courtsMediatorsRespondentLabel ||
  1141. i18n.courtsMediatorsLabel ||
  1142. 'Respondent mediators'
  1143. )
  1144. )
  1145. ),
  1146. tbody(
  1147. tr(
  1148. td(
  1149. mediatorLinksAccuser.length
  1150. ? mediatorLinksAccuser
  1151. : span('')
  1152. ),
  1153. td(
  1154. mediatorLinksRespondent.length
  1155. ? mediatorLinksRespondent
  1156. : span('')
  1157. )
  1158. )
  1159. )
  1160. ])
  1161. )
  1162. : null,
  1163. canShowFull ? RichTextBlock(c.description || '') : null,
  1164. !canShowFull
  1165. ? div(
  1166. { class: 'card-field' },
  1167. p(i18n.courtsDetailsNotPublic)
  1168. )
  1169. : null,
  1170. canShowFull && evidences.length
  1171. ? div(
  1172. { class: 'card-section evidences-section' },
  1173. h2(i18n.courtsDetailsEvidenceTitle),
  1174. ...evidences.map((e) => {
  1175. const bodyChildren = [];
  1176. if (e.text && String(e.text).trim()) {
  1177. bodyChildren.push(RichTextBlock(e.text));
  1178. }
  1179. if (e.link && String(e.link).trim()) {
  1180. bodyChildren.push(
  1181. a(
  1182. {
  1183. class: 'evidence-link',
  1184. href: e.link,
  1185. target: '_blank',
  1186. rel: 'noopener noreferrer'
  1187. },
  1188. e.link
  1189. )
  1190. );
  1191. }
  1192. if (e.imageUrl && String(e.imageUrl).trim()) {
  1193. bodyChildren.push(
  1194. br(),br(),
  1195. img({
  1196. class: 'evidence-image',
  1197. src: `/blob/${encodeURIComponent(e.imageUrl)}`,
  1198. alt: 'evidence'
  1199. })
  1200. );
  1201. }
  1202. if (!bodyChildren.length) {
  1203. bodyChildren.push(span(''));
  1204. }
  1205. const sideLabel = evidenceSideLabel(e);
  1206. const dateText = fmt(e.createdAt);
  1207. const dateWithSide = sideLabel
  1208. ? dateText + ' · ' + sideLabel
  1209. : dateText;
  1210. return div(
  1211. { class: 'evidence-item' },
  1212. div(
  1213. { class: 'evidence-date' },
  1214. dateWithSide
  1215. ),
  1216. div(
  1217. { class: 'evidence-body' },
  1218. ...bodyChildren
  1219. )
  1220. );
  1221. })
  1222. )
  1223. : null,
  1224. canShowFull && answers.length
  1225. ? div(
  1226. { class: 'card-section answers-section' },
  1227. h2(i18n.courtsDetailsAnswersTitle),
  1228. ...answers.map((aItem) => {
  1229. const stanceKey = methodKey(aItem.stance);
  1230. const stanceLabel =
  1231. stanceKey && i18n[`courtsStance${stanceKey}`]
  1232. ? i18n[`courtsStance${stanceKey}`]
  1233. : aItem.stance || '';
  1234. const dateText = fmt(aItem.createdAt);
  1235. const metaText = stanceLabel
  1236. ? dateText + ' · ' + stanceLabel
  1237. : dateText;
  1238. return div(
  1239. { class: 'answer-item' },
  1240. div(
  1241. { class: 'answer-meta' },
  1242. metaText
  1243. ),
  1244. RichTextBlock(aItem.text || '')
  1245. );
  1246. })
  1247. )
  1248. : null,
  1249. canShowFull && c.verdict
  1250. ? div(
  1251. { class: 'card-section verdict-section' },
  1252. h2(i18n.courtsDetailsVerdictTitle),
  1253. div(
  1254. { class: 'card-field' },
  1255. span({ class: 'card-label' }, i18n.courtsVerdictResult + ': '),
  1256. span({ class: 'card-value' }, c.verdict.result || '')
  1257. ),
  1258. c.verdict.orders
  1259. ? div(
  1260. { class: 'card-field' },
  1261. span(
  1262. { class: 'card-label' },
  1263. i18n.courtsVerdictOrders + ': '
  1264. ),
  1265. RichTextBlock(c.verdict.orders || '')
  1266. )
  1267. : null
  1268. )
  1269. : null,
  1270. canShowFull && settlements.length
  1271. ? div(
  1272. { class: 'card-section settlements-section' },
  1273. h2(i18n.courtsDetailsSettlementsTitle),
  1274. ...settlements.map((sItem) =>
  1275. div(
  1276. { class: 'settlement-item' },
  1277. div(
  1278. { class: 'settlement-date' },
  1279. fmt(sItem.createdAt)
  1280. ),
  1281. RichTextBlock(sItem.terms || '')
  1282. )
  1283. )
  1284. )
  1285. : null
  1286. );
  1287. };
  1288. const courtsView = async (state) => {
  1289. const {
  1290. filter = 'cases',
  1291. cases = [],
  1292. myCases = [],
  1293. history = [],
  1294. nominations = [],
  1295. search = '',
  1296. userId = ''
  1297. } = state;
  1298. return template(
  1299. i18n.courtsTitle,
  1300. section(
  1301. div(
  1302. { class: 'tags-header' },
  1303. h2(i18n.courtsTitle),
  1304. p(i18n.courtsDescription)
  1305. ),
  1306. Tabs(filter),
  1307. filter === 'cases' ? CaseSearch(filter, search) : null
  1308. ),
  1309. section(
  1310. filter === 'cases' ? CasesTable(cases) : null,
  1311. filter === 'mycases'
  1312. ? myCases.length
  1313. ? CasesTable(myCases, { showRole: true })
  1314. : div({ class: 'empty' }, p(i18n.courtsNoMyCases))
  1315. : null,
  1316. filter === 'actions'
  1317. ? myCases.length
  1318. ? MyCasesList(myCases)
  1319. : div({ class: 'empty' }, p(i18n.courtsNoMyCases))
  1320. : null,
  1321. filter === 'judges' ? JudgesSection(nominations, userId) : null,
  1322. filter === 'history' ? HistoryList(history) : null,
  1323. filter === 'rules' ? RulesContent() : null,
  1324. filter === 'open' ? CaseForm() : null
  1325. )
  1326. );
  1327. };
  1328. const courtsCaseView = async (state) => {
  1329. const { caseData } = state;
  1330. return template(
  1331. i18n.courtsTitle,
  1332. section(
  1333. div(
  1334. { class: 'tags-header' },
  1335. h2(i18n.courtsTitle),
  1336. p(i18n.courtsDescription)
  1337. ),
  1338. Tabs('cases')
  1339. ),
  1340. section(CaseDetailsBlock(caseData))
  1341. );
  1342. };
  1343. module.exports = { courtsView, courtsCaseView };