Просмотр исходного кода

Show when a connected integration still needs a required setting, and be clear about disconnecting (#334)

* Do not offer to leave a required integration setting empty

* Say when a connected integration still needs a required setting, and name the reason a calendar pull did nothing

* Say what a disconnect leaves behind for each kind of integration
Bernt Christian Egeland 3 недель назад
Родитель
Сommit
501cfc5cef

+ 8 - 2
messages/de/integrations.json

@@ -70,7 +70,8 @@
     "active": "Verbunden",
     "active": "Verbunden",
     "error": "Aufmerksamkeit erforderlich",
     "error": "Aufmerksamkeit erforderlich",
     "pending": "Einrichtung läuft",
     "pending": "Einrichtung läuft",
-    "disconnected": "Getrennt"
+    "disconnected": "Getrennt",
+    "needsSetup": "Einrichtung unvollständig"
   },
   },
   "connection": {
   "connection": {
     "back": "Alle Integrationen",
     "back": "Alle Integrationen",
@@ -78,6 +79,8 @@
     "lastHealth": "Zuletzt geprüft",
     "lastHealth": "Zuletzt geprüft",
     "lastSync": "Letzte Synchronisierung",
     "lastSync": "Letzte Synchronisierung",
     "lastError": "Der letzte Durchlauf ist fehlgeschlagen",
     "lastError": "Der letzte Durchlauf ist fehlgeschlagen",
+    "setupMissingTitle": "Einrichtung unvollständig",
+    "setupMissing": "Wähle unten in den Einstellungen {fields}. Bis dahin wird nichts synchronisiert.",
     "connected": "{name} ist verbunden.",
     "connected": "{name} ist verbunden.",
     "connectedShort": "Verbunden",
     "connectedShort": "Verbunden",
     "connectWith": "{name} verbinden",
     "connectWith": "{name} verbinden",
@@ -85,7 +88,10 @@
     "reconnect": "Erneut verbinden",
     "reconnect": "Erneut verbinden",
     "disconnect": "Trennen",
     "disconnect": "Trennen",
     "disconnectTitle": "{name} trennen?",
     "disconnectTitle": "{name} trennen?",
-    "disconnectDescription": "Torqvoice vergisst das Konto und beendet die Synchronisierung. Bereits im Kalender erstellte Termine bleiben erhalten.",
+    "disconnectDescription": "Torqvoice vergisst das Konto und nutzt es nicht mehr. Was bereits an {name} gesendet oder dort gespeichert wurde, bleibt unberührt.",
+    "disconnectDescriptionCalendar": "Torqvoice vergisst das Konto und synchronisiert nicht mehr. Aufträge, die bereits in {name} stehen, bleiben dort. Die aus diesem Kalender angezeigte belegte Zeit wird aus dem Torqvoice-Kalender entfernt.",
+    "disconnectDescriptionConferencing": "Torqvoice vergisst das Konto und erstellt keine Meetings mehr. Bereits erstellte Meetings bleiben in {name}, die Links an den Aufträgen bleiben erhalten.",
+    "disconnectDescriptionAccounting": "Torqvoice vergisst das Konto und sendet und liest nichts mehr. Rechnungen, Kunden und Zahlungen, die bereits in {name} sind, bleiben dort.",
     "removeSetup": "Entfernen",
     "removeSetup": "Entfernen",
     "removeSetupTitle": "{name} entfernen?",
     "removeSetupTitle": "{name} entfernen?",
     "removeSetupDescription": "Es ist noch nichts verbunden. Torqvoice vergisst die Eingaben, und {name} erscheint wieder in der Liste der verfügbaren Dienste.",
     "removeSetupDescription": "Es ist noch nichts verbunden. Torqvoice vergisst die Eingaben, und {name} erscheint wieder in der Liste der verfügbaren Dienste.",

+ 8 - 2
messages/en/integrations.json

@@ -70,7 +70,8 @@
     "active": "Connected",
     "active": "Connected",
     "error": "Needs attention",
     "error": "Needs attention",
     "pending": "Setup in progress",
     "pending": "Setup in progress",
-    "disconnected": "Disconnected"
+    "disconnected": "Disconnected",
+    "needsSetup": "Setup not finished"
   },
   },
   "connection": {
   "connection": {
     "back": "All integrations",
     "back": "All integrations",
@@ -78,6 +79,8 @@
     "lastHealth": "Last checked",
     "lastHealth": "Last checked",
     "lastSync": "Last sync",
     "lastSync": "Last sync",
     "lastError": "The last run failed",
     "lastError": "The last run failed",
+    "setupMissingTitle": "Setup not finished",
+    "setupMissing": "Choose {fields} in the settings below. Until then nothing is synced.",
     "connected": "{name} is connected.",
     "connected": "{name} is connected.",
     "connectedShort": "Connected",
     "connectedShort": "Connected",
     "connectWith": "Connect {name}",
     "connectWith": "Connect {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Reconnect",
     "reconnect": "Reconnect",
     "disconnect": "Disconnect",
     "disconnect": "Disconnect",
     "disconnectTitle": "Disconnect {name}?",
     "disconnectTitle": "Disconnect {name}?",
-    "disconnectDescription": "Torqvoice forgets the account and stops syncing. Events already created in the calendar stay there.",
+    "disconnectDescription": "Torqvoice forgets the account and stops using it. Nothing already sent to or stored in {name} is touched.",
+    "disconnectDescriptionCalendar": "Torqvoice forgets the account and stops syncing. Work orders already put in {name} stay there. The busy time shown from that calendar is removed from the Torqvoice calendar.",
+    "disconnectDescriptionConferencing": "Torqvoice forgets the account and stops creating meetings. Meetings already created stay in {name}, and the links on work orders are kept.",
+    "disconnectDescriptionAccounting": "Torqvoice forgets the account and stops sending and reading. Invoices, customers and payments already in {name} stay there.",
     "removeSetup": "Remove",
     "removeSetup": "Remove",
     "removeSetupTitle": "Remove {name}?",
     "removeSetupTitle": "Remove {name}?",
     "removeSetupDescription": "Nothing is connected yet. Torqvoice forgets what was entered, and {name} goes back to the list of available services.",
     "removeSetupDescription": "Nothing is connected yet. Torqvoice forgets what was entered, and {name} goes back to the list of available services.",

+ 8 - 2
messages/es/integrations.json

@@ -70,7 +70,8 @@
     "active": "Conectada",
     "active": "Conectada",
     "error": "Requiere atención",
     "error": "Requiere atención",
     "pending": "Configuración en curso",
     "pending": "Configuración en curso",
-    "disconnected": "Desconectada"
+    "disconnected": "Desconectada",
+    "needsSetup": "Configuración incompleta"
   },
   },
   "connection": {
   "connection": {
     "back": "Todas las integraciones",
     "back": "Todas las integraciones",
@@ -78,6 +79,8 @@
     "lastHealth": "Última comprobación",
     "lastHealth": "Última comprobación",
     "lastSync": "Última sincronización",
     "lastSync": "Última sincronización",
     "lastError": "La última ejecución falló",
     "lastError": "La última ejecución falló",
+    "setupMissingTitle": "Configuración incompleta",
+    "setupMissing": "Elige {fields} en los ajustes de abajo. Hasta entonces no se sincroniza nada.",
     "connected": "{name} está conectado.",
     "connected": "{name} está conectado.",
     "connectedShort": "Conectado",
     "connectedShort": "Conectado",
     "connectWith": "Conectar {name}",
     "connectWith": "Conectar {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Volver a conectar",
     "reconnect": "Volver a conectar",
     "disconnect": "Desconectar",
     "disconnect": "Desconectar",
     "disconnectTitle": "¿Desconectar {name}?",
     "disconnectTitle": "¿Desconectar {name}?",
-    "disconnectDescription": "Torqvoice olvida la cuenta y deja de sincronizar. Los eventos ya creados en el calendario se mantienen.",
+    "disconnectDescription": "Torqvoice olvida la cuenta y deja de usarla. Nada ya enviado o guardado en {name} se toca.",
+    "disconnectDescriptionCalendar": "Torqvoice olvida la cuenta y deja de sincronizar. Las órdenes de trabajo ya puestas en {name} se quedan allí. El tiempo ocupado mostrado desde ese calendario se quita del calendario de Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice olvida la cuenta y deja de crear reuniones. Las reuniones ya creadas se quedan en {name} y los enlaces en las órdenes de trabajo se conservan.",
+    "disconnectDescriptionAccounting": "Torqvoice olvida la cuenta y deja de enviar y leer. Las facturas, clientes y pagos ya en {name} se quedan allí.",
     "removeSetup": "Quitar",
     "removeSetup": "Quitar",
     "removeSetupTitle": "¿Quitar {name}?",
     "removeSetupTitle": "¿Quitar {name}?",
     "removeSetupDescription": "Todavía no hay nada conectado. Torqvoice olvida lo introducido y {name} vuelve a la lista de servicios disponibles.",
     "removeSetupDescription": "Todavía no hay nada conectado. Torqvoice olvida lo introducido y {name} vuelve a la lista de servicios disponibles.",

+ 8 - 2
messages/fr/integrations.json

@@ -70,7 +70,8 @@
     "active": "Connectée",
     "active": "Connectée",
     "error": "Attention requise",
     "error": "Attention requise",
     "pending": "Configuration en cours",
     "pending": "Configuration en cours",
-    "disconnected": "Déconnectée"
+    "disconnected": "Déconnectée",
+    "needsSetup": "Configuration inachevée"
   },
   },
   "connection": {
   "connection": {
     "back": "Toutes les intégrations",
     "back": "Toutes les intégrations",
@@ -78,6 +79,8 @@
     "lastHealth": "Dernière vérification",
     "lastHealth": "Dernière vérification",
     "lastSync": "Dernière synchronisation",
     "lastSync": "Dernière synchronisation",
     "lastError": "La dernière exécution a échoué",
     "lastError": "La dernière exécution a échoué",
+    "setupMissingTitle": "Configuration inachevée",
+    "setupMissing": "Choisissez {fields} dans les réglages ci-dessous. D'ici là, rien n'est synchronisé.",
     "connected": "{name} est connecté.",
     "connected": "{name} est connecté.",
     "connectedShort": "Connecté",
     "connectedShort": "Connecté",
     "connectWith": "Connecter {name}",
     "connectWith": "Connecter {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Reconnecter",
     "reconnect": "Reconnecter",
     "disconnect": "Déconnecter",
     "disconnect": "Déconnecter",
     "disconnectTitle": "Déconnecter {name} ?",
     "disconnectTitle": "Déconnecter {name} ?",
-    "disconnectDescription": "Torqvoice oublie le compte et arrête la synchronisation. Les événements déjà créés dans le calendrier y restent.",
+    "disconnectDescription": "Torqvoice oublie le compte et cesse de l'utiliser. Rien de ce qui a déjà été envoyé à {name} ou stocké dedans n'est touché.",
+    "disconnectDescriptionCalendar": "Torqvoice oublie le compte et cesse de synchroniser. Les ordres de travail déjà placés dans {name} y restent. Le temps occupé affiché depuis ce calendrier est retiré du calendrier Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice oublie le compte et cesse de créer des réunions. Les réunions déjà créées restent dans {name} et les liens sur les ordres de travail sont conservés.",
+    "disconnectDescriptionAccounting": "Torqvoice oublie le compte et cesse d'envoyer et de lire. Les factures, clients et paiements déjà dans {name} y restent.",
     "removeSetup": "Retirer",
     "removeSetup": "Retirer",
     "removeSetupTitle": "Retirer {name} ?",
     "removeSetupTitle": "Retirer {name} ?",
     "removeSetupDescription": "Rien n'est encore connecté. Torqvoice oublie ce qui a été saisi et {name} revient dans la liste des services disponibles.",
     "removeSetupDescription": "Rien n'est encore connecté. Torqvoice oublie ce qui a été saisi et {name} revient dans la liste des services disponibles.",

+ 8 - 2
messages/it/integrations.json

@@ -70,7 +70,8 @@
     "active": "Collegata",
     "active": "Collegata",
     "error": "Richiede attenzione",
     "error": "Richiede attenzione",
     "pending": "Configurazione in corso",
     "pending": "Configurazione in corso",
-    "disconnected": "Scollegata"
+    "disconnected": "Scollegata",
+    "needsSetup": "Configurazione incompleta"
   },
   },
   "connection": {
   "connection": {
     "back": "Tutte le integrazioni",
     "back": "Tutte le integrazioni",
@@ -78,6 +79,8 @@
     "lastHealth": "Ultimo controllo",
     "lastHealth": "Ultimo controllo",
     "lastSync": "Ultima sincronizzazione",
     "lastSync": "Ultima sincronizzazione",
     "lastError": "L'ultima esecuzione non è riuscita",
     "lastError": "L'ultima esecuzione non è riuscita",
+    "setupMissingTitle": "Configurazione incompleta",
+    "setupMissing": "Scegli {fields} nelle impostazioni qui sotto. Fino ad allora non viene sincronizzato nulla.",
     "connected": "{name} è collegato.",
     "connected": "{name} è collegato.",
     "connectedShort": "Collegato",
     "connectedShort": "Collegato",
     "connectWith": "Collega {name}",
     "connectWith": "Collega {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Ricollega",
     "reconnect": "Ricollega",
     "disconnect": "Scollega",
     "disconnect": "Scollega",
     "disconnectTitle": "Scollegare {name}?",
     "disconnectTitle": "Scollegare {name}?",
-    "disconnectDescription": "Torqvoice dimentica l'account e interrompe la sincronizzazione. Gli eventi già creati nel calendario restano lì.",
+    "disconnectDescription": "Torqvoice dimentica l'account e smette di usarlo. Nulla di già inviato a {name} o salvato lì viene toccato.",
+    "disconnectDescriptionCalendar": "Torqvoice dimentica l'account e smette di sincronizzare. Gli ordini di lavoro già inseriti in {name} restano lì. Il tempo occupato mostrato da quel calendario viene rimosso dal calendario di Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice dimentica l'account e smette di creare riunioni. Le riunioni già create restano in {name} e i link sugli ordini di lavoro vengono conservati.",
+    "disconnectDescriptionAccounting": "Torqvoice dimentica l'account e smette di inviare e leggere. Fatture, clienti e pagamenti già in {name} restano lì.",
     "removeSetup": "Rimuovi",
     "removeSetup": "Rimuovi",
     "removeSetupTitle": "Rimuovere {name}?",
     "removeSetupTitle": "Rimuovere {name}?",
     "removeSetupDescription": "Non c'è ancora nulla di collegato. Torqvoice dimentica quanto inserito e {name} torna nell'elenco dei servizi disponibili.",
     "removeSetupDescription": "Non c'è ancora nulla di collegato. Torqvoice dimentica quanto inserito e {name} torna nell'elenco dei servizi disponibili.",

+ 8 - 2
messages/lt/integrations.json

@@ -70,7 +70,8 @@
     "active": "Prijungta",
     "active": "Prijungta",
     "error": "Reikia dėmesio",
     "error": "Reikia dėmesio",
     "pending": "Nustatoma",
     "pending": "Nustatoma",
-    "disconnected": "Atjungta"
+    "disconnected": "Atjungta",
+    "needsSetup": "Sąranka nebaigta"
   },
   },
   "connection": {
   "connection": {
     "back": "Visos integracijos",
     "back": "Visos integracijos",
@@ -78,6 +79,8 @@
     "lastHealth": "Paskutinį kartą tikrinta",
     "lastHealth": "Paskutinį kartą tikrinta",
     "lastSync": "Paskutinis sinchronizavimas",
     "lastSync": "Paskutinis sinchronizavimas",
     "lastError": "Paskutinis vykdymas nepavyko",
     "lastError": "Paskutinis vykdymas nepavyko",
+    "setupMissingTitle": "Sąranka nebaigta",
+    "setupMissing": "Žemiau esančiuose nustatymuose pasirinkite {fields}. Iki tol niekas nesinchronizuojama.",
     "connected": "{name} prijungta.",
     "connected": "{name} prijungta.",
     "connectedShort": "Prijungta",
     "connectedShort": "Prijungta",
     "connectWith": "Prijungti {name}",
     "connectWith": "Prijungti {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Prijungti iš naujo",
     "reconnect": "Prijungti iš naujo",
     "disconnect": "Atjungti",
     "disconnect": "Atjungti",
     "disconnectTitle": "Atjungti {name}?",
     "disconnectTitle": "Atjungti {name}?",
-    "disconnectDescription": "Torqvoice pamirš paskyrą ir nustos sinchronizuoti. Kalendoriuje jau sukurti įvykiai liks.",
+    "disconnectDescription": "Torqvoice pamiršta paskyrą ir nustoja ja naudotis. Tai, kas jau išsiųsta į {name} ar ten saugoma, neliečiama.",
+    "disconnectDescriptionCalendar": "Torqvoice pamiršta paskyrą ir nustoja sinchronizuoti. Darbo užsakymai, jau įrašyti į {name}, lieka ten. Iš to kalendoriaus rodytas užimtas laikas pašalinamas iš Torqvoice kalendoriaus.",
+    "disconnectDescriptionConferencing": "Torqvoice pamiršta paskyrą ir nebekuria susitikimų. Jau sukurti susitikimai lieka {name}, o nuorodos darbo užsakymuose išsaugomos.",
+    "disconnectDescriptionAccounting": "Torqvoice pamiršta paskyrą ir nustoja siųsti bei skaityti. Sąskaitos, klientai ir mokėjimai, jau esantys {name}, lieka ten.",
     "removeSetup": "Pašalinti",
     "removeSetup": "Pašalinti",
     "removeSetupTitle": "Pašalinti {name}?",
     "removeSetupTitle": "Pašalinti {name}?",
     "removeSetupDescription": "Dar niekas neprijungta. Torqvoice pamiršta, kas buvo įvesta, o {name} grįžta į galimų paslaugų sąrašą.",
     "removeSetupDescription": "Dar niekas neprijungta. Torqvoice pamiršta, kas buvo įvesta, o {name} grįžta į galimų paslaugų sąrašą.",

+ 8 - 2
messages/nb/integrations.json

@@ -70,7 +70,8 @@
     "active": "Tilkoblet",
     "active": "Tilkoblet",
     "error": "Trenger oppfølging",
     "error": "Trenger oppfølging",
     "pending": "Oppsett pågår",
     "pending": "Oppsett pågår",
-    "disconnected": "Frakoblet"
+    "disconnected": "Frakoblet",
+    "needsSetup": "Oppsettet er ikke ferdig"
   },
   },
   "connection": {
   "connection": {
     "back": "Alle integrasjoner",
     "back": "Alle integrasjoner",
@@ -78,6 +79,8 @@
     "lastHealth": "Sist sjekket",
     "lastHealth": "Sist sjekket",
     "lastSync": "Siste synkronisering",
     "lastSync": "Siste synkronisering",
     "lastError": "Siste kjøring mislyktes",
     "lastError": "Siste kjøring mislyktes",
+    "setupMissingTitle": "Oppsettet er ikke ferdig",
+    "setupMissing": "Velg {fields} i innstillingene nedenfor. Inntil da synkroniseres ingenting.",
     "connected": "{name} er tilkoblet.",
     "connected": "{name} er tilkoblet.",
     "connectedShort": "Tilkoblet",
     "connectedShort": "Tilkoblet",
     "connectWith": "Koble til {name}",
     "connectWith": "Koble til {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Koble til på nytt",
     "reconnect": "Koble til på nytt",
     "disconnect": "Koble fra",
     "disconnect": "Koble fra",
     "disconnectTitle": "Koble fra {name}?",
     "disconnectTitle": "Koble fra {name}?",
-    "disconnectDescription": "Torqvoice glemmer kontoen og slutter å synkronisere. Hendelser som allerede er opprettet i kalenderen, blir liggende der.",
+    "disconnectDescription": "Torqvoice glemmer kontoen og slutter å bruke den. Ingenting som allerede er sendt til eller lagret i {name} røres.",
+    "disconnectDescriptionCalendar": "Torqvoice glemmer kontoen og slutter å synkronisere. Arbeidsordrer som allerede ligger i {name} blir der. Opptatt tid som ble vist fra den kalenderen fjernes fra Torqvoice-kalenderen.",
+    "disconnectDescriptionConferencing": "Torqvoice glemmer kontoen og slutter å opprette møter. Møter som allerede er opprettet blir i {name}, og lenkene på arbeidsordrene beholdes.",
+    "disconnectDescriptionAccounting": "Torqvoice glemmer kontoen og slutter å sende og lese. Fakturaer, kunder og betalinger som allerede er i {name} blir der.",
     "removeSetup": "Fjern",
     "removeSetup": "Fjern",
     "removeSetupTitle": "Fjerne {name}?",
     "removeSetupTitle": "Fjerne {name}?",
     "removeSetupDescription": "Ingenting er koblet til ennå. Torqvoice glemmer det som ble fylt inn, og {name} går tilbake til listen over tilgjengelige tjenester.",
     "removeSetupDescription": "Ingenting er koblet til ennå. Torqvoice glemmer det som ble fylt inn, og {name} går tilbake til listen over tilgjengelige tjenester.",

+ 8 - 2
messages/nl/integrations.json

@@ -70,7 +70,8 @@
     "active": "Gekoppeld",
     "active": "Gekoppeld",
     "error": "Aandacht vereist",
     "error": "Aandacht vereist",
     "pending": "Installatie bezig",
     "pending": "Installatie bezig",
-    "disconnected": "Losgekoppeld"
+    "disconnected": "Losgekoppeld",
+    "needsSetup": "Instellen niet afgerond"
   },
   },
   "connection": {
   "connection": {
     "back": "Alle integraties",
     "back": "Alle integraties",
@@ -78,6 +79,8 @@
     "lastHealth": "Laatst gecontroleerd",
     "lastHealth": "Laatst gecontroleerd",
     "lastSync": "Laatste synchronisatie",
     "lastSync": "Laatste synchronisatie",
     "lastError": "De laatste uitvoering is mislukt",
     "lastError": "De laatste uitvoering is mislukt",
+    "setupMissingTitle": "Instellen niet afgerond",
+    "setupMissing": "Kies {fields} in de instellingen hieronder. Tot die tijd wordt niets gesynchroniseerd.",
     "connected": "{name} is gekoppeld.",
     "connected": "{name} is gekoppeld.",
     "connectedShort": "Gekoppeld",
     "connectedShort": "Gekoppeld",
     "connectWith": "{name} koppelen",
     "connectWith": "{name} koppelen",
@@ -85,7 +88,10 @@
     "reconnect": "Opnieuw koppelen",
     "reconnect": "Opnieuw koppelen",
     "disconnect": "Loskoppelen",
     "disconnect": "Loskoppelen",
     "disconnectTitle": "{name} loskoppelen?",
     "disconnectTitle": "{name} loskoppelen?",
-    "disconnectDescription": "Torqvoice vergeet het account en stopt met synchroniseren. Afspraken die al in de agenda zijn aangemaakt, blijven daar staan.",
+    "disconnectDescription": "Torqvoice vergeet het account en gebruikt het niet meer. Niets wat al naar {name} is gestuurd of daar is opgeslagen wordt aangeraakt.",
+    "disconnectDescriptionCalendar": "Torqvoice vergeet het account en stopt met synchroniseren. Werkorders die al in {name} staan blijven daar. De drukke tijd die uit die agenda werd getoond wordt uit de Torqvoice-agenda verwijderd.",
+    "disconnectDescriptionConferencing": "Torqvoice vergeet het account en maakt geen vergaderingen meer aan. Al aangemaakte vergaderingen blijven in {name} en de links op werkorders blijven bewaard.",
+    "disconnectDescriptionAccounting": "Torqvoice vergeet het account en stopt met versturen en lezen. Facturen, klanten en betalingen die al in {name} staan blijven daar.",
     "removeSetup": "Verwijderen",
     "removeSetup": "Verwijderen",
     "removeSetupTitle": "{name} verwijderen?",
     "removeSetupTitle": "{name} verwijderen?",
     "removeSetupDescription": "Er is nog niets verbonden. Torqvoice vergeet wat is ingevuld en {name} gaat terug naar de lijst met beschikbare diensten.",
     "removeSetupDescription": "Er is nog niets verbonden. Torqvoice vergeet wat is ingevuld en {name} gaat terug naar de lijst met beschikbare diensten.",

+ 8 - 2
messages/pl/integrations.json

@@ -70,7 +70,8 @@
     "active": "Połączono",
     "active": "Połączono",
     "error": "Wymaga uwagi",
     "error": "Wymaga uwagi",
     "pending": "Konfiguracja w toku",
     "pending": "Konfiguracja w toku",
-    "disconnected": "Rozłączono"
+    "disconnected": "Rozłączono",
+    "needsSetup": "Konfiguracja nieukończona"
   },
   },
   "connection": {
   "connection": {
     "back": "Wszystkie integracje",
     "back": "Wszystkie integracje",
@@ -78,6 +79,8 @@
     "lastHealth": "Ostatnie sprawdzenie",
     "lastHealth": "Ostatnie sprawdzenie",
     "lastSync": "Ostatnia synchronizacja",
     "lastSync": "Ostatnia synchronizacja",
     "lastError": "Ostatnie uruchomienie nie powiodło się",
     "lastError": "Ostatnie uruchomienie nie powiodło się",
+    "setupMissingTitle": "Konfiguracja nieukończona",
+    "setupMissing": "Wybierz {fields} w ustawieniach poniżej. Do tego czasu nic nie jest synchronizowane.",
     "connected": "{name} jest połączony.",
     "connected": "{name} jest połączony.",
     "connectedShort": "Połączono",
     "connectedShort": "Połączono",
     "connectWith": "Połącz {name}",
     "connectWith": "Połącz {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Połącz ponownie",
     "reconnect": "Połącz ponownie",
     "disconnect": "Rozłącz",
     "disconnect": "Rozłącz",
     "disconnectTitle": "Rozłączyć {name}?",
     "disconnectTitle": "Rozłączyć {name}?",
-    "disconnectDescription": "Torqvoice zapomni konto i przestanie synchronizować. Wydarzenia już utworzone w kalendarzu pozostaną.",
+    "disconnectDescription": "Torqvoice zapomina konto i przestaje z niego korzystać. Nic, co już wysłano do {name} lub tam zapisano, nie jest ruszane.",
+    "disconnectDescriptionCalendar": "Torqvoice zapomina konto i przestaje synchronizować. Zlecenia już umieszczone w {name} tam zostają. Czas zajęty pokazywany z tego kalendarza jest usuwany z kalendarza Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice zapomina konto i przestaje tworzyć spotkania. Utworzone już spotkania zostają w {name}, a linki na zleceniach są zachowane.",
+    "disconnectDescriptionAccounting": "Torqvoice zapomina konto i przestaje wysyłać oraz odczytywać. Faktury, klienci i płatności już obecne w {name} tam zostają.",
     "removeSetup": "Usuń",
     "removeSetup": "Usuń",
     "removeSetupTitle": "Usunąć {name}?",
     "removeSetupTitle": "Usunąć {name}?",
     "removeSetupDescription": "Nic nie jest jeszcze połączone. Torqvoice zapomina wpisane dane, a {name} wraca na listę dostępnych usług.",
     "removeSetupDescription": "Nic nie jest jeszcze połączone. Torqvoice zapomina wpisane dane, a {name} wraca na listę dostępnych usług.",

+ 8 - 2
messages/pt-BR/integrations.json

@@ -70,7 +70,8 @@
     "active": "Conectada",
     "active": "Conectada",
     "error": "Precisa de atenção",
     "error": "Precisa de atenção",
     "pending": "Configuração em andamento",
     "pending": "Configuração em andamento",
-    "disconnected": "Desconectada"
+    "disconnected": "Desconectada",
+    "needsSetup": "Configuração incompleta"
   },
   },
   "connection": {
   "connection": {
     "back": "Todas as integrações",
     "back": "Todas as integrações",
@@ -78,6 +79,8 @@
     "lastHealth": "Última verificação",
     "lastHealth": "Última verificação",
     "lastSync": "Última sincronização",
     "lastSync": "Última sincronização",
     "lastError": "A última execução falhou",
     "lastError": "A última execução falhou",
+    "setupMissingTitle": "Configuração incompleta",
+    "setupMissing": "Escolha {fields} nas configurações abaixo. Até lá nada é sincronizado.",
     "connected": "{name} está conectado.",
     "connected": "{name} está conectado.",
     "connectedShort": "Conectado",
     "connectedShort": "Conectado",
     "connectWith": "Conectar {name}",
     "connectWith": "Conectar {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Reconectar",
     "reconnect": "Reconectar",
     "disconnect": "Desconectar",
     "disconnect": "Desconectar",
     "disconnectTitle": "Desconectar {name}?",
     "disconnectTitle": "Desconectar {name}?",
-    "disconnectDescription": "O Torqvoice esquece a conta e para de sincronizar. Os eventos já criados no calendário permanecem lá.",
+    "disconnectDescription": "O Torqvoice esquece a conta e para de usá-la. Nada já enviado para o {name} ou guardado lá é tocado.",
+    "disconnectDescriptionCalendar": "O Torqvoice esquece a conta e para de sincronizar. Ordens de serviço já colocadas no {name} ficam lá. O tempo ocupado mostrado a partir desse calendário é removido do calendário do Torqvoice.",
+    "disconnectDescriptionConferencing": "O Torqvoice esquece a conta e para de criar reuniões. Reuniões já criadas ficam no {name} e os links nas ordens de serviço são mantidos.",
+    "disconnectDescriptionAccounting": "O Torqvoice esquece a conta e para de enviar e ler. Faturas, clientes e pagamentos já no {name} ficam lá.",
     "removeSetup": "Remover",
     "removeSetup": "Remover",
     "removeSetupTitle": "Remover {name}?",
     "removeSetupTitle": "Remover {name}?",
     "removeSetupDescription": "Nada está conectado ainda. O Torqvoice esquece o que foi preenchido e {name} volta para a lista de serviços disponíveis.",
     "removeSetupDescription": "Nada está conectado ainda. O Torqvoice esquece o que foi preenchido e {name} volta para a lista de serviços disponíveis.",

+ 8 - 2
messages/ru/integrations.json

@@ -70,7 +70,8 @@
     "active": "Подключено",
     "active": "Подключено",
     "error": "Требует внимания",
     "error": "Требует внимания",
     "pending": "Идёт настройка",
     "pending": "Идёт настройка",
-    "disconnected": "Отключено"
+    "disconnected": "Отключено",
+    "needsSetup": "Настройка не завершена"
   },
   },
   "connection": {
   "connection": {
     "back": "Все интеграции",
     "back": "Все интеграции",
@@ -78,6 +79,8 @@
     "lastHealth": "Последняя проверка",
     "lastHealth": "Последняя проверка",
     "lastSync": "Последняя синхронизация",
     "lastSync": "Последняя синхронизация",
     "lastError": "Последний запуск завершился ошибкой",
     "lastError": "Последний запуск завершился ошибкой",
+    "setupMissingTitle": "Настройка не завершена",
+    "setupMissing": "Выберите {fields} в настройках ниже. До тех пор ничего не синхронизируется.",
     "connected": "{name} подключено.",
     "connected": "{name} подключено.",
     "connectedShort": "Подключено",
     "connectedShort": "Подключено",
     "connectWith": "Подключить {name}",
     "connectWith": "Подключить {name}",
@@ -85,7 +88,10 @@
     "reconnect": "Переподключить",
     "reconnect": "Переподключить",
     "disconnect": "Отключить",
     "disconnect": "Отключить",
     "disconnectTitle": "Отключить {name}?",
     "disconnectTitle": "Отключить {name}?",
-    "disconnectDescription": "Torqvoice забудет аккаунт и прекратит синхронизацию. События, уже созданные в календаре, останутся на месте.",
+    "disconnectDescription": "Torqvoice забывает аккаунт и перестаёт им пользоваться. Всё, что уже отправлено в {name} или хранится там, не затрагивается.",
+    "disconnectDescriptionCalendar": "Torqvoice забывает аккаунт и перестаёт синхронизировать. Заказ-наряды, уже добавленные в {name}, остаются там. Занятое время, показанное из этого календаря, удаляется из календаря Torqvoice.",
+    "disconnectDescriptionConferencing": "Torqvoice забывает аккаунт и перестаёт создавать встречи. Уже созданные встречи остаются в {name}, а ссылки в заказ-нарядах сохраняются.",
+    "disconnectDescriptionAccounting": "Torqvoice забывает аккаунт и перестаёт отправлять и читать. Счета, клиенты и платежи, уже находящиеся в {name}, остаются там.",
     "removeSetup": "Удалить",
     "removeSetup": "Удалить",
     "removeSetupTitle": "Удалить {name}?",
     "removeSetupTitle": "Удалить {name}?",
     "removeSetupDescription": "Пока ничего не подключено. Torqvoice забудет введённые данные, и {name} вернётся в список доступных сервисов.",
     "removeSetupDescription": "Пока ничего не подключено. Torqvoice забудет введённые данные, и {name} вернётся в список доступных сервисов.",

+ 8 - 2
messages/tr/integrations.json

@@ -70,7 +70,8 @@
     "active": "Bağlı",
     "active": "Bağlı",
     "error": "Dikkat gerekiyor",
     "error": "Dikkat gerekiyor",
     "pending": "Kurulum sürüyor",
     "pending": "Kurulum sürüyor",
-    "disconnected": "Bağlantı kesildi"
+    "disconnected": "Bağlantı kesildi",
+    "needsSetup": "Kurulum tamamlanmadı"
   },
   },
   "connection": {
   "connection": {
     "back": "Tüm entegrasyonlar",
     "back": "Tüm entegrasyonlar",
@@ -78,6 +79,8 @@
     "lastHealth": "Son kontrol",
     "lastHealth": "Son kontrol",
     "lastSync": "Son senkronizasyon",
     "lastSync": "Son senkronizasyon",
     "lastError": "Son çalıştırma başarısız oldu",
     "lastError": "Son çalıştırma başarısız oldu",
+    "setupMissingTitle": "Kurulum tamamlanmadı",
+    "setupMissing": "Aşağıdaki ayarlarda {fields} seçin. O zamana kadar hiçbir şey eşitlenmez.",
     "connected": "{name} bağlandı.",
     "connected": "{name} bağlandı.",
     "connectedShort": "Bağlı",
     "connectedShort": "Bağlı",
     "connectWith": "{name} bağlantısını kur",
     "connectWith": "{name} bağlantısını kur",
@@ -85,7 +88,10 @@
     "reconnect": "Yeniden bağlan",
     "reconnect": "Yeniden bağlan",
     "disconnect": "Bağlantıyı kes",
     "disconnect": "Bağlantıyı kes",
     "disconnectTitle": "{name} bağlantısı kesilsin mi?",
     "disconnectTitle": "{name} bağlantısı kesilsin mi?",
-    "disconnectDescription": "Torqvoice hesabı unutur ve senkronizasyonu durdurur. Takvimde zaten oluşturulmuş etkinlikler orada kalır.",
+    "disconnectDescription": "Torqvoice hesabı unutur ve kullanmayı bırakır. {name} hizmetine zaten gönderilen veya orada saklanan hiçbir şeye dokunulmaz.",
+    "disconnectDescriptionCalendar": "Torqvoice hesabı unutur ve eşitlemeyi bırakır. {name} takvimine zaten eklenmiş iş emirleri orada kalır. O takvimden gösterilen dolu zaman Torqvoice takviminden kaldırılır.",
+    "disconnectDescriptionConferencing": "Torqvoice hesabı unutur ve toplantı oluşturmayı bırakır. Zaten oluşturulmuş toplantılar {name} içinde kalır, iş emirlerindeki bağlantılar korunur.",
+    "disconnectDescriptionAccounting": "Torqvoice hesabı unutur; göndermeyi ve okumayı bırakır. {name} içinde zaten bulunan faturalar, müşteriler ve ödemeler orada kalır.",
     "removeSetup": "Kaldır",
     "removeSetup": "Kaldır",
     "removeSetupTitle": "{name} kaldırılsın mı?",
     "removeSetupTitle": "{name} kaldırılsın mı?",
     "removeSetupDescription": "Henüz hiçbir şey bağlı değil. Torqvoice girilenleri unutur ve {name} kullanılabilir hizmetler listesine geri döner.",
     "removeSetupDescription": "Henüz hiçbir şey bağlı değil. Torqvoice girilenleri unutur ve {name} kullanılabilir hizmetler listesine geri döner.",

+ 44 - 6
src/app/(authenticated)/settings/integrations/[connector]/connection-settings.tsx

@@ -98,6 +98,17 @@ export function ConnectionSettings({
   }, [search, router, t, manifest])
   }, [search, router, t, manifest])
 
 
   const isOAuth = manifest.auth.type === 'oauth2'
   const isOAuth = manifest.auth.type === 'oauth2'
+  // A connected service with a required setting still empty is not working,
+  // whatever the vendor's status says; the calendar to write to is the usual
+  // case. The page says so up top rather than leaving it to the job log.
+  const savedSettings = initialSettingValues(manifest.settings, connection?.settings ?? {})
+  const unfinishedSetup =
+    connection?.status === 'active'
+      ? visibleSettingFields(manifest.settings, savedSettings).filter(
+          (f) => f.required && !savedSettings[f.key]
+        )
+      : []
+  const setupMissing = unfinishedSetup.length > 0
   const oauthNeedsTenantApp = isOAuth && !view.platformApp
   const oauthNeedsTenantApp = isOAuth && !view.platformApp
   const connected = connection && (connection.status === 'active' || connection.status === 'error')
   const connected = connection && (connection.status === 'active' || connection.status === 'error')
   const oauthStartUrl = `/api/integrations/${manifest.id}/oauth/start`
   const oauthStartUrl = `/api/integrations/${manifest.id}/oauth/start`
@@ -128,7 +139,18 @@ export function ConnectionSettings({
   const disconnect = async () => {
   const disconnect = async () => {
     const ok = await confirm({
     const ok = await confirm({
       title: t('connection.disconnectTitle', { name: manifest.name }),
       title: t('connection.disconnectTitle', { name: manifest.name }),
-      description: t('connection.disconnectDescription'),
+      // What a disconnect leaves behind differs by kind of service, and the
+      // dialog is the one place people read before pressing it.
+      description: t(
+        manifest.category === 'calendar'
+          ? 'connection.disconnectDescriptionCalendar'
+          : manifest.category === 'conferencing'
+            ? 'connection.disconnectDescriptionConferencing'
+            : manifest.category === 'accounting'
+              ? 'connection.disconnectDescriptionAccounting'
+              : 'connection.disconnectDescription',
+        { name: manifest.name }
+      ),
       confirmLabel: t('connection.disconnect'),
       confirmLabel: t('connection.disconnect'),
       destructive: true,
       destructive: true,
     })
     })
@@ -193,7 +215,7 @@ export function ConnectionSettings({
               unoptimized
               unoptimized
             />
             />
             {manifest.name}
             {manifest.name}
-            {connection && <StatusBadge status={connection.status} />}
+            {connection && <StatusBadge status={setupMissing ? 'needsSetup' : connection.status} />}
           </span>
           </span>
         }
         }
         description={tc('description')}
         description={tc('description')}
@@ -207,6 +229,19 @@ export function ConnectionSettings({
           ))}
           ))}
         </div>
         </div>
 
 
+        {setupMissing && (
+          <div className="mb-4 flex items-start gap-2 rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-sm">
+            <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
+            <div>
+              <p className="font-medium">{t('connection.setupMissingTitle')}</p>
+              <p className="text-muted-foreground">
+                {t('connection.setupMissing', {
+                  fields: unfinishedSetup.map((f) => tc(`settings.${f.label}`)).join(', '),
+                })}
+              </p>
+            </div>
+          </div>
+        )}
         {connection?.lastError && connection.status === 'error' && (
         {connection?.lastError && connection.status === 'error' && (
           <div className="mb-4 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm">
           <div className="mb-4 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm">
             <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
             <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
@@ -451,7 +486,7 @@ export function ConnectionSettings({
                     isAccounting ? t('connection.pullQueued') : t('connection.syncQueued')
                     isAccounting ? t('connection.pullQueued') : t('connection.syncQueued')
                   )
                   )
                 }
                 }
-                disabled={busy !== null}
+                disabled={busy !== null || setupMissing}
               >
               >
                 {busy === 'sync' ? (
                 {busy === 'sync' ? (
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
@@ -472,7 +507,7 @@ export function ConnectionSettings({
                     t('connection.backfillQueued')
                     t('connection.backfillQueued')
                   )
                   )
                 }
                 }
-                disabled={busy !== null}
+                disabled={busy !== null || setupMissing}
               >
               >
                 {busy === 'backfill' ? (
                 {busy === 'backfill' ? (
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
@@ -498,7 +533,7 @@ export function ConnectionSettings({
                     }
                     }
                   )
                   )
                 }
                 }
-                disabled={busy !== null}
+                disabled={busy !== null || setupMissing}
               >
               >
                 {busy === 'backfill' ? (
                 {busy === 'backfill' ? (
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
                   <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
@@ -566,7 +601,9 @@ function StatusBadge({ status }: { status: string }) {
       ? 'border-emerald-500/30 text-emerald-600'
       ? 'border-emerald-500/30 text-emerald-600'
       : status === 'error'
       : status === 'error'
         ? 'border-destructive/30 text-destructive'
         ? 'border-destructive/30 text-destructive'
-        : 'text-muted-foreground'
+        : status === 'needsSetup'
+          ? 'border-amber-500/40 text-amber-600'
+          : 'text-muted-foreground'
   return (
   return (
     <Badge variant="outline" className={`text-[11px] font-normal ${cls}`}>
     <Badge variant="outline" className={`text-[11px] font-normal ${cls}`}>
       {t(status)}
       {t(status)}
@@ -820,6 +857,7 @@ function SettingFieldList({
                 value={String(values[f.key] ?? '')}
                 value={String(values[f.key] ?? '')}
                 options={remote[f.source ?? '']}
                 options={remote[f.source ?? '']}
                 onChange={(v) => setValues((s) => ({ ...s, [f.key]: v }))}
                 onChange={(v) => setValues((s) => ({ ...s, [f.key]: v }))}
+                required={f.required}
               />
               />
             )}
             )}
             {f.type === 'select' && (
             {f.type === 'select' && (

+ 19 - 13
src/app/(authenticated)/settings/integrations/[connector]/option-picker.tsx

@@ -18,20 +18,24 @@ import type { SettingOption } from '@/features/integrations/Lib/types'
 
 
 /**
 /**
  * A picker for a list the vendor supplies: products, tax codes, accounts. A
  * A picker for a list the vendor supplies: products, tax codes, accounts. A
- * company can have hundreds, so it searches as you type, and since every one
- * of these settings is optional there is a first entry that leaves it empty.
+ * company can have hundreds, so it searches as you type. An optional setting
+ * gets a first entry that leaves it empty; a required one, such as which
+ * calendar to write to, does not offer that, since nothing works without it.
  */
  */
 export function OptionPicker({
 export function OptionPicker({
   value,
   value,
   options,
   options,
   onChange,
   onChange,
   disabled,
   disabled,
+  required = false,
 }: {
 }: {
   value: string
   value: string
   /** undefined while the list is still loading. */
   /** undefined while the list is still loading. */
   options: SettingOption[] | null | undefined
   options: SettingOption[] | null | undefined
   onChange: (value: string) => void
   onChange: (value: string) => void
   disabled?: boolean
   disabled?: boolean
+  /** Whether the setting must have a value; hides the entry that clears it. */
+  required?: boolean
 }) {
 }) {
   const t = useTranslations('integrations')
   const t = useTranslations('integrations')
   const [open, setOpen] = useState(false)
   const [open, setOpen] = useState(false)
@@ -66,17 +70,19 @@ export function OptionPicker({
           <CommandList>
           <CommandList>
             <CommandEmpty>{t('connection.noMatch')}</CommandEmpty>
             <CommandEmpty>{t('connection.noMatch')}</CommandEmpty>
             <CommandGroup>
             <CommandGroup>
-              <CommandItem
-                value="__none__"
-                onSelect={() => {
-                  onChange('')
-                  setOpen(false)
-                }}
-                className="text-muted-foreground"
-              >
-                <Check className={cn('mr-2 h-4 w-4', value ? 'opacity-0' : 'opacity-100')} />
-                {t('connection.leaveEmpty')}
-              </CommandItem>
+              {!required && (
+                <CommandItem
+                  value="__none__"
+                  onSelect={() => {
+                    onChange('')
+                    setOpen(false)
+                  }}
+                  className="text-muted-foreground"
+                >
+                  <Check className={cn('mr-2 h-4 w-4', value ? 'opacity-0' : 'opacity-100')} />
+                  {t('connection.leaveEmpty')}
+                </CommandItem>
+              )}
               {(options ?? []).map((o) => (
               {(options ?? []).map((o) => (
                 <CommandItem
                 <CommandItem
                   key={o.value}
                   key={o.value}

+ 2 - 1
src/integrations/google-calendar/server.ts

@@ -146,7 +146,8 @@ function toPulled(e: GoogleEvent): PulledEvent | null {
 
 
 async function pullBusy(ctx: ConnectorContext) {
 async function pullBusy(ctx: ConnectorContext) {
   const settings = settingsOf(ctx)
   const settings = settingsOf(ctx)
-  if (!settings.pullEnabled || !settings.calendarId) return { summary: 'pull disabled' }
+  if (!settings.pullEnabled) return { summary: 'pull switched off' }
+  if (!settings.calendarId) return { summary: 'no calendar chosen' }
   const window = pullWindow()
   const window = pullWindow()
   const events: PulledEvent[] = []
   const events: PulledEvent[] = []
   let pageToken: string | undefined
   let pageToken: string | undefined

+ 2 - 1
src/integrations/microsoft-365/server.ts

@@ -154,7 +154,8 @@ function toPulled(e: GraphEvent): PulledEvent | null {
 
 
 async function pullBusy(ctx: ConnectorContext) {
 async function pullBusy(ctx: ConnectorContext) {
   const settings = settingsOf(ctx)
   const settings = settingsOf(ctx)
-  if (!settings.pullEnabled || !settings.calendarId) return { summary: 'pull disabled' }
+  if (!settings.pullEnabled) return { summary: 'pull switched off' }
+  if (!settings.calendarId) return { summary: 'no calendar chosen' }
   const window = pullWindow()
   const window = pullWindow()
   const events: PulledEvent[] = []
   const events: PulledEvent[] = []
   let next: string | undefined = (() => {
   let next: string | undefined = (() => {