N-Ways
Documentação para programadoresPágina de seguimento de entrega

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.

Dica:

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

  1. 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.
  2. 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.
  3. 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.

server.mjs
// 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

track.html
<!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_KEY está restrita aos endereços IP do seu servidor e à Frota. A_SUA_CHAVE_DE_SITE está restrita ao seu site e ao Maps. Cada createMap conta 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.