Expand new transaction form.

This commit is contained in:
James Cole
2024-01-03 17:43:05 +01:00
parent e6fe08dd61
commit 211526c032
10 changed files with 262 additions and 100 deletions

View File

@@ -28,6 +28,7 @@ use FireflyIII\Models\UserGroup;
use FireflyIII\Rules\BelongsUserGroup; use FireflyIII\Rules\BelongsUserGroup;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsDateOrTime; use FireflyIII\Rules\IsDateOrTime;
use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Support\NullArrayObject; use FireflyIII\Support\NullArrayObject;
use FireflyIII\Support\Request\AppendsLocationData; use FireflyIII\Support\Request\AppendsLocationData;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
@@ -74,7 +75,6 @@ class StoreRequest extends FormRequest
'fire_webhooks' => $this->boolean('fire_webhooks', true), 'fire_webhooks' => $this->boolean('fire_webhooks', true),
'transactions' => $this->getTransactionData(), 'transactions' => $this->getTransactionData(),
]; ];
// TODO include location and ability to process it.
} }
/** /**
@@ -107,8 +107,8 @@ class StoreRequest extends FormRequest
'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable', 'transactions.*.foreign_currency_code' => 'min:3|max:51|exists:transaction_currencies,code|nullable',
// amount // amount
'transactions.*.amount' => 'required|numeric|gt:0|max:1000000000', 'transactions.*.amount' => ['required', new IsValidPositiveAmount()],
'transactions.*.foreign_amount' => 'numeric|gt:0|max:1000000000', 'transactions.*.foreign_amount' => ['nullable', new IsValidPositiveAmount()],
// description // description
'transactions.*.description' => 'nullable|between:1,1000', 'transactions.*.description' => 'nullable|between:1,1000',
@@ -140,7 +140,8 @@ class StoreRequest extends FormRequest
// other interesting fields // other interesting fields
'transactions.*.reconciled' => [new IsBoolean()], 'transactions.*.reconciled' => [new IsBoolean()],
'transactions.*.notes' => 'min:1|max:50000|nullable', 'transactions.*.notes' => 'min:1|max:50000|nullable',
'transactions.*.tags' => 'between:0,255', 'transactions.*.tags' => 'between:0,1024',
'transactions.*.tags*' => 'between:0,1024',
// meta info fields // meta info fields
'transactions.*.internal_reference' => 'min:1|max:255|nullable', 'transactions.*.internal_reference' => 'min:1|max:255|nullable',
@@ -166,6 +167,9 @@ class StoreRequest extends FormRequest
'transactions.*.due_date' => 'date|nullable', 'transactions.*.due_date' => 'date|nullable',
'transactions.*.payment_date' => 'date|nullable', 'transactions.*.payment_date' => 'date|nullable',
'transactions.*.invoice_date' => 'date|nullable', 'transactions.*.invoice_date' => 'date|nullable',
// TODO include location and ability to process it.
]; ];
} }
@@ -222,7 +226,7 @@ class StoreRequest extends FormRequest
*/ */
foreach ($this->get('transactions') as $transaction) { foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction); $object = new NullArrayObject($transaction);
$return[] = [ $result= [
'type' => $this->clearString($object['type']), 'type' => $this->clearString($object['type']),
'date' => $this->dateFromValue($object['date']), 'date' => $this->dateFromValue($object['date']),
'order' => $this->integerFromValue((string)$object['order']), 'order' => $this->integerFromValue((string)$object['order']),
@@ -300,6 +304,8 @@ class StoreRequest extends FormRequest
'payment_date' => $this->dateFromValue($object['payment_date']), 'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']), 'invoice_date' => $this->dateFromValue($object['invoice_date']),
]; ];
$result = $this->addFromromTransactionStore($transaction, $result);
$return[] = $result;
} }
return $return; return $return;

View File

@@ -28,6 +28,7 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\DuplicateTransactionException; use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\Location;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@@ -318,10 +319,23 @@ class TransactionJournalFactory
$this->storePiggyEvent($journal, $row); $this->storePiggyEvent($journal, $row);
$this->storeTags($journal, $row['tags']); $this->storeTags($journal, $row['tags']);
$this->storeMetaFields($journal, $row); $this->storeMetaFields($journal, $row);
$this->storeLocation($journal, $row);
return $journal; return $journal;
} }
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (true === $data['store_location']) {
$location = new Location();
$location->longitude = $data['longitude'];
$location->latitude = $data['latitude'];
$location->zoom_level = $data['zoom_level'];
$location->locatable()->associate($journal);
$location->save();
}
}
private function hashArray(NullArrayObject $row): string private function hashArray(NullArrayObject $row): string
{ {
$dataRow = $row->getArrayCopy(); $dataRow = $row->getArrayCopy();
@@ -360,16 +374,12 @@ class TransactionJournalFactory
->where('transaction_journals.user_id', $this->user->id) ->where('transaction_journals.user_id', $this->user->id)
->where('data', json_encode($hash, JSON_THROW_ON_ERROR)) ->where('data', json_encode($hash, JSON_THROW_ON_ERROR))
->with(['transactionJournal', 'transactionJournal.transactionGroup']) ->with(['transactionJournal', 'transactionJournal.transactionGroup'])
->first() ->first(['journal_meta.*']);
;
if (null !== $result) { if (null !== $result) {
app('log')->warning(sprintf('Found a duplicate in errorIfDuplicate because hash %s is not unique!', $hash)); app('log')->warning(sprintf('Found a duplicate in errorIfDuplicate because hash %s is not unique!', $hash));
$journal = $result->transactionJournal()->withTrashed()->first(); $journal = $result->transactionJournal()->withTrashed()->first();
$group = $journal?->transactionGroup()->withTrashed()->first(); $group = $journal?->transactionGroup()->withTrashed()->first();
$groupId = $group?->id; $groupId = (int) $group?->id;
if (null === $group) {
$groupId = 0;
}
throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $groupId)); throw new DuplicateTransactionException(sprintf('Duplicate of transaction #%d.', $groupId));
} }

View File

@@ -93,12 +93,14 @@ class Location extends Model
return $rules; return $rules;
} }
/**
* Get all the accounts.
*/
public function accounts(): MorphMany public function accounts(): MorphMany
{ {
return $this->morphMany(Account::class, 'noteable'); return $this->morphMany(Account::class, 'locatable');
}
public function transactionJournals(): MorphMany
{
return $this->morphMany(TransactionJournal::class, 'locatable');
} }
/** /**

View File

@@ -119,6 +119,40 @@ trait AppendsLocationData
return $data; return $data;
} }
/**
* @param array $information
* @param array $return
*
* @return array
*/
public function addFromromTransactionStore(array $information, array $return): array
{
$return['store_location'] = false;
if (true === $information['store_location']) {
$long = array_key_exists('longitude', $information) ? $information['longitude'] : null;
$lat = array_key_exists('latitude', $information) ? $information['latitude'] : null;
if (null !== $long && null !== $lat && $this->validLongitude($long) && $this->validLatitude($lat)) {
$return['store_location'] = true;
$return['longitude'] = $information['longitude'];
$return['latitude'] = $information['latitude'];
$return['zoom_level'] = $information['zoom_level'];
}
}
return $return;
}
private function validLongitude(string $longitude): bool
{
$number = (float) $longitude;
return $number >= -180 && $number <= 180;
}
private function validLatitude(string $latitude): bool
{
$number = (float) $latitude;
return $number >= -90 && $number <= 90;
}
private function getLocationKey(?string $prefix, string $key): string private function getLocationKey(?string $prefix, string $key): string
{ {
if (null === $prefix) { if (null === $prefix) {

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Transformers\V2;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Location;
use FireflyIII\Models\Note; use FireflyIII\Models\Note;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@@ -47,11 +48,11 @@ class TransactionGroupTransformer extends AbstractTransformer
private TransactionCurrency $default; private TransactionCurrency $default;
private array $meta; private array $meta;
private array $notes; private array $notes;
private array $locations;
private array $tags; private array $tags;
public function collectMetaData(Collection $objects): void public function collectMetaData(Collection $objects): void
{ {
// start with currencies:
$currencies = []; $currencies = [];
$journals = []; $journals = [];
@@ -89,12 +90,20 @@ class TransactionGroupTransformer extends AbstractTransformer
$this->notes[$id] = $note; $this->notes[$id] = $note;
} }
// grab all locations for all journals:
$locations = Location::whereLocatableType(TransactionJournal::class)->whereIn('locatable_id', array_keys($journals))->get();
/** @var Location $location */
foreach ($locations as $location) {
$id = $location->locatable_id;
$this->locations[$id] = $location;
}
// grab all tags for all journals: // grab all tags for all journals:
$tags = DB::table('tag_transaction_journal') $tags = DB::table('tag_transaction_journal')
->leftJoin('tags', 'tags.id', 'tag_transaction_journal.tag_id') ->leftJoin('tags', 'tags.id', 'tag_transaction_journal.tag_id')
->whereIn('tag_transaction_journal.transaction_journal_id', array_keys($journals)) ->whereIn('tag_transaction_journal.transaction_journal_id', array_keys($journals))
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag']) ->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag']);
;
/** @var \stdClass $tag */ /** @var \stdClass $tag */
foreach ($tags as $tag) { foreach ($tags as $tag) {
@@ -167,6 +176,17 @@ class TransactionGroupTransformer extends AbstractTransformer
} }
$this->converter->summarize(); $this->converter->summarize();
$longitude = null;
$latitude = null;
$zoomLevel = null;
if (array_key_exists($journalId, $this->locations)) {
/** @var Location $location */
$location = $this->locations[$journalId];
$latitude = (string) $location->latitude;
$longitude = (string) $location->longitude;
$zoomLevel = $location->zoom_level;
}
return [ return [
'user' => (string) $transaction['user_id'], 'user' => (string) $transaction['user_id'],
'user_group' => (string) $transaction['user_group_id'], 'user_group' => (string) $transaction['user_group_id'],
@@ -241,9 +261,9 @@ class TransactionGroupTransformer extends AbstractTransformer
'invoice_date' => $this->date($meta['invoice_date']), 'invoice_date' => $this->date($meta['invoice_date']),
// location data // location data
// 'longitude' => $longitude, 'longitude' => $longitude,
// 'latitude' => $latitude, 'latitude' => $latitude,
// 'zoom_level' => $zoomLevel, 'zoom_level' => $zoomLevel,
// //
// 'has_attachments' => $this->hasAttachments((int) $row['transaction_journal_id']), // 'has_attachments' => $this->hasAttachments((int) $row['transaction_journal_id']),
]; ];

View File

@@ -25,6 +25,8 @@ export default class Preferences {
return api.get('/api/v1/preferences/' + name); return api.get('/api/v1/preferences/' + name);
} }
getByNameNow(name) { getByNameNow(name) {
return api.get('/api/v1/preferences/' + name); return api.get('/api/v1/preferences/' + name);
} }

View File

@@ -40,6 +40,10 @@ import L from "leaflet";
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
// TODO upload attachments to other file
// TODO fix two maps, perhaps disconnect from entries entirely.
// TODO group title
// TODO map location from preferences
let i18n; let i18n;
@@ -754,19 +758,41 @@ let transactions = function () {
let fieldName; let fieldName;
// todo add 'was-validated' to form. // todo add 'was-validated' to form.
console.log('Now processing errors.');
for (const key in data.errors) { for (const key in data.errors) {
if (data.errors.hasOwnProperty(key)) { if (data.errors.hasOwnProperty(key)) {
if (key === 'group_title') { if (key === 'group_title') {
console.log('Handling group title error.');
// todo handle group errors. // todo handle group errors.
//this.group_title_errors = errors.errors[key]; //this.group_title_errors = errors.errors[key];
} }
if (key !== 'group_title') { if (key !== 'group_title') {
console.log('Handling errors for ' + key);
// lol, the dumbest way to explode "transactions.0.something" ever. // lol, the dumbest way to explode "transactions.0.something" ever.
transactionIndex = parseInt(key.split('.')[1]); transactionIndex = parseInt(key.split('.')[1]);
fieldName = key.split('.')[2]; fieldName = key.split('.')[2];
console.log('Transaction index: ' + transactionIndex);
console.log('Field name: ' + fieldName);
console.log('Errors');
console.log(data.errors[key]);
// set error in this object thing. // set error in this object thing.
switch (fieldName) { switch (fieldName) {
case 'currency_code':
case 'foreign_currency_code':
case 'category_name':
case 'piggy_bank_id':
case 'notes':
case 'internal_reference':
case 'external_url':
case 'latitude':
case 'longitude':
case 'zoom_level':
case 'interest_date':
case 'book_date':
case 'process_date':
case 'due_date':
case 'payment_date':
case 'invoice_date':
case 'amount': case 'amount':
case 'date': case 'date':
case 'budget_id': case 'budget_id':
@@ -779,6 +805,10 @@ let transactions = function () {
case 'source_id': case 'source_id':
this.entries[transactionIndex].errors.source_account = this.entries[transactionIndex].errors.source_account.concat(data.errors[key]); this.entries[transactionIndex].errors.source_account = this.entries[transactionIndex].errors.source_account.concat(data.errors[key]);
break; break;
case 'type':
// put the error in the description:
this.entries[transactionIndex].errors.description = this.entries[transactionIndex].errors.source_account.concat(data.errors[key]);
break;
case 'destination_name': case 'destination_name':
case 'destination_id': case 'destination_id':
this.entries[transactionIndex].errors.destination_account = this.entries[transactionIndex].errors.destination_account.concat(data.errors[key]); this.entries[transactionIndex].errors.destination_account = this.entries[transactionIndex].errors.destination_account.concat(data.errors[key]);
@@ -810,6 +840,7 @@ let transactions = function () {
server: urls.tag, server: urls.tag,
liveServer: true, liveServer: true,
clearEnd: true, clearEnd: true,
allowNew: true,
notFoundMessage: '(nothing found)', notFoundMessage: '(nothing found)',
noCache: true, noCache: true,
fetchOptions: { fetchOptions: {
@@ -819,14 +850,28 @@ let transactions = function () {
} }
}); });
const count = this.entries.length - 1; const count = this.entries.length - 1;
this.entries[count].map = L.map('mappie').setView([this.latitude, this.longitude], this.zoomLevel); //let map = L.map('location_map_' + count).setView([this.latitude, this.longitude], this.zoomLevel);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { // L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, // maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' // attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap '+count+'</a>'
}).addTo(this.entries[count].map); // }).addTo(map);
this.entries[count].map.on('click', this.addPointToMap); // map.on('click', this.addPointToMap);
this.entries[count].map.on('zoomend', this.saveZoomOfMap); // map.on('zoomend', this.saveZoomOfMap);
const id = 'location_map_' + count;
const map = () => {
const el = document.getElementById(id),
map = L.map(id).setView([this.latitude, this.longitude], this.zoomLevel)
L.tileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap '+count+'</a>'}
).addTo(map)
map.on('click', this.addPointToMap);
map.on('zoomend', this.saveZoomOfMap);
return map
}
this.entries[count].map = map();
}, 250); }, 250);
@@ -878,13 +923,6 @@ let transactions = function () {
document.querySelector('#form')._x_dataStack[0].$data.entries[index].longitude = e.latlng.lng; document.querySelector('#form')._x_dataStack[0].$data.entries[index].longitude = e.latlng.lng;
document.querySelector('#form')._x_dataStack[0].$data.entries[index].zoomLevel = map.getZoom(); document.querySelector('#form')._x_dataStack[0].$data.entries[index].zoomLevel = map.getZoom();
} }
//this.entries[index].hasLocation = true;
// map.on('click', function (e) {
// if (false === this.hasLocation) {
// let marker = new L.marker(e.latlng).addTo(map);
// this.hasLocation = true;
// }
// });
} }
} }
} }

View File

@@ -76,11 +76,43 @@ export function createEmptySplit() {
invoice_date: '', invoice_date: '',
errors: { errors: {
'amount': [], description: [],
'foreign_amount': [],
'budget_id': [], // amount information:
'category_name': [], amount: [],
'piggy_bank_id': [], currency_code: [],
foreign_amount: [],
foreign_currency_code: [],
// source and destination
source_account: [],
destination_account: [],
// meta data information:
budget_id: [],
category_name: [],
piggy_bank_id: [],
bill_id: [],
tags: [],
notes: [],
// other meta fields:
internal_reference: [],
external_url: [],
// map
latitude: [],
longitude: [],
zoom_level: [],
// date and time
date: [],
interest_date: [],
book_date: [],
process_date: [],
due_date: [],
payment_date: [],
invoice_date: [],
}, },
}; };
} }

View File

@@ -55,14 +55,24 @@ export function parseFromEntries(entries, transactionType) {
current.category_name = entry.category_name; current.category_name = entry.category_name;
current.piggy_bank_id = entry.piggy_bank_id; current.piggy_bank_id = entry.piggy_bank_id;
current.bill_id = entry.bill_id; current.bill_id = entry.bill_id;
current.tags = entry.tags;
current.notes = entry.notes;
// more meta
current.internal_reference = entry.internal_reference;
current.external_url = entry.external_url;
// location // location
current.store_location = false;
if (entry.hasLocation) { if (entry.hasLocation) {
current.store_location = true;
current.longitude = entry.longitude.toString(); current.longitude = entry.longitude.toString();
current.latitude = entry.latitude.toString(); current.latitude = entry.latitude.toString();
current.zoom_level = entry.zoomLevel; current.zoom_level = entry.zoomLevel;
} }
// if foreign amount currency code is set: // if foreign amount currency code is set:
if (typeof entry.foreign_currency_code !== 'undefined' && '' !== entry.foreign_currency_code.toString()) { if (typeof entry.foreign_currency_code !== 'undefined' && '' !== entry.foreign_currency_code.toString()) {
current.foreign_currency_code = entry.foreign_currency_code; current.foreign_currency_code = entry.foreign_currency_code;

View File

@@ -343,6 +343,7 @@
<select <select
class="form-select ac-tags" class="form-select ac-tags"
:id="'tags_' + index" :id="'tags_' + index"
x-model="transaction.tags"
:name="'tags['+index+'][]'" :name="'tags['+index+'][]'"
multiple> multiple>
<option value="">Type a tag...</option> <option value="">Type a tag...</option>
@@ -406,12 +407,19 @@
<i class="fa-solid fa-link"></i> <i class="fa-solid fa-link"></i>
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<input type="search" <input type="text"
class="form-control" class="form-control"
:id="'external_url_' + index" :id="'external_url_' + index"
x-model="transaction.external_url" x-model="transaction.external_url"
:data-index="index" :data-index="index"
placeholder="{{ __('firefly.external_url') }}"> :class="{'is-invalid': transaction.errors.external_url.length > 0, 'form-control': true}"
placeholder="{{ __('firefly.external_url') }}" />
<template x-if="transaction.errors.external_url.length > 0">
<div class="invalid-feedback"
x-text="transaction.errors.external_url[0]">
</div>
</template>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
@@ -420,7 +428,7 @@
<i class="fa-solid fa-earth-europe"></i> <i class="fa-solid fa-earth-europe"></i>
</label> </label>
<div class="col-sm-10"> <div class="col-sm-10">
<div id="mappie" style="height:300px;" :data-index="index"></div> <div :id="'location_map_' + index" style="height:300px;" :data-index="index"></div>
<span class="muted small"> <span class="muted small">
<template x-if="!transaction.hasLocation"> <template x-if="!transaction.hasLocation">
<span>Tap the map to add a location</span> <span>Tap the map to add a location</span>