authscape 1.0.634 → 1.0.636

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "authscape",
3
- "version": "1.0.634",
3
+ "version": "1.0.636",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,4 +1,3 @@
1
- import AspectRatio from '@mui/icons-material/AspectRatio';
2
1
  import Box from '@mui/material/Box';
3
2
  import React, {useMemo} from 'react';
4
3
  import {useDropzone} from 'react-dropzone';
@@ -0,0 +1,237 @@
1
+ import React, {useEffect, useState, useRef} from 'react';
2
+ import { Box } from '@mui/system';
3
+ // import { apiService } from 'authscape';
4
+ // import MappedColumn from './MappedColumn';
5
+ import Container from '@mui/material/Container';
6
+ import Typography from '@mui/material/Typography';
7
+ import { Button, Grid } from '@mui/material';
8
+ // import ConditionBasedTool from './conditionBasedTool';
9
+ import Dialog from '@mui/material/Dialog';
10
+ import DialogActions from '@mui/material/DialogActions';
11
+ import DialogContent from '@mui/material/DialogContent';
12
+ import DialogTitle from '@mui/material/DialogTitle';
13
+ // import SpreadsheetViewer from '../spreadsheetViewer';
14
+ import CloseIcon from '@mui/icons-material/Close';
15
+ import IconButton from '@mui/material/IconButton';
16
+ import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
17
+
18
+ export function AssignMapping({currentUser, documentComponentId, setIsLoading = null, onCancel = null, onPublished = null}) {
19
+
20
+ const [documentId, setDocumentId] = useState(documentComponentId);
21
+
22
+ const [fromColumnOptions, setFromColumnOptions] = useState(null);
23
+ const [toColumnOptions, setToColumnOptions] = useState(null);
24
+ const [documentType, setDocumentType] = useState(null);
25
+ const [documentName, setDocumentName] = useState(null);
26
+ const [urlTick, setURLTick] = useState(1);
27
+ const [spreadSheetAddress, setSpreadSheetAddress] = useState(null);
28
+ const [showPreviewDialog, setShowPreviewDialog] = useState(false);
29
+
30
+ const [advanceQuery, setAdvanceQuery] = useState(null);
31
+
32
+ const spreadSheetRef = useRef(null);
33
+
34
+
35
+ const fetchMappingTo = async () => {
36
+
37
+ let response = await apiService().get("/DocumentMapping/GetMappedDynamicFieldsForCompany?companyId=" + currentUser.companyId + "&documentId=" + documentComponentId);
38
+ if (response != null)
39
+ {
40
+ setToColumnOptions(response.data);
41
+ }
42
+ }
43
+
44
+ const fetchMappingFrom = async () => {
45
+ let response = await apiService().post("/DocumentMapping/GetMapping", {
46
+ documentComponentId: documentComponentId,
47
+ companyId: currentUser.companyId
48
+ });
49
+ if (response != null)
50
+ {
51
+ setFromColumnOptions(response.data.documentMappings);
52
+ setDocumentName(response.data.name);
53
+ setDocumentType(response.data.documentType);
54
+ }
55
+ }
56
+
57
+ useEffect(() => {
58
+
59
+ if (documentComponentId != null)
60
+ {
61
+ if (setIsLoading != null)
62
+ {
63
+ setIsLoading(true);
64
+ }
65
+
66
+ setSpreadSheetAddress("/DocumentMappingPreview/PreviewMappedData?companyId=" + currentUser.companyId + "&documentComponentId=" + documentComponentId);
67
+
68
+ const fetchData = async () => {
69
+ await fetchMappingFrom();
70
+ await fetchMappingTo();
71
+
72
+
73
+ if (setIsLoading != null)
74
+ {
75
+ setIsLoading(false);
76
+ }
77
+ }
78
+
79
+ fetchData();
80
+
81
+ }
82
+
83
+ }, [documentComponentId])
84
+
85
+ return (
86
+ <Box>
87
+ <Container maxWidth="xl" sx={{marginTop:2}}>
88
+ <Grid container spacing={2}>
89
+ <Grid item xs={6}>
90
+ <Box sx={{position:"sticky", top:20}}>
91
+ <Typography variant="h4" gutterBottom sx={{paddingBottom:2}}>
92
+ File Uploaded: <br/> {documentName}
93
+ </Typography>
94
+
95
+ <Typography variant="subtitle1" gutterBottom sx={{paddingBottom:2}}>
96
+ You have {fromColumnOptions != null && fromColumnOptions.length} columns that can be created or mapped
97
+ </Typography>
98
+
99
+ <Button variant="outlined" sx={{marginRight:2}} onClick={async () => {
100
+
101
+ if (onCancel != null)
102
+ {
103
+ onCancel();
104
+ }
105
+
106
+ }}>Cancel</Button>
107
+
108
+ <Button variant="contained" endIcon={<ArrowRightAltRoundedIcon/>} sx={{marginRight:2}} onClick={() => {
109
+
110
+ setShowPreviewDialog(true);
111
+
112
+ }}>Next, Preview your mapping</Button>
113
+
114
+ </Box>
115
+ </Grid>
116
+
117
+ <Grid item xs={5}>
118
+ {fromColumnOptions != null && fromColumnOptions.map((column) => {
119
+ return (
120
+ <Box>
121
+
122
+ <MappedColumn companyId={currentUser.companyId} documentId={documentId} documentType={documentType} documentMappingId={column.id} name={column.name} toName={column.toName} isMapped={(column.toName == null || column.toName == "") ? true : false} toOptions={toColumnOptions} onResponse={() => {
123
+
124
+ fetchMappingFrom();
125
+ fetchMappingTo();
126
+
127
+ }} />
128
+
129
+ </Box>
130
+ )
131
+ })}
132
+ </Grid>
133
+ </Grid>
134
+ </Container>
135
+
136
+ <Dialog
137
+ open={showPreviewDialog}
138
+ onClose={() => {
139
+ setShowPreviewDialog(false);
140
+ }}
141
+ fullWidth={true}
142
+ maxWidth={"xl"}
143
+ aria-labelledby="alert-dialog-title"
144
+ aria-describedby="alert-dialog-description">
145
+ <DialogTitle id="alert-dialog-title" sx={{fontSize:"25px", paddingTop:4}}>
146
+ {"Preview your mapping"}
147
+ </DialogTitle>
148
+
149
+ <Box sx={{paddingLeft:3}}>
150
+ Ensure that the data uploaded is accurately mapped and all (Required) fields are completed.
151
+ </Box>
152
+
153
+ <IconButton
154
+ aria-label="close"
155
+ onClick={() => {
156
+ setShowPreviewDialog(false);
157
+ }}
158
+ sx={{
159
+ position: 'absolute',
160
+ right: 8,
161
+ top: 8,
162
+ color: (theme) => theme.palette.grey[500],
163
+ }}
164
+ >
165
+ <CloseIcon />
166
+ </IconButton>
167
+
168
+ <DialogContent>
169
+
170
+ <Box sx={{paddingBottom:1}}>
171
+ <ConditionBasedTool toColumnOptions={toColumnOptions} documentId={documentId} onConditionApplied={(currentQuery) => {
172
+
173
+ let incrementNum = urlTick + 1;
174
+ setURLTick(incrementNum);
175
+
176
+ setAdvanceQuery(currentQuery);
177
+
178
+ // setSpreadSheetAddress("/DocumentMappingPreview/PreviewMappedData?companyId=" + currentUser.companyId + "&documentComponentId=" + documentComponentId + "&tick=" + incrementNum);
179
+
180
+ }} />
181
+ </Box>
182
+
183
+ {spreadSheetAddress != null &&
184
+ <SpreadsheetViewer ref={spreadSheetRef} url={spreadSheetAddress} advanceQuery={advanceQuery} currentUser={currentUser} hideToolbar={true} loadedUser={true} />
185
+ }
186
+
187
+ </DialogContent>
188
+ <DialogActions>
189
+ <Button onClick={() => {
190
+
191
+ setShowPreviewDialog(false);
192
+
193
+ }}>Cancel</Button>
194
+ <Button variant="contained" onClick={async () => {
195
+
196
+ if (setIsLoading != null)
197
+ {
198
+ setIsLoading(true);
199
+ }
200
+
201
+ let publishedRows = spreadSheetRef.current.getRows();
202
+
203
+ let response = await apiService().post("/DocumentMapping/Publish", {
204
+ companyId: currentUser.companyId,
205
+ documentId: documentId,
206
+ publishedRows: publishedRows
207
+ });
208
+
209
+ if (response != null && response.status == 200)
210
+ {
211
+ setShowPreviewDialog(false);
212
+
213
+ if (onPublished != null)
214
+ {
215
+ onPublished();
216
+ }
217
+ }
218
+ else
219
+ {
220
+ alert(JSON.stringify(response.data));
221
+ }
222
+
223
+ if (setIsLoading != null)
224
+ {
225
+ setIsLoading(false);
226
+ }
227
+
228
+ }}>
229
+ Publish
230
+ </Button>
231
+ </DialogActions>
232
+ </Dialog>
233
+
234
+
235
+ </Box>
236
+ )
237
+ }
@@ -0,0 +1,99 @@
1
+
2
+ import React, {useEffect, useState, useRef} from 'react';
3
+ // import { apiService, FileUploader} from 'authscape';
4
+ import Button from '@mui/material/Button';
5
+ import { Box } from '@mui/system';
6
+ import Accordion from '@mui/material/Accordion';
7
+ import AccordionSummary from '@mui/material/AccordionSummary';
8
+ import AccordionDetails from '@mui/material/AccordionDetails';
9
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
10
+ import { QueryBuilder } from 'react-querybuilder';
11
+
12
+
13
+ export function ConditionBasedTool({toColumnOptions, documentId, onConditionApplied}) {
14
+
15
+ const [currentQuery, setCurrentQuery] = useState(null);
16
+
17
+ // const fields = [
18
+ // { name: 'firstName', label: 'First Name' },
19
+ // { name: 'lastName', label: 'Last Name' }
20
+ // ];
21
+
22
+ useEffect(() => {
23
+
24
+ if (documentId != null)
25
+ {
26
+ const fetchData = async () => {
27
+ let response = await apiService().get("/DocumentMapping/GetRules?documentComponentId=" + documentId);
28
+ if (response != null && response.status == 200)
29
+ {
30
+ if (response.data != null && response.data != "")
31
+ {
32
+ setCurrentQuery(response.data);
33
+ }
34
+ else
35
+ {
36
+ setCurrentQuery(null);
37
+ }
38
+ }
39
+ }
40
+ fetchData();
41
+ }
42
+
43
+
44
+ }, [documentId]);
45
+
46
+ const getFields = () => {
47
+
48
+ let fields = [];
49
+
50
+ for (let index = 0; index < toColumnOptions.length; index++) {
51
+ const toColumn = toColumnOptions[index];
52
+
53
+ if (toColumn.isMapped) // only show filters that are mapped
54
+ {
55
+ fields.push({ name: toColumn.name, label: toColumn.visibleName });
56
+ }
57
+ }
58
+
59
+ return fields;
60
+ }
61
+
62
+ const customOperators = [
63
+ { name: 'contains', label: 'Contains' },
64
+ { name: 'notContains', label: 'Does not contain' },
65
+ ];
66
+
67
+ return (
68
+ <>
69
+ <Box>
70
+ <Accordion>
71
+ <AccordionSummary
72
+ expandIcon={<ExpandMoreIcon />}
73
+ aria-controls="panel1-content"
74
+ id="panel1-header">
75
+ Advance filtering
76
+ </AccordionSummary>
77
+ <AccordionDetails>
78
+ <QueryBuilder fields={getFields()} operators={customOperators} query={currentQuery} onQueryChange={setCurrentQuery} />
79
+
80
+ <Button variant="contained" sx={{marginTop:1}} onClick={async () => {
81
+
82
+ let response = await apiService().put("/DocumentMapping/ApplyFilterForViewer", {
83
+ documentComponentId: documentId,
84
+ rules: JSON.stringify(currentQuery)
85
+ });
86
+
87
+ if (response != null && response.status == 200)
88
+ {
89
+ onConditionApplied(currentQuery);
90
+ }
91
+
92
+ }}>Apply Filter</Button>
93
+ </AccordionDetails>
94
+ </Accordion>
95
+
96
+ </Box>
97
+ </>
98
+ )
99
+ }
@@ -0,0 +1,259 @@
1
+ import React, {useEffect, useState, useRef} from 'react';
2
+ // import {apiService, authService, StripeConnect, ReactDraft, EditableDatagrid, FileUploader} from 'authscape';
3
+ import Button from '@mui/material/Button';
4
+ import { Box } from '@mui/system';
5
+ import Dialog from '@mui/material/Dialog';
6
+ import DialogActions from '@mui/material/DialogActions';
7
+ import DialogContent from '@mui/material/DialogContent';
8
+ import DialogContentText from '@mui/material/DialogContentText';
9
+ import DialogTitle from '@mui/material/DialogTitle';
10
+ import TextField from '@mui/material/TextField';
11
+ import InputLabel from '@mui/material/InputLabel';
12
+ import MenuItem from '@mui/material/MenuItem';
13
+ import FormControl from '@mui/material/FormControl';
14
+ import Select from '@mui/material/Select';
15
+ import Radio from '@mui/material/Radio';
16
+ import RadioGroup from '@mui/material/RadioGroup';
17
+ import FormControlLabel from '@mui/material/FormControlLabel';
18
+ import FormLabel from '@mui/material/FormLabel';
19
+
20
+ export function Datasources({disableTraining = false}) {
21
+
22
+ const documentColumns = [
23
+ { field: 'name', headerName: 'Name', width: 150, editable: false },
24
+ {
25
+ field: "type",
26
+ type: "actions",
27
+ width: 200,
28
+ flex:1,
29
+ headerName: "Data Source",
30
+ getActions: ({ id, row }) => {
31
+ return [
32
+ <Box sx={{textAlign:"left"}}>
33
+ {row.type == 0 ? "Database" : ""}
34
+ {row.type == 1 ? "Dynamic Mapping" : ""}
35
+ {row.type == 2 ? "Custom Model" : ""}
36
+ </Box>,
37
+ ];
38
+ },
39
+ },
40
+ {
41
+ field: "Detail",
42
+ type: "actions",
43
+ width: 200,
44
+ flex:1,
45
+ headerName: "Mapping To",
46
+ getActions: ({ id, row }) => {
47
+ return [
48
+ <Box sx={{textAlign:"left"}}>
49
+
50
+ {row.type == 0 ?
51
+ <>Table: {row.tableName}</>
52
+ :
53
+ ""
54
+ }
55
+
56
+ {row.type == 1 ?
57
+ <>Database driven mapping</>
58
+ :
59
+ ""
60
+ }
61
+
62
+ {row.type == 2 ?
63
+ <>Type Name: {row.typeName}
64
+ <br/> Assembly Fullname: {row.assemblyFullName}</>
65
+ :
66
+ ""
67
+ }
68
+
69
+ </Box>,
70
+ ];
71
+ },
72
+ },
73
+ // {
74
+ // field: "actions",
75
+ // type: "actions",
76
+ // width: 200,
77
+ // headerName: "Archive",
78
+ // cellClassName: "actions",
79
+ // getActions: ({ id, row }) => {
80
+ // return [
81
+ // <GridActionsCellItem key={id}
82
+ // icon={<DeleteRoundedIcon />}
83
+ // label="Archive"
84
+ // className="textPrimary"
85
+ // onClick={async () => {
86
+
87
+ // let documentMappingId = "";
88
+ // let documentComponentId = "";
89
+
90
+ // // archive the column
91
+ // await apiService().delete("/DocumentMapping/RemoveColumnFromDocumentComponent?documentMappingId=" + documentMappingId + "&documentComponentId=" + documentComponentId)
92
+
93
+ // }}
94
+ // />,
95
+ // ];
96
+ // },
97
+ // }
98
+
99
+ ];
100
+
101
+ const refDatabaseTableSelect = useRef(null);
102
+ const refNewDocTypeName = useRef(null);
103
+
104
+ const [dataGridRefreshKey, setDataGridRefreshKey] = useState(0);
105
+
106
+ const [document, setDocument] = useState(null);
107
+ const [showDatasource, setShowDatasource] = useState(false);
108
+ const [databaseTables, setDatabaseTables] = useState(null);
109
+
110
+ const [mappingType, setMappingType] = useState("database");
111
+
112
+
113
+ const refTypeName = useRef(null);
114
+ const refAssemblyFullName = useRef(null);
115
+
116
+
117
+
118
+ useEffect(() => {
119
+
120
+ let fetchData = async () => {
121
+ let response = await apiService().get("/DocumentMapping/GetTablesFromDatabase");
122
+ if (response != null && response.status == 200)
123
+ {
124
+ setDatabaseTables(response.data);
125
+ }
126
+ }
127
+
128
+ fetchData();
129
+
130
+ }, [])
131
+
132
+ return (
133
+ <Box sx={{marginTop:6}}>
134
+ <Box>
135
+ {!disableTraining &&
136
+ <Box sx={{textAlign:"right", marginBottom:2}}>
137
+ <Button variant="contained" onClick={() => {
138
+ setShowDatasource(true);
139
+ }}>Add Data Source</Button>
140
+ </Box>
141
+ }
142
+ <EditableDatagrid key={dataGridRefreshKey} loadedUser={true} url={"/DocumentMapping/GetDocumentTypes"} columns={documentColumns}/>
143
+ </Box>
144
+
145
+ <Dialog
146
+ open={showDatasource}
147
+ onClose={() => {
148
+ setShowDatasource(false);
149
+ }}
150
+ aria-labelledby="alert-dialog-title"
151
+ aria-describedby="alert-dialog-description"
152
+ >
153
+ <DialogTitle id="alert-dialog-title">
154
+ {"Data Source"}
155
+ </DialogTitle>
156
+ <DialogContent>
157
+ <DialogContentText id="alert-dialog-description">
158
+ A data source is a place or system where data is stored and collected. It can be a database, file, web service, or sensor. Data is extracted, transformed, and used for analysis and other purposes. Managing data sources is crucial for data-driven decision-making.
159
+ </DialogContentText>
160
+
161
+ <Box sx={{marginTop:3}}>
162
+ <TextField inputRef={refNewDocTypeName} label="Name for data source" variant="outlined" fullWidth={true} />
163
+ </Box>
164
+
165
+ <Box sx={{marginTop:2}}>
166
+ <FormControl>
167
+ <FormLabel id="demo-row-radio-buttons-group-label">How the data will connect:</FormLabel>
168
+ <RadioGroup value={mappingType} row aria-labelledby="demo-row-radio-buttons-group-label" name="row-radio-buttons-group" onChange={(env) => {
169
+ setMappingType(env.currentTarget.value);
170
+ }}>
171
+ <FormControlLabel value="database" control={<Radio />} label="Database" />
172
+ <FormControlLabel value="mappingTable" control={<Radio />} label="Dynamic Mapping Table" />
173
+ <FormControlLabel value="customModelMapping" control={<Radio />} label="Custom Model" />
174
+ </RadioGroup>
175
+ </FormControl>
176
+ </Box>
177
+
178
+ {mappingType == "database" &&
179
+ <Box sx={{marginTop:2}}>
180
+ <Box sx={{ minWidth: 120 }}>
181
+ <FormControl fullWidth>
182
+ <InputLabel id="demo-simple-select-label">Database Tables</InputLabel>
183
+ <Select
184
+ inputRef={refDatabaseTableSelect}
185
+ labelId="demo-simple-select-label"
186
+ label="Age">
187
+ {databaseTables != null && databaseTables.map((table, index) => {
188
+ return (<MenuItem value={table.tableName}>{table.tableName}</MenuItem>)
189
+ })}
190
+ </Select>
191
+ </FormControl>
192
+ </Box>
193
+ </Box>
194
+ }
195
+
196
+ {mappingType == "customModelMapping" &&
197
+
198
+ <Box sx={{marginTop:2}}>
199
+ <Box sx={{ minWidth: 120 }}>
200
+ <TextField inputRef={refTypeName} label="Type Name (Example: API.Controllers.InvoiceUpload)" variant="outlined" fullWidth={true} sx={{marginTop:1}} />
201
+ <TextField inputRef={refAssemblyFullName} label="Assembly Full Name (Example: API)" variant="outlined" fullWidth={true} sx={{marginTop:1}} />
202
+ </Box>
203
+ </Box>
204
+ }
205
+
206
+ </DialogContent>
207
+ <DialogActions>
208
+ <Button onClick={() => {
209
+ setShowDatasource(false);
210
+ }}>Cancel</Button>
211
+ <Button onClick={async () => {
212
+
213
+ if (mappingType == "customModelMapping")
214
+ {
215
+ let response = await apiService().post("/DocumentMapping/AddDataSource", {
216
+ name: refNewDocTypeName.current.value,
217
+ dataTable: refNewDocTypeName.current.value,
218
+ documentType: 2,
219
+ typeName: refTypeName.current.value,
220
+ assemblyFullName: refAssemblyFullName.current.value
221
+ });
222
+
223
+ if (response != null && response.data.error != null)
224
+ {
225
+ alert(response.data.error);
226
+ }
227
+ else
228
+ {
229
+ if (response != null && (response.status == 204 || response.status == 200))
230
+ {
231
+ setDataGridRefreshKey(dataGridRefreshKey + 1);
232
+ setShowDatasource(false);
233
+ }
234
+ }
235
+ }
236
+ else
237
+ {
238
+ let response = await apiService().post("/DocumentMapping/AddDataSource", {
239
+ name: refNewDocTypeName.current.value,
240
+ dataTable: refDatabaseTableSelect.current != null ? refDatabaseTableSelect.current.value : "",
241
+ documentType: (mappingType == "database" ? 0 : 1)
242
+ });
243
+
244
+ if (response != null && (response.status == 204 || response.status == 200))
245
+ {
246
+ setDataGridRefreshKey(dataGridRefreshKey + 1);
247
+ setShowDatasource(false);
248
+ }
249
+ }
250
+
251
+ }}>
252
+ Add Data Source
253
+ </Button>
254
+ </DialogActions>
255
+ </Dialog>
256
+
257
+ </Box>
258
+ )
259
+ }
@@ -0,0 +1,72 @@
1
+ import React, {useEffect, useState, useRef} from 'react';
2
+ import { Box } from '@mui/system';
3
+ // import ManageMappingDocuments from './manageMappingDocuments';
4
+ // import AssignMapping from './AssignMapping';
5
+
6
+ export function FileMapping({currentUser, fileUploadName = "Upload Document", hideDocumentManager = false, setIsLoading = null, documentTypeId = null, onOpened = null, onPublished = null, onCanceled = null, onArchived = null}) {
7
+
8
+ const [documentComponentId, setDocumentComponentId] = useState(null);
9
+
10
+ useEffect(() => {
11
+
12
+ if (documentComponentId != null)
13
+ {
14
+ if (onOpened != null)
15
+ {
16
+ onOpened(documentComponentId);
17
+ }
18
+ }
19
+
20
+ }, [documentComponentId]);
21
+
22
+ return (
23
+ <Box>
24
+ {documentComponentId == null &&
25
+ <ManageMappingDocuments
26
+ fileUploadName={fileUploadName}
27
+ documentTypeId={documentTypeId}
28
+ hideDocumentManager={hideDocumentManager}
29
+ companyId={currentUser != null ? currentUser.companyId : null}
30
+ onManageField={(documentComponentId) => {
31
+
32
+ setDocumentComponentId(documentComponentId);
33
+
34
+ }}
35
+ onArchive={(documentComponentId) => {
36
+
37
+ //alert(documentComponentId);
38
+ if (onArchived != null)
39
+ {
40
+ onArchived(documentComponentId);
41
+ }
42
+
43
+ }}
44
+ />
45
+ }
46
+
47
+ {documentComponentId != null &&
48
+ <AssignMapping currentUser={currentUser} setIsLoading={setIsLoading} documentComponentId={documentComponentId}
49
+ onCancel={() => {
50
+
51
+ setDocumentComponentId(null);
52
+
53
+ if (onCanceled != null)
54
+ {
55
+ onCanceled(documentComponentId);
56
+ }
57
+
58
+ }}
59
+ onPublished={() => {
60
+ setDocumentComponentId(null);
61
+
62
+ if (onPublished != null)
63
+ {
64
+ onPublished(documentComponentId);
65
+ }
66
+ }} />
67
+ }
68
+
69
+
70
+ </Box>
71
+ )
72
+ }