(transaction) possibility to change location

This commit is contained in:
Julien Calixte
2020-03-15 14:49:16 +01:00
parent 20463fa540
commit cab5a4e46d
6 changed files with 190 additions and 54 deletions

View File

@@ -5,59 +5,115 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator' import { Component, Prop, Vue } from 'vue-property-decorator'
import mapboxgl from 'mapbox-gl/dist/mapbox-gl' import mapboxgl from 'mapbox-gl/dist/mapbox-gl'
import { debounce } from 'lodash-es'
interface Location { import ILocation from '@/models/ILocation'
name?: string
lat: number
lon: number
}
@Component @Component
export default class EarthMap extends Vue { export default class EarthMap extends Vue {
@Prop({ type: Array, default: () => [] }) @Prop({ type: Array, default: () => [] })
private locations!: Location[] private locations!: ILocation[]
@Prop({ type: Boolean, default: false })
private defineLocation!: boolean
private earth: any | null = null
private markerLocation: any | null = null
private emit = debounce((earthMap: EarthMap) => {
if (!earthMap.markerLocation) {
return
}
const lnglat = earthMap.markerLocation.getLngLat()
earthMap.$emit('located', {
lat: lnglat.lat,
lon: lnglat.lng
})
}, 150)
private mounted() { private mounted() {
mapboxgl.accessToken = process.env.VUE_APP_MAPBOX_KEY this.initEarthMap()
if (!this.earth) {
return
}
const markers = this.locations.map((location) => [ const markers = this.locations.map((location) => [
location.lon, location.lon,
location.lat location.lat
]) ])
const latitudes = this.locations.map((location: Location) => location.lat)
const longitudes = this.locations.map((location: Location) => location.lon)
const minLat = Math.min(...latitudes)
const maxLat = Math.min(...latitudes)
const minLon = Math.min(...longitudes)
const maxLon = Math.min(...longitudes)
const earth = new mapboxgl.Map({ markers.forEach((marker) => {
const el = document.createElement('i')
el.classList.add('marker', 'gg-pin-alt')
new mapboxgl.Marker(el).setLngLat(marker).addTo(this.earth)
})
}
private initEarthMap() {
mapboxgl.accessToken = process.env.VUE_APP_MAPBOX_KEY
const latitudes = this.locations.map((location: ILocation) => location.lat)
const longitudes = this.locations.map((location: ILocation) => location.lon)
const minLat = Math.min(...latitudes)
const maxLat = Math.max(...latitudes)
const minLon = Math.min(...longitudes)
const maxLon = Math.max(...longitudes)
// [sw, ne]
const bounds = this.locations.length
? [
[minLon, minLat],
[maxLon, maxLat]
]
: undefined
this.earth = new mapboxgl.Map({
container: 'earth', container: 'earth',
style: 'mapbox://styles/mapbox/streets-v11', style: 'mapbox://styles/mapbox/streets-v11',
bounds: [ bounds,
[minLon, minLat],
[maxLon, maxLat]
],
fitBoundsOptions: { fitBoundsOptions: {
padding: 15, padding: 30,
maxZoom: 12 maxZoom: 12
} }
}) })
earth.addControl(new mapboxgl.NavigationControl()) this.earth.addControl(new mapboxgl.NavigationControl())
const geolocControl = new mapboxgl.GeolocateControl({
positionOptions: {
enableHighAccuracy: true
},
trackUserLocation: true
})
markers.forEach((marker) => { this.earth.addControl(geolocControl)
new mapboxgl.Marker().setLngLat(marker).addTo(earth)
if (!this.defineLocation) {
return
}
geolocControl.trigger()
const el = document.createElement('i')
el.classList.add('gg-pin-alt', 'marker', 'marker-location')
this.markerLocation = new mapboxgl.Marker(el)
.setLngLat(this.earth.getCenter())
.addTo(this.earth)
this.earth.on('move', (evt: any) => {
this.markerLocation.setLngLat(this.earth.getCenter())
this.emit(this)
}) })
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
@import '../styles/earth-map.css'; @import '../styles/earth-map.css';
.earth-container { .earth-container {
min-height: 50vh; min-height: 50vh;
height: 100%; height: 100%;
} }
.marker-location {
color: var(--primary-color);
z-index: 1;
}
</style> </style>

View File

@@ -94,15 +94,27 @@
<label class="label">Localisation</label> <label class="label">Localisation</label>
</div> </div>
<div class="field-body"> <div class="field-body">
<div class="field"> <div
<div class="control" v-if="transaction.location"> class="field"
:class="{ 'has-addons': !!transaction.location }"
>
<div class="control">
<input <input
type="text" type="text"
readonly readonly
class="input" class="input"
v-if="transaction.location"
v-model="transaction.location.place" v-model="transaction.location.place"
/> />
</div> </div>
<div class="control set-location">
<button
class="button is-primary"
@click="displayLocationModal = true"
>
définir
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -176,6 +188,33 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal" :class="{ 'is-active': displayLocationModal }">
<div class="modal-background"></div>
<div class="modal-card">
<section>
<earth-map
v-if="displayLocationModal"
:locations="
transaction.location ? [transaction.location] : undefined
"
:define-location="true"
@located="located"
/>
</section>
<footer class="modal-card-foot">
<button
class="button is-primary"
:class="{ 'is-loading': isSetLocation }"
@click="validLocation"
>
valider
</button>
<button class="button" @click="displayLocationModal = false">
annuler
</button>
</footer>
</div>
</div>
<fab-button <fab-button
@valid="submitTransaction" @valid="submitTransaction"
:margin="true" :margin="true"
@@ -203,11 +242,14 @@ import formatDate from '@/utils/format-date'
import notif from '@/utils/notif' import notif from '@/utils/notif'
import { money } from '@/utils/filters' import { money } from '@/utils/filters'
import { confirmation, alertMessage, findContrastColor } from '@/utils' import { confirmation, alertMessage, findContrastColor } from '@/utils'
import ILocation from '@/models/ILocation'
import MapService from '../services/MapService'
const today: Date = new Date() const today: Date = new Date()
@Component({ @Component({
components: { components: {
'earth-map': () => import('@/components/EarthMap.vue'),
'fab-button': () => import('@/components/FabButton.vue'), 'fab-button': () => import('@/components/FabButton.vue'),
'transaction-split': () => import('@/components/TransactionSplit.vue'), 'transaction-split': () => import('@/components/TransactionSplit.vue'),
'transaction-tag-update': () => 'transaction-tag-update': () =>
@@ -226,13 +268,15 @@ export default class TransactionCreate extends Vue {
public tag: TransactionTag = TransactionTag.None public tag: TransactionTag = TransactionTag.None
public today: string = formatDate(today) public today: string = formatDate(today)
public date: string = formatDate(today) public date: string = formatDate(today)
public location: string = ''
public currency: ICurrency | null = null public currency: ICurrency | null = null
public payBy: string = '' public payBy: string = ''
public payFor: ISplit[] = [] public payFor: ISplit[] = []
public maxAmount: number = 1000000 public maxAmount: number = 1000000
public noTag: string = TransactionTag.None public noTag: string = TransactionTag.None
public transactionTagLabel: typeof TransactionTagLabel = TransactionTagLabel public transactionTagLabel: typeof TransactionTagLabel = TransactionTagLabel
public displayLocationModal = false
public isSetLocation = false
public location: ILocation | null = null
public async created(): Promise<void> { public async created(): Promise<void> {
this.setData(this.transaction) this.setData(this.transaction)
@@ -325,6 +369,29 @@ export default class TransactionCreate extends Vue {
this.payFor.forEach((p: ISplit) => (p.weight = p.weight || 1)) this.payFor.forEach((p: ISplit) => (p.weight = p.weight || 1))
} }
public located(location: ILocation) {
this.location = location
}
public async validLocation() {
try {
this.isSetLocation = true
if (this.location) {
const place = await MapService.getPlace(this.location)
const location = {
...this.location,
place
}
this.$set<ILocation>(this.transaction, 'location', location)
}
} catch (error) {
this.$set(this.transaction, 'location', this.transaction.location)
} finally {
this.displayLocationModal = false
this.isSetLocation = false
}
}
public get payForUsers(): ISplit[] { public get payForUsers(): ISplit[] {
return this.payFor.filter((p, index) => p.weight > 0) return this.payFor.filter((p, index) => p.weight > 0)
} }
@@ -394,4 +461,7 @@ export default class TransactionCreate extends Vue {
.help { .help {
text-align: left; text-align: left;
} }
.set-location {
text-align: center;
}
</style> </style>

View File

@@ -4,7 +4,25 @@ import ILocationQuery from '@/models/ILocationQuery'
class MapService { class MapService {
private key: string = process.env.VUE_APP_MAP_KEY || '' private key: string = process.env.VUE_APP_MAP_KEY || ''
public async getLocation(location: ILocation): Promise<string> { public async getPosition(): Promise<ILocation | null> {
const position = await this.getCurrentPosition()
if (!position) {
return null
}
const { coords } = position
const location = {
lat: coords.latitude,
lon: coords.longitude
}
const place = await this.getPlace(location)
return {
...location,
place
}
}
public async getPlace(location: ILocation): Promise<string> {
const result = await fetch(this.url(location.lat, location.lon)) const result = await fetch(this.url(location.lat, location.lon))
if (!result) { if (!result) {
return '' return ''
@@ -25,6 +43,15 @@ class MapService {
private url(latitude: number, longitude: number) { private url(latitude: number, longitude: number) {
return `https://dev.virtualearth.net/REST/v1/Locations/${latitude},${longitude}?key=${this.key}` return `https://dev.virtualearth.net/REST/v1/Locations/${latitude},${longitude}?key=${this.key}`
} }
private getCurrentPosition(): Promise<Position | null> {
return new Promise((resolve, reject) => {
if (!('geolocation' in navigator)) {
resolve(null)
}
return navigator.geolocation.getCurrentPosition(resolve, reject)
})
}
} }
export default new MapService() export default new MapService()

View File

@@ -1,9 +1,9 @@
// sass-lint:disable final-newline empty-line-between-blocks class-name-format no-url-protocols no-url-domains nesting-depth // sass-lint:disable final-newline empty-line-between-blocks class-name-format no-url-protocols no-url-domains nesting-depth
@charset 'utf-8'; @charset 'utf-8';
@import url('https://fonts.googleapis.com/css?family=Nunito|Raleway|Open+Sans&display=swap'); @import url('https://fonts.googleapis.com/css?family=Nunito|Raleway|Open+Sans&display=swap');
@import url('https://css.gg/c?=|pin-alt');
@import './framework'; @import './framework';
@import './transitions'; @import './transitions';
:root { :root {
--primary-color: #{$primary}; --primary-color: #{$primary};
--primary-font-color: #{$main}; --primary-font-color: #{$main};

View File

@@ -78,9 +78,3 @@ export const toHex = (text: string): string => {
.join('') .join('')
) )
} }
export const getCurrentPosition = (options = {}): Promise<Position> => {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options)
})
}

View File

@@ -19,7 +19,6 @@ import ITransaction from '@/models/ITransaction'
import IUser from '@/models/IUser' import IUser from '@/models/IUser'
import accountService from '@/services/AccountService' import accountService from '@/services/AccountService'
import transactionService from '@/services/TransactionService' import transactionService from '@/services/TransactionService'
import { getCurrentPosition } from '@/utils'
import mapService from '@/services/MapService' import mapService from '@/services/MapService'
const today: Date = new Date() const today: Date = new Date()
@@ -48,25 +47,15 @@ export default class TransactionNew extends Vue {
} }
try { try {
if ('geolocation' in navigator) { if (!this.transaction) {
getCurrentPosition().then(({ coords }) => { return
mapService
.getLocation({
lat: coords.latitude,
lon: coords.longitude
})
.then((place) => {
if (!this.transaction) {
return
}
this.$set(this.transaction, 'location', {
lat: coords.latitude,
lon: coords.longitude,
place
})
})
})
} }
const location = await mapService.getPosition()
if (!location) {
return
}
this.$set(this.transaction, 'location', { ...location })
} catch (error) { } catch (error) {
// tslint:disable-next-line: no-console // tslint:disable-next-line: no-console
console.error(error) console.error(error)