@tasenor/common-plugins 1.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +4 -0
- package/.turbo/turbo-fix.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-version.log +5 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/data/FinnishBalanceSheetReport.mjs +9 -0
- package/data/FinnishBalanceSheetReportInvestment.mjs +11 -0
- package/data/FinnishBalanceSheetReportLite.mjs +9 -0
- package/data/FinnishIncomeStatementReport.mjs +9 -0
- package/data/FinnishIncomeStatementReportInvestment.mjs +11 -0
- package/data/FinnishIncomeStatementReportLite.mjs +9 -0
- package/data/FinnishInvestmentCompany.mjs +5 -0
- package/data/FinnishLimitedCompanyComplete.mjs +5 -0
- package/data/FinnishLimitedCompanyLite.mjs +9 -0
- package/data/IncomeAndExpenses.mjs +64 -0
- package/data/README.md +8 -0
- package/data/VATFinland.mjs +22 -0
- package/data/bin/build_all +11 -0
- package/data/lib/utils.mjs +314 -0
- package/data/src/Assets Tree - Definitions.tsv +101 -0
- package/data/src/Expense Tree - Definitions.tsv +99 -0
- package/data/src/Finland VAT - Definitions.tsv +22 -0
- package/data/src/FinnishBalanceSheetReport - balance-sheet-detailed-fi.tsv +215 -0
- package/data/src/FinnishBalanceSheetReport - balance-sheet-fi.tsv +93 -0
- package/data/src/FinnishBalanceSheetReportInvestment - balance-sheet-detailed-fi.tsv +100 -0
- package/data/src/FinnishBalanceSheetReportInvestment - balance-sheet-fi.tsv +52 -0
- package/data/src/FinnishBalanceSheetReportLite - balance-sheet-en.tsv +21 -0
- package/data/src/FinnishBalanceSheetReportLite - balance-sheet-fi.tsv +21 -0
- package/data/src/FinnishBalanceSheetReportLite - balance-sheet-lite-en.tsv +21 -0
- package/data/src/FinnishBalanceSheetReportLite - balance-sheet-lite-fi.tsv +21 -0
- package/data/src/FinnishIncomeStatementReport - income-statement-detailed-fi.tsv +118 -0
- package/data/src/FinnishIncomeStatementReport - income-statement-fi.tsv +45 -0
- package/data/src/FinnishIncomeStatementReportInvestment - income-statement-detailed-fi.tsv +96 -0
- package/data/src/FinnishIncomeStatementReportInvestment - income-statement-fi.tsv +39 -0
- package/data/src/FinnishIncomeStatementReportLite - income-statement-lite-en.tsv +23 -0
- package/data/src/FinnishIncomeStatementReportLite - income-statement-lite-fi.tsv +23 -0
- package/data/src/FinnishInvestmentCompany - fi-EUR.tsv +722 -0
- package/data/src/FinnishLimitedCompanyComplete - fi-EUR.tsv +1086 -0
- package/data/src/FinnishLimitedCompanyLite - en-EUR.tsv +97 -0
- package/data/src/FinnishLimitedCompanyLite - fi-EUR.tsv +99 -0
- package/data/src/Income Tree - Definitions.tsv +60 -0
- package/data/src/Tax Types - Definitions.tsv +11 -0
- package/package.json +51 -0
- package/src/CoinAPI/backend/index.ts +102 -0
- package/src/CoinbaseImport/backend/CoinbaseHandler.ts +35 -0
- package/src/CoinbaseImport/backend/index.ts +24 -0
- package/src/CoinbaseImport/backend/rules.json +64 -0
- package/src/DocumentCleaner/ui/index.tsx +165 -0
- package/src/Euro/ui/index.tsx +27 -0
- package/src/Finnish/ui/finnish.json +341 -0
- package/src/Finnish/ui/index.tsx +54 -0
- package/src/FinnishBalanceSheetReport/backend/balance-sheet-detailed-fi.tsv +215 -0
- package/src/FinnishBalanceSheetReport/backend/balance-sheet-fi.tsv +93 -0
- package/src/FinnishBalanceSheetReport/backend/index.ts +107 -0
- package/src/FinnishBalanceSheetReportInvestment/backend/balance-sheet-investment-detailed-fi.tsv +100 -0
- package/src/FinnishBalanceSheetReportInvestment/backend/balance-sheet-investment-fi.tsv +52 -0
- package/src/FinnishBalanceSheetReportInvestment/backend/index.ts +107 -0
- package/src/FinnishBalanceSheetReportLite/backend/balance-sheet-lite-en.tsv +21 -0
- package/src/FinnishBalanceSheetReportLite/backend/balance-sheet-lite-fi.tsv +21 -0
- package/src/FinnishBalanceSheetReportLite/backend/index.ts +121 -0
- package/src/FinnishIncomeStatementReport/backend/income-statement-detailed-fi.tsv +118 -0
- package/src/FinnishIncomeStatementReport/backend/income-statement-fi.tsv +45 -0
- package/src/FinnishIncomeStatementReport/backend/index.ts +212 -0
- package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-detailed-fi.tsv +118 -0
- package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-fi.tsv +45 -0
- package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-investment-detailed-fi.tsv +96 -0
- package/src/FinnishIncomeStatementReportInvestment/backend/income-statement-investment-fi.tsv +39 -0
- package/src/FinnishIncomeStatementReportInvestment/backend/index.ts +212 -0
- package/src/FinnishIncomeStatementReportLite/backend/income-statement-lite-en.tsv +23 -0
- package/src/FinnishIncomeStatementReportLite/backend/income-statement-lite-fi.tsv +23 -0
- package/src/FinnishIncomeStatementReportLite/backend/index.ts +210 -0
- package/src/FinnishInvestmentCompany/backend/fi-EUR.tsv +722 -0
- package/src/FinnishInvestmentCompany/backend/index.ts +46 -0
- package/src/FinnishInvestmentCompany/ui/index.tsx +26 -0
- package/src/FinnishLimitedCompanyComplete/backend/fi-EUR.tsv +1086 -0
- package/src/FinnishLimitedCompanyComplete/backend/index.ts +46 -0
- package/src/FinnishLimitedCompanyComplete/ui/index.tsx +26 -0
- package/src/FinnishLimitedCompanyLite/backend/en-EUR.tsv +97 -0
- package/src/FinnishLimitedCompanyLite/backend/fi-EUR.tsv +99 -0
- package/src/FinnishLimitedCompanyLite/backend/index.ts +53 -0
- package/src/FinnishLimitedCompanyLite/ui/index.tsx +28 -0
- package/src/GitBackup/backend/index.ts +109 -0
- package/src/GitBackup/ui/index.tsx +127 -0
- package/src/IncomeAndExpenses/backend/assetCodes.json +126 -0
- package/src/IncomeAndExpenses/backend/expense.json +190 -0
- package/src/IncomeAndExpenses/backend/income.json +120 -0
- package/src/IncomeAndExpenses/backend/index.ts +354 -0
- package/src/IncomeAndExpenses/backend/taxTypes.json +12 -0
- package/src/JournalReport/backend/index.ts +157 -0
- package/src/KrakenImport/backend/KrakenHandler.ts +88 -0
- package/src/KrakenImport/backend/index.ts +24 -0
- package/src/KrakenImport/backend/rules.json +52 -0
- package/src/LedgerReport/backend/index.ts +161 -0
- package/src/LynxImport/backend/LynxHandler.ts +389 -0
- package/src/LynxImport/backend/index.ts +24 -0
- package/src/LynxImport/backend/rules.json +412 -0
- package/src/NordeaImport/backend/NordeaHandler.ts +44 -0
- package/src/NordeaImport/backend/index.ts +24 -0
- package/src/NordeaImport/backend/rules.json +4 -0
- package/src/NordnetImport/backend/NordnetHandler.ts +78 -0
- package/src/NordnetImport/backend/index.ts +24 -0
- package/src/NordnetImport/backend/rules.json +271 -0
- package/src/Rand/ui/index.tsx +27 -0
- package/src/RapidAPI/backend/index.ts +133 -0
- package/src/TITOImport/backend/TITOHandler.ts +268 -0
- package/src/TITOImport/backend/index.ts +24 -0
- package/src/TITOImport/backend/rules.json +4 -0
- package/src/TagEditor/ui/index.tsx +510 -0
- package/src/USDollar/ui/index.tsx +27 -0
- package/src/VAT/ui/index.tsx +572 -0
- package/src/VATFinland/backend/index.ts +22 -0
- package/src/VATFinland/backend/vat.json +23 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ImportPlugin } from '@dataplug/tasenor-common-node'
|
|
2
|
+
import { PluginCode, Version } from '@dataplug/tasenor-common'
|
|
3
|
+
import { TITOHandler } from './TITOHandler'
|
|
4
|
+
|
|
5
|
+
class TITOImportPlugin extends ImportPlugin {
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
super(new TITOHandler())
|
|
9
|
+
|
|
10
|
+
this.code = 'TITOImport' as PluginCode
|
|
11
|
+
this.title = 'Import for TITO'
|
|
12
|
+
this.version = '1.0.30' as Version
|
|
13
|
+
this.icon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M5 6.99h3V14h2V6.99h3L9 3zM14 10v7.01h-3L15 21l4-3.99h-3V10z"/></svg>'
|
|
14
|
+
this.releaseDate = '2023-07-03'
|
|
15
|
+
this.use = 'backend'
|
|
16
|
+
this.type = 'import'
|
|
17
|
+
this.description = 'Import plugin for importing transaction data from old fixed length field TITO format.'
|
|
18
|
+
|
|
19
|
+
this.languages = this.getLanguages()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default TITOImportPlugin
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { Title, ToolPlugin, QuestionMarkInline, FileUploadData, FileUploader } from '@dataplug/tasenor-common-ui'
|
|
3
|
+
import { DatabaseModel, haveCursor, isTag, Tag, TagModel, TagType, Url } from '@dataplug/tasenor-common'
|
|
4
|
+
import { Trans, useTranslation } from 'react-i18next'
|
|
5
|
+
import { Box, Button, Card, CardContent, CardMedia, Grid, IconButton, Tab, Tabs, TextField, Typography } from '@mui/material'
|
|
6
|
+
import { Add, Cancel, CheckCircle } from '@mui/icons-material'
|
|
7
|
+
import { runInAction } from 'mobx'
|
|
8
|
+
import clone from 'clone'
|
|
9
|
+
import { green, red } from '@mui/material/colors'
|
|
10
|
+
import { observer } from 'mobx-react'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single tag display and editor.
|
|
14
|
+
* @param props
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
type TagCardProps = {
|
|
18
|
+
tag?: TagModel
|
|
19
|
+
existingTags: Set<Tag>
|
|
20
|
+
onSave?: (tag: Partial<TagModel>) => Promise<void>
|
|
21
|
+
onCreate?: (tag: Partial<TagModel>) => Promise<void>
|
|
22
|
+
onDelete?: (tag: Tag) => Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TagCard = observer((props: TagCardProps): JSX.Element => {
|
|
26
|
+
|
|
27
|
+
const { t } = useTranslation()
|
|
28
|
+
const cursor = haveCursor()
|
|
29
|
+
|
|
30
|
+
const [edit, setEdit] = useState(false)
|
|
31
|
+
const [changed, setChanged] = useState(false)
|
|
32
|
+
const [isNew, setIsNew] = useState(false)
|
|
33
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
34
|
+
|
|
35
|
+
const { name, tag, order, picture, mime } = props.tag ? props.tag : { name: '', tag: '', order: 0, picture: '', mime: '' }
|
|
36
|
+
|
|
37
|
+
const [tagName, setTagName] = useState<string>(name)
|
|
38
|
+
const [tagTag, setTagTag] = useState<Tag>(tag as Tag)
|
|
39
|
+
const [tagPicture, setTagPicture] = useState<string>('')
|
|
40
|
+
const [tagMime, setTagMime] = useState(mime)
|
|
41
|
+
|
|
42
|
+
let url = props.tag && props.tag.url
|
|
43
|
+
if (tagPicture) {
|
|
44
|
+
url = `data:${tagMime};base64,${tagPicture}` as Url
|
|
45
|
+
} else if (props.tag && props.tag.picture) {
|
|
46
|
+
url = `data:image/png;base64,${props.tag.picture}` as Url
|
|
47
|
+
}
|
|
48
|
+
if (!url) {
|
|
49
|
+
url = QuestionMarkInline as Url
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle saving the tag.
|
|
53
|
+
const onSave = async (): Promise<void> => {
|
|
54
|
+
if (isNew) {
|
|
55
|
+
props.onCreate && await props.onCreate({ name: tagName, tag: tagTag, picture: tagPicture, mime: tagMime })
|
|
56
|
+
} else {
|
|
57
|
+
props.onSave && await props.onSave({ name: tagName, tag: tagTag, picture: tagPicture, mime: tagMime })
|
|
58
|
+
}
|
|
59
|
+
setEdit(false)
|
|
60
|
+
setTagName('')
|
|
61
|
+
setTagTag('' as Tag)
|
|
62
|
+
setTagPicture('')
|
|
63
|
+
setTagMime('')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle deleting the tag.
|
|
67
|
+
const onDelete = async (): Promise<void> => {
|
|
68
|
+
props.onDelete && await props.onDelete(tag as Tag)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle file upload.
|
|
72
|
+
const onUpload = (file: FileUploadData): void => {
|
|
73
|
+
setTagPicture(file.data)
|
|
74
|
+
setTagMime(file.type as TagType)
|
|
75
|
+
setChanged(true)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Cancel editing.
|
|
79
|
+
const onCancel = (): void => {
|
|
80
|
+
setEdit(false)
|
|
81
|
+
setTagName('')
|
|
82
|
+
setTagTag('' as Tag)
|
|
83
|
+
setTagPicture('')
|
|
84
|
+
setTagMime('')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Key shortcuts.
|
|
88
|
+
const onKeyUp = (key: string): void => {
|
|
89
|
+
if (key === 'Enter') {
|
|
90
|
+
if (validateName() === null && validateTag() === null) {
|
|
91
|
+
onSave()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (key === 'Escape') {
|
|
95
|
+
onCancel()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validator for name.
|
|
100
|
+
const validateName = (): string | null => {
|
|
101
|
+
if (!changed) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
return tagName ? null : t('Tag name is required')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Validator for tag.
|
|
108
|
+
const validateTag = (): string | null => {
|
|
109
|
+
if (!changed) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
if (!tagTag) {
|
|
113
|
+
return t('Tag code is required')
|
|
114
|
+
}
|
|
115
|
+
if ((!props.tag || tagTag !== props.tag.tag) && props.existingTags.has(tagTag as Tag)) {
|
|
116
|
+
return 'Tag already exists'
|
|
117
|
+
}
|
|
118
|
+
return isTag(tagTag as Tag) ? null : 'Invalid tag'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Card raised sx={{ display: 'flex', width: 450 }}>
|
|
123
|
+
<CardContent sx={{ flex: '1 0 auto' }}>
|
|
124
|
+
{
|
|
125
|
+
!edit && <>
|
|
126
|
+
<Typography component="div" variant="h5">
|
|
127
|
+
{name}
|
|
128
|
+
</Typography>
|
|
129
|
+
<Typography variant="subtitle1" color="text.secondary" component="div">
|
|
130
|
+
{tag}
|
|
131
|
+
<Typography variant="subtitle2" component="span">
|
|
132
|
+
{order ? `#${order}` : ''}
|
|
133
|
+
</Typography>
|
|
134
|
+
</Typography>
|
|
135
|
+
{props.tag && !isDeleting &&
|
|
136
|
+
<>
|
|
137
|
+
<Button size="small" onClick={() => {
|
|
138
|
+
setIsNew(false)
|
|
139
|
+
setEdit(true)
|
|
140
|
+
setTagName(name)
|
|
141
|
+
setTagTag(tag as Tag)
|
|
142
|
+
setTagPicture(picture || '')
|
|
143
|
+
setTagMime(mime)
|
|
144
|
+
}}><Trans>Edit</Trans></Button>
|
|
145
|
+
<Button size="small" onClick={() => { setIsDeleting(true) }}><Trans>Delete</Trans></Button>
|
|
146
|
+
</>
|
|
147
|
+
}
|
|
148
|
+
{isDeleting &&
|
|
149
|
+
<>
|
|
150
|
+
<Trans>Are you sure?</Trans>
|
|
151
|
+
<Button size="small" onClick={() => { setIsDeleting(false) }}><Trans>No</Trans></Button>
|
|
152
|
+
<Button size="small" onClick={() => { setIsDeleting(false); onDelete() }}><Trans>Yes</Trans></Button>
|
|
153
|
+
</>
|
|
154
|
+
}
|
|
155
|
+
{!props.tag &&
|
|
156
|
+
<Button size="small" onClick={() => { setIsNew(true); setEdit(!edit) }}><Trans>Add New</Trans></Button>}
|
|
157
|
+
</>
|
|
158
|
+
}
|
|
159
|
+
{
|
|
160
|
+
edit && <>
|
|
161
|
+
<TextField
|
|
162
|
+
autoFocus
|
|
163
|
+
onFocus={() => cursor.disableHandler()}
|
|
164
|
+
onBlur={() => cursor.enableHandler()}
|
|
165
|
+
error={!!validateName()}
|
|
166
|
+
label={validateName()}
|
|
167
|
+
value={tagName}
|
|
168
|
+
onChange={(ev) => { setTagName(ev.target.value); setChanged(true) }}
|
|
169
|
+
onKeyUp={(ev) => onKeyUp(ev.key)}
|
|
170
|
+
/>
|
|
171
|
+
<IconButton title={t('Cancel')} onClick={onCancel} sx={{ color: red[800] }}><Cancel/></IconButton>
|
|
172
|
+
<br />
|
|
173
|
+
<TextField
|
|
174
|
+
onFocus={() => cursor.disableHandler()}
|
|
175
|
+
onBlur={() => cursor.enableHandler()}
|
|
176
|
+
value={tagTag}
|
|
177
|
+
error={!!validateTag()}
|
|
178
|
+
label={validateTag()}
|
|
179
|
+
onChange={(ev) => { setTagTag(ev.target.value as Tag); setChanged(true) }}
|
|
180
|
+
onKeyUp={(ev) => onKeyUp(ev.key)}
|
|
181
|
+
/>
|
|
182
|
+
<IconButton title={t('Save')} onClick={onSave} sx={{ color: green[800] }}><CheckCircle/></IconButton>
|
|
183
|
+
</>
|
|
184
|
+
}
|
|
185
|
+
</CardContent>
|
|
186
|
+
{
|
|
187
|
+
edit &&
|
|
188
|
+
<Box
|
|
189
|
+
sx={{
|
|
190
|
+
width: 150,
|
|
191
|
+
height: 150,
|
|
192
|
+
borderLeft: 1,
|
|
193
|
+
opacity: 0.5,
|
|
194
|
+
borderColor: 'divider',
|
|
195
|
+
backgroundImage: `url(${url})`,
|
|
196
|
+
backgroundSize: 'cover',
|
|
197
|
+
backgroundRepeat: 'no-repeat'
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<FileUploader text="" color="info" iconSize={130} onUpload={(files) => onUpload(files[0])} />
|
|
201
|
+
</Box>
|
|
202
|
+
}
|
|
203
|
+
{
|
|
204
|
+
!edit &&
|
|
205
|
+
<CardMedia
|
|
206
|
+
component="img"
|
|
207
|
+
sx={{ width: 150, height: 150, borderLeft: 1, borderColor: 'divider' }}
|
|
208
|
+
image={url}
|
|
209
|
+
alt={name}
|
|
210
|
+
/>
|
|
211
|
+
}
|
|
212
|
+
</Card>
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A panel providing editors for all tags in the group.
|
|
218
|
+
*/
|
|
219
|
+
type GroupPanelProps = {
|
|
220
|
+
group: TagType
|
|
221
|
+
tags: TagModel[]
|
|
222
|
+
onSave: (tag: TagModel) => Promise<void>
|
|
223
|
+
onDelete: (tag: TagModel) => Promise<void>
|
|
224
|
+
onCreate: (tag: Partial<TagModel>) => Promise<void>
|
|
225
|
+
existingTags: Set<Tag>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const GroupPanel = observer((props: GroupPanelProps): JSX.Element => {
|
|
229
|
+
|
|
230
|
+
// Handler for saving a tag
|
|
231
|
+
const onSave = async (tag: TagModel, changes: Partial<TagModel>): Promise<void> => {
|
|
232
|
+
runInAction(() => {
|
|
233
|
+
tag.name = changes.name || ''
|
|
234
|
+
tag.tag = changes.tag as Tag
|
|
235
|
+
tag.picture = changes.picture || null
|
|
236
|
+
tag.mime = changes.mime || null
|
|
237
|
+
tag.type = props.group
|
|
238
|
+
})
|
|
239
|
+
await props.onSave(tag)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Handler for creating a tag
|
|
243
|
+
const onCreate = async (changes: Partial<TagModel>): Promise<void> => {
|
|
244
|
+
await props.onCreate(changes)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Handler for deleting a tag
|
|
248
|
+
const onDelete = async (tag: TagModel): Promise<void> => {
|
|
249
|
+
await props.onDelete(tag)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<Grid container spacing={2}>
|
|
254
|
+
{props.tags.map((tag, idx) => (
|
|
255
|
+
<Grid item key={idx}>
|
|
256
|
+
<TagCard tag={tag} existingTags={props.existingTags} onSave={(changes) => onSave(tag, changes)} onDelete={() => onDelete(tag)} />
|
|
257
|
+
</Grid>
|
|
258
|
+
))}
|
|
259
|
+
<Grid item key={-1}><TagCard existingTags={props.existingTags} onCreate={(tag) => onCreate(tag)} /></Grid>
|
|
260
|
+
</Grid>
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* An editor for tags collection in a database.
|
|
266
|
+
*/
|
|
267
|
+
export type TagsEditorProps = {
|
|
268
|
+
database: DatabaseModel
|
|
269
|
+
onSave: (tag: TagModel) => Promise<void>
|
|
270
|
+
onDelete: (tag: TagModel) => Promise<void>
|
|
271
|
+
onCreate: (tag: Partial<TagModel>) => Promise<TagModel>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const TagsEditor = observer((props: TagsEditorProps): JSX.Element => {
|
|
275
|
+
|
|
276
|
+
const cursor = haveCursor()
|
|
277
|
+
const { t } = useTranslation()
|
|
278
|
+
const tags = props.database.tagsByTag
|
|
279
|
+
const [tab, setTab] = useState(0)
|
|
280
|
+
const [addingTab, setAddingTab] = useState<TagType>('' as TagType)
|
|
281
|
+
|
|
282
|
+
// Sort types and tags by their order number found.
|
|
283
|
+
const typeRange: Record<TagType, { min: number, max: number }> = {}
|
|
284
|
+
const byGroups: Record<TagType, TagModel[]> = {}
|
|
285
|
+
const orderNumbers: Set<number> = new Set()
|
|
286
|
+
const existingTags: Set<Tag> = new Set()
|
|
287
|
+
|
|
288
|
+
Object.values(tags).forEach(tag => {
|
|
289
|
+
if (!tag.tag || !tag.type) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
if (typeRange[tag.type] === undefined) {
|
|
293
|
+
typeRange[tag.type] = { min: tag.order, max: tag.order }
|
|
294
|
+
} else {
|
|
295
|
+
if (tag.order < typeRange[tag.type].min) {
|
|
296
|
+
typeRange[tag.type].min = tag.order
|
|
297
|
+
} else if (tag.order > typeRange[tag.type].max) {
|
|
298
|
+
typeRange[tag.type].max = tag.order
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
orderNumbers.add(tag.order)
|
|
302
|
+
existingTags.add(tag.tag)
|
|
303
|
+
byGroups[tag.type] = byGroups[tag.type] || []
|
|
304
|
+
byGroups[tag.type].push(tag)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// Sort each group and types and make types as a state.
|
|
308
|
+
const sortedTypes: TagType[] = Object.keys(typeRange).sort((a: TagType, b: TagType) => typeRange[a].min - typeRange[b].min) as TagType[]
|
|
309
|
+
Object.keys(byGroups).forEach(name => {
|
|
310
|
+
byGroups[name] = byGroups[name].sort((a, b) => a.order - b.order)
|
|
311
|
+
})
|
|
312
|
+
const [types, setTypes] = useState<TagType[]>(sortedTypes)
|
|
313
|
+
const [groups, setGroups] = useState<Record<string, TagModel[]>>(byGroups)
|
|
314
|
+
|
|
315
|
+
// Handler for new type creation.
|
|
316
|
+
const onCreateTab = (): void => {
|
|
317
|
+
if (!addingTab) {
|
|
318
|
+
setTab(types.length - 1)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
setTypes(types.concat(addingTab))
|
|
322
|
+
setGroups({ ...groups, [addingTab]: [] })
|
|
323
|
+
setAddingTab('' as TagType)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Handler for saving a tag
|
|
327
|
+
const onSave = async (tag: TagModel): Promise<void> => {
|
|
328
|
+
await props.onSave(tag)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handler for deleting a tag
|
|
332
|
+
const onDelete = async (tag: TagModel): Promise<void> => {
|
|
333
|
+
await props.onDelete(tag)
|
|
334
|
+
const newGroups = clone(groups)
|
|
335
|
+
newGroups[tag.type] = newGroups[tag.type].filter(t => t.tag !== tag.tag)
|
|
336
|
+
setGroups(newGroups)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Handler for creating a tag
|
|
340
|
+
const onCreate = async (tag: Partial<TagModel>): Promise<void> => {
|
|
341
|
+
const type = types[tab]
|
|
342
|
+
let order = Math.max(...groups[type].map(t => t.order)) + 1
|
|
343
|
+
// Set number to new group.
|
|
344
|
+
if (groups[type].length === 0) {
|
|
345
|
+
const idx = types.indexOf(type)
|
|
346
|
+
order = (idx + 1) * 1000 + 1
|
|
347
|
+
}
|
|
348
|
+
// Ensure non-overlapping.
|
|
349
|
+
while (orderNumbers.has(order)) {
|
|
350
|
+
order++
|
|
351
|
+
}
|
|
352
|
+
if (tag.tag) {
|
|
353
|
+
const model = await props.onCreate({
|
|
354
|
+
tag: tag.tag as Tag,
|
|
355
|
+
name: tag.name,
|
|
356
|
+
picture: tag.picture,
|
|
357
|
+
mime: tag.mime,
|
|
358
|
+
type,
|
|
359
|
+
order
|
|
360
|
+
})
|
|
361
|
+
setGroups({ ...groups, [type]: groups[type].concat([model]) })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Handler for tab editor key presses.
|
|
366
|
+
const onKeyUp = (key: string): void => {
|
|
367
|
+
if (key === 'Enter') {
|
|
368
|
+
onCreateTab()
|
|
369
|
+
}
|
|
370
|
+
if (key === 'Escape') {
|
|
371
|
+
setAddingTab('' as TagType)
|
|
372
|
+
if (tab) setTab(tab - 1)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// TODO: Editing type name by clicking already selected tab.
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
|
380
|
+
<Tabs value={tab} onChange={(event, newValue) => setTab(newValue)}>
|
|
381
|
+
{
|
|
382
|
+
types.map((type, idx) => (
|
|
383
|
+
<Tab key={idx} label={type} />
|
|
384
|
+
))
|
|
385
|
+
}
|
|
386
|
+
<Tab
|
|
387
|
+
key={-1}
|
|
388
|
+
disableRipple
|
|
389
|
+
icon={types[tab] && <Add />}
|
|
390
|
+
iconPosition="start"
|
|
391
|
+
label={types[tab]
|
|
392
|
+
? t('Add Tag Type')
|
|
393
|
+
: <TextField
|
|
394
|
+
autoFocus
|
|
395
|
+
onFocus={() => cursor.disableHandler()}
|
|
396
|
+
onBlur={() => cursor.enableHandler()}
|
|
397
|
+
value={addingTab}
|
|
398
|
+
onChange={ev => setAddingTab(ev.target.value as TagType)}
|
|
399
|
+
onKeyUp={(ev) => onKeyUp(ev.key)}
|
|
400
|
+
/>
|
|
401
|
+
}
|
|
402
|
+
/>
|
|
403
|
+
</Tabs>
|
|
404
|
+
{types[tab] && (
|
|
405
|
+
<>
|
|
406
|
+
<GroupPanel
|
|
407
|
+
group={types[tab] as TagType}
|
|
408
|
+
tags={groups[types[tab]]}
|
|
409
|
+
existingTags={existingTags}
|
|
410
|
+
onSave={tag => onSave(tag)}
|
|
411
|
+
onCreate={tag => onCreate(tag)}
|
|
412
|
+
onDelete={tag => onDelete(tag)}
|
|
413
|
+
/>
|
|
414
|
+
</>
|
|
415
|
+
)}
|
|
416
|
+
{types.length === 0 && (
|
|
417
|
+
<Typography variant="h6" sx={{ padding: 1 }}>
|
|
418
|
+
<Trans>You don't have any tag groups yet. Please create the firt one by naming it above.</Trans>
|
|
419
|
+
</Typography>
|
|
420
|
+
)}
|
|
421
|
+
</Box>
|
|
422
|
+
)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
class TagEditor extends ToolPlugin {
|
|
426
|
+
|
|
427
|
+
static code = 'TagEditor'
|
|
428
|
+
static title = 'Tag Editor'
|
|
429
|
+
static version = '1.0.33'
|
|
430
|
+
static icon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 7H5v10h11l3.55-5z" opacity=".3"/><path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16zM16 17H5V7h11l3.55 5L16 17z"/></svg>'
|
|
431
|
+
static releaseDate = '2022-03-11'
|
|
432
|
+
static use = 'ui'
|
|
433
|
+
static type = 'tool'
|
|
434
|
+
static description = 'Allows creating, deleting and editing tags and tag groups. Tags are useful for example for filtering transaction lists and combining reports by customizable groups.'
|
|
435
|
+
|
|
436
|
+
constructor() {
|
|
437
|
+
super()
|
|
438
|
+
this.languages = {
|
|
439
|
+
fi: {
|
|
440
|
+
'Add Tag Type': 'Lisää uusi ryhmä',
|
|
441
|
+
'Tag Editor': 'Tägieditori',
|
|
442
|
+
'Tag name is required': 'Tägillä pitää olla nimi',
|
|
443
|
+
'Tag code is required': 'Tägillä pitää olla koodi',
|
|
444
|
+
'Tag already exists': 'Tägi on jo olemassa',
|
|
445
|
+
'Invalid tag': 'Virheellinen tägi',
|
|
446
|
+
'Edit Tags in the Database': 'Muokkaa tietokannan tägejä',
|
|
447
|
+
'A tag is a special notation that can be added to the beginning of the transaction description.': 'Tägi on erityisnotaatio, jota voidaan käyttää tapahtuman kuvauksen alussa.',
|
|
448
|
+
'A short code, e.g. XYZ an be added in the beginning of the description as [XYZ].': 'Esimerkiksi tägi, jonka koodi on XYZ, voidaan lisätä kuvauksen alkuun kirjoittamalla [XYZ].',
|
|
449
|
+
'Once added, the tag can be used in transaction list as a filter by showing only the transactions with the give tag.': 'Kun tapahtumaan on lisätty tägi, sitä voidaan käyttää tapahtumalistan filtteröintiin näyttämään vain kyseise tägin sisältämät rivit.',
|
|
450
|
+
'Tag grouping is used to keep tags of the specific dimension in the single group.': 'Tägien ryhmittelyä käytetään tietyn dimension tägien yhdistämiseen saman nimen alle.',
|
|
451
|
+
'It is usable in some reports for example.': 'Ryhmittelyä voidaan hyödyntää esimerkiksi joissakin raporteissa.',
|
|
452
|
+
'You don't have any tag groups yet. Please create the firt one by naming it above.': 'Yhtään tägiryhmää ei ole vielä olemassa. Ole hyvä ja luo ensimmäinen antamalla sille nimi yllä.',
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
toolMenu() {
|
|
458
|
+
return [{ title: 'Tag Editor', disabled: !this.store || !this.store.db }]
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
toolTitle() {
|
|
462
|
+
return 'Tag Editor'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
toolTopPanel() {
|
|
466
|
+
return <Typography sx={{ padding: 1 }}>
|
|
467
|
+
<Trans>A tag is a special notation that can be added to the beginning of the transaction description.</Trans>
|
|
468
|
+
<Trans>A short code, e.g. XYZ an be added in the beginning of the description as [XYZ].</Trans>
|
|
469
|
+
<Trans>Once added, the tag can be used in transaction list as a filter by showing only the transactions with the give tag.</Trans>
|
|
470
|
+
<Trans>Tag grouping is used to keep tags of the specific dimension in the single group.</Trans>
|
|
471
|
+
<Trans>It is usable in some reports for example.</Trans>
|
|
472
|
+
</Typography>
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
toolMainPanel(): JSX.Element {
|
|
476
|
+
const { store } = this
|
|
477
|
+
|
|
478
|
+
if (!store || !store.database) {
|
|
479
|
+
return <></>
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const onSave = async (tag: TagModel) => {
|
|
483
|
+
await tag.save()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const onDelete = async (tag: TagModel) => {
|
|
487
|
+
await tag.delete()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const onCreate = async (tag: Partial<TagModel>): Promise<TagModel> => {
|
|
491
|
+
const model = await store.database.addTag(tag)
|
|
492
|
+
await model.save()
|
|
493
|
+
return model
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<Box sx={{ padding: 1 }}>
|
|
498
|
+
<Title><Trans>Edit Tags in the Database</Trans></Title>
|
|
499
|
+
<TagsEditor
|
|
500
|
+
database={store.database}
|
|
501
|
+
onDelete={onDelete}
|
|
502
|
+
onSave={onSave}
|
|
503
|
+
onCreate={onCreate}
|
|
504
|
+
/>
|
|
505
|
+
</Box>
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export default TagEditor
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CurrencyPlugin } from '@dataplug/tasenor-common-ui'
|
|
2
|
+
|
|
3
|
+
class USDollar extends CurrencyPlugin {
|
|
4
|
+
|
|
5
|
+
static code = 'USDollar'
|
|
6
|
+
static title = 'Currency US Dollar'
|
|
7
|
+
static version = '1.0.9'
|
|
8
|
+
static icon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z"/></svg>'
|
|
9
|
+
static releaseDate = '2022-04-08'
|
|
10
|
+
static use = 'ui'
|
|
11
|
+
static type = 'currency'
|
|
12
|
+
static description = 'Support for US dollar currency.'
|
|
13
|
+
|
|
14
|
+
getCurrencySymbol() {
|
|
15
|
+
return '$'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getCurrencyCode() {
|
|
19
|
+
return 'USD'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
money2str(cents) {
|
|
23
|
+
return this.makeMoney(cents, 100, 2, '$ ', ',', '.', '')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default USDollar
|