CRM/Contacts Google Maps Integration
This commit is contained in:
105
src/components/AddressMap.vue
Normal file
105
src/components/AddressMap.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="address-map">
|
||||
<div class="toolbar-row mb-2">
|
||||
<h3 class="section-title">Location map</h3>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="hasAddress"
|
||||
:href="linkUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
size="small"
|
||||
variant="text"
|
||||
prepend-icon="mdi-open-in-new"
|
||||
>Open in Google Maps</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasAddress" class="map-empty quiet text-body-2">
|
||||
Enter an address above to preview the location on a map.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="!show" class="map-empty">
|
||||
<v-btn variant="tonal" prepend-icon="mdi-map-search-outline" @click="show = true">Show map</v-btn>
|
||||
<div class="quiet text-caption mt-2">{{ activeQuery }}</div>
|
||||
</div>
|
||||
<iframe
|
||||
v-else
|
||||
:key="embedUrl"
|
||||
:src="embedUrl"
|
||||
class="map-frame"
|
||||
title="Location map"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
allowfullscreen
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Embeds an interactive Google map for an address using the keyless maps.google.com `output=embed`
|
||||
// endpoint (no API key required). The address is debounced so live typing in the form doesn't reload
|
||||
// the iframe on every keystroke.
|
||||
export default {
|
||||
props: {
|
||||
line1: { type: String, default: '' },
|
||||
line2: { type: String, default: '' },
|
||||
city: { type: String, default: '' },
|
||||
region: { type: String, default: '' },
|
||||
postalCode: { type: String, default: '' },
|
||||
country: { type: String, default: '' },
|
||||
// Start with the map already visible (e.g. when viewing an existing contact that has an address).
|
||||
autoShow: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return { activeQuery: '', show: this.autoShow, timer: null };
|
||||
},
|
||||
computed: {
|
||||
formattedAddress() {
|
||||
return [this.line1, this.line2, this.city, this.region, this.postalCode, this.country]
|
||||
.map((part) => String(part || '').trim())
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
},
|
||||
hasAddress() {
|
||||
return this.formattedAddress.length > 0;
|
||||
},
|
||||
embedUrl() {
|
||||
return `https://maps.google.com/maps?q=${encodeURIComponent(this.activeQuery)}&z=15&output=embed`;
|
||||
},
|
||||
linkUrl() {
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(this.activeQuery)}`;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Debounce: only refresh the embedded query a beat after the user stops typing.
|
||||
formattedAddress(value) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => { this.activeQuery = value; }, 600);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.activeQuery = this.formattedAddress;
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-frame {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
.map-empty {
|
||||
border: 1px dashed rgba(var(--v-theme-on-surface), 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -84,6 +84,18 @@
|
||||
<v-text-field v-model="form.country" label="Country" />
|
||||
</div>
|
||||
|
||||
<AddressMap
|
||||
:key="form.id || 'new'"
|
||||
class="mt-3"
|
||||
:line1="form.address_line1"
|
||||
:line2="form.address_line2"
|
||||
:city="form.city"
|
||||
:region="form.state_region"
|
||||
:postal-code="form.postal_code"
|
||||
:country="form.country"
|
||||
:auto-show="Boolean(form.id && (form.address_line1 || form.city))"
|
||||
/>
|
||||
|
||||
<v-textarea v-model="form.notes" class="mt-3" label="Notes" rows="2" />
|
||||
|
||||
<template v-if="form.id">
|
||||
@@ -120,6 +132,7 @@
|
||||
|
||||
<script>
|
||||
import DataTable from '../components/DataTable.vue';
|
||||
import AddressMap from '../components/AddressMap.vue';
|
||||
import { apiRequest } from '../services/api';
|
||||
import { contactTypeItems, creditTermItems } from '../constants';
|
||||
import { clonePlain } from '../utils/clone';
|
||||
@@ -135,7 +148,7 @@ function blankContact() {
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { DataTable },
|
||||
components: { DataTable, AddressMap },
|
||||
props: {
|
||||
token: { type: String, required: true },
|
||||
canDelete: { type: Boolean, default: false }
|
||||
|
||||
Reference in New Issue
Block a user