Fix location

This commit is contained in:
Sander Dorigo
2026-08-21 10:56:08 +02:00
parent 3ccc5f72a7
commit 7d8f5e6e5d
18 changed files with 265 additions and 141 deletions
@@ -125,7 +125,6 @@ final class UpdateController extends Controller
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
@@ -79,11 +79,12 @@ class UpdateRequest extends FormRequest
'category_id',
'bill_id',
'recurrence_id',
'zoom_level'
];
$this->dateFields = ['date', 'interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date'];
$this->textareaFields = ['notes'];
// not really floats, for validation.
$this->floatFields = ['amount', 'foreign_amount'];
$this->floatFields = ['amount', 'foreign_amount','longitude','latitude'];
$this->stringFields = [
'type',
'currency_code',
@@ -35,6 +35,7 @@ use FireflyIII\Factory\TagFactory;
use FireflyIII\Factory\TransactionJournalMetaFactory;
use FireflyIII\Factory\TransactionTypeFactory;
use FireflyIII\Models\Account;
use FireflyIII\Models\Location;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
@@ -194,6 +195,7 @@ class JournalUpdateService
$this->updateCurrency();
$this->updateAmount();
$this->updateForeignAmount();
$this->updateLocation();
Preferences::mark();
@@ -911,4 +913,28 @@ class JournalUpdateService
}
Log::debug('No type field present.');
}
private function updateLocation(): void
{
if($this->hasFields(['longitude','latitude','zoom_level'])) {
// if all are null or zero, delete current location.
if(null === $this->data['longitude'] && null === $this->data['latitude'] && null === $this->data['zoom_level']) {
$this->transactionJournal->locations()->delete();
return;
}
if('' === $this->data['longitude'] && '' === $this->data['latitude'] && null === $this->data['zoom_level']) {
$this->transactionJournal->locations()->delete();
return;
}
$location = $this->transactionJournal->locations()->first();
if(null === $location) {
$location = new Location();
$location->locatable()->associate($this->transactionJournal);
}
$location->longitude = $this->data['longitude'] ?? null;
$location->latitude = $this->data['latitude'];
$location->zoom_level = $this->data['zoom_level'];
$location->save();
}
}
}
+27 -32
View File
@@ -1,9 +1,10 @@
{
"name": "firefly-iii",
"name": "release",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "release",
"hasInstallScript": true,
"workspaces": [
"resources/assets/v3"
@@ -1054,23 +1055,13 @@
}
}
},
"node_modules/deepmerge-ts": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.6.tgz",
"integrity": "sha512-gQhL1ksGBLQbHeAo47YU6cs2ahd3Pv+8PFYoWogNZbQnNwQyOh9Ad5kbeHFiWwbKdeWQJ5Z7Y7mwb9ew2hXBLw==",
"funding": [
{
"type": "ko-fi",
"url": "https://ko-fi.com/rebeccastevens"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/deepmerge-ts"
}
],
"license": "BSD-3-Clause",
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
"node": ">=0.10.0"
}
},
"node_modules/define-data-property": {
@@ -2288,19 +2279,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/shepherd.js": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-15.2.3.tgz",
"integrity": "sha512-FyLH/c25YlTS5d+cL9oLajzfW0TzNl43sysQG6EK4bf60n9les7TsIUekHTTKyMEtcARIVHW2d1N9sqeDkIsbQ==",
"license": "AGPL-3.0",
"dependencies": {
"@floating-ui/dom": "^1.8.0",
"deepmerge-ts": "^7.1.5"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
@@ -2726,7 +2704,7 @@
"bootstrap5-autocomplete": "^1",
"bootstrap5-tags": "^1.7.17",
"cally": "^0.9.2",
"date-fns": "4.4.0",
"date-fns": "^4.4.0",
"fullcalendar": "^7.0.1",
"i18next": "^26.0.3",
"i18next-chained-backend": "^5.0.0",
@@ -2734,7 +2712,7 @@
"i18next-localstorage-backend": "^4.2.0",
"leaflet": "^1.9.4",
"patch-package": "^8.0.1",
"shepherd.js": "^15.2.2",
"shepherd.js": "^11.2.0",
"store": "^2.0.12"
},
"devDependencies": {
@@ -2746,6 +2724,23 @@
"vite": "^8.0.0",
"vite-plugin-manifest-sri": "^0.2.0"
}
},
"resources/assets/v3/node_modules/shepherd.js": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-11.2.0.tgz",
"integrity": "sha512-2hbz3N7GuuTjI7y3sfnoqKnH0cNhExx67IJtCTGQI2KhBEyvegsDYW5qjj5BlvvVtQjmL/O/J1GQEciwfoZWpw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.5.1",
"deepmerge": "^4.3.1"
},
"engines": {
"node": "16.* || >= 18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/RobbieTheWagner"
}
}
}
}
@@ -50,12 +50,17 @@ import {clearSourceAccount, clearDestinationAccount} from './shared/clear-fields
import {detectTransactionType} from './shared/detect-transaction-type.js';
import {determineAmountCurrency} from './shared/determine-amount-currency.js';
import {loadCustomFields} from './shared/load-custom-fields.js';
import {loadDefaultCoordinates} from './shared/load-default-coordinates.js';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import {loadDefaultCoordinates} from './shared/load-default-coordinates.js';
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerRetinaIcon from "leaflet/dist/images/marker-icon-2x.png";
import shadow from 'leaflet/dist/images/marker-shadow.png';
import {displayMap} from './shared/display-map.js';
import {renderMap} from './shared/render-map.js';
import {onMapClick} from './shared/on-map-click.js';
import {onMapZoom} from "./shared/on-map-zoom.js";
// TODO fix two maps, perhaps disconnect from entries entirely.
// TODO map location from preferences
@@ -174,6 +179,10 @@ let create = function () {
addAllAutocompleteToForm: addAllAutocompleteToForm,
loadCustomFields: loadCustomFields,
loadDefaultCoordinates: loadDefaultCoordinates,
displayMap: displayMap,
renderMap: renderMap,
onMapClick: onMapClick,
onMapZoom: onMapZoom,
filterForeignCurrencies(code) {
@@ -224,69 +233,8 @@ let create = function () {
clearCategory(index) {
this.entries[index].category_name = '';
},
displayMap(index) {
index = parseInt(index);
if (true === this.formBehaviour.customFields.location) {
if (false === this.formBehaviour.defaultCoordinates.loaded) {
// load first, then show map.
this.loadDefaultCoordinates().then(data => {
this.formBehaviour.defaultCoordinates = data;
this.renderMap(index, true);
});
return;
}
this.renderMap(index, false);
}
},
onMapClick(event) {
L.Marker.prototype.setIcon(L.icon({
iconUrl: markerIcon,
iconRetinaIcon: markerRetinaIcon,
shadowUrl: shadow,
iconSize: [25, 41],
iconAnchor: [12, 41]
}));
let index = parseInt(event.originalEvent.currentTarget.dataset.index);
this.entries[index].hasLocation = true;
this.entries[index].latitude = event.latlng.lat;
this.entries[index].longitude = event.latlng.lng;
this.entries[index].zoomLevel = this.maps[index].getZoom();
// add marker.
if(this.markers.hasOwnProperty(index)) {
this.maps[index].removeLayer(this.markers[index]);
}
this.markers[index] = L.marker(event.latlng);
this.markers[index].addTo(this.maps[index]);
},
renderMap(index, useDefault) {
const el = document.getElementById('location_map_' + index);
let latitude = parseFloat(this.formBehaviour.defaultCoordinates.latitude);
let longitude = parseFloat(this.formBehaviour.defaultCoordinates.longitude);
let zoomLevel = parseInt(this.formBehaviour.defaultCoordinates.zoom_level);
if(!useDefault) {
console.log('Using coordinates from data attributes');
latitude = parseFloat(el.dataset.latitude);
longitude = parseFloat(el.dataset.longitude);
zoomLevel = parseInt(el.dataset.zoomLevel);
}
// console.log('lat', latitude);
// console.log('long', longitude);
// console.log('zoom', zoomLevel);
this.maps[index] = L.map(el).setView([latitude, longitude], zoomLevel);
this.maps[index].on('click', this.onMapClick.bind(this));
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
referrerPolicy: 'origin-when-cross-origin',
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.maps[index]);
},
respondToTabSwitch(event) {
// event.target // newly activated tab
// event.relatedTarget // previous active tab
@@ -47,7 +47,13 @@ import {addSplit} from "./shared/add-split.js";
import {clearDestinationAccount, clearSourceAccount} from "./shared/clear-fields.js";
import {detectTransactionType} from "./shared/detect-transaction-type.js";
import {determineAmountCurrency} from "./shared/determine-amount-currency.js";
import {loadCustomFields} from './shared/load-custom-fields.js';
import {displayMap} from "./shared/display-map.js";
import {loadDefaultCoordinates} from './shared/load-default-coordinates.js';
import {renderMap} from './shared/render-map.js';
import 'leaflet/dist/leaflet.css';
import {onMapClick} from './shared/on-map-click.js';
import {onMapZoom} from './shared/on-map-zoom.js';
// TODO upload attachments to other file
// TODO fix two maps, perhaps disconnect from entries entirely.
// TODO group title
@@ -67,6 +73,10 @@ let transactions = function () {
entries: [],
originals: [],
// maps are stored in this array so they can be referred to.
maps: [],
markers: [],
// state of the form is stored in formState:
formStates: {
loadingCurrencies: true,
@@ -87,6 +97,12 @@ let transactions = function () {
foreignCurrencyEnabled: true,
categorySelectVisible: false,
customFields: {},
defaultCoordinates: {
loaded: false,
latitude: 30,
longitude: 20,
zoom_level: 9,
},
},
// form data (except transactions) is stored in formData
@@ -136,6 +152,12 @@ let transactions = function () {
detectTransactionType: detectTransactionType,
determineAmountCurrency: determineAmountCurrency,
addAllAutocompleteToForm: addAllAutocompleteToForm,
loadCustomFields: loadCustomFields,
displayMap: displayMap,
loadDefaultCoordinates: loadDefaultCoordinates,
renderMap: renderMap,
onMapClick: onMapClick,
onMapZoom: onMapZoom,
// part of the account selection auto-complete
@@ -159,7 +181,18 @@ let transactions = function () {
getTags(index) {
return this.entries[index].tags ?? [];
},
clearLocation(e) {
let index = parseInt(e.currentTarget.dataset.index);
this.entries[index].hasLocation = false;
this.entries[index].latitude = null;
this.entries[index].longitude = null;
this.entries[index].zoom_level = null;
if(this.markers.hasOwnProperty(index)) {
this.maps[index].removeLayer(this.markers[index]);
}
},
getTransactionGroup() {
this.entries = [];
const page = window.location.href.split('/');
@@ -271,22 +304,23 @@ let transactions = function () {
document.addEventListener('upload-error', (event) => {
this.processUploadError(event);
});
document.addEventListener('location-move', (event) => {
this.entries[event.detail.index].latitude = event.detail.latitude;
this.entries[event.detail.index].longitude = event.detail.longitude;
});
// document.addEventListener('location-move', (event) => {
// this.entries[event.detail.index].latitude = event.detail.latitude;
// this.entries[event.detail.index].longitude = event.detail.longitude;
// });
document.addEventListener('location-set', (event) => {
this.entries[event.detail.index].hasLocation = true;
this.entries[event.detail.index].latitude = event.detail.latitude;
this.entries[event.detail.index].longitude = event.detail.longitude;
this.entries[event.detail.index].zoomLevel = event.detail.zoomLevel;
});
// document.addEventListener('location-set', (event) => {
// this.entries[event.detail.index].hasLocation = true;
// this.entries[event.detail.index].latitude = event.detail.latitude;
// this.entries[event.detail.index].longitude = event.detail.longitude;
// this.entries[event.detail.index].zoom_level = event.detail.zoomLevel;
// });
document.addEventListener('location-zoom', (event) => {
this.entries[event.detail.index].hasLocation = true;
this.entries[event.detail.index].zoomLevel = event.detail.zoomLevel;
});
// document.addEventListener('location-zoom', (event) => {
// console.log('this happens?');
// this.entries[event.detail.index].hasLocation = true;
// this.entries[event.detail.index].zoom_level = event.detail.zoomLevel;
// });
},
@@ -104,7 +104,7 @@ export function createEmptySplit() {
hasLocation: false,
latitude: null,
longitude: null,
zoomLevel: null,
zoom_level: null,
// date and time
@@ -0,0 +1,19 @@
export function displayMap(index) {
index = parseInt(index);
// show location?
if (true === this.formBehaviour.customFields.location) {
if(true === this.entries[index].hasLocation) {
this.renderMap(index, false);
return;
}
if (false === this.formBehaviour.defaultCoordinates.loaded) {
// load first, then show map.
this.loadDefaultCoordinates().then(data => {
this.formBehaviour.defaultCoordinates = data;
this.renderMap(index, true);
});
return;
}
this.renderMap(index, false);
}
}
@@ -0,0 +1,30 @@
import L from 'leaflet';
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerRetinaIcon from "leaflet/dist/images/marker-icon-2x.png";
import shadow from 'leaflet/dist/images/marker-shadow.png';
export function onMapClick(event) {
L.Marker.prototype.setIcon(L.icon({
iconUrl: markerIcon,
iconRetinaIcon: markerRetinaIcon,
shadowUrl: shadow,
iconSize: [25, 41],
iconAnchor: [12, 41]
}));
let index = parseInt(event.originalEvent.currentTarget.dataset.index);
this.entries[index].hasLocation = true;
this.entries[index].latitude = event.latlng.lat;
this.entries[index].longitude = event.latlng.lng;
this.entries[index].zoom_level = this.maps[index].getZoom();
// add marker.
if(this.markers.hasOwnProperty(index)) {
this.maps[index].removeLayer(this.markers[index]);
}
this.markers[index] = L.marker(event.latlng);
this.markers[index].addTo(this.maps[index]);
}
@@ -0,0 +1,9 @@
import L from 'leaflet';
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerRetinaIcon from "leaflet/dist/images/marker-icon-2x.png";
import shadow from 'leaflet/dist/images/marker-shadow.png';
export function onMapZoom(event) {
let index = parseInt(event.target._container.dataset.index);
this.entries[index].zoom_level = this.maps[index].getZoom();
}
@@ -86,7 +86,7 @@ export function parseDownloadedSplits(downloads, groupId) {
current.hasLocation = true;
current.latitude = download.latitude;
current.longitude = download.longitude;
current.zoomLevel = download.zoom_level;
current.zoom_level = download.zoom_level;
}
returnArray.push(current);
}
@@ -70,13 +70,15 @@ export function parseFromEntries(entries, originals, transactionType) {
current.external_url = entry.external_url;
// location
current.store_location = false;
current.longitude = null;
current.latitude = null;
current.zoom_level = null;
if (entry.hasLocation) {
current.store_location = true;
current.longitude = entry.longitude.toString();
current.latitude = entry.latitude.toString();
current.zoom_level = entry.zoomLevel;
current.zoom_level = entry.zoom_level;
}
console.log('Zoom level is now ', entry.zoom_level);
if ('__NO_CODE__' === entry.foreign_currency_code) {
entry.foreign_currency_code = '';
}
@@ -0,0 +1,49 @@
import L from 'leaflet';
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerRetinaIcon from "leaflet/dist/images/marker-icon-2x.png";
import shadow from 'leaflet/dist/images/marker-shadow.png';
export function renderMap(index, useDefault) {
const el = document.getElementById('location_map_' + index);
let latitude = parseFloat(this.formBehaviour.defaultCoordinates.latitude);
let longitude = parseFloat(this.formBehaviour.defaultCoordinates.longitude);
let zoomLevel = parseInt(this.formBehaviour.defaultCoordinates.zoom_level);
if(!useDefault) {
console.log('Using coordinates from data attributes');
latitude = parseFloat(el.dataset.latitude);
longitude = parseFloat(el.dataset.longitude);
zoomLevel = parseInt(el.dataset.zoomLevel);
}
// console.log('lat', latitude);
// console.log('long', longitude);
// console.log('zoom', zoomLevel);
this.maps[index] = L.map(el).setView([latitude, longitude], zoomLevel);
this.maps[index].on('click', this.onMapClick.bind(this));
this.maps[index].on('zoom', this.onMapZoom.bind(this));
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
referrerPolicy: 'origin-when-cross-origin',
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.maps[index]);
if('true' === el.dataset.addMarker) {
L.Marker.prototype.setIcon(L.icon({
iconUrl: markerIcon,
iconRetinaIcon: markerRetinaIcon,
shadowUrl: shadow,
iconSize: [25, 41],
iconAnchor: [12, 41]
}));
// add marker.
if(this.markers.hasOwnProperty(index)) {
this.maps[index].removeLayer(this.markers[index]);
}
this.markers[index] = L.marker([latitude, longitude]);
this.markers[index].addTo(this.maps[index]);
}
}
@@ -23,6 +23,7 @@ import sidebar from '../../pages/shared/sidebar.js';
import dates from '../shared/dates.js';
import i18next from "i18next";
import Get from '../../api/model/transaction/get.js';
import {format} from "date-fns";
let show = function () {
return {
@@ -45,7 +46,16 @@ let show = function () {
downloadTransactionGroup() {
(new Get()).show(this.group.id).then((response) => {
const info = response.data.data;
this.group.transactions = info.attributes.transactions;
this.group.transactions = [];
for(let i =0;i<info.attributes.transactions.length;i++){
if(info.attributes.transactions.hasOwnProperty(i)) {
let current =info.attributes.transactions[i];
current.dateObject = new Date(current.date);
current.dateFormatted = format(current.dateObject, this.i18next.t('config.date_time_fns'))
this.group.transactions.push(current);
}
}
this.loading = false;
});
}
+2 -2
View File
@@ -12,7 +12,7 @@
"bootstrap5-autocomplete": "^1",
"bootstrap5-tags": "^1.7.17",
"cally": "^0.9.2",
"date-fns": "4.4.0",
"date-fns": "^4.4.0",
"fullcalendar": "^7.0.1",
"i18next": "^26.0.3",
"i18next-chained-backend": "^5.0.0",
@@ -20,7 +20,7 @@
"i18next-localstorage-backend": "^4.2.0",
"leaflet": "^1.9.4",
"patch-package": "^8.0.1",
"shepherd.js": "^15.2.2",
"shepherd.js": "^11.2.0",
"store": "^2.0.12"
},
"type": "module",
@@ -12,9 +12,10 @@
<template x-if="true || 0 === index">
<div class="col-sm-10">
<div x-init="displayMap(index)"
:data-longitude="formBehaviour.defaultCoordinates.longitude"
:data-latitude="formBehaviour.defaultCoordinates.latitude"
:data-zoom-level="formBehaviour.defaultCoordinates.zoom_level"
:data-add-marker="transaction.hasLocation ? 'true' : 'false'"
:data-longitude="transaction.longitude ?? formBehaviour.defaultCoordinates.longitude"
:data-latitude="transaction.latitude ?? formBehaviour.defaultCoordinates.latitude"
:data-zoom-level="transaction.zoom_level ?? formBehaviour.defaultCoordinates.zoom_level"
:id="'location_map_' + index" class="map-size location-map" :data-index="index"></div>
<span class="muted small">
@@ -22,7 +23,7 @@
<span>{{ __('firefly.click_tap_location') }}</span>
</template>
<template x-if="transaction.hasLocation">
<a :data-index="index" href="#" @click="clearLocation">{{ __('firefly.clear_location') }}</a>
<a :data-index="index" href="#" @click.prevent="clearLocation">{{ __('firefly.clear_location') }}</a>
</template>
</span>
</div>
+1 -6
View File
@@ -10,12 +10,7 @@
<x-transaction.tab-list />
<div class="tab-content" id="splitTabsContent">
<template x-for="transaction,index in entries">
<x-transaction.split
:zoomLevel="$zoomLevel"
:latitude="$latitude"
:longitude="$longitude"
:optionalFields="$optionalFields"
:optionalDateFields="$optionalDateFields"></x-transaction.split>
<x-transaction.split />
</template>
</div>
</div>
+18 -12
View File
@@ -90,12 +90,9 @@
<tr>
<td class="thirty">{{ trans('list.date') }}</td>
<td>
<span class="date-time" data-date="{{ $first['date']->toIso8601ZuluString() }}" title="{{ $first['date']->isoFormat($dateTimeFormat) }}@if('' !== $first['date_tz']) ({{ trans('firefly.stored_in_tz', ['timezone' => $first['date_tz']]) }}, {{ trans('firefly.displayed_in_tz', ['timezone' => config('app.timezone')]) }})@endif">
{{ $first['date']->isoFormat($dateTimeFormat) }}
@if('' !== $first['date_tz'])
({{ trans('firefly.stored_in_tz', ['timezone' => $first['date_tz']]) }})
@endif
</span>
<template x-if="group.transactions.length > 0">
<span x-text="group.transactions[0].dateFormatted" :title="group.transactions[0].date"></span>
</template>
</td>
</tr>
</tbody>
@@ -363,12 +360,13 @@
</td>
</tr>
<tr>
<td>{{ __('firefly.transaction_journal_id') }}</td>
<td>#{{ $journal['transaction_journal_id'] }}</td>
<td class="w-30">{{ __('firefly.transaction_journal_id') }}</td>
<td>#{{ $journal['transaction_journal_id'] }}
</td>
</tr>
@if(null !== $journal['category_id'])
<tr>
<td class="thirty">{{ __('firefly.category') }}</td>
<td class="w-30">{{ __('firefly.category') }}</td>
<td>
<a href="{{ route('categories.show', [$journal['category_id']]) }}">{{ $journal['category_name'] }}</a>
</td>
@@ -376,7 +374,7 @@
@endif
@if(null !== $journal['budget_id'] && $first['transaction_type_type'] === 'Withdrawal')
<tr>
<td class="forty">{{ __('firefly.budget') }}</td>
<td class="w-30">{{ __('firefly.budget') }}</td>
<td>
<a href="{{ route('budgets.show', [$journal['budget_id']]) }}">{{ $journal['budget_name'] }}</a>
</td>
@@ -384,17 +382,25 @@
@endif
@if(null !== $journal['bill_id'] && $first['transaction_type_type'] === 'Withdrawal')
<tr>
<td class="forty">{{ __('firefly.bill') }}</td>
<td class="w-30">{{ __('firefly.bill') }}</td>
<td>
<a href="{{ route('subscriptions.show', [$journal['bill_id']]) }}">{{ $journal['bill_name'] }}</a>
</td>
</tr>
@endif
@if(null !== $journal['location']['latitude'])
<tr>
<td class="w-30"> {{ __('firefly.location') }}</td>
<td>
<div id="map_index_0">Map here</div>
</td>
</tr>
@endif
<!-- other fields -->
@foreach(['interest_date','book_date','process_date','due_date','payment_date','invoice_date'] as $dateField)
@if(journal_has_meta($journal['transaction_journal_id'], $dateField))
<tr>
<td class="forty">{{ trans('list.' . $dateField) }}</td>
<td class="w-30">{{ trans('list.' . $dateField) }}</td>
<td>{{ journal_get_meta_date($journal['transaction_journal_id'], $dateField)->isoFormat($monthAndDayFormat) }}</td>
</tr>
@endif