feat(places): phone, e-mail and opening hours by hand when the search has none (#2472, #2356)

This commit is contained in:
Maurice
2026-10-01 13:42:44 +02:00
parent 8c37597bd7
commit d61d8ca9d7
45 changed files with 726 additions and 22 deletions
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'وقت النهاية قبل وقت البداية',
'places.timeCollision': 'تداخل في الوقت مع:',
'places.formWebsite': 'الموقع الإلكتروني',
'places.formPhone': 'الهاتف',
'places.formEmail': 'البريد الإلكتروني',
'places.hoursAdd': 'إضافة ساعات العمل',
'places.hoursClosed': 'مغلق',
'places.hoursOpens': 'يفتح',
'places.hoursCloses': 'يغلق',
'places.hoursCopyFirst': 'استخدم ساعات {day} لكل الأيام',
'places.hoursRemove': 'إزالة ساعات العمل',
'places.formNotes': 'ملاحظات',
'places.formNotesPlaceholder': 'ملاحظات شخصية...',
'places.assignmentNotes': 'ملاحظات لهذا اليوم',
+8
View File
@@ -104,6 +104,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Bitmə vaxtı başlanğıc vaxtından əvvəldir',
'places.timeCollision': 'Vaxt üst-üstə düşür:',
'places.formWebsite': 'Veb-sayt',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-poçt',
'places.hoursAdd': 'İş saatlarını əlavə et',
'places.hoursClosed': 'Bağlı',
'places.hoursOpens': 'açılır',
'places.hoursCloses': 'bağlanır',
'places.hoursCopyFirst': '{day} saatlarını bütün günlərə tətbiq et',
'places.hoursRemove': 'İş saatlarını sil',
'places.formNotes': 'Qeydlər',
'places.formNotesPlaceholder': 'Şəxsi qeydlər...',
'places.assignmentNotes': 'Bu gün üçün qeydlər',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'O horário de fim é antes do início',
'places.timeCollision': 'Sobreposição de horário com:',
'places.formWebsite': 'Site',
'places.formPhone': 'Telefone',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Adicionar horário de funcionamento',
'places.hoursClosed': 'Fechado',
'places.hoursOpens': 'abre',
'places.hoursCloses': 'fecha',
'places.hoursCopyFirst': 'Usar o horário de {day} em todos os dias',
'places.hoursRemove': 'Remover horário de funcionamento',
'places.formNotes': 'Notas',
'places.formNotesPlaceholder': 'Notas pessoais...',
'places.assignmentNotes': 'Notas para este dia',
+8
View File
@@ -93,6 +93,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': "L'hora de fi és anterior a la d'inici",
'places.timeCollision': 'Solapament horari amb:',
'places.formWebsite': 'Lloc web',
'places.formPhone': 'Telèfon',
'places.formEmail': 'Correu electrònic',
'places.hoursAdd': "Afegeix l'horari",
'places.hoursClosed': 'Tancat',
'places.hoursOpens': 'obre',
'places.hoursCloses': 'tanca',
'places.hoursCopyFirst': "Fes servir l'horari de {day} per a tots els dies",
'places.hoursRemove': "Elimina l'horari",
'places.formNotes': 'Notes',
'places.formNotesPlaceholder': 'Notes personals...',
'places.assignmentNotes': 'Notes per a aquest dia',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Čas konce je před časem začátku',
'places.timeCollision': 'Časový překryv s:',
'places.formWebsite': 'Webové stránky',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Přidat otevírací dobu',
'places.hoursClosed': 'Zavřeno',
'places.hoursOpens': 'otevírá',
'places.hoursCloses': 'zavírá',
'places.hoursCopyFirst': 'Použít časy z {day} pro všechny dny',
'places.hoursRemove': 'Odebrat otevírací dobu',
'places.formNotes': 'Poznámky',
'places.formNotesPlaceholder': 'Osobní poznámky...',
'places.assignmentNotes': 'Poznámky pro tento den',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Endzeit liegt vor der Startzeit',
'places.timeCollision': 'Zeitliche Überschneidung mit:',
'places.formWebsite': 'Website',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-Mail',
'places.hoursAdd': 'Öffnungszeiten hinzufügen',
'places.hoursClosed': 'Geschlossen',
'places.hoursOpens': 'öffnet',
'places.hoursCloses': 'schließt',
'places.hoursCopyFirst': 'Zeiten vom {day} für alle Tage übernehmen',
'places.hoursRemove': 'Öffnungszeiten entfernen',
'places.formNotes': 'Notizen',
'places.formNotesPlaceholder': 'Persönliche Notizen...',
'places.assignmentNotes': 'Notizen für diesen Tag',
+8
View File
@@ -98,6 +98,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'End time is before start time',
'places.timeCollision': 'Time overlap with:',
'places.formWebsite': 'Website',
'places.formPhone': 'Phone',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Add opening hours',
'places.hoursClosed': 'Closed',
'places.hoursOpens': 'opens',
'places.hoursCloses': 'closes',
'places.hoursCopyFirst': "Use {day}'s hours for every day",
'places.hoursRemove': 'Remove opening hours',
'places.formNotes': 'Notes',
'places.formNotesPlaceholder': 'Personal notes...',
'places.assignmentNotes': 'Notes for this day',
+8
View File
@@ -95,6 +95,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'La hora de fin es anterior a la de inicio',
'places.timeCollision': 'Solapamiento horario con:',
'places.formWebsite': 'Página web',
'places.formPhone': 'Teléfono',
'places.formEmail': 'Correo electrónico',
'places.hoursAdd': 'Añadir horario',
'places.hoursClosed': 'Cerrado',
'places.hoursOpens': 'abre',
'places.hoursCloses': 'cierra',
'places.hoursCopyFirst': 'Usar el horario del {day} para todos los días',
'places.hoursRemove': 'Quitar horario',
'places.formNotes': 'Notas',
'places.formNotesPlaceholder': 'Notas personales...',
'places.assignmentNotes': 'Notas para este día',
+8
View File
@@ -95,6 +95,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Lõpuaeg on algusajast varasem',
'places.timeCollision': 'Ajaline kattuvus:',
'places.formWebsite': 'Veebisait',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-post',
'places.hoursAdd': 'Lisa lahtiolekuajad',
'places.hoursClosed': 'Suletud',
'places.hoursOpens': 'avatakse',
'places.hoursCloses': 'suletakse',
'places.hoursCopyFirst': 'Kasuta päeva {day} aegu kõigil päevadel',
'places.hoursRemove': 'Eemalda lahtiolekuajad',
'places.formNotes': 'Märkmed',
'places.formNotesPlaceholder': 'Isiklikud märkmed...',
'places.assignmentNotes': 'Selle päeva märkmed',
+8
View File
@@ -95,6 +95,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': "L'heure de fin est antérieure à l'heure de début",
'places.timeCollision': 'Chevauchement horaire avec :',
'places.formWebsite': 'Site web',
'places.formPhone': 'Téléphone',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Ajouter les horaires',
'places.hoursClosed': 'Fermé',
'places.hoursOpens': 'ouvre',
'places.hoursCloses': 'ferme',
'places.hoursCopyFirst': 'Appliquer les horaires du {day} à tous les jours',
'places.hoursRemove': 'Supprimer les horaires',
'places.formNotes': 'Notes',
'places.formNotesPlaceholder': 'Notes personnelles…',
'places.assignmentNotes': 'Notes pour ce jour',
+8
View File
@@ -95,6 +95,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Η ώρα λήξης είναι πριν την ώρα έναρξης',
'places.timeCollision': 'Επικάλυψη ώρας με:',
'places.formWebsite': 'Ιστοσελίδα',
'places.formPhone': 'Τηλέφωνο',
'places.formEmail': 'Email',
'places.hoursAdd': 'Προσθήκη ωραρίου',
'places.hoursClosed': 'Κλειστό',
'places.hoursOpens': 'ανοίγει',
'places.hoursCloses': 'κλείνει',
'places.hoursCopyFirst': 'Χρήση του ωραρίου της ημέρας {day} για όλες τις ημέρες',
'places.hoursRemove': 'Αφαίρεση ωραρίου',
'places.formNotes': 'Σημειώσεις',
'places.formNotesPlaceholder': 'Προσωπικές σημειώσεις...',
'places.assignmentNotes': 'Σημειώσεις για αυτή την ημέρα',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'A befejezési idő a kezdési idő előtt van',
'places.timeCollision': 'Időbeli átfedés:',
'places.formWebsite': 'Weboldal',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Nyitvatartás hozzáadása',
'places.hoursClosed': 'Zárva',
'places.hoursOpens': 'nyit',
'places.hoursCloses': 'zár',
'places.hoursCopyFirst': 'A(z) {day} nyitvatartása minden napra',
'places.hoursRemove': 'Nyitvatartás törlése',
'places.formNotes': 'Jegyzetek',
'places.formNotesPlaceholder': 'Személyes jegyzetek...',
'places.assignmentNotes': 'Jegyzetek erre a napra',
+8
View File
@@ -93,6 +93,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Waktu selesai lebih awal dari waktu mulai',
'places.timeCollision': 'Waktu tumpang tindih dengan:',
'places.formWebsite': 'Situs web',
'places.formPhone': 'Telepon',
'places.formEmail': 'Email',
'places.hoursAdd': 'Tambah jam buka',
'places.hoursClosed': 'Tutup',
'places.hoursOpens': 'buka',
'places.hoursCloses': 'tutup',
'places.hoursCopyFirst': 'Pakai jam {day} untuk semua hari',
'places.hoursRemove': 'Hapus jam buka',
'places.formNotes': 'Catatan',
'places.formNotesPlaceholder': 'Catatan pribadi...',
'places.assignmentNotes': 'Catatan untuk hari ini',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': "L'ora di fine è precedente all'ora di inizio",
'places.timeCollision': 'Sovrapposizione di orario con:',
'places.formWebsite': 'Sito web',
'places.formPhone': 'Telefono',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Aggiungi orari di apertura',
'places.hoursClosed': 'Chiuso',
'places.hoursOpens': 'apre',
'places.hoursCloses': 'chiude',
'places.hoursCopyFirst': 'Usa gli orari di {day} per tutti i giorni',
'places.hoursRemove': 'Rimuovi orari di apertura',
'places.formNotes': 'Note',
'places.formNotesPlaceholder': 'Note personali...',
'places.assignmentNotes': 'Note per questo giorno',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': '終了時間が開始時間より前です',
'places.timeCollision': '時間が重複しています:',
'places.formWebsite': 'ウェブサイト',
'places.formPhone': '電話',
'places.formEmail': 'メール',
'places.hoursAdd': '営業時間を追加',
'places.hoursClosed': '定休日',
'places.hoursOpens': '開店',
'places.hoursCloses': '閉店',
'places.hoursCopyFirst': '{day}の時間を全曜日に適用',
'places.hoursRemove': '営業時間を削除',
'places.formNotes': 'メモ',
'places.formNotesPlaceholder': '個人的なメモ…',
'places.assignmentNotes': 'この日のメモ',
+8
View File
@@ -93,6 +93,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': '종료 시간이 시작 시간보다 앞입니다',
'places.timeCollision': '시간 겹침:',
'places.formWebsite': '웹사이트',
'places.formPhone': '전화',
'places.formEmail': '이메일',
'places.hoursAdd': '영업시간 추가',
'places.hoursClosed': '휴무',
'places.hoursOpens': '오픈',
'places.hoursCloses': '마감',
'places.hoursCopyFirst': '{day} 시간을 모든 요일에 적용',
'places.hoursRemove': '영업시간 삭제',
'places.formNotes': '메모',
'places.formNotesPlaceholder': '개인 메모...',
'places.assignmentNotes': '이 날의 메모',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Eindtijd is vóór de starttijd',
'places.timeCollision': 'Tijdoverlap met:',
'places.formWebsite': 'Website',
'places.formPhone': 'Telefoon',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Openingstijden toevoegen',
'places.hoursClosed': 'Gesloten',
'places.hoursOpens': 'opent',
'places.hoursCloses': 'sluit',
'places.hoursCopyFirst': 'Tijden van {day} voor alle dagen gebruiken',
'places.hoursRemove': 'Openingstijden verwijderen',
'places.formNotes': 'Notities',
'places.formNotesPlaceholder': 'Persoonlijke notities...',
'places.assignmentNotes': 'Notities voor deze dag',
+8
View File
@@ -84,6 +84,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Godzina zakończenia jest przed godziną rozpoczęcia',
'places.timeCollision': 'Nakładanie się godzin z:',
'places.formWebsite': 'Strona internetowa',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Dodaj godziny otwarcia',
'places.hoursClosed': 'Zamknięte',
'places.hoursOpens': 'otwarcie',
'places.hoursCloses': 'zamknięcie',
'places.hoursCopyFirst': 'Użyj godzin z dnia {day} dla wszystkich dni',
'places.hoursRemove': 'Usuń godziny otwarcia',
'places.formNotes': 'Notatki',
'places.formNotesPlaceholder': 'Osobiste notatki...',
'places.assignmentNotes': 'Notatki na ten dzień',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Время окончания раньше времени начала',
'places.timeCollision': 'Пересечение по времени с:',
'places.formWebsite': 'Сайт',
'places.formPhone': 'Телефон',
'places.formEmail': 'Эл. почта',
'places.hoursAdd': 'Добавить часы работы',
'places.hoursClosed': 'Закрыто',
'places.hoursOpens': 'открывается',
'places.hoursCloses': 'закрывается',
'places.hoursCopyFirst': 'Применить часы дня «{day}» ко всем дням',
'places.hoursRemove': 'Удалить часы работы',
'places.formNotes': 'Заметки',
'places.formNotesPlaceholder': 'Личные заметки...',
'places.assignmentNotes': 'Заметки на этот день',
+8
View File
@@ -95,6 +95,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Čas konca je pred časom začiatku',
'places.timeCollision': 'Časový prekryv s:',
'places.formWebsite': 'Webová stránka',
'places.formPhone': 'Telefón',
'places.formEmail': 'E-mail',
'places.hoursAdd': 'Pridať otváracie hodiny',
'places.hoursClosed': 'Zatvorené',
'places.hoursOpens': 'otvára',
'places.hoursCloses': 'zatvára',
'places.hoursCopyFirst': 'Použiť časy z {day} pre všetky dni',
'places.hoursRemove': 'Odstrániť otváracie hodiny',
'places.formNotes': 'Poznámky',
'places.formNotesPlaceholder': 'Osobné poznámky...',
'places.assignmentNotes': 'Poznámky pre tento deň',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Sluttiden infaller före starttiden',
'places.timeCollision': 'Tidsöverlappning med:',
'places.formWebsite': 'Hemsida',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-post',
'places.hoursAdd': 'Lägg till öppettider',
'places.hoursClosed': 'Stängt',
'places.hoursOpens': 'öppnar',
'places.hoursCloses': 'stänger',
'places.hoursCopyFirst': 'Använd tiderna för {day} alla dagar',
'places.hoursRemove': 'Ta bort öppettider',
'places.formNotes': 'Noteringar',
'places.formNotesPlaceholder': 'Personliga anteckningar...',
'places.assignmentNotes': 'Anteckningar för denna dag',
+8
View File
@@ -88,6 +88,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'เวลาสิ้นสุดอยู่ก่อนเวลาเริ่มต้น',
'places.timeCollision': 'เวลาที่ทับซ้อนกันกับ:',
'places.formWebsite': 'เว็บไซต์',
'places.formPhone': 'โทรศัพท์',
'places.formEmail': 'อีเมล',
'places.hoursAdd': 'เพิ่มเวลาทำการ',
'places.hoursClosed': 'ปิด',
'places.hoursOpens': 'เปิด',
'places.hoursCloses': 'ปิด',
'places.hoursCopyFirst': 'ใช้เวลาของวัน{day}กับทุกวัน',
'places.hoursRemove': 'ลบเวลาทำการ',
'places.formNotes': 'หมายเหตุ',
'places.formNotesPlaceholder': 'บันทึกส่วนตัว...',
'places.assignmentNotes': 'หมายเหตุสำหรับวันนี้',
+8
View File
@@ -96,6 +96,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Bitiş zamanı başlangıç ​​zamanından önce',
'places.timeCollision': 'Zaman şununla çakışıyor:',
'places.formWebsite': 'Web sitesi',
'places.formPhone': 'Telefon',
'places.formEmail': 'E-posta',
'places.hoursAdd': 'Çalışma saatleri ekle',
'places.hoursClosed': 'Kapalı',
'places.hoursOpens': 'açılış',
'places.hoursCloses': 'kapanış',
'places.hoursCopyFirst': '{day} saatlerini tüm günlere uygula',
'places.hoursRemove': 'Çalışma saatlerini kaldır',
'places.formNotes': 'Notlar',
'places.formNotesPlaceholder': 'Kişisel notlar...',
'places.assignmentNotes': 'Bu gün için notlar',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Час закінчення раніше за час початку',
'places.timeCollision': 'Перетин за часом із:',
'places.formWebsite': 'Веб-сайт',
'places.formPhone': 'Телефон',
'places.formEmail': 'Ел. пошта',
'places.hoursAdd': 'Додати години роботи',
'places.hoursClosed': 'Зачинено',
'places.hoursOpens': 'відкривається',
'places.hoursCloses': 'зачиняється',
'places.hoursCopyFirst': 'Застосувати години дня «{day}» до всіх днів',
'places.hoursRemove': 'Видалити години роботи',
'places.formNotes': 'Нотатки',
'places.formNotesPlaceholder': 'Особисті нотатки...',
'places.assignmentNotes': 'Нотатки на цей день',
+8
View File
@@ -94,6 +94,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': 'Thời gian kết thúc trước thời gian bắt đầu',
'places.timeCollision': 'Thời gian trùng lặp với:',
'places.formWebsite': 'Trang web',
'places.formPhone': 'Điện thoại',
'places.formEmail': 'Email',
'places.hoursAdd': 'Thêm giờ mở cửa',
'places.hoursClosed': 'Đóng cửa',
'places.hoursOpens': 'mở cửa',
'places.hoursCloses': 'đóng cửa',
'places.hoursCopyFirst': 'Dùng giờ của {day} cho mọi ngày',
'places.hoursRemove': 'Xóa giờ mở cửa',
'places.formNotes': 'Ghi chú',
'places.formNotesPlaceholder': 'Ghi chú cá nhân...',
'places.assignmentNotes': 'Ghi chú cho ngày này',
+8
View File
@@ -93,6 +93,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': '結束時間早於開始時間',
'places.timeCollision': '時間衝突:',
'places.formWebsite': '網站',
'places.formPhone': '電話',
'places.formEmail': '電子郵件',
'places.hoursAdd': '新增營業時間',
'places.hoursClosed': '公休',
'places.hoursOpens': '開門',
'places.hoursCloses': '關門',
'places.hoursCopyFirst': '將{day}的時間套用到每一天',
'places.hoursRemove': '移除營業時間',
'places.formNotes': '備註',
'places.formNotesPlaceholder': '個人備註...',
'places.assignmentNotes': '當天備註',
+8
View File
@@ -93,6 +93,14 @@ const places: TranslationStrings = {
'places.endTimeBeforeStart': '结束时间早于开始时间',
'places.timeCollision': '时间冲突:',
'places.formWebsite': '网站',
'places.formPhone': '电话',
'places.formEmail': '电子邮件',
'places.hoursAdd': '添加营业时间',
'places.hoursClosed': '休息',
'places.hoursOpens': '开门',
'places.hoursCloses': '关门',
'places.hoursCopyFirst': '将{day}的时间用于每一天',
'places.hoursRemove': '移除营业时间',
'places.formNotes': '备注',
'places.formNotesPlaceholder': '个人备注...',
'places.assignmentNotes': '当天备注',
+1
View File
@@ -50,6 +50,7 @@ export * from './place/track-colors';
export * from './collection/collection.schema';
export * from './collection/collection-file.schema';
export * from './trip/trip.schema';
export * from './place/place-hours';
export * from './trip/day-grid';
export * from './trip-invite/trip-invite.schema';
export * from './collab/collab.schema';
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { hasPlaceHours, parsePlaceHours, placeEmailField, placeOpeningHoursField } from './place-hours';
import { placeCreateRequestSchema, placeUpdateRequestSchema } from './place.schema';
const WEEK = '[{"closed":false,"open":"09:00","close":"17:00"},{"closed":false},{"closed":false},{"closed":false},{"closed":false},{"closed":true},{"closed":true}]';
describe('place hours and e-mail (#2472)', () => {
it('parses seven days and refuses anything else', () => {
expect(parsePlaceHours(WEEK)).toHaveLength(7);
expect(parsePlaceHours('[]')).toBeNull();
expect(parsePlaceHours('{nope')).toBeNull();
expect(parsePlaceHours('[{"closed":false,"open":"25:00"},{},{},{},{},{},{}]')).toBeNull();
expect(parsePlaceHours('')).toBeNull();
expect(parsePlaceHours(null)).toBeNull();
});
it('tells a week that says something from an empty one', () => {
expect(hasPlaceHours(parsePlaceHours(WEEK))).toBe(true);
expect(hasPlaceHours(Array.from({ length: 7 }, () => ({ closed: false })))).toBe(false);
expect(hasPlaceHours(null)).toBe(false);
});
it('validates both fields on the request bodies, empty meaning clear', () => {
expect(placeOpeningHoursField.safeParse(WEEK).success).toBe(true);
expect(placeOpeningHoursField.safeParse('').success).toBe(true);
expect(placeOpeningHoursField.safeParse('[1,2]').success).toBe(false);
expect(placeEmailField.safeParse('a@b.co').success).toBe(true);
expect(placeEmailField.safeParse('').success).toBe(true);
expect(placeEmailField.safeParse('nope').success).toBe(false);
expect(placeCreateRequestSchema.safeParse({ name: 'X', email: 'nope' }).success).toBe(false);
expect(placeUpdateRequestSchema.safeParse({ opening_hours: WEEK, email: null }).success).toBe(true);
});
});
+58
View File
@@ -0,0 +1,58 @@
import { z } from 'zod';
/**
* A place's own opening hours and e-mail, typed in by hand (#2472). The search
* fills phone and website when it knows them; when it does not, or for a place
* nobody has listed, these are how the traveller adds what they found out.
*
* Hours are stored as JSON text on the place: seven days, Monday first, the
* same order Google's weekday lines come in, so the inspector shows either the
* same way.
*/
export const PLACE_HOURS_DAYS = 7;
const TIME = /^([01]\d|2[0-3]):[0-5]\d$/;
export const placeHoursDaySchema = z.object({
closed: z.boolean(),
open: z.string().regex(TIME).optional(),
close: z.string().regex(TIME).optional(),
});
export type PlaceHoursDay = z.infer<typeof placeHoursDaySchema>;
export const placeOpeningHoursSchema = z.array(placeHoursDaySchema).length(PLACE_HOURS_DAYS);
export type PlaceOpeningHours = z.infer<typeof placeOpeningHoursSchema>;
/** The stored text as seven days, or null when it is empty or not hours at all. */
export function parsePlaceHours(raw: unknown): PlaceOpeningHours | null {
if (typeof raw !== 'string' || raw.trim() === '') return null;
try {
const parsed = placeOpeningHoursSchema.safeParse(JSON.parse(raw));
return parsed.success ? parsed.data : null;
} catch {
return null;
}
}
/** Whether a week says anything: a day that is closed or has a time. */
export function hasPlaceHours(hours: PlaceOpeningHours | null): boolean {
return !!hours && hours.some(d => d.closed || !!d.open || !!d.close);
}
/** The request field: the JSON text, empty or null to clear it. */
export const placeOpeningHoursField = z
.string()
.max(1000)
.refine(v => v === '' || parsePlaceHours(v) !== null, { message: 'opening_hours must be seven days, Monday first' })
.nullable()
.optional();
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/** An e-mail address, or empty to clear it. */
export const placeEmailField = z
.string()
.max(254)
.refine(v => v === '' || EMAIL.test(v.trim()), { message: 'email must be an e-mail address' })
.nullable()
.optional();
+9
View File
@@ -1,5 +1,6 @@
import { tagSchema } from '../tag/tag.schema';
import { PLACE_WEBSITE_MAX_LENGTH, normalizePlaceWebsite } from './place-website';
import { placeEmailField, placeOpeningHoursField } from './place-hours';
import { z } from 'zod';
@@ -178,6 +179,10 @@ export const placeSchema = z.object({
fill_percent: fillPercentSchema,
website: z.string().nullable().optional(),
phone: z.string().nullable().optional(),
/** Typed in by hand (#2472). */
email: z.string().nullable().optional(),
/** Hand-kept hours as JSON text, seven days Monday first (#2472); see place-hours.ts. */
opening_hours: z.string().nullable().optional(),
transport_mode: z.string().nullable().optional(),
created_at: z.string().optional(),
updated_at: z.string().optional(),
@@ -251,6 +256,10 @@ export type AssignmentPlace = z.infer<typeof assignmentPlaceSchema>;
* service already reads it that way rather than as "leave alone".
*/
const stopTypeField = z.object({
// Named on the open body like the two below: an e-mail and the hand-kept hours have a
// fixed shape, and a malformed one would otherwise sit on the place unseen (#2472).
email: placeEmailField,
opening_hours: placeOpeningHoursField,
stop_type: roadtripStopTypeSchema.nullable().optional(),
// Named for the same reason: a bounded vocabulary on an otherwise open body. Zero is
// outside it on purpose — a stop that fills nothing is not a stop, and letting one