+
+ {{! ── Live preview ───────────────────────────────────────────── }}
+
+
{{t "tracking-page.settings.preview"}}
+
+
+
{{t "tracking-page.settings.preview-help"}}
+
+
+ {{/if}}
+
+
+
diff --git a/addon/utils/tracking-page-theme.js b/addon/utils/tracking-page-theme.js
new file mode 100644
index 000000000..44cf283b6
--- /dev/null
+++ b/addon/utils/tracking-page-theme.js
@@ -0,0 +1,123 @@
+/**
+ * Colour tokens for the customer tracking page, derived from a company's one accent colour.
+ *
+ * The server sanitizes and checks the same values (TrackingPageConfig); this mirrors it so
+ * the settings preview and the page itself re-theme live without a round trip.
+ */
+export const DEFAULT_ACCENT = '#1F5FA8';
+export const LIGHT_INK = '#FFFFFF';
+export const DARK_INK = '#14191A';
+
+const LIGHT_BASE = {
+ bg: '#F3F4F1',
+ surface: '#FFFFFF',
+ surface2: '#ECEEEA',
+ text: '#14191A',
+ muted: '#545D5F',
+ border: '#D9DED9',
+ warn: '#A8430B',
+ 'warn-soft': '#FBEADF',
+ neutral: '#5F686A',
+};
+
+const DARK_BASE = {
+ bg: '#0D1112',
+ surface: '#151A1B',
+ surface2: '#1D2324',
+ text: '#E9EDEA',
+ muted: '#A1ABAB',
+ border: '#2B3335',
+ warn: '#F2A262',
+ 'warn-soft': '#3A2416',
+ neutral: '#8E9899',
+};
+
+/**
+ * `#RGB` or `#RRGGBB` as uppercase `#RRGGBB`, or null.
+ */
+export function normalizeHex(value) {
+ const match = typeof value === 'string' ? value.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i) : null;
+ if (!match) {
+ return null;
+ }
+
+ let hex = match[1];
+ if (hex.length === 3) {
+ hex = hex.replace(/./g, (char) => char + char);
+ }
+
+ return `#${hex.toUpperCase()}`;
+}
+
+function channels(hex) {
+ const value = hex.replace('#', '');
+ return [0, 2, 4].map((offset) => parseInt(value.slice(offset, offset + 2), 16));
+}
+
+function luminance(hex) {
+ const [r, g, b] = channels(hex).map((channel) => {
+ const value = channel / 255;
+ return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
+ });
+
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
+}
+
+/**
+ * The WCAG contrast ratio of two `#RRGGBB` colours, rounded to two decimals.
+ */
+export function contrastRatio(a, b) {
+ const lighter = Math.max(luminance(a), luminance(b));
+ const darker = Math.min(luminance(a), luminance(b));
+
+ return Math.round(((lighter + 0.05) / (darker + 0.05)) * 100) / 100;
+}
+
+/**
+ * The text colour readable on an accent: white when it reaches 4.5:1, otherwise dark.
+ */
+export function inkFor(accent) {
+ return contrastRatio(accent, LIGHT_INK) >= 4.5 ? LIGHT_INK : DARK_INK;
+}
+
+/**
+ * A lighter accent for dark mode: the accent mixed toward white until it reads on the dark surface.
+ */
+export function darkAccentFor(accent) {
+ const [r, g, b] = channels(accent);
+ for (let weight = 0.25; weight <= 0.85; weight += 0.05) {
+ const mixed = [r, g, b].map((channel) => Math.round(channel + (255 - channel) * weight));
+ const hex = `#${mixed.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`.toUpperCase();
+ if (contrastRatio(hex, DARK_BASE.surface) >= 4.5) {
+ return hex;
+ }
+ }
+
+ return '#FFFFFF';
+}
+
+/**
+ * The CSS custom properties for a page in the given mode, as a style string.
+ *
+ * @param {Object} branding the config's branding group
+ * @param {Boolean} dark whether the page renders dark
+ */
+export default function trackingPageTheme(branding = {}, dark = false) {
+ const accent = normalizeHex(branding.accent) ?? DEFAULT_ACCENT;
+ const darkAccent = normalizeHex(branding.accent_dark) ?? darkAccentFor(accent);
+ const modeAccent = dark ? darkAccent : accent;
+ const base = dark ? DARK_BASE : LIGHT_BASE;
+ const tokens = {
+ ...base,
+ accent: modeAccent,
+ ink: inkFor(modeAccent),
+ 'accent-soft': `color-mix(in oklab, ${modeAccent} 14%, ${base.surface})`,
+ };
+
+ const declarations = [`color-scheme: ${dark ? 'dark' : 'light'}`];
+ for (const name of Object.keys(tokens)) {
+ declarations.push(`--tp-${name}: ${tokens[name]}`);
+ }
+
+ return declarations.join('; ');
+}
diff --git a/app/components/admin/tracking-page-settings.js b/app/components/admin/tracking-page-settings.js
new file mode 100644
index 000000000..1c9909605
--- /dev/null
+++ b/app/components/admin/tracking-page-settings.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/fleetops-engine/components/admin/tracking-page-settings';
diff --git a/app/components/tracking-page/settings-preview.js b/app/components/tracking-page/settings-preview.js
new file mode 100644
index 000000000..ae1b64f67
--- /dev/null
+++ b/app/components/tracking-page/settings-preview.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/fleetops-engine/components/tracking-page/settings-preview';
diff --git a/app/controllers/settings/tracking-page.js b/app/controllers/settings/tracking-page.js
new file mode 100644
index 000000000..36aeae542
--- /dev/null
+++ b/app/controllers/settings/tracking-page.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/fleetops-engine/controllers/settings/tracking-page';
diff --git a/app/routes/settings/tracking-page.js b/app/routes/settings/tracking-page.js
new file mode 100644
index 000000000..60bd69c6c
--- /dev/null
+++ b/app/routes/settings/tracking-page.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/fleetops-engine/routes/settings/tracking-page';
diff --git a/app/templates/settings/tracking-page.js b/app/templates/settings/tracking-page.js
new file mode 100644
index 000000000..ff6633827
--- /dev/null
+++ b/app/templates/settings/tracking-page.js
@@ -0,0 +1 @@
+export { default } from '@fleetbase/fleetops-engine/templates/settings/tracking-page';
diff --git a/app/utils/tracking-page-theme.js b/app/utils/tracking-page-theme.js
new file mode 100644
index 000000000..d705ae50f
--- /dev/null
+++ b/app/utils/tracking-page-theme.js
@@ -0,0 +1 @@
+export { default, normalizeHex, contrastRatio, inkFor, darkAccentFor, DEFAULT_ACCENT, LIGHT_INK, DARK_INK } from '@fleetbase/fleetops-engine/utils/tracking-page-theme';
diff --git a/server/src/Auth/Schemas/FleetOps.php b/server/src/Auth/Schemas/FleetOps.php
index 8a652e2f0..247e1da84 100644
--- a/server/src/Auth/Schemas/FleetOps.php
+++ b/server/src/Auth/Schemas/FleetOps.php
@@ -229,6 +229,11 @@ class FleetOps
'actions' => [],
'remove_actions' => ['delete', 'export', 'create'],
],
+ [
+ 'name' => 'tracking-page-settings', // the customer tracking page: branding, access and what verified visitors see
+ 'actions' => [],
+ 'remove_actions' => ['delete', 'export', 'create'],
+ ],
[
'name' => 'telematics-settings', // organization device event and position history preferences
'action' => [],
diff --git a/server/src/Http/Controllers/Internal/v1/SettingController.php b/server/src/Http/Controllers/Internal/v1/SettingController.php
index 296363a07..3699c4bf7 100644
--- a/server/src/Http/Controllers/Internal/v1/SettingController.php
+++ b/server/src/Http/Controllers/Internal/v1/SettingController.php
@@ -5,10 +5,13 @@
use Fleetbase\FleetOps\Jobs\DispatchTelematicsRetentionJobs;
use Fleetbase\FleetOps\Support\Telematics\Retention\RetentionPolicy;
use Fleetbase\FleetOps\Support\Telematics\Telemetry\Queue;
+use Fleetbase\FleetOps\Support\TrackingPage\TrackingPageConfig;
+use Fleetbase\FleetOps\Support\Utils;
use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
use Fleetbase\FleetOps\Traits\AuthorizesMethods;
use Fleetbase\Http\Controllers\Controller;
use Fleetbase\Models\Setting;
+use Fleetbase\Services\SmsService;
use Fleetbase\Support\Auth;
use Fleetbase\Support\NotificationRegistry;
use Illuminate\Database\MySqlConnection;
@@ -47,6 +50,11 @@ public function __construct()
'saveTelematicsSettings' => 'update telematics-settings',
'getTelematicsStorageUsage' => 'admin',
'runTelematicsRetention' => 'admin',
+ 'getTrackingPageSettings' => 'view tracking-page-settings',
+ 'saveTrackingPageSettings' => 'update tracking-page-settings',
+ 'validateTrackingPageSlug' => 'update tracking-page-settings',
+ 'getAdminTrackingPageSettings' => 'admin',
+ 'saveAdminTrackingPageSettings' => 'admin',
]);
}
@@ -1074,6 +1082,194 @@ protected function storageTableStatistics(array $tables): array
return $statistics;
}
+ /**
+ * The company's customer tracking page settings, with what the settings screen needs to
+ * explain them: whether the slug is usable, whether SMS can be sent, and whether the
+ * customer portal is there for sign-in.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function getTrackingPageSettings()
+ {
+ return response()->json($this->trackingPageView($this->trackingPageConfig()));
+ }
+
+ /**
+ * Save the company's tracking page settings.
+ *
+ * The slug is indexed so the public page can find the company in one read. A slug that
+ * is invalid, reserved or taken can't be saved while the organization page is on; while
+ * it is off, the slug is kept but not indexed.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function saveTrackingPageSettings(Request $request)
+ {
+ $previous = $this->trackingPageConfig();
+ $config = TrackingPageConfig::sanitize((array) $request->input('trackingPage', []), $this->trackingPageAdminConfig(), $this->trackingPageCompanyName());
+ $slug = $config['org_page']['slug'];
+ $validation = $this->trackingPageSlugValidation($slug);
+
+ if ($config['org_page']['enabled'] && !$validation['valid']) {
+ return response()->error($validation['message'], 422);
+ }
+
+ if ($validation['valid']) {
+ $previousSlug = $previous['org_page']['slug'];
+ if ($previousSlug !== $slug && $this->trackingPageSlugOwner($previousSlug) === $this->trackingPageCompanyUuid()) {
+ $this->configureSetting(TrackingPageConfig::SLUG_INDEX_PREFIX . $previousSlug, null);
+ }
+
+ $this->configureSetting(TrackingPageConfig::SLUG_INDEX_PREFIX . $slug, $this->trackingPageCompanyUuid());
+ }
+
+ $this->configureCompanySetting(TrackingPageConfig::SETTING_KEY, $config);
+
+ return response()->json($this->trackingPageView($config));
+ }
+
+ /**
+ * Check a slug while it is typed.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function validateTrackingPageSlug(Request $request)
+ {
+ $slug = strtolower(trim((string) $request->input('slug', '')));
+
+ return response()->json(array_merge(['slug' => $slug], $this->trackingPageSlugValidation($slug)));
+ }
+
+ /**
+ * Instance-wide defaults for the generic tracking page.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function getAdminTrackingPageSettings()
+ {
+ return response()->json($this->trackingPageAdminConfig());
+ }
+
+ /**
+ * Save the instance-wide defaults for the generic tracking page.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function saveAdminTrackingPageSettings(Request $request)
+ {
+ $config = TrackingPageConfig::sanitizeAdmin((array) $request->input('trackingPage', []));
+ $this->configureSetting(TrackingPageConfig::ADMIN_SETTING_KEY, $config);
+
+ return response()->json($config);
+ }
+
+ protected function trackingPageConfig(): array
+ {
+ return TrackingPageConfig::sanitize(
+ (array) $this->lookupCompanySetting(TrackingPageConfig::SETTING_KEY, []),
+ $this->trackingPageAdminConfig(),
+ $this->trackingPageCompanyName()
+ );
+ }
+
+ protected function trackingPageAdminConfig(): array
+ {
+ return TrackingPageConfig::sanitizeAdmin((array) $this->lookupSetting(TrackingPageConfig::ADMIN_SETTING_KEY, []));
+ }
+
+ /**
+ * The saved config plus the read-only facts the settings screen shows beside it.
+ */
+ protected function trackingPageView(array $config): array
+ {
+ $accent = $config['branding']['accent'];
+
+ return array_merge($config, [
+ 'slug_validation' => $this->trackingPageSlugValidation($config['org_page']['slug']),
+ 'accent_ink' => TrackingPageConfig::inkFor($accent),
+ 'accent_contrast' => TrackingPageConfig::contrastRatio($accent, TrackingPageConfig::inkFor($accent)),
+ 'sms_available' => $this->smsProviderConfigured(),
+ 'customer_portal_installed' => $this->customerPortalInstalled(),
+ 'admin' => $this->trackingPageAdminConfig(),
+ ]);
+ }
+
+ /**
+ * @return array{valid: bool, code: string, message: string}
+ */
+ protected function trackingPageSlugValidation(string $slug): array
+ {
+ $error = TrackingPageConfig::slugError($slug);
+ if ($error === 'invalid') {
+ return ['valid' => false, 'code' => 'invalid', 'message' => 'Use 3 to 40 lowercase letters, numbers and single hyphens, starting and ending with a letter or number.'];
+ }
+
+ if ($error === 'reserved') {
+ return ['valid' => false, 'code' => 'reserved', 'message' => 'This address is reserved. Please choose another.'];
+ }
+
+ $owner = $this->trackingPageSlugOwner($slug);
+ if ($owner !== null && $owner !== $this->trackingPageCompanyUuid()) {
+ return ['valid' => false, 'code' => 'taken', 'message' => 'Another organization already uses this address.'];
+ }
+
+ return ['valid' => true, 'code' => 'available', 'message' => 'This address is available.'];
+ }
+
+ protected function trackingPageSlugOwner(string $slug): ?string
+ {
+ $owner = $this->lookupSetting(TrackingPageConfig::SLUG_INDEX_PREFIX . $slug);
+
+ return is_string($owner) && $owner !== '' ? $owner : null;
+ }
+
+ protected function trackingPageCompanyUuid(): ?string
+ {
+ return data_get($this->currentCompany(), 'uuid');
+ }
+
+ protected function trackingPageCompanyName(): ?string
+ {
+ return data_get($this->currentCompany(), 'name');
+ }
+
+ /**
+ * Whether the instance can send SMS. Twilio is listed as always available, so it counts
+ * only with credentials; other providers report their own configuration.
+ */
+ protected function smsProviderConfigured(): bool
+ {
+ if (filled(config('services.twilio.sid')) && filled(config('services.twilio.token'))) {
+ return true;
+ }
+
+ return collect($this->smsProviders())
+ ->except(SmsService::PROVIDER_TWILIO)
+ ->contains(fn ($provider) => (bool) data_get($provider, 'available'));
+ }
+
+ protected function smsProviders(): array
+ {
+ // @codeCoverageIgnoreStart
+ // Reads the live SMS provider configuration; tests replace this seam.
+ return (new SmsService())->getAvailableProviders();
+ // @codeCoverageIgnoreEnd
+ }
+
+ protected function customerPortalInstalled(): bool
+ {
+ return collect($this->installedFleetbaseExtensions())
+ ->contains(fn ($package) => data_get($package, 'name') === 'fleetbase/customer-portal-api');
+ }
+
+ protected function installedFleetbaseExtensions(): array
+ {
+ // @codeCoverageIgnoreStart
+ // Reads the installed composer packages; tests replace this seam.
+ return Utils::getInstalledFleetbaseExtensions();
+ // @codeCoverageIgnoreEnd
+ }
+
protected function dispatchTelematicsPrune(): void
{
Queue::dispatch(new DispatchTelematicsRetentionJobs());
diff --git a/server/src/Support/TrackingPage/TrackingPageConfig.php b/server/src/Support/TrackingPage/TrackingPageConfig.php
new file mode 100644
index 000000000..646c79de1
--- /dev/null
+++ b/server/src/Support/TrackingPage/TrackingPageConfig.php
@@ -0,0 +1,264 @@
+ [
+ 'enabled' => static::bool(data_get($input, 'generic_page.enabled'), true),
+ ],
+ 'branding' => [
+ 'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
+ 'accent' => static::color(data_get($input, 'branding.accent')) ?? static::DEFAULT_ACCENT,
+ 'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
+ 'support_email' => static::email(data_get($input, 'branding.support_email')),
+ 'website' => static::url(data_get($input, 'branding.website')),
+ 'powered_by' => static::bool(data_get($input, 'branding.powered_by'), true),
+ ],
+ ];
+ }
+
+ /**
+ * A company's config, with every key present and every value valid.
+ *
+ * @param array $admin the sanitized instance defaults
+ * @param string|null $companyName used for the default slug
+ */
+ public static function sanitize(array $input, array $admin = [], ?string $companyName = null): array
+ {
+ $admin = static::sanitizeAdmin($admin);
+ $locale = static::oneOf(data_get($input, 'branding.default_locale'), static::LOCALES, 'en-us');
+
+ return [
+ 'org_page' => [
+ 'enabled' => static::bool(data_get($input, 'org_page.enabled'), false),
+ 'slug' => static::normalizeSlug(data_get($input, 'org_page.slug')) ?: static::slugify((string) $companyName),
+ ],
+ 'generic_page' => [
+ 'allowed' => static::bool(data_get($input, 'generic_page.allowed'), true),
+ ],
+ 'links' => [
+ 'target' => static::oneOf(data_get($input, 'links.target'), ['auto', 'org', 'generic'], 'auto'),
+ ],
+ 'branding' => [
+ 'display_name' => static::text(data_get($input, 'branding.display_name'), 80),
+ 'logo_uuid' => static::text(data_get($input, 'branding.logo_uuid'), 64),
+ 'accent' => static::color(data_get($input, 'branding.accent')) ?? $admin['branding']['accent'],
+ 'accent_dark' => static::color(data_get($input, 'branding.accent_dark')),
+ 'support_phone' => static::text(data_get($input, 'branding.support_phone'), 32),
+ 'support_email' => static::email(data_get($input, 'branding.support_email')),
+ 'website' => static::url(data_get($input, 'branding.website')),
+ 'powered_by' => static::bool(data_get($input, 'branding.powered_by'), $admin['branding']['powered_by']),
+ 'theme' => static::oneOf(data_get($input, 'branding.theme'), ['system', 'light'], 'system'),
+ 'default_locale' => $locale,
+ 'locales' => static::locales(data_get($input, 'branding.locales'), $locale),
+ ],
+ 'access' => [
+ 'public_status' => static::bool(data_get($input, 'access.public_status'), true),
+ 'channels' => [
+ 'sms' => static::bool(data_get($input, 'access.channels.sms'), true),
+ 'email' => static::bool(data_get($input, 'access.channels.email'), true),
+ ],
+ 'session_hours' => static::clampInt(data_get($input, 'access.session_hours'), 1, 168, 24),
+ 'account_sign_in' => static::bool(data_get($input, 'access.account_sign_in'), true),
+ 'account_upsell' => static::bool(data_get($input, 'access.account_upsell'), true),
+ ],
+ 'visibility' => [
+ 'map' => static::bool(data_get($input, 'visibility.map'), true),
+ 'driver_name' => static::bool(data_get($input, 'visibility.driver_name'), true),
+ 'vehicle' => static::bool(data_get($input, 'visibility.vehicle'), true),
+ 'driver_contact' => static::oneOf(data_get($input, 'visibility.driver_contact'), ['company', 'driver'], 'company'),
+ 'items' => static::bool(data_get($input, 'visibility.items'), true),
+ 'item_prices' => static::bool(data_get($input, 'visibility.item_prices'), false),
+ 'pod_photo' => static::bool(data_get($input, 'visibility.pod_photo'), true),
+ 'pod_signature' => static::bool(data_get($input, 'visibility.pod_signature'), true),
+ 'instructions_edit' => static::bool(data_get($input, 'visibility.instructions_edit'), false),
+ 'report_problem' => static::bool(data_get($input, 'visibility.report_problem'), true),
+ ],
+ ];
+ }
+
+ /**
+ * Why a slug can't be used, or null when its shape is fine. Availability is the caller's check.
+ *
+ * @return string|null `invalid` or `reserved`
+ */
+ public static function slugError(?string $slug): ?string
+ {
+ if (!is_string($slug) || preg_match('/^[a-z0-9](?:[a-z0-9-]{1,38})[a-z0-9]$/', $slug) !== 1 || str_contains($slug, '--')) {
+ return 'invalid';
+ }
+
+ return in_array($slug, static::RESERVED_SLUGS, true) ? 'reserved' : null;
+ }
+
+ /**
+ * A slug made from a company name: lowercase, hyphenated, 3 to 40 characters.
+ */
+ public static function slugify(string $name): string
+ {
+ $slug = trim(Str::limit(Str::slug($name), 40, ''), '-');
+
+ if (strlen($slug) >= 3) {
+ return $slug;
+ }
+
+ return $slug === '' ? 'company-tracking' : $slug . '-tracking';
+ }
+
+ /**
+ * The text colour readable on an accent: white when it reaches 4.5:1, otherwise dark.
+ */
+ public static function inkFor(string $accent): string
+ {
+ return static::contrastRatio($accent, static::LIGHT_INK) >= 4.5 ? static::LIGHT_INK : static::DARK_INK;
+ }
+
+ /**
+ * The WCAG contrast ratio of two `#RRGGBB` colours.
+ */
+ public static function contrastRatio(string $a, string $b): float
+ {
+ $lighter = max(static::luminance($a), static::luminance($b));
+ $darker = min(static::luminance($a), static::luminance($b));
+
+ return round(($lighter + 0.05) / ($darker + 0.05), 2);
+ }
+
+ protected static function luminance(string $hex): float
+ {
+ $channels = array_map(function (string $pair) {
+ $value = hexdec($pair) / 255;
+
+ return $value <= 0.03928 ? $value / 12.92 : (($value + 0.055) / 1.055) ** 2.4;
+ }, str_split(ltrim($hex, '#'), 2));
+
+ return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2];
+ }
+
+ protected static function normalizeSlug(mixed $value): string
+ {
+ return is_string($value) ? strtolower(trim($value)) : '';
+ }
+
+ protected static function bool(mixed $value, bool $default): bool
+ {
+ if (is_bool($value)) {
+ return $value;
+ }
+
+ // filter_var() reads null as false, so a missing value must fall back explicitly.
+ if ($value === null || $value === '') {
+ return $default;
+ }
+
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $default;
+ }
+
+ protected static function text(mixed $value, int $max): ?string
+ {
+ if (!is_string($value) && !is_numeric($value)) {
+ return null;
+ }
+
+ $value = trim((string) $value);
+
+ return $value === '' ? null : mb_substr($value, 0, $max);
+ }
+
+ protected static function email(mixed $value): ?string
+ {
+ $value = static::text($value, 191);
+
+ return $value !== null && filter_var($value, FILTER_VALIDATE_EMAIL) ? $value : null;
+ }
+
+ protected static function url(mixed $value): ?string
+ {
+ $value = static::text($value, 255);
+
+ return $value !== null && preg_match('#^https?://#i', $value) === 1 && filter_var($value, FILTER_VALIDATE_URL) ? $value : null;
+ }
+
+ /**
+ * `#RGB` or `#RRGGBB` as uppercase `#RRGGBB`, or null.
+ */
+ protected static function color(mixed $value): ?string
+ {
+ if (!is_string($value) || preg_match('/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i', trim($value), $match) !== 1) {
+ return null;
+ }
+
+ $hex = $match[1];
+ if (strlen($hex) === 3) {
+ $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
+ }
+
+ return '#' . strtoupper($hex);
+ }
+
+ protected static function oneOf(mixed $value, array $allowed, string $default): string
+ {
+ return is_string($value) && in_array($value, $allowed, true) ? $value : $default;
+ }
+
+ protected static function clampInt(mixed $value, int $min, int $max, int $default): int
+ {
+ if (!is_numeric($value)) {
+ return $default;
+ }
+
+ return max($min, min($max, (int) $value));
+ }
+
+ /**
+ * The enabled locales, always including the default one; every locale when none are valid.
+ */
+ protected static function locales(mixed $value, string $default): array
+ {
+ $locales = array_values(array_intersect(static::LOCALES, is_array($value) ? $value : []));
+ if ($locales === []) {
+ return static::LOCALES;
+ }
+
+ return in_array($default, $locales, true) ? $locales : array_merge([$default], $locales);
+ }
+}
diff --git a/server/src/routes.php b/server/src/routes.php
index dd757f07c..791078a04 100644
--- a/server/src/routes.php
+++ b/server/src/routes.php
@@ -806,6 +806,11 @@ function ($router) {
$router->post('tracking-settings', 'SettingController@saveTrackingSettings');
$router->get('admin-tracking-settings', 'SettingController@getAdminTrackingSettings');
$router->post('admin-tracking-settings', 'SettingController@saveAdminTrackingSettings');
+ $router->get('tracking-page-settings', 'SettingController@getTrackingPageSettings');
+ $router->post('tracking-page-settings', 'SettingController@saveTrackingPageSettings');
+ $router->post('tracking-page-settings/validate-slug', 'SettingController@validateTrackingPageSlug');
+ $router->get('admin-tracking-page-settings', 'SettingController@getAdminTrackingPageSettings');
+ $router->post('admin-tracking-page-settings', 'SettingController@saveAdminTrackingPageSettings');
$router->get('map', 'SettingController@getMapSettings');
$router->post('map', 'SettingController@saveMapSettings');
$router->get('admin-map', 'SettingController@getAdminMapSettings');
diff --git a/server/tests/TrackingPageSettingsTest.php b/server/tests/TrackingPageSettingsTest.php
new file mode 100644
index 000000000..9a027c73c
--- /dev/null
+++ b/server/tests/TrackingPageSettingsTest.php
@@ -0,0 +1,343 @@
+configured[] = [$key, $value];
+ $this->settings[$key] = $value;
+
+ return null;
+ }
+
+ protected function configureCompanySetting(string $key, mixed $value): mixed
+ {
+ $this->companySettings[$key] = $value;
+
+ return null;
+ }
+
+ protected function lookupSetting(string $key, mixed $defaultValue = null): mixed
+ {
+ return $this->settings[$key] ?? $defaultValue;
+ }
+
+ protected function lookupCompanySetting(string $key, mixed $defaultValue = null): mixed
+ {
+ return $this->companySettings[$key] ?? $defaultValue;
+ }
+
+ protected function currentCompany(): mixed
+ {
+ return $this->company;
+ }
+
+ protected function smsProviders(): array
+ {
+ return $this->smsProviders;
+ }
+
+ protected function installedFleetbaseExtensions(): array
+ {
+ return $this->extensions;
+ }
+}
+
+function fleetopsTrackingPageProbe(): FleetOpsTrackingPageSettingProbe
+{
+ $probe = new FleetOpsTrackingPageSettingProbe();
+ $probe->company = (object) ['uuid' => 'company-1', 'name' => 'Northwind Couriers'];
+
+ config(['services.twilio.sid' => null, 'services.twilio.token' => null]);
+
+ return $probe;
+}
+
+function fleetopsTrackingPageRequest(array $trackingPage): Request
+{
+ return new Request(['trackingPage' => $trackingPage]);
+}
+
+test('tracking page config fills every key with defaults for an empty company setting', function () {
+ $config = TrackingPageConfig::sanitize([], [], 'Northwind Couriers');
+
+ expect($config['org_page'])->toBe(['enabled' => false, 'slug' => 'northwind-couriers'])
+ ->and($config['generic_page'])->toBe(['allowed' => true])
+ ->and($config['links'])->toBe(['target' => 'auto'])
+ ->and($config['branding'])->toBe([
+ 'display_name' => null,
+ 'logo_uuid' => null,
+ 'accent' => '#1F5FA8',
+ 'accent_dark' => null,
+ 'support_phone' => null,
+ 'support_email' => null,
+ 'website' => null,
+ 'powered_by' => true,
+ 'theme' => 'system',
+ 'default_locale' => 'en-us',
+ 'locales' => TrackingPageConfig::LOCALES,
+ ])
+ ->and($config['access'])->toBe([
+ 'public_status' => true,
+ 'channels' => ['sms' => true, 'email' => true],
+ 'session_hours' => 24,
+ 'account_sign_in' => true,
+ 'account_upsell' => true,
+ ])
+ ->and($config['visibility'])->toBe([
+ 'map' => true,
+ 'driver_name' => true,
+ 'vehicle' => true,
+ 'driver_contact' => 'company',
+ 'items' => true,
+ 'item_prices' => false,
+ 'pod_photo' => true,
+ 'pod_signature' => true,
+ 'instructions_edit' => false,
+ 'report_problem' => true,
+ ]);
+});
+
+test('tracking page config keeps valid values and replaces invalid ones', function () {
+ $config = TrackingPageConfig::sanitize([
+ 'org_page' => ['enabled' => 'true', 'slug' => ' Northwind '],
+ 'generic_page' => ['allowed' => '0'],
+ 'links' => ['target' => 'org'],
+ 'branding' => [
+ 'display_name' => ' Northwind ',
+ 'logo_uuid' => 'file-uuid',
+ 'accent' => '0b6',
+ 'accent_dark' => '#5cc9be',
+ 'support_phone' => 5105550142,
+ 'support_email' => 'help@northwind.example',
+ 'website' => 'https://northwind.example',
+ 'powered_by' => false,
+ 'theme' => 'light',
+ 'default_locale' => 'de-de',
+ 'locales' => ['fr-fr', 'xx-xx'],
+ 'unknown' => 'dropped',
+ ],
+ 'access' => ['public_status' => 'no', 'channels' => ['sms' => 'maybe', 'email' => false], 'session_hours' => 500],
+ 'visibility' => ['driver_contact' => 'driver', 'item_prices' => 1, 'map' => 'off'],
+ 'extra' => ['dropped' => true],
+ ], ['branding' => ['accent' => '#123456', 'powered_by' => false]], 'Ignored');
+
+ expect($config['org_page'])->toBe(['enabled' => true, 'slug' => 'northwind'])
+ ->and($config['generic_page']['allowed'])->toBeFalse()
+ ->and($config['links']['target'])->toBe('org')
+ ->and($config['branding']['display_name'])->toBe('Northwind')
+ ->and($config['branding']['logo_uuid'])->toBe('file-uuid')
+ ->and($config['branding']['accent'])->toBe('#00BB66')
+ ->and($config['branding']['accent_dark'])->toBe('#5CC9BE')
+ ->and($config['branding']['support_phone'])->toBe('5105550142')
+ ->and($config['branding']['support_email'])->toBe('help@northwind.example')
+ ->and($config['branding']['website'])->toBe('https://northwind.example')
+ ->and($config['branding']['powered_by'])->toBeFalse()
+ ->and($config['branding']['theme'])->toBe('light')
+ ->and($config['branding']['default_locale'])->toBe('en-us')
+ ->and($config['branding']['locales'])->toBe(['en-us', 'fr-fr'])
+ ->and($config['branding'])->not->toHaveKey('unknown')
+ ->and($config)->not->toHaveKey('extra')
+ ->and($config['access']['public_status'])->toBeFalse()
+ ->and($config['access']['channels'])->toBe(['sms' => true, 'email' => false])
+ ->and($config['access']['session_hours'])->toBe(168)
+ ->and($config['visibility']['driver_contact'])->toBe('driver')
+ ->and($config['visibility']['item_prices'])->toBeTrue()
+ ->and($config['visibility']['map'])->toBeFalse();
+
+ $fallback = TrackingPageConfig::sanitize([
+ 'branding' => [
+ 'accent' => 'teal',
+ 'accent_dark' => ['#000000'],
+ 'support_email' => 'not-an-email',
+ 'website' => 'ftp://northwind.example',
+ 'display_name' => ' ',
+ 'locales' => ['ar-ae', 'en-us'],
+ ],
+ 'access' => ['session_hours' => 'soon'],
+ ], ['branding' => ['accent' => '#123456', 'powered_by' => false]], 'N');
+
+ expect($fallback['org_page']['slug'])->toBe('n-tracking')
+ ->and($fallback['branding']['accent'])->toBe('#123456')
+ ->and($fallback['branding']['accent_dark'])->toBeNull()
+ ->and($fallback['branding']['support_email'])->toBeNull()
+ ->and($fallback['branding']['website'])->toBeNull()
+ ->and($fallback['branding']['display_name'])->toBeNull()
+ ->and($fallback['branding']['powered_by'])->toBeFalse()
+ ->and($fallback['branding']['locales'])->toBe(['en-us', 'ar-ae'])
+ ->and($fallback['access']['session_hours'])->toBe(24)
+ ->and(TrackingPageConfig::sanitize(['access' => ['session_hours' => 0]])['access']['session_hours'])->toBe(1)
+ ->and(TrackingPageConfig::sanitize([])['org_page']['slug'])->toBe('company-tracking');
+});
+
+test('tracking page admin defaults are sanitized on their own', function () {
+ expect(TrackingPageConfig::sanitizeAdmin([]))->toBe([
+ 'generic_page' => ['enabled' => true],
+ 'branding' => [
+ 'display_name' => null,
+ 'accent' => '#1F5FA8',
+ 'support_phone' => null,
+ 'support_email' => null,
+ 'website' => null,
+ 'powered_by' => true,
+ ],
+ ])->and(TrackingPageConfig::sanitizeAdmin([
+ 'generic_page' => ['enabled' => false],
+ 'branding' => ['display_name' => 'Fleetbase Cloud', 'accent' => '#abcdef', 'support_email' => 'ops@fleetbase.example'],
+ ]))->toBe([
+ 'generic_page' => ['enabled' => false],
+ 'branding' => [
+ 'display_name' => 'Fleetbase Cloud',
+ 'accent' => '#ABCDEF',
+ 'support_phone' => null,
+ 'support_email' => 'ops@fleetbase.example',
+ 'website' => null,
+ 'powered_by' => true,
+ ],
+ ]);
+});
+
+test('tracking page slugs are checked for shape and reserved words', function () {
+ expect(TrackingPageConfig::slugError('northwind'))->toBeNull()
+ ->and(TrackingPageConfig::slugError('north-wind-2'))->toBeNull()
+ ->and(TrackingPageConfig::slugError('ab'))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError(str_repeat('a', 41)))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError('-northwind'))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError('north--wind'))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError('North'))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError(null))->toBe('invalid')
+ ->and(TrackingPageConfig::slugError('track'))->toBe('reserved')
+ ->and(TrackingPageConfig::slugError('customer-portal'))->toBe('reserved')
+ ->and(TrackingPageConfig::slugify(str_repeat('Northwind ', 8)))->toHaveLength(39)
+ ->and(TrackingPageConfig::slugify('Northwind Couriers, Inc.'))->toBe('northwind-couriers-inc');
+});
+
+test('tracking page accents get a readable ink and a contrast ratio', function () {
+ expect(TrackingPageConfig::inkFor('#0B6E68'))->toBe('#FFFFFF')
+ ->and(TrackingPageConfig::inkFor('#5CC9BE'))->toBe('#14191A')
+ ->and(TrackingPageConfig::contrastRatio('#FFFFFF', '#000000'))->toBe(21.0)
+ ->and(TrackingPageConfig::contrastRatio('#1F5FA8', '#FFFFFF'))->toBeGreaterThan(4.5)
+ ->and(TrackingPageConfig::contrastRatio('#FFFFFF', '#FFFFFF'))->toBe(1.0);
+});
+
+test('tracking page settings read back with what the settings screen explains', function () {
+ $probe = fleetopsTrackingPageProbe();
+
+ $view = $probe->getTrackingPageSettings()->getData(true);
+
+ expect($view['org_page'])->toBe(['enabled' => false, 'slug' => 'northwind-couriers'])
+ ->and($view['slug_validation'])->toBe(['valid' => true, 'code' => 'available', 'message' => 'This address is available.'])
+ ->and($view['accent_ink'])->toBe('#FFFFFF')
+ ->and($view['accent_contrast'])->toBeGreaterThan(4.5)
+ ->and($view['sms_available'])->toBeFalse()
+ ->and($view['customer_portal_installed'])->toBeFalse()
+ ->and($view['admin']['generic_page'])->toBe(['enabled' => true]);
+
+ $probe->extensions = [['name' => 'fleetbase/customer-portal-api']];
+ $probe->smsProviders = ['twilio' => ['available' => true], 'vonage' => ['available' => true]];
+
+ expect($probe->getTrackingPageSettings()->getData(true))
+ ->toMatchArray(['sms_available' => true, 'customer_portal_installed' => true]);
+
+ $probe->smsProviders = ['twilio' => ['available' => true]];
+ expect($probe->getTrackingPageSettings()->getData(true)['sms_available'])->toBeFalse();
+
+ config(['services.twilio.sid' => 'AC123', 'services.twilio.token' => 'secret']);
+ expect($probe->getTrackingPageSettings()->getData(true)['sms_available'])->toBeTrue();
+});
+
+test('saving tracking page settings indexes the slug and frees the old one', function () {
+ $probe = fleetopsTrackingPageProbe();
+
+ $saved = $probe->saveTrackingPageSettings(fleetopsTrackingPageRequest([
+ 'org_page' => ['enabled' => true, 'slug' => 'northwind'],
+ 'branding' => ['accent' => '#0B6E68'],
+ ]))->getData(true);
+
+ expect($saved['org_page'])->toBe(['enabled' => true, 'slug' => 'northwind'])
+ ->and($saved['branding']['accent'])->toBe('#0B6E68')
+ ->and($probe->companySettings[TrackingPageConfig::SETTING_KEY]['org_page']['slug'])->toBe('northwind')
+ ->and($probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind'])->toBe('company-1')
+ ->and($probe->configured)->toBe([[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind', 'company-1']]);
+
+ $probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => true, 'slug' => 'northwind-couriers']]));
+
+ expect($probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind'])->toBeNull()
+ ->and($probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind-couriers'])->toBe('company-1');
+
+ // A previous slug that another company now holds is left alone.
+ $probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind-couriers'] = 'company-2';
+ $probe->configured = [];
+ $probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => false, 'slug' => 'northwind-express']]));
+
+ expect($probe->configured)->toBe([[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind-express', 'company-1']])
+ ->and($probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind-couriers'])->toBe('company-2');
+});
+
+test('an enabled organization page refuses an unusable slug while a disabled one keeps it unindexed', function () {
+ $probe = fleetopsTrackingPageProbe();
+ $probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'taken-slug'] = 'company-2';
+
+ expect($probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => true, 'slug' => 'taken-slug']]))->getData(true))
+ ->toBe(['error' => 'Another organization already uses this address.'])
+ ->and($probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => true, 'slug' => 'track']]))->getData(true))
+ ->toBe(['error' => 'This address is reserved. Please choose another.'])
+ ->and($probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => true, 'slug' => 'a b']]))->getData(true)['error'])
+ ->toStartWith('Use 3 to 40 lowercase letters')
+ ->and($probe->companySettings)->toBe([]);
+
+ $saved = $probe->saveTrackingPageSettings(fleetopsTrackingPageRequest(['org_page' => ['enabled' => false, 'slug' => 'taken-slug']]))->getData(true);
+
+ expect($saved['org_page'])->toBe(['enabled' => false, 'slug' => 'taken-slug'])
+ ->and($saved['slug_validation']['code'])->toBe('taken')
+ ->and($probe->configured)->toBe([])
+ ->and($probe->companySettings[TrackingPageConfig::SETTING_KEY]['org_page']['slug'])->toBe('taken-slug');
+});
+
+test('tracking page slugs can be checked while typed', function () {
+ $probe = fleetopsTrackingPageProbe();
+ $probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'northwind'] = 'company-1';
+ $probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'harbor'] = 'company-2';
+
+ expect($probe->validateTrackingPageSlug(new Request(['slug' => ' Northwind ']))->getData(true))
+ ->toBe(['slug' => 'northwind', 'valid' => true, 'code' => 'available', 'message' => 'This address is available.'])
+ ->and($probe->validateTrackingPageSlug(new Request(['slug' => 'harbor']))->getData(true)['code'])->toBe('taken')
+ ->and($probe->validateTrackingPageSlug(new Request(['slug' => 'admin']))->getData(true)['code'])->toBe('reserved')
+ ->and($probe->validateTrackingPageSlug(new Request())->getData(true)['code'])->toBe('invalid');
+
+ $probe->settings[TrackingPageConfig::SLUG_INDEX_PREFIX . 'empty-owner'] = '';
+ expect($probe->validateTrackingPageSlug(new Request(['slug' => 'empty-owner']))->getData(true)['valid'])->toBeTrue();
+});
+
+test('admin tracking page defaults are read and saved sanitized', function () {
+ $probe = fleetopsTrackingPageProbe();
+
+ expect($probe->getAdminTrackingPageSettings()->getData(true)['generic_page'])->toBe(['enabled' => true]);
+
+ $saved = $probe->saveAdminTrackingPageSettings(fleetopsTrackingPageRequest([
+ 'generic_page' => ['enabled' => false],
+ 'branding' => ['accent' => 'nope', 'website' => 'https://fleetbase.example'],
+ ]))->getData(true);
+
+ expect($saved['generic_page'])->toBe(['enabled' => false])
+ ->and($saved['branding']['accent'])->toBe('#1F5FA8')
+ ->and($saved['branding']['website'])->toBe('https://fleetbase.example')
+ ->and($probe->settings[TrackingPageConfig::ADMIN_SETTING_KEY])->toBe($saved)
+ ->and($probe->getAdminTrackingPageSettings()->getData(true))->toBe($saved)
+ ->and($probe->getTrackingPageSettings()->getData(true)['admin'])->toBe($saved);
+});
diff --git a/translations/ar-ae.yml b/translations/ar-ae.yml
index 41a63f5cd..67da921fb 100644
--- a/translations/ar-ae.yml
+++ b/translations/ar-ae.yml
@@ -2032,3 +2032,91 @@ common:
offline: غير متصل
"yes": نعم
"no": لا
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/bg-bg.yaml b/translations/bg-bg.yaml
index c0fd4cddb..bc47638da 100644
--- a/translations/bg-bg.yaml
+++ b/translations/bg-bg.yaml
@@ -2347,3 +2347,91 @@ trailer:
dolly: Доли
specialty: Специализирано
other: Друго
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/en-us.yaml b/translations/en-us.yaml
index 058bd0f46..0a4b1b6bc 100644
--- a/translations/en-us.yaml
+++ b/translations/en-us.yaml
@@ -3612,3 +3612,91 @@ resource-summary:
work-order: Work order
zone: Zone
zones: Zones
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/es-pa.yaml b/translations/es-pa.yaml
index 29a939394..82ae796e4 100644
--- a/translations/es-pa.yaml
+++ b/translations/es-pa.yaml
@@ -2226,3 +2226,91 @@ trailer:
dolly: Dolly
specialty: Especializado
other: Otro
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/fr-fr.yaml b/translations/fr-fr.yaml
index ea8c3ee59..23806d26d 100644
--- a/translations/fr-fr.yaml
+++ b/translations/fr-fr.yaml
@@ -2372,3 +2372,91 @@ trailer:
dolly: Dolly
specialty: Spécialisée
other: Autre
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/mn-mn.yaml b/translations/mn-mn.yaml
index 556d1b3c4..c48782398 100644
--- a/translations/mn-mn.yaml
+++ b/translations/mn-mn.yaml
@@ -2316,3 +2316,91 @@ trailer:
dolly: Долли
specialty: Тусгай зориулалтын
other: Бусад
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/pt-br.yaml b/translations/pt-br.yaml
index 40d2fdde7..ffedae57b 100644
--- a/translations/pt-br.yaml
+++ b/translations/pt-br.yaml
@@ -2337,3 +2337,91 @@ trailer:
dolly: Dolly
specialty: Especial
other: Outro
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/ru-ru.yaml b/translations/ru-ru.yaml
index 492dacfbb..089e1773c 100644
--- a/translations/ru-ru.yaml
+++ b/translations/ru-ru.yaml
@@ -2330,3 +2330,91 @@ trailer:
dolly: Подкатная тележка
specialty: Специальный
other: Другое
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/uk-ua.yaml b/translations/uk-ua.yaml
index 86363b30f..e3985830d 100644
--- a/translations/uk-ua.yaml
+++ b/translations/uk-ua.yaml
@@ -3472,4 +3472,92 @@ resource-summary:
window: Вікно (часове)
work-order: Робоче завдання
zone: Зона
- zones: Зони
\ No newline at end of file
+ zones: Зони
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"
diff --git a/translations/vi-vn.yaml b/translations/vi-vn.yaml
index c0ebc5e5a..16c0f1496 100644
--- a/translations/vi-vn.yaml
+++ b/translations/vi-vn.yaml
@@ -2146,3 +2146,91 @@ trailer:
dolly: Dolly
specialty: Chuyên dụng
other: Khác
+tracking-page:
+ settings:
+ title: "Tracking Page"
+ menu: "Tracking Page"
+ saved: "Tracking page settings saved."
+ load-failed: "We couldn't load the tracking page settings."
+ load-failed-help: "Reload the page to try again. If it keeps happening, check that you have permission to view these settings."
+ pages: "Pages"
+ org-page-enabled: "Your own tracking page"
+ org-page-enabled-help: "A tracking page with your branding at its own address. It only finds your organization's tracking numbers."
+ slug: "Page address"
+ slug-checking: "Checking the address..."
+ open-page: "Open the page"
+ generic-page-allowed: "Allow tracking on the shared page"
+ generic-page-allowed-help: "Lets customers look up your tracking numbers at {url}. Turn it off to keep tracking on your own page only."
+ links-target: "Links in emails and labels"
+ links-target-help: "Which page your notifications and labels send customers to."
+ links-target-auto: "Your page when it is on, otherwise the shared page"
+ links-target-org: "Always your page"
+ links-target-generic: "Always the shared page"
+ branding: "Branding"
+ display-name: "Display name"
+ logo-help: "The page uses your organization logo. Without one it shows a monogram of the display name."
+ accent: "Accent colour"
+ accent-hex: "Accent colour as a hex value"
+ contrast-ok: "Text on this colour is readable ({ratio}:1)."
+ contrast-low: "Text on this colour is hard to read ({ratio}:1). Choose a darker or lighter colour."
+ accent-help: "Used for buttons, progress and highlights. The text colour on it is chosen automatically."
+ accent-dark: "Accent colour in dark mode"
+ accent-dark-auto: "Automatic"
+ support-phone: "Support phone"
+ support-email: "Support email"
+ website: "Website"
+ support-help: "Leave these empty to use your organization's details. Customers' calls and messages go here."
+ powered-by: "Show \"Powered by Fleetbase\""
+ powered-by-help: "A small line in the page footer."
+ theme: "Light and dark"
+ theme-system: "Follow the customer's device"
+ theme-light: "Always light"
+ default-language: "Default language"
+ languages: "Languages customers can choose"
+ access: "Access"
+ public-status: "Show the status without verification"
+ public-status-help: "With only a tracking number, customers see the delivery stage and the date of the last update. Turn it off to show nothing until they verify."
+ channel-sms: "Verify by text message"
+ channel-sms-help: "Sends a 6-digit code to the phone number on the order."
+ channel-sms-unavailable: "No SMS provider is configured for this instance, so codes can't be sent by text message."
+ channel-email: "Verify by email"
+ channel-email-help: "Sends a 6-digit code to the email address on the order."
+ session-hours: "Stay verified for (hours)"
+ session-hours-help: "How long a device stays verified after a correct code. Between 1 and 168 hours."
+ account-sign-in: "Let customers sign in"
+ account-sign-in-help: "Customers with a customer portal account can sign in to see all their orders without a code."
+ account-upsell: "Suggest creating an account"
+ account-upsell-help: "After delivery, verified customers without an account are invited to create one."
+ customer-portal-missing: "Install the Customer Portal extension to let customers sign in and see all their orders."
+ code-rules: "Codes have 6 digits and expire after 10 minutes. After 3 wrong codes, codes pause for 5 minutes."
+ visibility: "What verified customers see"
+ visibility-map: "Live map"
+ visibility-map-help: "The route and the vehicle's position while it is on its way to the customer."
+ visibility-driver_name: "Driver's first name"
+ visibility-driver_name-help: "Only the first name, never a surname or phone number."
+ visibility-vehicle: "Vehicle description"
+ visibility-vehicle-help: "For example \"White Ford Transit\"."
+ visibility-items: "Items in the delivery"
+ visibility-items-help: "Names, descriptions, quantities and photos."
+ visibility-item_prices: "Item prices"
+ visibility-item_prices-help: "Shown only to signed-in customers who placed the order."
+ visibility-pod_photo: "Delivery photo"
+ visibility-pod_photo-help: "The photo taken as proof of delivery."
+ visibility-pod_signature: "Signature"
+ visibility-pod_signature-help: "The signature captured as proof of delivery."
+ visibility-instructions_edit: "Let customers add delivery instructions"
+ visibility-instructions_edit-help: "For example a gate code or where to leave the parcel."
+ visibility-report_problem: "Report a problem"
+ visibility-report_problem-help: "Lets customers send you a message about their delivery."
+ driver-contact: "Calls and messages go to"
+ driver-contact-help: "Who customers reach from the driver card."
+ driver-contact-company: "Your support team"
+ driver-contact-driver: "The driver directly"
+ privacy-rules: "Always applied: customers only ever see their own stops and items, the map and driver are hidden after delivery, and the driver's location is shown only while they are on the way."
+ preview: "Preview"
+ preview-help: "A sample in light and dark mode. Save to apply the changes."
+ preview-chip: "Verified on this device"
+ preview-headline: "Arriving today"
+ preview-sub: "2:10 to 2:40 pm"
+ preview-button: "Verify it's you"
+ preview-powered-by: "Powered by Fleetbase"