Exemplo: página de seguimento de entrega
A página que o seu cliente abre a partir da mensagem «A sua encomenda está a caminho»: o estafeta, o percurso até à porta e a hora de chegada.
Nesta página
Quando uma encomenda sai, o seu cliente recebe uma mensagem: «A sua encomenda está a caminho». A ligação abre esta página: o estafeta no mapa, o ponto de entrega e a hora de chegada, atualizados a cada poucos segundos.
Não precisa de código? Cada entrega já tem uma página de seguimento pronta: envie o trackingUrl da tarefa (https://nways.ng/t/…). Crie a sua própria página, como abaixo, quando a quiser no seu próprio site, com as suas cores, ao lado dos detalhes da encomenda. Ligações de seguimento
Como as peças se encaixam
- A sua aplicação de estafeta envia as posições para o seguimento de frota, e o N-Ways mantém atualizada a hora de chegada da entrega.
- O seu servidor lê a entrega e o respetivo veículo com uma chave de servidor (as chaves Frota nunca vão para páginas web) e responde à sua página apenas com o que o cliente deve ver.
- A página mostra-o com o SDK Maps, usando uma chave de site restrita ao seu site e ao Maps.
O servidor
Node.js 18 ou mais recente, nada para instalar. Serve a página e /track/{order}.json.
// node server.mjs (NWAYS_SERVER_KEY no ambiente)
import { readFile } from 'node:fs/promises'
import { createServer } from 'node:http'
const KEY = process.env.NWAYS_SERVER_KEY
const fleet = async (path) => {
const res = await fetch('https://nways.ng/maps/v1/fleet' + path, { headers: { 'X-NWays-Api-Key': KEY } })
const data = await res.json()
if (!res.ok) throw Object.assign(new Error(data.error?.message), { status: res.status, reason: data.error?.reason })
return data
}
// O que o cliente pode ver da sua encomenda: nem nome nem telefone do estafeta, nem histórico.
async function tracking(orderId) {
const task = await fleet(`/tasks/${encodeURIComponent(orderId)}`)
const vehicle = task.vehicleId ? await fleet(`/vehicles/${encodeURIComponent(task.vehicleId)}`) : null
const live = ['assigned', 'picked_up', 'arriving'].includes(task.status)
return {
status: task.status,
dropoff: { lat: task.dropoff.lat, lng: task.dropoff.lng },
eta: task.eta ? { at: task.eta.at, minutes: Math.round(task.eta.durationS / 60) } : null,
rider: live && vehicle?.online && vehicle.lastLocation ? { lat: vehicle.lastLocation.lat, lng: vehicle.lastLocation.lng } : null,
}
}
createServer(async (req, res) => {
const m = req.url.match(/^\/track\/([\w.:-]{1,64})\.json$/)
try {
if (m) {
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
return res.end(JSON.stringify(await tracking(m[1])))
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(await readFile(new URL('./track.html', import.meta.url)))
} catch (e) {
res.writeHead(e.reason === 'TASK_NOT_FOUND' ? 404 : 502, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: e.reason ?? 'ERROR' }))
}
}).listen(8080, () => console.log('http://localhost:8080/?order=order-1041'))A página
<!doctype html>
<html lang="pt">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<title>A sua entrega</title>
<link rel="stylesheet" href="https://nways.ng/sdk/v1/maplibre-gl.css" />
<style>
body { margin: 0; font: 16px/1.4 system-ui, sans-serif }
#map { height: 70vh }
#info { padding: 16px }
#info b { font-size: 1.5em; display: block }
</style>
</head>
<body>
<div id="map"></div>
<div id="info" aria-live="polite">A carregar…</div>
<script type="module">
import { createMap, maplibregl } from 'https://nways.ng/sdk/v1/nways-maps.standalone.js'
const order = new URLSearchParams(location.search).get('order')
const info = document.getElementById('info')
const map = createMap({ container: 'map', key: 'A_SUA_CHAVE_DE_SITE', lang: 'pt', zoom: 14 })
const home = new maplibregl.Marker({ color: '#0b7a3e' })
const rider = new maplibregl.Marker({ color: '#1a73e8' })
const WORDS = { assigned: 'O seu estafeta vai buscar a sua encomenda', picked_up: 'A caminho de si', arriving: 'A chegar agora', completed: 'Entregue', failed: 'A entrega falhou', cancelled: 'Cancelada', created: 'Em preparação' }
let framed = false
async function update() {
const res = await fetch(`/track/${encodeURIComponent(order)}.json`)
if (!res.ok) return void (info.textContent = 'Não encontramos esta entrega.')
const t = await res.json()
home.setLngLat([t.dropoff.lng, t.dropoff.lat]).addTo(map)
if (t.rider) rider.setLngLat([t.rider.lng, t.rider.lat]).addTo(map)
else rider.remove()
if (!framed) {
const box = new maplibregl.LngLatBounds([t.dropoff.lng, t.dropoff.lat], [t.dropoff.lng, t.dropoff.lat])
if (t.rider) box.extend([t.rider.lng, t.rider.lat])
map.fitBounds(box, { padding: 80, maxZoom: 16, duration: 0 })
framed = true
}
const when = t.eta ? new Date(t.eta.at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : null
info.innerHTML = ''
info.append(
Object.assign(document.createElement('b'), { textContent: when ? `Chegada por volta das ${when}` : (WORDS[t.status] ?? t.status) }),
t.eta ? `${WORDS[t.status] ?? ''} · ${t.eta.minutes} min` : '',
)
if (['completed', 'failed', 'cancelled'].includes(t.status)) clearInterval(timer)
}
const timer = setInterval(update, 10000)
update()
</script>
</body>
</html>Notas
- Duas chaves.
NWAYS_SERVER_KEYestá restrita aos endereços IP do seu servidor e à Frota.A_SUA_CHAVE_DE_SITEestá restrita ao seu site e ao Maps. CadacreateMapconta como um carregamento de mapa. - Privacidade. A página só mostra o estafeta enquanto a entrega decorre e o veículo está ligado, e nunca o seu nome ou número de telefone. Não registe as posições dos seus clientes na página.
- Atualizações mais rápidas. Consulte o servidor a cada 10 segundos, como aqui, ou envie as alterações para a página a partir do seu servidor quando chegarem os webhooks (
task.arriving,task.completed). - Português.
createMap({ lang: 'pt' })mostra os nomes do mapa em português onde existem ('fr'em francês), para condizer com as palavras da página.