Browse Source

Add more v2 pages.

pull/581/head
James Cole 2 years ago
parent
commit
b964d9eb23
No known key found for this signature in database GPG Key ID: B49A324B7EAD6D80
  1. 5
      app/Http/Controllers/Import/Nordigen/SelectionController.php
  2. 22
      resources/js/v2/src/pages/configuration/index.js
  3. 195
      resources/js/v2/src/pages/conversion/index.js
  4. 53
      resources/js/v2/src/pages/selection/gocardless.js
  5. 187
      resources/js/v2/src/pages/submit/index.js
  6. 12
      resources/js/v2/vite.config.js
  7. 2
      resources/views/v1/import/004-configure/index.twig
  8. 72
      resources/views/v2/components/conversion-messages.blade.php
  9. 22
      resources/views/v2/components/firefly-iii-account-generic.blade.php
  10. 31
      resources/views/v2/components/importer-account-title.blade.php
  11. 21
      resources/views/v2/components/importer-account.blade.php
  12. 861
      resources/views/v2/import/004-configure/index.blade.php
  13. 140
      resources/views/v2/import/005-roles/index-csv.blade.php
  14. 129
      resources/views/v2/import/006-mapping/index.blade.php
  15. 109
      resources/views/v2/import/007-convert/index.blade.php
  16. 104
      resources/views/v2/import/008-submit/index.blade.php
  17. 211
      resources/views/v2/import/009-selection/index.blade.php
  18. 116
      resources/views/v2/import/011-connection/index.blade.php
  19. 12
      resources/views/v2/index.blade.php

5
app/Http/Controllers/Import/Nordigen/SelectionController.php

@ -64,7 +64,8 @@ class SelectionController extends Controller
{
app('log')->debug(sprintf('Now at %s', __METHOD__));
$countries = config('nordigen.countries');
$mainTitle = 'Selection';
$mainTitle = 'Select your country and bank';
$pageTitle = 'Select your country and bank';
$subTitle = 'Select your country and the bank you wish to use.';
$configuration = $this->restoreConfiguration();
@ -96,7 +97,7 @@ class SelectionController extends Controller
throw new ImporterErrorException($response->message);
}
return view('import.009-selection.index', compact('mainTitle', 'subTitle', 'response', 'countries', 'configuration'));
return view('import.009-selection.index', compact('mainTitle','pageTitle', 'subTitle', 'response', 'countries', 'configuration'));
}
/**

22
resources/js/v2/src/pages/configuration/index.js

@ -24,11 +24,31 @@ import '../../boot/bootstrap.js';
let index = function () {
return {
loadingParsedDate: true,
dateFormat: 'Y-m-d',
parsedDateFormat: 'hello',
dateRange: '',
detectionMethod: '',
functionName() {
},
getParsedDate() {
this.loadingParsedDate = true;
const parseUrl = './import/php_date';
window.axios.get(parseUrl, {params: {format: this.dateFormat}}).then((response) => {
this.parsedDateFormat = response.data.result;
this.loadingParsedDate = false;
}).catch((error) => {
this.parsedDateFormat = ':(';
this.loadingParsedDate = false;
});
},
init() {
console.log('hello');
this.dateRange = document.querySelector('#date-range-helper').dataset.dateRange;
this.detectionMethod = document.querySelector('#detection-method-helper').dataset.method;
this.dateFormat = document.querySelector('#date-format-helper').dataset.dateFormat;
console.log('detection method', this.detectionMethod);
this.getParsedDate();
},
}
}

195
resources/js/v2/src/pages/conversion/index.js

@ -0,0 +1,195 @@
/*
* index.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of the Firefly III Data Importer
* (https://github.com/firefly-iii/data-importer).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import '../../boot/bootstrap.js';
let index = function () {
return {
flow: '',
identifier: '',
nextUrl: '',
pageStatus: {
triedToStart: false,
status: 'init',
},
post: {
result: '',
errored: false,
running: false,
done: false,
},
messages: {
messages: [],
warnings: [],
errors: [],
},
checkCount: 0,
maxCheckCount: 600,
functionName() {
},
showJobMessages() {
return this.messages.messages.length > 0 || this.messages.warnings.length > 0 || this.messages.errors.length > 0;
},
showStartButton() {
return('init' === this.pageStatus.status || 'waiting_to_start' === this.pageStatus.status) && false === this.pageStatus.triedToStart && false === this.post.errored;
},
showWaitingButton() {
return 'waiting_to_start' === this.pageStatus.status && true === this.pageStatus.triedToStart && false === this.post.errored;
},
showTooManyChecks() {
return 'too_long_checks' === this.pageStatus.status;
},
showPostError() {
return 'conv_errored' === this.pageStatus.status || this.post.errored
},
showWhenRunning() {
return 'conv_running' === this.pageStatus.status;
},
showWhenDone() {
return 'conv_done' === this.pageStatus.status;
},
showIfError() {
return 'conv_errored' === this.pageStatus.status;
},
init() {
this.flow = document.querySelector('#data-helper').dataset.flow;
this.identifier = document.querySelector('#data-helper').dataset.identifier;
this.nextUrl = document.querySelector('#data-helper').dataset.url;
console.log('Flow is ' + this.flow);
console.log('Identifier is ' + this.identifier);
this.getJobStatus();
},
startJobButton() {
this.pageStatus.triedToStart = true;
this.pageStatus.status = 'waiting_to_start';
this.postJobStart();
},
postJobStart() {
this.triedToStart = true;
this.post.running = true;
const jobStartUrl = './import/convert/start';
window.axios.post(jobStartUrl, null,{params: {identifier: this.identifier}}).then((response) => {
console.log('POST was OK');
this.getJobStatus();
this.post.running = false;
}).catch((error) => {
console.error('JOB HAS FAILED :(');
this.post.result = error;
this.post.errored = true;
}).finally(() => {
this.getJobStatus();
this.triedToStart = true;
}
);
this.getJobStatus();
this.triedToStart = true;
},
redirectToImport() {
window.location.href = this.nextUrl;
},
getJobStatus() {
this.checkCount++;
if (this.checkCount >= this.maxCheckCount) {
console.log('Block getJobStatus (' + this.checkCount + ')');
this.pageStatus.status = 'too_long_checks';
return;
}
const statusUrl = './import/convert/status';
window.axios.get(statusUrl, {params: {identifier: this.identifier}}).then((response) => {
this.pageStatus.status = response.data.status;
console.log('Status is now ' + response.data.status + ' (' + this.checkCount + ')');
if (this.checkCount >= this.maxCheckCount) {
// error
this.pageStatus.status = 'too_long_checks';
console.log('Status is now ' + this.pageStatus.status + ' (' + this.checkCount + ')');
}
// process messages, warnings and errors:
this.messages.errors = response.data.errors;
this.messages.warnings = response.data.warnings;
this.messages.messages = response.data.messages;
// job has not started yet. Let's wait.
if (false === this.pageStatus.triedToStart && 'waiting_to_start' === this.pageStatus.status) {
this.pageStatus.status = response.data.status;
return;
}
// user pressed start, but it takes a moment.
if (true === this.pageStatus.triedToStart && 'waiting_to_start' === this.pageStatus.status) {
//console.log('Job hasn\'t started yet, but its been tried.');
}
if (true === this.pageStatus.triedToStart && 'conv_errored' === this.pageStatus.status) {
console.error('Job status noticed job failed.');
this.status = response.data.status;
return;
}
if ('conv_running' === this.pageStatus.status) {
console.log('Conversion is running...')
}
if ('conv_done' === this.pageStatus.status) {
console.log('Job is done!');
this.post.done = true;
setTimeout(function () {
console.log('Do redirect!')
this.redirectToImport();
}.bind(this), 4000);
return;
}
if ('conv_errored' === this.pageStatus.status) {
console.error('Job is kill.');
console.error(response.data);
return;
}
}).catch((error) => {
console.error('JOB HAS FAILED :(');
this.post.result = error;
this.post.errored = true;
});
if (this.checkCount < this.maxCheckCount && !this.post.errored && !this.post.done) {
setTimeout(function () {
this.getJobStatus();
}.bind(this), 1000);
}
}
}
}
function loadPage() {
Alpine.data('index', () => index());
Alpine.start();
}
// wait for load until bootstrapped event is received.
document.addEventListener('data-importer-bootstrapped', () => {
console.log('Loaded through event listener.');
loadPage();
});
// or is bootstrapped before event is triggered.
if (window.bootstrapped) {
console.log('Loaded through window variable.');
loadPage();
}

53
resources/js/v2/src/pages/selection/gocardless.js

@ -0,0 +1,53 @@
/*
* gocardless.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of the Firefly III Data Importer
* (https://github.com/firefly-iii/data-importer).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import '../../boot/bootstrap.js';
let gocardless = function () {
return {
selectedCountry: 'XX',
selectedBank: '',
functionName() {
},
init() {
console.log('hello gocardless');
},
}
}
function loadPage() {
Alpine.data('gocardless', () => gocardless());
Alpine.start();
}
// wait for load until bootstrapped event is received.
document.addEventListener('data-importer-bootstrapped', () => {
console.log('Loaded through event listener.');
loadPage();
});
// or is bootstrapped before event is triggered.
if (window.bootstrapped) {
console.log('Loaded through window variable.');
loadPage();
}

187
resources/js/v2/src/pages/submit/index.js

@ -0,0 +1,187 @@
/*
* index.js
* Copyright (c) 2024 james@firefly-iii.org
*
* This file is part of the Firefly III Data Importer
* (https://github.com/firefly-iii/data-importer).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import '../../boot/bootstrap.js';
let index = function () {
return {
identifier: '',
pageStatus: {
triedToStart: false,
status: 'init',
},
post: {
result: '',
errored: false,
running: false,
done: false,
},
messages: {
messages: [],
warnings: [],
errors: [],
},
checkCount: 0,
maxCheckCount: 600,
functionName() {
},
showJobMessages() {
return this.messages.messages.length > 0 || this.messages.warnings.length > 0 || this.messages.errors.length > 0;
},
showStartButton() {
return('init' === this.pageStatus.status || 'waiting_to_start' === this.pageStatus.status) && false === this.pageStatus.triedToStart && false === this.post.errored;
},
showWaitingButton() {
return 'waiting_to_start' === this.pageStatus.status && true === this.pageStatus.triedToStart && false === this.post.errored;
},
showTooManyChecks() {
return 'too_long_checks' === this.pageStatus.status;
},
showPostError() {
return 'submission_errored' === this.pageStatus.status || this.post.errored
},
showWhenRunning() {
return 'submission_running' === this.pageStatus.status;
},
showWhenDone() {
return 'submission_done' === this.pageStatus.status;
},
showIfError() {
return 'submission_errored' === this.pageStatus.status;
},
init() {
this.identifier = document.querySelector('#data-helper').dataset.identifier;
console.log('Identifier is ' + this.identifier);
this.getJobStatus();
},
startJobButton() {
this.pageStatus.triedToStart = true;
this.pageStatus.status = 'waiting_to_start';
this.postJobStart();
},
postJobStart() {
this.triedToStart = true;
this.post.running = true;
const jobStartUrl = './import/submit/start';
window.axios.post(jobStartUrl, null,{params: {identifier: this.identifier}}).then((response) => {
console.log('POST was OK');
this.getJobStatus();
this.post.running = false;
}).catch((error) => {
console.error('JOB HAS FAILED :(');
this.post.result = error;
this.post.errored = true;
}).finally(() => {
this.getJobStatus();
this.triedToStart = true;
}
);
this.getJobStatus();
this.triedToStart = true;
},
getJobStatus() {
this.checkCount++;
if (this.checkCount >= this.maxCheckCount) {
console.log('Block getJobStatus (' + this.checkCount + ')');
this.pageStatus.status = 'too_long_checks';
return;
}
const submitUrl = './import/submit/status';
window.axios.get(submitUrl, {params: {identifier: this.identifier}}).then((response) => {
this.pageStatus.status = response.data.status;
console.log('Status is now ' + response.data.status + ' (' + this.checkCount + ')');
if (this.checkCount >= this.maxCheckCount) {
// error
this.pageStatus.status = 'too_long_checks';
console.log('Status is now ' + this.pageStatus.status + ' (' + this.checkCount + ')');
}
// process messages, warnings and errors:
this.messages.errors = response.data.errors;
this.messages.warnings = response.data.warnings;
this.messages.messages = response.data.messages;
// job has not started yet. Let's wait.
if (false === this.pageStatus.triedToStart && 'waiting_to_start' === this.pageStatus.status) {
this.pageStatus.status = response.data.status;
return;
}
// user pressed start, but it takes a moment.
if (true === this.pageStatus.triedToStart && 'waiting_to_start' === this.pageStatus.status) {
//console.log('Job hasn\'t started yet, but it\'s been tried.');
}
if (true === this.pageStatus.triedToStart && 'submission_errored' === this.pageStatus.status) {
console.error('Job status noticed job failed.');
this.status = response.data.status;
return;
}
if ('submission_running' === this.pageStatus.status) {
console.log('Conversion is running...')
}
if ('submission_done' === this.pageStatus.status) {
console.log('Job is done!');
this.post.done = true;
setTimeout(function () {
console.log('Do redirect!')
this.redirectToImport();
}.bind(this), 4000);
return;
}
if ('submission_errored' === this.pageStatus.status) {
console.error('Job is kill.');
console.error(response.data);
return;
}
}).catch((error) => {
console.error('JOB HAS FAILED :(');
this.post.result = error;
this.post.errored = true;
});
if (this.checkCount < this.maxCheckCount && !this.post.errored && !this.post.done) {
setTimeout(function () {
this.getJobStatus();
}.bind(this), 1000);
}
}
}
}
function loadPage() {
Alpine.data('index', () => index());
Alpine.start();
}
// wait for load until bootstrapped event is received.
document.addEventListener('data-importer-bootstrapped', () => {
console.log('Loaded through event listener.');
loadPage();
});
// or is bootstrapped before event is triggered.
if (window.bootstrapped) {
console.log('Loaded through window variable.');
loadPage();
}

12
resources/js/v2/vite.config.js

@ -49,6 +49,18 @@ export default defineConfig({
// index
'src/pages/index/index.js',
// configuration
'src/pages/configuration/index.js',
// selection
'src/pages/selection/gocardless.js',
// conversion
'src/pages/conversion/index.js',
// submission
'src/pages/submission/index.js',
],
publicDirectory: '../../../public',
refresh: true

2
resources/views/v1/import/004-configure/index.twig

@ -7,7 +7,7 @@
</div>
</div>
{% if 0 == fireflyIIIaccounts and ('nordigen' == flow or 'spectre' == flow ) %}
{% if 0 == fireflyIIIaccounts.assets|length and 0 == fireflyIIIaccounts.liabilities|length and ('nordigen' == flow or 'spectre' == flow ) %}
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">

72
resources/views/v2/components/conversion-messages.blade.php

@ -0,0 +1,72 @@
<div x-show="showJobMessages()">
<div x-show="messages.errors.length > 0">
<strong class="text-danger">Error(s) from the import process</strong>
<ul>
<template x-for="(messageList, index) in messages.errors" :key="index">
<li>
Line #<span x-text="index"></span>:
<template x-if="messageList.length === 1">
<template x-for="message in messageList">
<span x-text="message"></span>
</template>
</template>
<template x-if="messageList.length > 1">
<ol>
<template x-for="message in messageList">
<li x-text="message"></li>
</template>
</ol>
</template>
</li>
</template>
</ul>
</div>
<div x-show="messages.warnings.length > 0">
<strong class="text-warning">Warning(s) from the import process</strong>
<ul>
<template x-for="(messageList, index) in messages.warnings" :key="index">
<li>
Line #<span x-text="index"></span>:
<template x-if="messageList.length === 1">
<template x-for="message in messageList">
<span x-text="message"></span>
</template>
</template>
<template x-if="messageList.length > 1">
<ol>
<template x-for="message in messageList">
<li x-text="message"></li>
</template>
</ol>
</template>
</li>
</template>
</ul>
</div>
<div x-show="messages.messages.length > 0">
<strong class="text-info">Message(s) from the import process</strong>
<ul>
<template x-for="(messageList, index) in messages.messages" :key="index">
<li>
Line #<span x-text="index"></span>:
<template x-if="messageList.length === 1">
<template x-for="message in messageList">
<span x-text="message"></span>
</template>
</template>
<template x-if="messageList.length > 1">
<ol>
<template x-for="message in messageList">
<li x-text="message"></li>
</template>
</ol>
</template>
</li>
</template>
</ul>
</div>
</div>

22
resources/views/v2/components/firefly-iii-account-generic.blade.php

@ -0,0 +1,22 @@
@if('disabled' !== $account['import_account']->status)
<select style="width:100%;"
class="custom-select custom-select-sm form-control"
name="accounts[{{ $account['import_account']->id }}]">
<!-- loop all Firefly III accounts -->
@foreach($account['firefly_iii_accounts'] as $ff3Account)
<option value="{{ $ff3Account->id }}"
{{-- loop configuration --}}
@foreach($configuration->getAccounts() as $key => $preConfig)
{{-- if this account matches, pre-select dropdown. --}}
@if($key === $account['import_account']->id && $preConfig === $ff3Account->id) selected="selected"
@endif
@endforeach
label="{{ $ff3Account->name }} @if($ff3Account->iban) ({{ $ff3Account->iban }}) @endif">
{{ $ff3Account->id }}:
{{ $ff3Account->name }} @if($ff3Account->iban)
({{ $ff3Account->iban }})
@endif
</option>
@endforeach
</select>
@endif

31
resources/views/v2/components/importer-account-title.blade.php

@ -0,0 +1,31 @@
<input
id="do_import_{{ $account['import_account']->id }}"
type="checkbox"
name="do_import[{{ $account['import_account']->id }}]"
value="1"
aria-describedby="accountsHelp"
@if('disabled' === $account['import_account']->status) disabled="disabled" @endif
@if(0 !== ($configuration->getAccounts()[$account['import_account']->id] ?? '')) checked="checked" @endif
/>
<label
class="form-check-label"
for="do_import_{{ $account['import_account']->id }}"
@if('' !== $account['import_account']->iban) title="IBAN: {{ $account['import_account']->iban }}" @endif
>
@if('' !== $account['import_account']->name)
Account "{{ $account['import_account']->name }}"
@else
Account with no name
@endif
</label>
<br>
<small>
@foreach($account['import_account']->extra as $key => $item)
@if('' !== $item)
{{ $key }}: {{ $item }}<br>
@endif
@endforeach
</small>
@if('disabled' === $account['import_account']->status)
<small class="text-danger">(this account is disabled)</small>
@endif

21
resources/views/v2/components/importer-account.blade.php

@ -0,0 +1,21 @@
<tr>
<td style="width:45%">
<x-importer-account-title :account="$account" :configuration="$configuration"/>
</td>
<td style="width:10%">
@if('disabled' !== $account['import_account']->status)
@if(count($account['firefly_iii_accounts']) > 0)
&rarr;
@endif
@endif
</td>
<td style="width:45%">
<!-- TODO this is one of those things to merge into one generic type -->
@if(0 === count($account['firefly_iii_accounts']))
<span class="text-danger">There are no Firefly III accounts to import into</span>
@endif
@if(0 !== count($account['firefly_iii_accounts']))
<x-firefly-iii-account-generic :account="$account" :configuration="$configuration"/>
@endif
</td>
</tr>

861
resources/views/v2/import/004-configure/index.blade.php

@ -1,14 +1,41 @@
@extends('layout.v2')
@section('content')
<div class="container" x-data="index">
<!-- this is a bit of a hack, but it works well enough to sync AlpineJS and the configuration object -->
<span id="date-range-helper" data-date-range="{{$configuration->getDateRange() }}"></span>
<span id="date-format-helper" data-date-format="{{$configuration->getDate() }}"></span>
<span id="detection-method-helper" data-method="{{$configuration->getDuplicateDetectionMethod()}}"></span>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<!-- error -->
@if(!$errors->isEmpty())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Errors :(
</div>
<div class="card-body">
<p class="text-danger">Some error(s) occurred:</p>
<ul>
@foreach($errors->all() as $error)
<li class="text-danger">{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
@endif
<!-- end of error -->
<!-- user has no accounts -->
<!-- TODO validate me -->
@if(0 === count($fireflyIIIaccounts) && ('nordigen' === $flow || 'spectre' === $flow))
@if(0 === count($fireflyIIIaccounts['assets']) && 0 === count($fireflyIIIaccounts['liabilities']) && ('nordigen' === $flow || 'spectre' === $flow))
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
@ -28,13 +55,17 @@
<ul>
@foreach($importerAccounts as $info)
<li>
Name: {{ $info['import_account']['name'] }}<br>
(Internal) identifier: {{ $info['import_account']['identifier'] }}<br>
Resource identifier: {{ $info['import_account']['resourceId'] }}<br>
BBAN: {{ $info['import_account']['bban'] }}<br>
BIC: {{ $info['import_account']['bic'] }}<br>
IBAN: {{ $info['import_account']['iban'] }}<br>
Owner name: {{ $info['import_account']['ownerName'] }}<br>
Name: <strong>{{ $info['import_account']->name ?? '' }}</strong>
<ul>
<li>(Internal)
identifier: {{ $info['import_account']->identifier ?? '' }}</li>
<li>Resource
identifier: {{ $info['import_account']->resourceId ?? '' }}</li>
<li>BBAN: {{ $info['import_account']->bban ?? '' }}</li>
<li>BIC: {{ $info['import_account']->bic ?? ''}}</li>
<li>IBAN: {{ $info['import_account']->iban ?? '' }}</li>
<li>Owner name: {{ $info['import_account']->ownerName ?? '' }}</li>
</ul>
</li>
@endforeach
</ul>
@ -44,8 +75,820 @@
</div>
</div>
@endif
<!-- user has accounts! -->
@if(count($fireflyIIIaccounts['assets']) > 0 || count($fireflyIIIaccounts['liabilities']) > 0)
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
{{ $subTitle }}
</div>
<div class="card-body">
@if('file' === $flow)
<p>
@if('camt' === $configuration->getContentType())
Even though camt.053 is a defined standard, you might want to customize. Some of
the most important settings are below.
They apply to all records in the uploaded files. If you would like some support,
you won't find anything at <a
href="https://docs.firefly-iii.org/how-to/data-importer/import/csv/"
target="_blank">
this page.</a> right now.
@endif
@if('csv' === $configuration->getContentType())
Importable files come in many shapes and forms. Some of the most important
settings are below.
They apply to all lines in the file. If you would like some support, <a
href="https://docs.firefly-iii.org/how-to/data-importer/import/csv/"
target="_blank">
check out the documentation for this page.</a>
@endif
</p>
@endif
@if('nordigen' === $flow || 'spectre' === $flow)
<p>
Your
@if('nordigen' === $flow)
GoCardless
@endif
@if('spectre' === $flow)
Spectre
@endif
import can be configured and fine-tuned.
<a href="https://docs.firefly-iii.org/how-to/data-importer/import/gocardless/"
target="_blank">Check
out the documentation for this page.</a>
</p>
@endif
</div>
</div>
</div>
</div>
<!-- start of form -->
<form method="post" action="{{ route('004-configure.post') }}" accept-charset="UTF-8" id="store">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="flow" value="{{ $flow }}"/>
<input type="hidden" name="content_type" value="{{ $configuration->getContentType() }}"/>
<!-- these values are used by Spectre + Nordigen and must be preserved -->
<input type="hidden" name="identifier" value="{{ $configuration->getIdentifier() }}"/>
<input type="hidden" name="connection" value="{{ $configuration->getConnection() }}"/>
<input type="hidden" name="nordigen_country" value="{{ $configuration->getNordigenCountry() }}"/>
<input type="hidden" name="nordigen_max_days" value="{{ $configuration->getNordigenMaxDays() }}"/>
<input type="hidden" name="nordigen_bank" value="{{ $configuration->getNordigenBank() }}"/>
<input type="hidden" name="nordigen_requisitions"
value="{{ json_encode($configuration->getNordigenRequisitions()) }}"/>
@if('nordigen' === $flow || 'spectre' === $flow)
<input type="hidden" name="ignore_duplicate_transactions" value="1"/>
@endif
<!-- Account selection for Gocardless and Spectre -->
<!-- also date range settings -->
@if('nordigen' === $flow || 'spectre' === $flow)
<!-- start of account selection -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Account selection for
@if('nordigen' === $flow)
GoCardless
@endif
@if('spectre' === $flow)
Spectre
@endif
import
</div>
<div class="card-body">
<div class="form-group row mb-3">
<div class="col-sm-3">Accounts to be matched</div>
<div class="col-sm-9">
@php
$errorAccounts = 0;
$warningAccounts = 0;
@endphp
<table class="table table-sm table-bordered table-striped">
<thead>
<tr>
<th>
@if('nordigen' === $flow)
GoCardless
@endif
@if('spectre' === $flow)
Spectre
@endif
account
</th>
<th>&nbsp;</th>
<th>Firefly III account</th>
</tr>
</thead>
<tbody>
@foreach($importerAccounts as $information)
<!-- update variables -->
<!-- account is disabled at provider -->
@if('disabled' === $information['import_account']->status)
@php
$errorAccounts++;
@endphp
@endif
<!-- have no info about account at provider -->
@if('no-info' === $information['import_account']->status)
@php
$warningAccounts++;
@endphp
@endif
<!-- have nothing about account at provider -->
@if('nothing' === $information['import_account']->status)
@php
$warningAccounts++;
@endphp
@endif
<!-- have no balance about account at provider -->
@if('nothing' === $information['import_account']->status)
@php
$warningAccounts++;
@endphp
@endif
<!-- end of update variables -->
<x-importer-account :account="$information"
:configuration="$configuration"/>
@endforeach
</tbody>
<caption>Select and match the
@if('nordigen' === $flow)
GoCardless
@endif
@if('spectre' === $flow)
Spectre
@endif
accounts you want to import into your Firefly III installation.
</caption>
</table>
@if($errorAccounts > 0)
<div class="alert alert-danger" role="alert">
<em class="fas fa-exclamation-triangle"></em>
The importer could not download information on some of your
accounts. This does not have to be an issue, but it may lead to
import errors.
</div>
@endif
@if($warningAccounts > 0)
<div class="alert alert-warning" role="alert">
<em class="fas fa-exclamation-triangle"></em>
The importer could not download information on some of your
accounts. This does not have to be an issue, but it may lead to
import errors.
</div>
@endif
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of account selection -->
<!-- start of date options -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Date range import options
</div>
<div class="card-body">
<div class="form-group row mb-3">
<label for="default_account" class="col-sm-3 col-form-label">Date range</label>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input date-range-radio" id="date_range_all"
type="radio" name="date_range" value="all" x-model="dateRange"
@if('all' === $configuration->getDateRange()) checked @endif
aria-describedby="rangeHelp"/>
<label class="form-check-label" for="date_range_all">Import
everything</label>
</div>
<div class="form-check">
<input class="form-check-input date-range-radio" id="date_range_partial"
type="radio" name="date_range" x-model="dateRange"
value="partial"
@if('partial' === $configuration->getDateRange()) checked @endif
aria-describedby="rangeHelp"/>
<label class="form-check-label" for="date_range_partial">Go back some
time</label>
</div>
<div class="form-check">
<input class="form-check-input date-range-radio" id="date_range_range"
type="radio" name="date_range" value="range" x-model="dateRange"
@if('range' === $configuration->getDateRange()) checked @endif
aria-describedby="rangeHelp"/>
<label class="form-check-label" for="date_range_range">Import a specific
range</label>
<small id="rangeHelp" class="form-text text-muted">
<br>What range to grab from your bank through
@if('nordigen' === $flow)
GoCardless?
@endif
@if('spectre' === $flow)
Spectre?
@endif
</small>
</div>
</div>
</div>
<div class="form-group row mb-3" id="date_range_partial_settings" x-show="'partial' === dateRange">
<div class="col-sm-3">
Date range settings
</div>
<div class="col-sm-3">
<input
name="date_range_number"
id="date_range_number"
class="form-control" value="{{ $configuration->getDateRangeNumber() }}"
type="number" step="1" min="1" max="365">
</div>
<div class="col-sm-6">
<select class="form-control"
name="date_range_unit"
id="date_range_unit">
<option
@if('d' === $configuration->getDateRangeUnit()) selected @endif
value="d" label="days">days
</option>
<option
@if('w' === $configuration->getDateRangeUnit()) selected @endif
value="w" label="weeks">weeks
</option>
<option
@if('m' === $configuration->getDateRangeUnit()) selected @endif
value="m" label="months">months
</option>
<option
@if('y' === $configuration->getDateRangeUnit()) selected @endif
value="y" label="years">years
</option>
</select>
</div>
</div>
<div class="form-group row mb-3" id="date_range_range_settings" x-show="'range' === dateRange">
<div class="col-sm-3">
Date range settings (from, to)
</div>
<div class="col-sm-4">
<input type="date" name="date_not_before" class="form-control"
value="{{ $configuration->getDateNotBefore() }}">
</div>
<div class="col-sm-4">
<input type="date" name="date_not_after" class="form-control"
value="{{ $configuration->getDateNotAfter() }}">
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of date range options -->
@endif
<!-- end of account selection and date range settings -->
<!-- spectre specific options -->
@if('spectre' === $flow)
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Spectre import options
</div>
<div class="card-body">
<div class="form-group row">
<label for="X" class="col-sm-3 col-form-label">Ignore Spectre categories</label>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isIgnoreSpectreCategories()) checked @endif
type="checkbox" value="1" id="ignore_spectre_categories"
name="ignore_spectre_categories"
aria-describedby="duplicateSpectre">
<label class="form-check-label" for="ignore_spectre_categories">
Ignore Spectre's categories.
</label>
</div>
<small class="form-text text-muted" id="duplicateSpectre">
Spectre adds categories to each transaction. You can choose to ignore
them.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- end of spectre options -->
<!-- camt.053 options -->
@if('file' === $flow && 'camt' === $configuration->getContentType())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
CAMT.053 import options
</div>
<div class="card-body">
<div class="form-group row mb-3">
<div class="col-sm-3">How to handle "Level-D" data</div>
<div class="col-sm-9">
<select id="grouped_transaction_handling"
name="grouped_transaction_handling"
class="form-control"
aria-describedby="grouped_transaction_handling_help">
<option
label="Create multiple single transactions, for each Level-D record"
@if('single' === $configuration->getGroupedTransactionHandling()) selected
@endif
value="single">Create multiple single transactions, for each Level-D
record
</option>
<option disabled
label="Create one split transaction with splits for each record"
@if('split' === $configuration->getGroupedTransactionHandling()) selected
@endif
value="split">Create one split transaction with splits for each
record
</option>
<option
label='Drop "level-D" data, sum and merge all details in a single transaction'
@if('group' === $configuration->getGroupedTransactionHandling()) selected
@endif
value="group">Drop "level-D" data, sum and merge all details in a
single transaction
</option>
</select>
<small class="form-text text-muted">
It's not recommended to drop the "level-D" data, you may lose details.
</small>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-sm-3">Use the entire address of the opposing part?</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isUseEntireOpposingAddress()) checked @endif
type="checkbox" id="use_entire_opposing_address"
name="use_entire_opposing_address" value="1"
aria-describedby="useEntireOpposingAddressHelp">
<label class="form-check-label" for="use_entire_opposing_address">
Yes
</label>
<small id="use_entire_opposing_address_help"
class="form-text text-muted">
<br>
The default is to only use the name, and only use the address
details when no name is available.
If you select this option, both name and address will always be used
(when available).
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- end of camt.053 options -->
<!-- start of CSV options -->
@if('file' === $flow && 'csv' === $configuration->getContentType())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
CSV file import options
</div>
<div class="card-body">
<div class="form-group row mb-3">
<div class="col-sm-3">Headers</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id="headers" name="headers" value="1"
aria-describedby="headersHelp"
@if($configuration->isHeaders()) checked @endif
>
<label class="form-check-label" for="headers">
Yes
</label><br>
<small id="headersHelp" class="form-text text-muted">
Select this checkbox when your importable file is a CSV-like file
and has headers on the first line of the
file.
</small>
</div>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-sm-3">Convert to UTF-8</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
@if($configuration->isConversion()) checked @endif
id="conversion" name="conversion" value="1"
aria-describedby="conversionHelp">
<label class="form-check-label" for="conversion">
Yes
</label><br>
<small id="conversionHelp" class="form-text text-muted">
Try to convert your file to UTF-8. This may lead to weird
characters.
</small>
</div>
</div>
</div>
<div class="form-group row mb-3">
<label for="delimiter" class="col-sm-3 col-form-label">CSV-file
delimiter</label>
<div class="col-sm-9">
<select id="delimiter" name="delimiter" class="form-control"
aria-describedby="delimiterHelp">
<option value="comma"
@if('comma' === $configuration->getDelimiter()) selected @endif
label="A comma (,)">A comma (,)
</option>
<option value="semicolon"
@if('semicolon' === $configuration->getDelimiter()) selected
@endif
label="A semicolon (;)">A semicolon (;)
</option>
<option value="tab"
@if('tab' === $configuration->getDelimiter()) selected @endif
label="A tab (invisible)">A tab (invisible)
</option>
</select>
<small id="delimiterHelp" class="form-text text-muted">
If your file is a CSV file, select the field separator of the file. This
is almost always a comma.
</small>
</div>
</div>
<div class="form-group row mb-3">
<label for="date" class="col-sm-3 col-form-label">Date format</label>
<div class="col-sm-9">
<input type="text" name="date" class="form-control"
placeholder="Date format" x-model="dateFormat"
value="{{ $configuration->getDate() ?? 'Y-m-d' }}"
@change="getParsedDate"
aria-describedby="dateHelp">
<small id="dateHelp" class="form-text text-muted">
1. Read more about the date format <a
href="https://www.php.net/manual/en/datetime.format.php">on this
page</a><br>
2. Make sure this example date's format matches your file:
<strong x-show="!loadingParsedDate" x-text="parsedDateFormat">1984-09-17</strong>
<em x-show="loadingParsedDate" class="fas fa-cog fa-spin"></em>
<br>
3. If your file contains something like "5 mei 2023", prefix with your
country code like so <code>nl:d F Y</code>
</small>
</div>
</div>
</div>
</div>
</div>
</div>
@endif
<!-- end of CSV options -->
<!-- generic import options -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Various import options
</div>
<div class="card-body">
<div class="form-group row mb-3">
<label for="default_account" class="col-sm-3 col-form-label">Default import
account</label>
<div class="col-sm-9">
<select id="default_account" name="default_account" class="form-control"
aria-describedby="defaultAccountHelp">
@foreach($fireflyIIIaccounts as $accountGroup => $accountList)
<optgroup label="{{ $accountGroup }}">
{% for account in accountList %}
@foreach($accountList as $account)
<option
@if($configuration->getDefaultAccount() === $account->id) selected @endif
value="{{ $account->id }}"
label="{{ $account->name }}">{{ $account->name }}</option>
@endforeach
</optgroup>
@endforeach
</select>
<small id="defaultAccountHelp" class="form-text text-muted">
Select the asset account you want to link transactions to, if your import
doesn't have enough meta data to determine this.
</small>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-sm-3">Rules</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isRules()) checked @endif
type="checkbox" id="rules" name="rules" value="1"
aria-describedby="rulesHelp">
<label class="form-check-label" for="rules">
Yes
</label>
<small id="rulesHelp" class="form-text text-muted">
<br>Select if you want Firefly III to apply your rules to the import.
</small>
</div>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-sm-3">Import tag</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isAddImportTag()) checked @endif
type="checkbox" id="add_import_tag" name="add_import_tag" value="1"
aria-describedby="add_import_tagHelp">
<label class="form-check-label" for="add_import_tag">
Yes
</label>
<small id="add_import_tagHelp" class="form-text text-muted">
<br>When selected Firefly III will add a tag to each imported transaction
denoting the import; this groups your import under a tag.
</small>
</div>
</div>
</div>
<!-- both camt and csv support custom tag -->
<div class="form-group row mb-3">
<label for="date" class="col-sm-3 col-form-label">Custom import tag</label>
<div class="col-sm-9">
<input type="text" name="custom_tag" class="form-control" id="custom_tag"
placeholder="Custom import tag"
value="{{ $configuration->getCustomTag() }}"
aria-describedby="custom_tagHelp">
<small id="custom_tagHelp" class="form-text text-muted">
You can set your own import tag to easily distinguish imports
or just because you don't like the default one.
<a href="https://docs.firefly-iii.org/how-to/data-importer/advanced/custom-import-tag/" target="_blank">Read more in the documentation</a>.
</small>
</div>
</div>
<div class="form-group row mb-3">
<div class="col-sm-3">Map data</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isMapAllData()) checked @endif
type="checkbox"
id="map_all_data"
name="map_all_data" value="1" aria-describedby="mapAllDataHelp">
<label class="form-check-label" for="map_all_data">
Yes
</label>
<small id="mapAllDataHelp" class="form-text text-muted">
<br>You get the opportunity to link your data to existing Firefly III
data, for a cleaner import.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of generic import options -->
<!-- duplicate detection options -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Duplicate transaction detection
</div>
<div class="card-body">
<div class="col-sm-9 offset-sm-3">
<p class="text-muted">
Firefly III can automatically detect duplicate transactions. This is pretty
foolproof. In some special cases however,
you want more control over this process. Read more about the options below in <a
href="https://docs.firefly-iii.org/how-to/data-importer/import/csv/"
target="_blank">the documentation</a>.
</p>
</div>
@if('file' === $flow)
<div class="form-group row mb-3">
<label for="X" class="col-sm-3 col-form-label">General detection options</label>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isIgnoreDuplicateLines()) checked @endif
type="checkbox" value="1" id="ignore_duplicate_lines"
name="ignore_duplicate_lines" aria-describedby="duplicateHelp">
<label class="form-check-label" for="ignore_duplicate_lines">
Do not import duplicate lines or entries in the importable file.
</label>
<br>
<small class="form-text text-muted" id="duplicateHelp">
Whatever method you choose ahead, it's smart to make the importer ignore
any
duplicated lines or entries in your importable file.
</small>
</div>
</div>
</div>
@endif
<div class="form-group row mb-3">
<label for="duplicate_detection_method" class="col-sm-3 col-form-label">Detection
method</label>
<div class="col-sm-9">
<select id="duplicate_detection_method" name="duplicate_detection_method" x-model="detectionMethod"
class="form-control" aria-describedby="duplicate_detection_method_help">
<option label="No duplicate detection"
@if('none' === $configuration->getDuplicateDetectionMethod()) selected @endif
value="none">No duplicate detection
</option>
<option label="Content-based"
@if('classic' === $configuration->getDuplicateDetectionMethod()) selected @endif
value="classic">Content-based detection
</option>
<option label="Identifier-based"
@if('cell' === $configuration->getDuplicateDetectionMethod()) selected @endif
value="cell">Identifier-based detection
</option>
</select>
<small id="duplicate_detection_method_help" class="form-text text-muted">
For more details on these detection method see <a
href="https://docs.firefly-iii.org/references/data-importer/duplicate-detection/"
target="_blank">the documentation</a>. If you're not sure, select
"content-based" detection.
</small>
</div>
</div>
@if('file' === $flow)
<div class="form-group row mb-3" id="unique_column_index_holder" x-show="'cell' === detectionMethod">
<label for="unique_column_index" class="col-sm-3 col-form-label">Unique column
index</label>
<div class="col-sm-9">
<input type="number" step="1" name="unique_column_index" class="form-control"
id="unique_column_index" placeholder="Column index"
value="{{ $configuration->getUniqueColumnIndex() }}"
aria-describedby="unique_column_index_help">
<small id="unique_column_index_help" class="form-text text-muted">
This field is only relevant for the "identifier-based" detection option.
Indicate which column / field contains the unique identifier. Start counting from
zero!
</small>
</div>
</div>
@endif
<div class="form-group row" id="unique_column_type_holder" x-show="'cell' === detectionMethod">
<label for="unique_column_type" class="col-sm-3 col-form-label">Unique column
type</label>
<div class="col-sm-9">
<select id="unique_column_type" name="unique_column_type" class="form-control"
aria-describedby="unique_column_type_help">
@foreach($uniqueColumns as $columnType => $columnName)
<option label="{{ $columnName }}"
@if($configuration->getUniqueColumnType() === $columnType) selected @endif
value="{{ $columnType }}">{{ $columnName }}</option>
@endforeach
</select>
<small id="unique_column_type_help" class="form-text text-muted">
This field is only relevant for the "identifier-based" detection option.
Select
the type of value you expect in
the unique identifier. What must Firefly III search for?
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of duplicate detection options -->
<!-- other options -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Other options
</div>
<div class="card-body">
<h4>Other options</h4>
<div class="form-group row mb-3">
<div class="col-sm-3">Skip form</div>
<div class="col-sm-9">
<div class="form-check">
<input class="form-check-input"
@if($configuration->isSkipForm()) checked @endif
type="checkbox"
id="skip_form" name="skip_form" value="1" aria-describedby="skipHelp">
<label class="form-check-label" for="skip_form">
Yes
</label>
<small id="skipHelp" class="form-text text-muted">
<br>Skip the options the next time you import and go straight to processing.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of other options -->
<!-- start of submit button -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Submit!
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="float-end btn btn-primary">Submit &rarr;</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- end of submit button -->
<!-- next form steps here -->
<!--
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Box title
</div>
<div class="card-body">
BOX CONTENT
</div>
</div>
</div>
</div>
-->
<!-- end of form -->
</form>
@endif
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ route('back.upload') }}" class="btn btn-secondary"><span
class="fas fa-arrow-left"></span> Go back to upload</a>
<a href="{{ route('flush') }}" class="btn text-white btn-danger btn-sm"><span
class="fas fa-redo-alt"></span> Start over</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['src/pages/configuration/index.js'])

140
resources/views/v2/import/005-roles/index-csv.blade.php

@ -0,0 +1,140 @@
@extends('layout.v2')
@section('content')
<div class="container">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<div class="row">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
{{ $subTitle }}
</div>
<div class="card-body">
<p>
Set up the meaning of each column in your file.
</p>
<p>
Each column in your importable file has a role, it contains a specific type of content.
By configuring these roles here, you tell the importer how to approach and treat
the data in each column. <a target="_blank"
href="https://docs.firefly-iii.org/references/data-importer/roles/">Read
the documentation</a> to learn more
about this process.
</p>
</div>
</div>
</div>
</div>
@if(!$errors->isEmpty())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Errors :(
</div>
<div class="card-body">
<p class="text-danger">Some error(s) occurred:</p>
<ul>
@foreach($errors->all() as $error)
<li class="text-danger">{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
@endif
<div class="row mt-3">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
Role configuration
</div>
<div class="card-body">
<form method="post" action="{{ route('005-roles.post') }}" accept-charset="UTF-8">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<table class="table">
<tr>
<th>Column</th>
<th>Example data</th>
<th>Role</th>
<th>Map data?</th>
</tr>
@foreach($columns as $index => $column)
<tr>
<td>{{ $column }}
@if($index === $configuration->getUniqueColumnIndex() && 'cell' === $configuration->getDuplicateDetectionMethod())
<br/>
<span class="text-muted small">This is the unique column</span>
@endif
</td>
<td>
@if(count($examples[$index] ?? []) > 0)
@foreach($examples[$index] as $example)
<pre style="color:#e83e8c;margin-bottom:0;">{{ $example }}</pre>
@endforeach
@endif
</td>
<td>
@if($index === $configuration->getUniqueColumnIndex() && 'cell' === $configuration->getDuplicateDetectionMethod())
<p class="form-text">
<span class="text-muted small">
This column is your unique identifier, so it will be fixed to
</span>
<code class="small">{{ $configuration->getUniqueColumnType() }}</code>
</p>
<input type="hidden" name="roles[{{ $index }}]"
value="{{ $configuration->getUniqueColumnType() }}"/>
@else
<select name="roles[{{ $index }}]" id="roles_{{ $index }}"
class="form-control">
@foreach($roles as $key => $role)
<option value="{{ $key }}"
@if(($configuredRoles[$index] ?? false) === $key) selected @endif
label="{{ __('import.column_' . $key) }}">{{ __('import.column_'. $key) }}</option>
@endforeach
</select>
@endif
</td>
<td>
<label for="do_mapping_{{ $index }}">
{{-- reverse if statement is pretty sloppy but OK. --}}
@if($index === $configuration->getUniqueColumnIndex() && 'cell' === $configuration->getDuplicateDetectionMethod())
&nbsp;
@else
<input type="checkbox"
@if($configuredDoMapping[$index] ?? false) checked @endif
name="do_mapping[{{ $index }}]" id="do_mapping_{{ $index }}"
value="1"/>
@endif
</label>
</td>
</tr>
@endforeach
</table>
<button type="submit" class="float-end btn btn-primary">Submit &rarr;</button>
</form>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ route('back.config') }}" class="btn btn-secondary"><span
class="fas fa-arrow-left"></span> Go back to configuration</a>
<a href="{{ route('flush') }}" class="btn btn-danger text-white btn-sm"><span
class="fas fa-redo-alt"></span> Start over</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

129
resources/views/v2/import/006-mapping/index.blade.php

@ -0,0 +1,129 @@
@extends('layout.v2')
@section('content')
<div class="container">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
{{ $subTitle }}
</div>
<div class="card-body">
<p>Map data in your import to your Firefly III instance.</p>
<p>
Entries in your import may already exist in another form in your own Firefly III
instance. Be sure to <a target="_blank"
href="https://docs.firefly-iii.org/how-to/data-importer/import/map-data/">
check out the documentation</a>, because this is where
the magic happens.
</p>
<p class="text-info">
Account names with "lots&nbsp;&nbsp;&nbsp;&nbsp;of&nbsp;&nbsp;spaces" may seemingly lose
those spaces. Fear not, those will be perfectly preserved.
</p>
</div>
</div>
</div>
</div>
@if(!$errors->isEmpty())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Errors :(
</div>
<div class="card-body">
<ul>
@foreach($errors->all() as $error)
<li class="text-danger">{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
@endif
<div class="row mt-3">
<div class="col-lg-12">
<div class="card">
<div class="card-header">
Form
</div>
<div class="card-body">
<form method="post" action="{{ route('006-mapping.post') }}" accept-charset="UTF-8">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
@foreach($data as $index => $row)
<h3>{{ $index }}: {{ __('import.column_'.$row['role']) }}</h3>
<table class="table">
<tr>
<th style="width:50%;">Field value</th>
<th style="width:50%;">Mapped to</th>
</tr>
@foreach($row['values'] as $valueIndex => $value)
<tr>
<td>
<pre style="color:#e83e8c;">{{ $value }}</pre>
<input type="hidden" name="values[{{ $index }}][{{ $loop->index }}]"
value="{{ $value }}"/>
</td>
<td>
<select name="mapping[{{ $index }}][{{ $loop->index }}]"
class="form-control">
<option value="0" label="(do not map / automap)">(do not map /
automap)
</option>
@foreach($row['mapping_data'] as $key => $maps)
<!-- if is array go one level deeper -->
@if(is_iterable($maps))
<optgroup label="{{ $key }}">
@foreach($maps as $singleId => $singleEntry)
<option
@if($singleId === ($row['mapped'][$value] ?? false)) selected @endif
label="{{ $singleEntry }}"
value="{{ $singleId }}">
{{ $singleEntry }}
</option>
@endforeach
</optgroup>
@else
<option
@if($key === ($row['mapped'][$value] ?? false)) selected @endif
label="{{ $maps }}" value="{{ $key }}">
{{ $maps }}
</option>
@endif
@endforeach
</select>
</td>
</tr>
@endforeach
</table>
@endforeach
<button type="submit" class="btn btn-primary float-end">Submit &rarr;</button>
</form>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ route('back.roles') }}" class="btn btn-secondary"><span
class="fas fa-arrow-left"></span> Go back to role selection</a>
<a href="{{ route('flush') }}" class="btn btn-danger text-white btn-sm"><span
class="fas fa-redo-alt"></span> Start over</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

109
resources/views/v2/import/007-convert/index.blade.php

@ -0,0 +1,109 @@
@extends('layout.v2')
@section('content')
<!-- another tiny hack to get data from a to b -->
<span id="data-helper" data-flow="{{ $flow }}" data-identifier="{{ $identifier }}" data-url="{{ $nextUrl }}"></span>
<div class="container" x-data="index">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<div id="app">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Data conversion
</div>
<!-- show start of process button -->
<div x-show="showStartButton()" class="card-body">
<p>
The first step in the import process is a <strong>conversion</strong>.
<span x-show="'file' === flow">The CSV file you uploaded</span>
<span x-show="'nordigen' === flow">The transactions downloaded from GoCardless</span>
<span x-show="'spectre' === flow">The transactions downloaded from Spectre</span>
will be converted to Firefly III compatible transactions. Please press <strong>Start
job</strong> to start.
</p>
<p>
<button class="btn btn-success float-end text-white" type="button" @click="startJobButton">Start job
&rarr;
</button>
</p>
</div>
<div x-show="showWaitingButton()" class="card-body">
<p><span class="fas fa-cog fa-spin"></span> Please wait for the job to start..</p>
</div>
<div x-show="showTooManyChecks()" class="card-body">
<p>
<em class="fa-solid fa-face-dizzy"></em>
The data importer has been polling for more than <span x-text="checkCount"></span> seconds. It has stopped, to prevent eternal loops.</p>
</div>
<div x-show="showPostError()" class="card-body">
<p class="text-danger">
The conversion could not be started, or failed due to an error. Please check the log files.
Sorry about this :(
</p>
<p x-show="'' !== post.result" x-text="post.result"></p>
<x-conversion-messages />
</div>
<div x-show="showWhenRunning()" class="card-body">
<p>
<span class="fas fa-cog fa-spin"></span> The conversion is running, please wait. Messages may appear below the progress bar.
</p>
<div class="progress">
<div aria-valuemax="100" aria-valuemin="0"
aria-valuenow="100" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 100%"></div>
</div>
<x-conversion-messages />
</div>
<div x-show="showWhenDone()" class="card-body">
<p>
<span class="fas fa-sync fa-spin"></span> The conversion routine has finished 🎉. Please wait to be redirected!
</p>
<x-conversion-messages />
</div>
<div x-show="showIfError()" class="card-body">
<p class="text-danger">
The conversion could not be started, or failed due to an error. Please check the log files.
Sorry about this :(
</p>
<x-conversion-messages />
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ $jobBackUrl }}" class="btn btn-secondary"><span class="fas fa-arrow-left"></span>
Go back to the previous step</a>
<a class="btn btn-danger text-white btn-sm" href="{{ route('flush') }}" data-bs-toggle="tooltip"
data-bs-placement="top" title="If the conversion seems stuck, you can reset it."><span
class="fas fa-redo-alt"></span> Start over</a>
<a class="btn btn-info text-white btn-sm" href="{{ route('004-configure.download') }}"
data-bs-toggle="tooltip" data-bs-placement="top"
title="You can download a configuration file of your import, so you can make a quick start the next time you import.">
<span class="fas fa-download"></span> Download configuration file
</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['src/pages/conversion/index.js'])
@endsection

104
resources/views/v2/import/008-submit/index.blade.php

@ -0,0 +1,104 @@
@extends('layout.v2')
@section('content')
<!-- another tiny hack to get data from a to b -->
<span id="data-helper" data-identifier="{{ $identifier }}"></span>
<div class="container" x-data="index">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<div id="app">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Data submission to Firefly III
</div>
<!-- show start of process button -->
<div x-show="showStartButton()" class="card-body">
<p>
Your converted content will be submitted to your Firefly III installation. Press <strong>Start job</strong> to start.
</p>
<p>
<button class="btn btn-success float-end text-white" type="button" @click="startJobButton">Start job
&rarr;
</button>
</p>
</div>
<div x-show="showWaitingButton()" class="card-body">
<p><span class="fas fa-cog fa-spin"></span> Please wait for the job to start..</p>
</div>
<div x-show="showTooManyChecks()" class="card-body">
<p>
<em class="fa-solid fa-face-dizzy"></em>
The data importer has been polling for more than <span x-text="checkCount"></span> seconds. It has stopped, to prevent eternal loops.</p>
</div>
<div x-show="showPostError()" class="card-body">
<p class="text-danger">
The conversion could not be started, or failed due to an error. Please check the log files.
Sorry about this :(
</p>
<p x-show="'' !== post.result" x-text="post.result"></p>
<x-conversion-messages />
</div>
<div x-show="showWhenRunning()" class="card-body">
<p>
<span class="fas fa-cog fa-spin"></span> The conversion is running, please wait. Messages may appear below the progress bar.
</p>
<div class="progress">
<div aria-valuemax="100" aria-valuemin="0"
aria-valuenow="100" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 100%"></div>
</div>
<x-conversion-messages />
</div>
<div x-show="showWhenDone()" class="card-body">
<p>
The submission routine has finished 🎉. Errors and messages can be seen below.
</p>
<x-conversion-messages />
</div>
<div x-show="showIfError()" class="card-body">
<p class="text-danger">
The conversion could not be started, or failed due to an error. Please check the log files.
Sorry about this :(
</p>
<x-conversion-messages />
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ $jobBackUrl }}" class="btn btn-secondary"><span class="fas fa-arrow-left"></span>
Go back to the previous step</a>
<a class="btn btn-danger text-white btn-sm" href="{{ route('flush') }}" data-bs-toggle="tooltip"
data-bs-placement="top" title="If the conversion seems stuck, you can reset it."><span
class="fas fa-redo-alt"></span> Start over</a>
<a class="btn btn-info text-white btn-sm" href="{{ route('004-configure.download') }}"
data-bs-toggle="tooltip" data-bs-placement="top"
title="You can download a configuration file of your import, so you can make a quick start the next time you import.">
<span class="fas fa-download"></span> Download configuration file
</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('scripts')
@vite(['src/pages/submit/index.js'])
@endsection

211
resources/views/v2/import/009-selection/index.blade.php

@ -0,0 +1,211 @@
@extends('layout.v2')
@section('content')
<div class="container" x-data="gocardless">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
{{ $subTitle }}
</div>
<div class="card-body">
<p>
Select your country and the bank you would like to import from.
If you would like some support, <a href="https://docs.firefly-iii.org/how-to/data-importer/import/gocardless/"
target="_blank">check out the documentation for this
page.</a>
</p>
</div>
</div>
</div>
</div>
@if(!$errors->isEmpty())
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Errors
</div>
<div class="card-body">
<p class="text-danger">Some error(s) occurred:</p>
<ul>
@foreach($errors->all() as $error)
<li class="text-danger">{{ $error }}</li>
@endforeach
</ul>
</div>
</div>
</div>
</div>
@endif
<form method="post" action="{{ route('009-selection.post') }}" accept-charset="UTF-8">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Select country
</div>
<div class="card-body">
<div class="form-group row">
<label for="country" class="col-sm-3 col-form-label">Country</label>
<div class="col-sm-9">
<select class="form-control"
name="country"
x-model="selectedCountry">
<option label="(no selection)" value="XX">(no selection)</option>
@foreach($response as $country)
<option label="{{ $countries[$country->code] ?? 'Unknown' }}"
@if($country->code == $configuration->getNordigenCountry())selected="selected"@endif
value="{{ $country->code }}">{{ $countries[$country->code] ?? 'Unknown' }}</option>
@endforeach
</select>
<small class="form-text text-muted">
Which country is your bank in?
</small>
</div>
</div>
</div>
</div>
</div>
</div>
@foreach($response as $country)
<div class="row mt-3 bank-box" x-show="'{{ $country->code }}' === selectedCountry && 'XX' !== selectedCountry">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Select your bank in {{ $country->code }}
</div>
<div class="card-body">
<div class="form-group row">
<label for="bank_{{ $country->code }}" class="col-sm-3 col-form-label">Bank
({{ $country->code }})</label>
<div class="col-sm-9">
<select class="form-control bank-selector" name="bank_{{ $country->code }}"
x-model="selectedBank">
<option label="(no bank)" value="XX" data-days="0">(no bank)</option>
@foreach($country->banks as $bank)
<option label="{{ $bank->name }}"
@if($bank->id === $configuration->getNordigenBank())selected="selected"@endif
data-days="{{ $bank->transactionTotalDays }}"
value="{{ $bank->id }}">{{ $bank->name }}</option>
@endforeach
</select>
</div>
</div>
<p class="mt-3 text-info bank-selected" style="display: none;">Imports from this bank
go no further back than <strong class="days">XX</strong> days.
</p>
</div>
</div>
</div>
</div>
@endforeach
<div class="row mt-3 bank-box" x-show="'XX' === selectedCountry">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Select a bank
</div>
<div class="card-body">
<small class="form-text text-muted">
(Please select a country first)
</small>
</div>
</div>
</div>
</div>
<div class="row mt-3 bank-selected" x-show="'XX' !== selectedCountry && '' !== selectedBank">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Number of days the connection is valid
</div>
<div class="card-body">
<div class="form-group row">
<label for="days" class="col-sm-3 col-form-label">Number of days the connection is valid</label>
<div class="col-sm-9">
<input name="days" class="form-control" step="1" min="1" type="number"
value="{{ $configuration->getNordigenMaxDays() }}"/>
<small class="form-text text-muted">
The connection to your bank can be recycled for this number of days. It will be stored
in the import configuration file. Keep in mind most banks don't support more than 90 days.
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-3 submit-button" x-show="'XX' !== selectedCountry && '' !== selectedBank">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Submit!
</div>
<div class="card-body">
<button type="submit" class="float-end btn btn-primary">Submit &rarr;</button>
</div>
</div>
</div>
</div>
</form>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ route('back.upload') }}" class="btn btn-secondary"><span
class="fas fa-arrow-left"></span> Go back to upload</a>
<a href="{{ route('flush') }}" class="btn btn-danger text-white btn-sm"><span
class="fas fa-redo-alt"></span> Start over</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!--
<script type="text/javascript">
$(document).ready(function () {
$('#country').change(selectCountry)
$('.bank-selector').change(showDayCounter);
selectCountry();
});
function showDayCounter() {
$('.bank-selected').show();
let val = $('#country').val();
let maxDays = parseInt($('#bank_' + val + ' option:selected').data('days'));
$('.days').text(maxDays);
}
function selectCountry() {
$('.bank-selected').hide();
var val = $('#country').val();
$('.country-code').text(val);
$('.bank-box').hide();
$('.submit-button').show();
$('#' + val + '-box').show();
}
</script>
-->
@endsection
@section('scripts')
@vite(['src/pages/selection/gocardless.js'])
@endsection

116
resources/views/v2/import/011-connection/index.blade.php

@ -0,0 +1,116 @@
@extends('layout.v2')
@section('content')
<div class="container">
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<h1>{{ $mainTitle }}</h1>
</div>
</div>
<form method="post" action="{{ route('011-connections.post') }}" accept-charset="UTF-8" id="store">
<input type="hidden" name="_token" value="{{ csrf_token() }}"/>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
{{ $subTitle }}
</div>
<div class="card-body">
<p>Select the connection to use or make a new connection</p>
<p>
Spectre creates connections; a representation of the connection to your financial
institution.
Select below which one the importer must use, or opt to create a new connection if no
connections are visible.
Please read
<a href="https://docs.firefly-iii.org/"
target="_blank"> the documentation for this page</a> if you want to know more.
</p>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-header">
Connections
</div>
<div class="card-body">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>&nbsp;</th>
<th>Bank</th>
<th>Last used</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach($list as $item)
<tr>
<td>
<input
id="{{ $item->id }}"
type="radio"
name="spectre_connection_id"
value="{{ $item->id }}"
@if('disabled' === $item->status) disabled @endif
@if($configuration->getConnection() === $item->id) checked @endif
@if(1 === count($list)) checked @endif
>
</td>
<td>
<label for="{{ $item->id }}">
{{ $item->providerName }} ({{ $item->countryCode }})
</label>
</td>
<td>
Last success: {{ $item->lastSuccess->format("Y-m-d H:i:s") }}<br>
Updated at: {{ $item->updatedAt->format("Y-m-d H:i:s") }}<br>
</td>
<td>
{{ $item->status }}
</td>
</tr>
@endforeach
<tr>
<td>
<input id="new_login" type="radio" name="spectre_connection_id" value="00"
@if(0 === $configuration->getConnection()) checked @endif
>
</td>
<td colspan="3">
<label for="new_login"><em>
Create a new connection
</em>
</label>
</td>
</tr>
</tbody>
</table>
<button type="submit" class="btn btn-primary float-end">Submit &rarr;</button>
</div>
</div>
</div>
</div>
<!-- end of selection -->
<div class="row mt-3">
<div class="col-lg-10 offset-lg-1">
<div class="card">
<div class="card-body">
<div class="btn-group btn-group-sm">
<a href="{{ route('back.upload') }}" class="btn btn-secondary"><span
class="fas fa-arrow-left"></span> Go back to upload</a>
<a href="{{ route('flush') }}" class="btn btn-danger text-white btn-sm"><span
class="fas fa-redo-alt"></span> Start over</a>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
@section('scripts')
@endsection

12
resources/views/v2/index.blade.php

@ -83,7 +83,7 @@
><span class="fas fa-cog fa-spin"></span></button>
<button x-show="!loadingFunctions.file && importFunctions.file" class="btn btn-info" value="file" name="flow"
>Import file</button>
<button x-show="!loadingFunctions.file && !importFunctions.file" class="btn btn-danger disabled" value="file" name="flow"
<button x-show="!loadingFunctions.file && !importFunctions.file" class="btn text-white btn-danger disabled" value="file" name="flow"
disabled
><em class="fa-solid fa-face-dizzy"></em></button>
@ -101,7 +101,7 @@
><span class="fas fa-cog fa-spin"></span></button>
<button x-show="!loadingFunctions.gocardless && importFunctions.gocardless" class="btn btn-info" value="nordigen" name="flow"
>Import from GoCardless</button>
<button x-show="!loadingFunctions.gocardless && !importFunctions.gocardless" class="btn btn-danger disabled" value="nordigen" name="flow"
<button x-show="!loadingFunctions.gocardless && !importFunctions.gocardless" class="btn text-white btn-danger disabled" value="nordigen" name="flow"
disabled
><em class="fa-solid fa-face-dizzy"></em></button>
</div>
@ -114,11 +114,11 @@
</div>
<div class="card-body">
<p class="text-danger" x-text="errors.spectre" x-show="'' !== errors.spectre"></p>
<button x-show="loadingFunctions.spectre" class="btn btn-info disabled" value="file" name="flow" disabled="disabled"
<button x-show="loadingFunctions.spectre" class="btn btn-info disabled" value="spectre" name="flow" disabled="disabled"
><span class="fas fa-cog fa-spin"></span></button>
<button x-show="!loadingFunctions.spectre && importFunctions.spectre" class="btn btn-info" value="file" name="flow"
<button x-show="!loadingFunctions.spectre && importFunctions.spectre" class="btn btn-info" value="spectre" name="flow"
>Import from Spectre</button>
<button x-show="!loadingFunctions.spectre && !importFunctions.spectre" class="btn btn-danger disabled" value="file" name="flow"
<button x-show="!loadingFunctions.spectre && !importFunctions.spectre" class="btn btn-danger text-white disabled" value="spectre" name="flow"
disabled
><em class="fa-solid fa-face-dizzy"></em></button>
</div>
@ -146,7 +146,7 @@
has to stay the same when you simply refresh the page.
</p>
<p>
<a class="btn btn-danger btn-sm" href="{{ route('flush') }}" data-bs-toggle="tooltip"
<a class="btn btn-danger text-white btn-sm" href="{{ route('flush') }}" data-bs-toggle="tooltip"
data-bs-placement="top"
title="This button resets your progress">Start over</a>
</p>

Loading…
Cancel
Save