Format values in FormView, using the new FormatType LineOption.
[ustaxviewer.git] / src / FormView.tsx
1 // Copyright 2020 Blue Static <https://www.bluestatic.org>
2 // This program is free software licensed under the GNU General Public License,
3 // version 3.0. The full text of the license can be found in LICENSE.txt.
4 // SPDX-License-Identifier: GPL-3.0-only
5
6 import { createEffect, createMemo, createState } from 'solid-js';
7 import { For, Show } from 'solid-js/dom';
8 import { TaxReturn, Form, Line, FormatType } from 'ustaxlib/core';
9 import * as Trace from 'ustaxlib/core/Trace';
10 import { Edge } from 'ustaxlib/core/Trace';
11 import { graphviz } from 'd3-graphviz';
12
13 const S = require('./FormView.css');
14
15 interface FormProps {
16 tr: TaxReturn;
17 form: Form<any>;
18 }
19
20 export default function FormView(props: FormProps) {
21 const lines = createMemo(() => {
22 const keys = Object.keys(props.form.lines);
23 keys.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
24 return keys.map(k => props.form.lines[k]);
25 });
26
27 return (
28 <div class={S.form}>
29 <h2 class={S.formName}>Form {props.form.name}</h2>
30
31 <div class={S.table}>
32 <For each={lines()}>
33 {line => <LineView tr={props.tr} line={line} />}
34 </For>
35 </div>
36 </div>
37 );
38 }
39
40 interface LineProps {
41 tr: TaxReturn;
42 line: Line<any>;
43 }
44
45 class Formatter {
46 private static _instance: Formatter;
47
48 private _dollar = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', 'currencySign': 'accounting' } as Intl.NumberFormatOptions);
49 private _percent = new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 3 });
50 private _decimal = new Intl.NumberFormat('en-US', { style: 'decimal', minimumFractionDigits: 5 });
51
52 private constructor() {}
53
54 static getInstance() {
55 if (!Formatter._instance) {
56 Formatter._instance = new Formatter();
57 }
58 return Formatter._instance;
59 }
60
61 dollar(value: number): string {
62 return this._dollar.format(value);
63 }
64
65 percent(value: number): string {
66 return this._percent.format(value);
67 }
68
69 decimal(value: number): string {
70 return this._decimal.format(value);
71 }
72
73 string(value: any): string {
74 return JSON.stringify(value, null, 1);
75 }
76 }
77
78 function formatLine(value: any, line: Line<any>): string {
79 const formatter = Formatter.getInstance();
80 const formatType = line.options.formatType;
81
82 if (typeof(value) === 'number') {
83 if (formatType == FormatType.Decimal) {
84 return formatter.decimal(value);
85 } else if (formatType == FormatType.Percent) {
86 return formatter.percent(value);
87 } else if (formatType == FormatType.Dollar || formatType == undefined) {
88 return formatter.dollar(value);
89 }
90 }
91
92 return formatter.string(value);
93 }
94
95 function LineView(props: LineProps) {
96 const { tr, line } = props;
97
98 const [ state, setState ] = createState({
99 value: undefined as any,
100 error: undefined as any,
101 trace: [] as readonly Edge[],
102 showTrace: false
103 });
104
105 createEffect(() => {
106 const newState = {
107 value: undefined,
108 error: undefined,
109 trace: [] as readonly Edge[]
110 };
111 try {
112 Trace.reset();
113 newState.value = line.value(tr);
114 } catch (e) {
115 newState.error = e;
116 }
117 newState.trace = Trace.getLastTraceList();
118 setState(newState);
119 });
120
121 const valueDisplay = createMemo(() => {
122 if (state.error) {
123 return <span class={S.error} title={state.error.stack}>{state.error.message}</span>;
124 }
125 return formatLine(state.value, line);
126 });
127
128 const toggleTrace = () => setState('showTrace', !state.showTrace);
129
130 return (
131 <>
132 <div class={S.line}>
133 <div class={S.id} onclick={toggleTrace}>{line.id}</div>
134 <div class={S.description}>
135 {line.description}
136
137 </div>
138 <div class={S.value}>{valueDisplay()}</div>
139 </div>
140 <Show when={state.showTrace}>
141 <TraceViewer line={line} trace={state.trace} onClose={() => setState('showTrace', false)} />
142 </Show>
143 </>
144 );
145 }
146
147 interface TraceProps {
148 line: Line<any>;
149 trace: readonly Edge[];
150
151 onClose: () => void;
152 }
153
154 function TraceViewer(props: TraceProps) {
155 const renderGraph = (ref) => {
156 let graph = '';
157 for (const edge of props.trace) {
158 graph += `"${edge[1]}" -> "${edge[0]}"; `;
159 }
160 graphviz(ref)
161 .zoomScaleExtent([0.1, 1])
162 .renderDot(`digraph { ${graph} }`, () => {
163 if (ref.querySelector('svg').clientWidth > ref.parentNode.clientWidth) {
164 ref.parentNode.classList.add(S.large);
165 }
166 });
167 };
168 return (
169 <div class={S.traceViewer}>
170 <h2>Trace {props.line.id} <button class={S.close} onclick={props.onClose}>\u24E7</button></h2>
171 <div forwardRef={renderGraph}></div>
172 </div>
173 );
174 }