Browse Source

Add Telegram messaging integration (#88)

* implemented telegram integration

* translations

* translations

* add telgram to layout
Bernt Christian Egeland 6 months ago
parent
commit
aad431258d
100 changed files with 3629 additions and 135 deletions
  1. 3 1
      messages/de/customers.json
  2. 3 0
      messages/de/navigation.json
  3. 6 1
      messages/de/settings.json
  4. 3 1
      messages/de/share.json
  5. 62 0
      messages/de/telegram.json
  6. 45 0
      messages/de/telegramMessages.json
  7. 3 1
      messages/en/customers.json
  8. 3 0
      messages/en/navigation.json
  9. 6 1
      messages/en/settings.json
  10. 3 1
      messages/en/share.json
  11. 62 0
      messages/en/telegram.json
  12. 45 0
      messages/en/telegramMessages.json
  13. 3 1
      messages/es/customers.json
  14. 15 12
      messages/es/navigation.json
  15. 6 1
      messages/es/settings.json
  16. 3 1
      messages/es/share.json
  17. 62 0
      messages/es/telegram.json
  18. 45 0
      messages/es/telegramMessages.json
  19. 3 1
      messages/fr/customers.json
  20. 19 16
      messages/fr/navigation.json
  21. 6 1
      messages/fr/settings.json
  22. 3 1
      messages/fr/share.json
  23. 62 0
      messages/fr/telegram.json
  24. 45 0
      messages/fr/telegramMessages.json
  25. 3 1
      messages/it/customers.json
  26. 3 0
      messages/it/navigation.json
  27. 6 1
      messages/it/settings.json
  28. 3 1
      messages/it/share.json
  29. 62 0
      messages/it/telegram.json
  30. 45 0
      messages/it/telegramMessages.json
  31. 3 1
      messages/nb/customers.json
  32. 3 0
      messages/nb/navigation.json
  33. 6 1
      messages/nb/settings.json
  34. 3 1
      messages/nb/share.json
  35. 62 0
      messages/nb/telegram.json
  36. 45 0
      messages/nb/telegramMessages.json
  37. 3 1
      messages/nl/customers.json
  38. 3 0
      messages/nl/navigation.json
  39. 6 1
      messages/nl/settings.json
  40. 3 1
      messages/nl/share.json
  41. 62 0
      messages/nl/telegram.json
  42. 45 0
      messages/nl/telegramMessages.json
  43. 3 1
      messages/pl/customers.json
  44. 3 0
      messages/pl/navigation.json
  45. 6 1
      messages/pl/settings.json
  46. 3 1
      messages/pl/share.json
  47. 62 0
      messages/pl/telegram.json
  48. 45 0
      messages/pl/telegramMessages.json
  49. 3 1
      messages/pt-BR/customers.json
  50. 3 0
      messages/pt-BR/navigation.json
  51. 6 1
      messages/pt-BR/settings.json
  52. 3 1
      messages/pt-BR/share.json
  53. 62 0
      messages/pt-BR/telegram.json
  54. 45 0
      messages/pt-BR/telegramMessages.json
  55. 3 1
      messages/ru/customers.json
  56. 3 0
      messages/ru/navigation.json
  57. 8 3
      messages/ru/settings.json
  58. 3 1
      messages/ru/share.json
  59. 62 0
      messages/ru/telegram.json
  60. 45 0
      messages/ru/telegramMessages.json
  61. 3 1
      messages/tr/customers.json
  62. 3 0
      messages/tr/navigation.json
  63. 6 1
      messages/tr/settings.json
  64. 3 1
      messages/tr/share.json
  65. 62 0
      messages/tr/telegram.json
  66. 45 0
      messages/tr/telegramMessages.json
  67. 218 7
      package-lock.json
  68. 3 1
      package.json
  69. 33 0
      prisma/migrations/20260323160120_telegram_messages/migration.sql
  70. 69 42
      prisma/schema.prisma
  71. 70 14
      src/app/(authenticated)/customers/[id]/customer-detail-client.tsx
  72. 30 1
      src/app/(authenticated)/customers/[id]/page.tsx
  73. 20 0
      src/app/(authenticated)/layout.tsx
  74. 16 2
      src/app/(authenticated)/settings/invoice/invoice-settings.tsx
  75. 12 0
      src/app/(authenticated)/settings/invoice/page.tsx
  76. 2 0
      src/app/(authenticated)/settings/settings-nav.tsx
  77. 37 0
      src/app/(authenticated)/settings/telegram/page.tsx
  78. 74 0
      src/app/(authenticated)/telegram/page.tsx
  79. 28 0
      src/app/(authenticated)/telegram/telegram-not-configured-message.tsx
  80. 15 0
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  81. 5 0
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  82. 13 0
      src/app/api/protected/services/[id]/pdf/route.ts
  83. 13 0
      src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts
  84. 148 0
      src/app/api/webhooks/telegram/[organizationId]/route.ts
  85. 7 1
      src/components/app-sidebar.tsx
  86. 6 2
      src/features/settings/Components/InvoiceLayoutEditor.tsx
  87. 17 1
      src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx
  88. 3 1
      src/features/settings/Schema/invoiceLayoutSchema.ts
  89. 7 0
      src/features/settings/Schema/settingsSchema.ts
  90. 184 0
      src/features/telegram/Actions/telegramActions.ts
  91. 165 0
      src/features/telegram/Actions/telegramSettingsActions.ts
  92. 71 0
      src/features/telegram/Actions/telegramThreadActions.ts
  93. 122 0
      src/features/telegram/Components/SendTelegramDialog.tsx
  94. 200 0
      src/features/telegram/Components/TelegramConversation.tsx
  95. 118 0
      src/features/telegram/Components/TelegramConversationPanel.tsx
  96. 72 0
      src/features/telegram/Components/TelegramDisconnectButton.tsx
  97. 76 0
      src/features/telegram/Components/TelegramMessageBubble.tsx
  98. 160 0
      src/features/telegram/Components/TelegramMessagesClient.tsx
  99. 79 0
      src/features/telegram/Components/TelegramQrCode.tsx
  100. 164 0
      src/features/telegram/Components/TelegramSettingsForm.tsx

+ 3 - 1
messages/de/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Fahrzeuge ({count})",
       "vehicles": "Fahrzeuge ({count})",
       "messages": "Nachrichten",
       "messages": "Nachrichten",
-      "requests": "Anfragen ({count})"
+      "requests": "Anfragen ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Kennzeichen",
       "plate": "Kennzeichen",

+ 3 - 0
messages/de/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Erscheinungsbild",
     "appearance": "Erscheinungsbild",
     "email": "E-Mail",
     "email": "E-Mail",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Über",
     "about": "Über",
     "home": "Startseite",
     "home": "Startseite",
     "newServiceRecord": "Neuer Serviceeintrag",
     "newServiceRecord": "Neuer Serviceeintrag",
@@ -62,6 +63,8 @@
     "clients": "Kunden",
     "clients": "Kunden",
     "customers": "Kunden",
     "customers": "Kunden",
     "messages": "Nachrichten",
     "messages": "Nachrichten",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Werkstatt",
     "workshop": "Werkstatt",
     "vehicles": "Fahrzeuge",
     "vehicles": "Fahrzeuge",
     "reminders": "Erinnerungen",
     "reminders": "Erinnerungen",

+ 6 - 1
messages/de/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "SMS-Anbieter & Nachrichten"
         "description": "SMS-Anbieter & Nachrichten"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Kundenportal",
         "title": "Kundenportal",
         "description": "Kunden-Self-Service-Portal"
         "description": "Kunden-Self-Service-Portal"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Diagnosenotizen",
       "diagnostic_notes": "Diagnosenotizen",
       "bank_account": "Bankkonto",
       "bank_account": "Bankkonto",
       "footer": "Fußzeile",
       "footer": "Fußzeile",
-      "general": "Allgemein"
+      "general": "Allgemein",
+      "telegram_qr": "Telegram-QR"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Kundenname",
       "customer_name": "Kundenname",

+ 3 - 1
messages/de/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Bereitgestellt von",
     "poweredBy": "Bereitgestellt von",
     "termsOfSale": "Verkaufsbedingungen",
     "termsOfSale": "Verkaufsbedingungen",
     "portalMessage": "Sehen Sie Ihre Fahrzeuge, Rechnungen und mehr in Ihrem",
     "portalMessage": "Sehen Sie Ihre Fahrzeuge, Rechnungen und mehr in Ihrem",
-    "customerPortal": "Kundenportal"
+    "customerPortal": "Kundenportal",
+    "telegramConnect": "Chatten Sie mit uns auf Telegram",
+    "telegramScan": "Scannen Sie diesen QR-Code, um uns auf Telegram für Updates und Support zu kontaktieren."
   }
   }
 }
 }

+ 62 - 0
messages/de/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Telegram-Einstellungen",
+  "description": "Konfigurieren Sie Ihren Telegram-Bot zum Senden und Empfangen von Nachrichten mit Kunden.",
+  "botToken": {
+    "label": "Bot-Token",
+    "placeholder": "Bot-Token von @BotFather eingeben",
+    "help": "Erstellen Sie einen Bot über @BotFather auf Telegram, um Ihr Token zu erhalten."
+  },
+  "botUsername": {
+    "label": "Bot-Benutzername",
+    "notConnected": "Nicht verbunden"
+  },
+  "webhook": {
+    "label": "Webhook-URL",
+    "description": "Diese URL wird automatisch konfiguriert, wenn Sie Ihr Bot-Token speichern."
+  },
+  "deepLink": {
+    "title": "Kunden-Verbindungslink",
+    "description": "Teilen Sie diesen Link mit Kunden, damit sie sich mit Ihrem Bot verbinden können. Ersetzen Sie den Kunden-ID-Platzhalter durch die tatsächliche Kunden-ID.",
+    "copied": "Link kopiert!"
+  },
+  "test": {
+    "title": "Testnachricht",
+    "chatId": "Chat-ID",
+    "chatIdPlaceholder": "Telegram Chat-ID eingeben",
+    "message": "Nachricht",
+    "messagePlaceholder": "Testnachricht eingeben...",
+    "send": "Test senden",
+    "success": "Testnachricht gesendet!",
+    "error": "Testnachricht konnte nicht gesendet werden"
+  },
+  "save": "Einstellungen speichern",
+  "saving": "Speichern...",
+  "saved": "Telegram-Einstellungen gespeichert",
+  "saveError": "Einstellungen konnten nicht gespeichert werden",
+  "disconnect": {
+    "button": "Bot trennen",
+    "title": "Telegram-Bot trennen?",
+    "description": "Dies entfernt die Bot-Konfiguration und stoppt den Nachrichtenempfang. Der bestehende Nachrichtenverlauf bleibt erhalten.",
+    "success": "Telegram-Bot getrennt",
+    "error": "Trennung fehlgeschlagen"
+  },
+  "qr": {
+    "title": "Auf Telegram verbinden",
+    "description": "Scannen Sie diesen QR-Code, um mit uns auf Telegram zu chatten.",
+    "copyLink": "Link kopieren",
+    "copied": "Link kopiert!",
+    "sendEmail": "Per E-Mail senden",
+    "emailSubject": "Verbinden Sie sich mit uns auf Telegram",
+    "emailBody": "Hallo {name},\n\nSie können jetzt mit uns auf Telegram chatten, um Updates zu Ihrem Fahrzeug zu erhalten.\n\nKlicken Sie auf diesen Link: {link}\n\nVielen Dank!"
+  },
+  "invoiceQr": {
+    "label": "QR-Code auf Rechnungen anzeigen",
+    "description": "Einen Telegram-QR-Code auf geteilten digitalen Rechnungen anzeigen, damit Kunden sich verbinden können."
+  },
+  "helpLink": "Mehr erfahren",
+  "enable": {
+    "label": "Telegram aktivieren",
+    "hint": "Telegram in der Seitenleiste und auf Kundenseiten anzeigen.",
+    "disabledInfo": "Telegram ist derzeit deaktiviert. Aktivieren Sie es, um Nachrichten über Telegram zu senden und zu empfangen."
+  }
+}

+ 45 - 0
messages/de/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Telegram-Nachrichten",
+    "featureDescription": "Senden und empfangen Sie Telegram-Nachrichten mit Ihren Kunden direkt aus Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram nicht konfiguriert",
+    "description": "Richten Sie einen Telegram-Bot ein, um Nachrichten mit Ihren Kunden zu senden und zu empfangen. Erstellen Sie einen Bot über @BotFather und konfigurieren Sie ihn in den Einstellungen.",
+    "goToSettings": "Zu Telegram-Einstellungen"
+  },
+  "threads": {
+    "title": "Gespräche",
+    "empty": "Noch keine Telegram-Gespräche. Kunden können einen Chat starten, indem sie Ihrem Bot eine Nachricht senden.",
+    "loadMore": "Mehr laden",
+    "selectConversation": "Gespräch auswählen",
+    "you": "Sie: ",
+    "deleteConversation": "Gespräch löschen",
+    "deleteTitle": "Gespräch löschen?",
+    "deleteDescription": "Dies löscht alle Telegram-Nachrichten mit {name} dauerhaft. Diese Aktion kann nicht rückgängig gemacht werden.",
+    "deleted": "Gespräch gelöscht",
+    "deleteError": "Gespräch konnte nicht gelöscht werden"
+  },
+  "conversation": {
+    "notLinked": "Dieser Kunde hat sich noch nicht mit Ihrem Telegram-Bot verbunden. Teilen Sie den Bot-Link, um ihn zu verbinden.",
+    "loadOlder": "Ältere laden",
+    "empty": "Noch keine Nachrichten. Senden Sie die erste Nachricht an {name}.",
+    "placeholder": "Nachricht an {name}...",
+    "deleteMessage": "Nachricht löschen",
+    "deleteTitle": "Nachricht löschen?",
+    "deleteDescription": "Dies löscht diese Nachricht dauerhaft. Diese Aktion kann nicht rückgängig gemacht werden.",
+    "messageDeleted": "Nachricht gelöscht",
+    "deleteError": "Nachricht konnte nicht gelöscht werden",
+    "sendError": "Nachricht konnte nicht gesendet werden"
+  },
+  "sendDialog": {
+    "send": "{label} senden",
+    "message": "Nachricht",
+    "placeholder": "Nachricht eingeben...",
+    "characters": "{count} / 4096 Zeichen",
+    "sendTelegram": "Nachricht senden",
+    "messageSent": "Nachricht gesendet",
+    "sentTo": "Nachricht an {name} via Telegram gesendet",
+    "sendError": "Nachricht konnte nicht gesendet werden"
+  }
+}

+ 3 - 1
messages/en/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Vehicles ({count})",
       "vehicles": "Vehicles ({count})",
       "messages": "Messages",
       "messages": "Messages",
-      "requests": "Requests ({count})"
+      "requests": "Requests ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Plate",
       "plate": "Plate",

+ 3 - 0
messages/en/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Appearance",
     "appearance": "Appearance",
     "email": "Email",
     "email": "Email",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "About",
     "about": "About",
     "home": "Home",
     "home": "Home",
     "newServiceRecord": "New Service Record",
     "newServiceRecord": "New Service Record",
@@ -62,6 +63,8 @@
     "clients": "Clients",
     "clients": "Clients",
     "customers": "Customers",
     "customers": "Customers",
     "messages": "Messages",
     "messages": "Messages",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Workshop",
     "workshop": "Workshop",
     "vehicles": "Vehicles",
     "vehicles": "Vehicles",
     "reminders": "Reminders",
     "reminders": "Reminders",

+ 6 - 1
messages/en/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "SMS provider & messaging"
         "description": "SMS provider & messaging"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Customer Portal",
         "title": "Customer Portal",
         "description": "Customer self-service portal"
         "description": "Customer self-service portal"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Diagnostic Notes",
       "diagnostic_notes": "Diagnostic Notes",
       "bank_account": "Bank Account",
       "bank_account": "Bank Account",
       "footer": "Footer",
       "footer": "Footer",
-      "general": "General"
+      "general": "General",
+      "telegram_qr": "Telegram QR"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Customer Name",
       "customer_name": "Customer Name",

+ 3 - 1
messages/en/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Powered by",
     "poweredBy": "Powered by",
     "termsOfSale": "Terms of Sale",
     "termsOfSale": "Terms of Sale",
     "portalMessage": "View your vehicles, invoices, and more at your",
     "portalMessage": "View your vehicles, invoices, and more at your",
-    "customerPortal": "customer portal"
+    "customerPortal": "customer portal",
+    "telegramConnect": "Chat with us on Telegram",
+    "telegramScan": "Scan this QR code to message us on Telegram for updates and support."
   }
   }
 }
 }

+ 62 - 0
messages/en/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Telegram Settings",
+  "description": "Configure your Telegram bot to send and receive messages with customers.",
+  "botToken": {
+    "label": "Bot Token",
+    "placeholder": "Enter your bot token from @BotFather",
+    "help": "Create a bot via @BotFather on Telegram to get your token."
+  },
+  "botUsername": {
+    "label": "Bot Username",
+    "notConnected": "Not connected"
+  },
+  "webhook": {
+    "label": "Webhook URL",
+    "description": "This URL is automatically configured when you save your bot token."
+  },
+  "deepLink": {
+    "title": "Customer Connection Link",
+    "description": "Share this link with customers so they can connect to your bot. Replace the customer ID placeholder with the actual customer ID.",
+    "copied": "Link copied!"
+  },
+  "test": {
+    "title": "Test Message",
+    "chatId": "Chat ID",
+    "chatIdPlaceholder": "Enter a Telegram chat ID",
+    "message": "Message",
+    "messagePlaceholder": "Type a test message...",
+    "send": "Send Test",
+    "success": "Test message sent!",
+    "error": "Failed to send test message"
+  },
+  "save": "Save Settings",
+  "saving": "Saving...",
+  "saved": "Telegram settings saved",
+  "saveError": "Failed to save settings",
+  "disconnect": {
+    "button": "Disconnect Bot",
+    "title": "Disconnect Telegram bot?",
+    "description": "This will remove the bot configuration and stop receiving messages. Existing conversation history will be preserved.",
+    "success": "Telegram bot disconnected",
+    "error": "Failed to disconnect"
+  },
+  "qr": {
+    "title": "Connect on Telegram",
+    "description": "Scan this QR code to chat with us on Telegram.",
+    "copyLink": "Copy link",
+    "copied": "Link copied!",
+    "sendEmail": "Send by email",
+    "emailSubject": "Connect with us on Telegram",
+    "emailBody": "Hi {name},\n\nYou can now chat with us on Telegram for updates on your vehicle.\n\nClick this link to connect: {link}\n\nThank you!"
+  },
+  "invoiceQr": {
+    "label": "Show QR code on invoices",
+    "description": "Display a Telegram QR code on shared digital invoices so customers can connect."
+  },
+  "helpLink": "Learn more",
+  "enable": {
+    "label": "Enable Telegram",
+    "hint": "Show Telegram in the sidebar and on customer pages.",
+    "disabledInfo": "Telegram is currently disabled. Enable it to start sending and receiving messages via Telegram."
+  }
+}

+ 45 - 0
messages/en/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Telegram Messaging",
+    "featureDescription": "Send and receive Telegram messages with your customers directly from Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram Not Configured",
+    "description": "Set up a Telegram bot to send and receive messages with your customers. Create a bot via @BotFather and configure it in settings.",
+    "goToSettings": "Go to Telegram Settings"
+  },
+  "threads": {
+    "title": "Conversations",
+    "empty": "No Telegram conversations yet. Customers can start chatting by messaging your bot.",
+    "loadMore": "Load more",
+    "selectConversation": "Select a conversation",
+    "you": "You: ",
+    "deleteConversation": "Delete conversation",
+    "deleteTitle": "Delete conversation?",
+    "deleteDescription": "This will permanently delete all Telegram messages with {name}. This action cannot be undone.",
+    "deleted": "Conversation deleted",
+    "deleteError": "Failed to delete conversation"
+  },
+  "conversation": {
+    "notLinked": "This customer hasn't connected to your Telegram bot yet. Share the bot link to connect them.",
+    "loadOlder": "Load older",
+    "empty": "No messages yet. Send the first message to {name}.",
+    "placeholder": "Message {name}...",
+    "deleteMessage": "Delete message",
+    "deleteTitle": "Delete message?",
+    "deleteDescription": "This will permanently delete this message. This action cannot be undone.",
+    "messageDeleted": "Message deleted",
+    "deleteError": "Failed to delete message",
+    "sendError": "Failed to send message"
+  },
+  "sendDialog": {
+    "send": "Send {label}",
+    "message": "Message",
+    "placeholder": "Type your message...",
+    "characters": "{count} / 4096 characters",
+    "sendTelegram": "Send Message",
+    "messageSent": "Message Sent",
+    "sentTo": "Message sent to {name} via Telegram",
+    "sendError": "Failed to send message"
+  }
+}

+ 3 - 1
messages/es/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Vehículos ({count})",
       "vehicles": "Vehículos ({count})",
       "messages": "Mensajes",
       "messages": "Mensajes",
-      "requests": "Solicitudes ({count})"
+      "requests": "Solicitudes ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Placa",
       "plate": "Placa",

+ 15 - 12
messages/es/navigation.json

@@ -1,32 +1,32 @@
 {
 {
   "breadcrumbs": {
   "breadcrumbs": {
     "dashboard": "Panel de control",
     "dashboard": "Panel de control",
-    "vehicles": "Veh\u00edculos",
-    "allVehicles": "Todos los veh\u00edculos",
-    "vehicleDetails": "Detalles del veh\u00edculo",
+    "vehicles": "Vehículos",
+    "allVehicles": "Todos los vehículos",
+    "vehicleDetails": "Detalles del vehículo",
     "customers": "Clientes",
     "customers": "Clientes",
     "allCustomers": "Todos los clientes",
     "allCustomers": "Todos los clientes",
     "customerDetails": "Detalles del cliente",
     "customerDetails": "Detalles del cliente",
-    "workOrders": "\u00d3rdenes de trabajo",
-    "allWorkOrders": "Todas las \u00f3rdenes de trabajo",
+    "workOrders": "Órdenes de trabajo",
+    "allWorkOrders": "Todas las órdenes de trabajo",
     "quotes": "Presupuestos",
     "quotes": "Presupuestos",
     "allQuotes": "Todos los presupuestos",
     "allQuotes": "Todos los presupuestos",
     "newQuote": "Nuevo presupuesto",
     "newQuote": "Nuevo presupuesto",
     "quoteDetails": "Detalles del presupuesto",
     "quoteDetails": "Detalles del presupuesto",
     "edit": "Editar",
     "edit": "Editar",
-    "billing": "Facturaci\u00f3n",
-    "billingHistory": "Historial de facturaci\u00f3n",
+    "billing": "Facturación",
+    "billingHistory": "Historial de facturación",
     "inventory": "Inventario",
     "inventory": "Inventario",
     "allParts": "Todas las piezas",
     "allParts": "Todas las piezas",
     "laborPresets": "Plantillas de mano de obra",
     "laborPresets": "Plantillas de mano de obra",
     "reports": "Informes",
     "reports": "Informes",
     "workBoard": "Tablero de trabajo",
     "workBoard": "Tablero de trabajo",
     "presenter": "Presentador",
     "presenter": "Presentador",
-    "adminOverview": "Resumen de administraci\u00f3n",
-    "admin": "Administraci\u00f3n",
+    "adminOverview": "Resumen de administración",
+    "admin": "Administración",
     "users": "Usuarios",
     "users": "Usuarios",
     "organizations": "Organizaciones",
     "organizations": "Organizaciones",
-    "settings": "Configuraci\u00f3n",
+    "settings": "Configuración",
     "company": "Empresa",
     "company": "Empresa",
     "account": "Cuenta",
     "account": "Cuenta",
     "customFields": "Campos personalizados",
     "customFields": "Campos personalizados",
@@ -37,15 +37,16 @@
     "currency": "Moneda",
     "currency": "Moneda",
     "workshop": "Taller",
     "workshop": "Taller",
     "appearance": "Apariencia",
     "appearance": "Apariencia",
-    "email": "Correo electr\u00f3nico",
+    "email": "Correo electrónico",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Acerca de",
     "about": "Acerca de",
     "home": "Inicio",
     "home": "Inicio",
     "newServiceRecord": "Nuevo registro de servicio",
     "newServiceRecord": "Nuevo registro de servicio",
     "serviceDetails": "Detalles del servicio",
     "serviceDetails": "Detalles del servicio",
     "data": "Datos",
     "data": "Datos",
     "license": "Licencia",
     "license": "Licencia",
-    "subscription": "Suscripci\u00f3n",
+    "subscription": "Suscripción",
     "maintenance": "Mantenimiento",
     "maintenance": "Mantenimiento",
     "customerPortal": "Portal del cliente",
     "customerPortal": "Portal del cliente",
     "reminders": "Recordatorios",
     "reminders": "Recordatorios",
@@ -62,6 +63,8 @@
     "clients": "Clientes",
     "clients": "Clientes",
     "customers": "Clientes",
     "customers": "Clientes",
     "messages": "Mensajes",
     "messages": "Mensajes",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Taller",
     "workshop": "Taller",
     "vehicles": "Vehículos",
     "vehicles": "Vehículos",
     "reminders": "Recordatorios",
     "reminders": "Recordatorios",

+ 6 - 1
messages/es/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Proveedor de SMS y mensajeria"
         "description": "Proveedor de SMS y mensajeria"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Portal del cliente",
         "title": "Portal del cliente",
         "description": "Portal de autoservicio del cliente"
         "description": "Portal de autoservicio del cliente"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Notas de diagnóstico",
       "diagnostic_notes": "Notas de diagnóstico",
       "bank_account": "Cuenta bancaria",
       "bank_account": "Cuenta bancaria",
       "footer": "Pie de página",
       "footer": "Pie de página",
-      "general": "General"
+      "general": "General",
+      "telegram_qr": "QR de Telegram"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Nombre del cliente",
       "customer_name": "Nombre del cliente",

+ 3 - 1
messages/es/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Desarrollado por",
     "poweredBy": "Desarrollado por",
     "termsOfSale": "Términos de venta",
     "termsOfSale": "Términos de venta",
     "portalMessage": "Vea sus vehículos, facturas y más en su",
     "portalMessage": "Vea sus vehículos, facturas y más en su",
-    "customerPortal": "portal de clientes"
+    "customerPortal": "portal de clientes",
+    "telegramConnect": "Chatea con nosotros en Telegram",
+    "telegramScan": "Escanea este código QR para enviarnos un mensaje en Telegram para actualizaciones y soporte."
   }
   }
 }
 }

+ 62 - 0
messages/es/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Configuración de Telegram",
+  "description": "Configura tu bot de Telegram para enviar y recibir mensajes con clientes.",
+  "botToken": {
+    "label": "Token del bot",
+    "placeholder": "Ingresa el token de @BotFather",
+    "help": "Crea un bot a través de @BotFather en Telegram para obtener tu token."
+  },
+  "botUsername": {
+    "label": "Nombre de usuario del bot",
+    "notConnected": "No conectado"
+  },
+  "webhook": {
+    "label": "URL del webhook",
+    "description": "Esta URL se configura automáticamente al guardar el token del bot."
+  },
+  "deepLink": {
+    "title": "Enlace de conexión del cliente",
+    "description": "Comparte este enlace con los clientes para que se conecten a tu bot. Reemplaza el marcador de ID del cliente con el ID real.",
+    "copied": "¡Enlace copiado!"
+  },
+  "test": {
+    "title": "Mensaje de prueba",
+    "chatId": "ID de chat",
+    "chatIdPlaceholder": "Ingresa un ID de chat de Telegram",
+    "message": "Mensaje",
+    "messagePlaceholder": "Escribe un mensaje de prueba...",
+    "send": "Enviar prueba",
+    "success": "¡Mensaje de prueba enviado!",
+    "error": "No se pudo enviar el mensaje de prueba"
+  },
+  "save": "Guardar configuración",
+  "saving": "Guardando...",
+  "saved": "Configuración de Telegram guardada",
+  "saveError": "No se pudo guardar la configuración",
+  "disconnect": {
+    "button": "Desconectar bot",
+    "title": "¿Desconectar bot de Telegram?",
+    "description": "Esto eliminará la configuración del bot y dejará de recibir mensajes. El historial de conversaciones existente se conservará.",
+    "success": "Bot de Telegram desconectado",
+    "error": "No se pudo desconectar"
+  },
+  "qr": {
+    "title": "Conectar en Telegram",
+    "description": "Escanea este código QR para chatear con nosotros en Telegram.",
+    "copyLink": "Copiar enlace",
+    "copied": "¡Enlace copiado!",
+    "sendEmail": "Enviar por correo",
+    "emailSubject": "Conéctate con nosotros en Telegram",
+    "emailBody": "Hola {name},\n\nAhora puedes chatear con nosotros en Telegram para recibir actualizaciones sobre tu vehículo.\n\nHaz clic en este enlace: {link}\n\n¡Gracias!"
+  },
+  "invoiceQr": {
+    "label": "Mostrar código QR en facturas",
+    "description": "Mostrar un código QR de Telegram en las facturas digitales compartidas para que los clientes puedan conectarse."
+  },
+  "helpLink": "Más información",
+  "enable": {
+    "label": "Activar Telegram",
+    "hint": "Mostrar Telegram en la barra lateral y en las páginas de clientes.",
+    "disabledInfo": "Telegram está desactivado actualmente. Actívalo para empezar a enviar y recibir mensajes por Telegram."
+  }
+}

+ 45 - 0
messages/es/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Mensajes de Telegram",
+    "featureDescription": "Envía y recibe mensajes de Telegram con tus clientes directamente desde Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram no configurado",
+    "description": "Configura un bot de Telegram para enviar y recibir mensajes con tus clientes. Crea un bot a través de @BotFather y configúralo en ajustes.",
+    "goToSettings": "Ir a ajustes de Telegram"
+  },
+  "threads": {
+    "title": "Conversaciones",
+    "empty": "Aún no hay conversaciones de Telegram. Los clientes pueden iniciar un chat enviando un mensaje a tu bot.",
+    "loadMore": "Cargar más",
+    "selectConversation": "Seleccionar conversación",
+    "you": "Tú: ",
+    "deleteConversation": "Eliminar conversación",
+    "deleteTitle": "¿Eliminar conversación?",
+    "deleteDescription": "Esto eliminará permanentemente todos los mensajes de Telegram con {name}. Esta acción no se puede deshacer.",
+    "deleted": "Conversación eliminada",
+    "deleteError": "No se pudo eliminar la conversación"
+  },
+  "conversation": {
+    "notLinked": "Este cliente aún no se ha conectado a tu bot de Telegram. Comparte el enlace del bot para conectarlo.",
+    "loadOlder": "Cargar anteriores",
+    "empty": "Aún no hay mensajes. Envía el primer mensaje a {name}.",
+    "placeholder": "Mensaje a {name}...",
+    "deleteMessage": "Eliminar mensaje",
+    "deleteTitle": "¿Eliminar mensaje?",
+    "deleteDescription": "Esto eliminará permanentemente este mensaje. Esta acción no se puede deshacer.",
+    "messageDeleted": "Mensaje eliminado",
+    "deleteError": "No se pudo eliminar el mensaje",
+    "sendError": "No se pudo enviar el mensaje"
+  },
+  "sendDialog": {
+    "send": "Enviar {label}",
+    "message": "Mensaje",
+    "placeholder": "Escribe tu mensaje...",
+    "characters": "{count} / 4096 caracteres",
+    "sendTelegram": "Enviar mensaje",
+    "messageSent": "Mensaje enviado",
+    "sentTo": "Mensaje enviado a {name} vía Telegram",
+    "sendError": "No se pudo enviar el mensaje"
+  }
+}

+ 3 - 1
messages/fr/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Véhicules ({count})",
       "vehicles": "Véhicules ({count})",
       "messages": "Messages",
       "messages": "Messages",
-      "requests": "Demandes ({count})"
+      "requests": "Demandes ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Plaque",
       "plate": "Plaque",

+ 19 - 16
messages/fr/navigation.json

@@ -1,37 +1,37 @@
 {
 {
   "breadcrumbs": {
   "breadcrumbs": {
     "dashboard": "Tableau de bord",
     "dashboard": "Tableau de bord",
-    "vehicles": "V\u00e9hicules",
-    "allVehicles": "Tous les v\u00e9hicules",
-    "vehicleDetails": "D\u00e9tails du v\u00e9hicule",
+    "vehicles": "Véhicules",
+    "allVehicles": "Tous les véhicules",
+    "vehicleDetails": "Détails du véhicule",
     "customers": "Clients",
     "customers": "Clients",
     "allCustomers": "Tous les clients",
     "allCustomers": "Tous les clients",
-    "customerDetails": "D\u00e9tails du client",
+    "customerDetails": "Détails du client",
     "workOrders": "Ordres de travail",
     "workOrders": "Ordres de travail",
     "allWorkOrders": "Tous les ordres de travail",
     "allWorkOrders": "Tous les ordres de travail",
     "quotes": "Devis",
     "quotes": "Devis",
     "allQuotes": "Tous les devis",
     "allQuotes": "Tous les devis",
     "newQuote": "Nouveau devis",
     "newQuote": "Nouveau devis",
-    "quoteDetails": "D\u00e9tails du devis",
+    "quoteDetails": "Détails du devis",
     "edit": "Modifier",
     "edit": "Modifier",
     "billing": "Facturation",
     "billing": "Facturation",
     "billingHistory": "Historique de facturation",
     "billingHistory": "Historique de facturation",
     "inventory": "Inventaire",
     "inventory": "Inventaire",
-    "allParts": "Toutes les pi\u00e8ces",
+    "allParts": "Toutes les pièces",
     "laborPresets": "Modèles de main-d'œuvre",
     "laborPresets": "Modèles de main-d'œuvre",
     "reports": "Rapports",
     "reports": "Rapports",
     "workBoard": "Tableau de travail",
     "workBoard": "Tableau de travail",
-    "presenter": "Pr\u00e9sentateur",
-    "adminOverview": "Vue d\u2019ensemble admin",
+    "presenter": "Présentateur",
+    "adminOverview": "Vue d’ensemble admin",
     "admin": "Administration",
     "admin": "Administration",
     "users": "Utilisateurs",
     "users": "Utilisateurs",
     "organizations": "Organisations",
     "organizations": "Organisations",
-    "settings": "Param\u00e8tres",
+    "settings": "Paramètres",
     "company": "Entreprise",
     "company": "Entreprise",
     "account": "Compte",
     "account": "Compte",
-    "customFields": "Champs personnalis\u00e9s",
-    "templates": "Mod\u00e8les",
-    "team": "\u00c9quipe",
+    "customFields": "Champs personnalisés",
+    "templates": "Modèles",
+    "team": "Équipe",
     "invoice": "Facture",
     "invoice": "Facture",
     "payment": "Paiement",
     "payment": "Paiement",
     "currency": "Devise",
     "currency": "Devise",
@@ -39,11 +39,12 @@
     "appearance": "Apparence",
     "appearance": "Apparence",
     "email": "E-mail",
     "email": "E-mail",
     "sms": "SMS",
     "sms": "SMS",
-    "about": "\u00c0 propos",
+    "telegram": "Telegram",
+    "about": "À propos",
     "home": "Accueil",
     "home": "Accueil",
     "newServiceRecord": "Nouvel entretien",
     "newServiceRecord": "Nouvel entretien",
-    "serviceDetails": "D\u00e9tails de l\u2019entretien",
-    "data": "Donn\u00e9es",
+    "serviceDetails": "Détails de l’entretien",
+    "data": "Données",
     "license": "Licence",
     "license": "Licence",
     "subscription": "Abonnement",
     "subscription": "Abonnement",
     "maintenance": "Maintenance",
     "maintenance": "Maintenance",
@@ -53,7 +54,7 @@
     "auditLog": "Journal d'audit",
     "auditLog": "Journal d'audit",
     "aiAssistant": "Assistant IA"
     "aiAssistant": "Assistant IA"
   },
   },
-  "search": "Rechercher\u2026",
+  "search": "Rechercher…",
   "shortcut": "Ctrl+K",
   "shortcut": "Ctrl+K",
   "newWorkOrder": "Nouvel ordre de travail",
   "newWorkOrder": "Nouvel ordre de travail",
   "purchaseWhiteLabel": "Acheter la marque blanche",
   "purchaseWhiteLabel": "Acheter la marque blanche",
@@ -62,6 +63,8 @@
     "clients": "Clients",
     "clients": "Clients",
     "customers": "Clients",
     "customers": "Clients",
     "messages": "Messages",
     "messages": "Messages",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Atelier",
     "workshop": "Atelier",
     "vehicles": "Véhicules",
     "vehicles": "Véhicules",
     "reminders": "Rappels",
     "reminders": "Rappels",

+ 6 - 1
messages/fr/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Fournisseur SMS et messagerie"
         "description": "Fournisseur SMS et messagerie"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Portail client",
         "title": "Portail client",
         "description": "Portail libre-service client"
         "description": "Portail libre-service client"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Notes de diagnostic",
       "diagnostic_notes": "Notes de diagnostic",
       "bank_account": "Compte bancaire",
       "bank_account": "Compte bancaire",
       "footer": "Pied de page",
       "footer": "Pied de page",
-      "general": "Général"
+      "general": "Général",
+      "telegram_qr": "QR Telegram"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Nom du client",
       "customer_name": "Nom du client",

+ 3 - 1
messages/fr/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Propulsé par",
     "poweredBy": "Propulsé par",
     "termsOfSale": "Conditions de vente",
     "termsOfSale": "Conditions de vente",
     "portalMessage": "Consultez vos véhicules, factures et plus encore sur votre",
     "portalMessage": "Consultez vos véhicules, factures et plus encore sur votre",
-    "customerPortal": "portail client"
+    "customerPortal": "portail client",
+    "telegramConnect": "Discutez avec nous sur Telegram",
+    "telegramScan": "Scannez ce code QR pour nous envoyer un message sur Telegram pour les mises à jour et le support."
   }
   }
 }
 }

+ 62 - 0
messages/fr/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Paramètres Telegram",
+  "description": "Configurez votre bot Telegram pour envoyer et recevoir des messages avec les clients.",
+  "botToken": {
+    "label": "Token du bot",
+    "placeholder": "Entrez le token de @BotFather",
+    "help": "Créez un bot via @BotFather sur Telegram pour obtenir votre token."
+  },
+  "botUsername": {
+    "label": "Nom d'utilisateur du bot",
+    "notConnected": "Non connecté"
+  },
+  "webhook": {
+    "label": "URL du webhook",
+    "description": "Cette URL est configurée automatiquement lorsque vous enregistrez le token du bot."
+  },
+  "deepLink": {
+    "title": "Lien de connexion client",
+    "description": "Partagez ce lien avec les clients pour qu'ils se connectent à votre bot. Remplacez l'espace réservé de l'ID client par l'ID réel.",
+    "copied": "Lien copié !"
+  },
+  "test": {
+    "title": "Message test",
+    "chatId": "ID du chat",
+    "chatIdPlaceholder": "Entrez un ID de chat Telegram",
+    "message": "Message",
+    "messagePlaceholder": "Tapez un message test...",
+    "send": "Envoyer le test",
+    "success": "Message test envoyé !",
+    "error": "Échec de l'envoi du message test"
+  },
+  "save": "Enregistrer",
+  "saving": "Enregistrement...",
+  "saved": "Paramètres Telegram enregistrés",
+  "saveError": "Échec de l'enregistrement",
+  "disconnect": {
+    "button": "Déconnecter le bot",
+    "title": "Déconnecter le bot Telegram ?",
+    "description": "Cela supprimera la configuration du bot et arrêtera la réception des messages. L'historique des conversations existant sera conservé.",
+    "success": "Bot Telegram déconnecté",
+    "error": "Échec de la déconnexion"
+  },
+  "qr": {
+    "title": "Se connecter sur Telegram",
+    "description": "Scannez ce code QR pour discuter avec nous sur Telegram.",
+    "copyLink": "Copier le lien",
+    "copied": "Lien copié !",
+    "sendEmail": "Envoyer par e-mail",
+    "emailSubject": "Connectez-vous avec nous sur Telegram",
+    "emailBody": "Bonjour {name},\n\nVous pouvez maintenant discuter avec nous sur Telegram pour les mises à jour de votre véhicule.\n\nCliquez sur ce lien : {link}\n\nMerci !"
+  },
+  "invoiceQr": {
+    "label": "Afficher le code QR sur les factures",
+    "description": "Afficher un code QR Telegram sur les factures numériques partagées pour que les clients puissent se connecter."
+  },
+  "helpLink": "En savoir plus",
+  "enable": {
+    "label": "Activer Telegram",
+    "hint": "Afficher Telegram dans la barre latérale et sur les pages clients.",
+    "disabledInfo": "Telegram est actuellement désactivé. Activez-le pour commencer à envoyer et recevoir des messages via Telegram."
+  }
+}

+ 45 - 0
messages/fr/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Messages Telegram",
+    "featureDescription": "Envoyez et recevez des messages Telegram avec vos clients directement depuis Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram non configuré",
+    "description": "Configurez un bot Telegram pour envoyer et recevoir des messages avec vos clients. Créez un bot via @BotFather et configurez-le dans les paramètres.",
+    "goToSettings": "Aller aux paramètres Telegram"
+  },
+  "threads": {
+    "title": "Conversations",
+    "empty": "Aucune conversation Telegram pour le moment. Les clients peuvent commencer à discuter en envoyant un message à votre bot.",
+    "loadMore": "Charger plus",
+    "selectConversation": "Sélectionner une conversation",
+    "you": "Vous : ",
+    "deleteConversation": "Supprimer la conversation",
+    "deleteTitle": "Supprimer la conversation ?",
+    "deleteDescription": "Cela supprimera définitivement tous les messages Telegram avec {name}. Cette action est irréversible.",
+    "deleted": "Conversation supprimée",
+    "deleteError": "Échec de la suppression de la conversation"
+  },
+  "conversation": {
+    "notLinked": "Ce client ne s'est pas encore connecté à votre bot Telegram. Partagez le lien du bot pour le connecter.",
+    "loadOlder": "Charger les anciens",
+    "empty": "Aucun message pour le moment. Envoyez le premier message à {name}.",
+    "placeholder": "Message à {name}...",
+    "deleteMessage": "Supprimer le message",
+    "deleteTitle": "Supprimer le message ?",
+    "deleteDescription": "Cela supprimera définitivement ce message. Cette action est irréversible.",
+    "messageDeleted": "Message supprimé",
+    "deleteError": "Échec de la suppression du message",
+    "sendError": "Échec de l'envoi du message"
+  },
+  "sendDialog": {
+    "send": "Envoyer {label}",
+    "message": "Message",
+    "placeholder": "Tapez votre message...",
+    "characters": "{count} / 4096 caractères",
+    "sendTelegram": "Envoyer le message",
+    "messageSent": "Message envoyé",
+    "sentTo": "Message envoyé à {name} via Telegram",
+    "sendError": "Échec de l'envoi du message"
+  }
+}

+ 3 - 1
messages/it/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Veicoli ({count})",
       "vehicles": "Veicoli ({count})",
       "messages": "Messaggi",
       "messages": "Messaggi",
-      "requests": "Richieste ({count})"
+      "requests": "Richieste ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Targa",
       "plate": "Targa",

+ 3 - 0
messages/it/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Aspetto",
     "appearance": "Aspetto",
     "email": "Email",
     "email": "Email",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Informazioni",
     "about": "Informazioni",
     "home": "Home",
     "home": "Home",
     "newServiceRecord": "Nuovo intervento di servizio",
     "newServiceRecord": "Nuovo intervento di servizio",
@@ -62,6 +63,8 @@
     "clients": "Clienti",
     "clients": "Clienti",
     "customers": "Clienti",
     "customers": "Clienti",
     "messages": "Messaggi",
     "messages": "Messaggi",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Officina",
     "workshop": "Officina",
     "vehicles": "Veicoli",
     "vehicles": "Veicoli",
     "reminders": "Promemoria",
     "reminders": "Promemoria",

+ 6 - 1
messages/it/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Provider SMS e messaggistica"
         "description": "Provider SMS e messaggistica"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Portale clienti",
         "title": "Portale clienti",
         "description": "Portale self-service clienti"
         "description": "Portale self-service clienti"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Note diagnostiche",
       "diagnostic_notes": "Note diagnostiche",
       "bank_account": "Conto bancario",
       "bank_account": "Conto bancario",
       "footer": "Piè di pagina",
       "footer": "Piè di pagina",
-      "general": "Generale"
+      "general": "Generale",
+      "telegram_qr": "QR Telegram"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Nome cliente",
       "customer_name": "Nome cliente",

+ 3 - 1
messages/it/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Powered by",
     "poweredBy": "Powered by",
     "termsOfSale": "Condizioni di Vendita",
     "termsOfSale": "Condizioni di Vendita",
     "portalMessage": "Visualizza i tuoi veicoli, fatture e altro nel tuo",
     "portalMessage": "Visualizza i tuoi veicoli, fatture e altro nel tuo",
-    "customerPortal": "portale clienti"
+    "customerPortal": "portale clienti",
+    "telegramConnect": "Chatta con noi su Telegram",
+    "telegramScan": "Scansiona questo codice QR per inviarci un messaggio su Telegram per aggiornamenti e supporto."
   }
   }
 }
 }

+ 62 - 0
messages/it/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Impostazioni Telegram",
+  "description": "Configura il tuo bot Telegram per inviare e ricevere messaggi con i clienti.",
+  "botToken": {
+    "label": "Token del bot",
+    "placeholder": "Inserisci il token da @BotFather",
+    "help": "Crea un bot tramite @BotFather su Telegram per ottenere il tuo token."
+  },
+  "botUsername": {
+    "label": "Nome utente del bot",
+    "notConnected": "Non connesso"
+  },
+  "webhook": {
+    "label": "URL del webhook",
+    "description": "Questo URL viene configurato automaticamente quando salvi il token del bot."
+  },
+  "deepLink": {
+    "title": "Link di connessione cliente",
+    "description": "Condividi questo link con i clienti per connettersi al tuo bot. Sostituisci il segnaposto dell'ID cliente con l'ID reale.",
+    "copied": "Link copiato!"
+  },
+  "test": {
+    "title": "Messaggio di prova",
+    "chatId": "ID chat",
+    "chatIdPlaceholder": "Inserisci un ID chat di Telegram",
+    "message": "Messaggio",
+    "messagePlaceholder": "Scrivi un messaggio di prova...",
+    "send": "Invia prova",
+    "success": "Messaggio di prova inviato!",
+    "error": "Invio messaggio di prova fallito"
+  },
+  "save": "Salva impostazioni",
+  "saving": "Salvataggio...",
+  "saved": "Impostazioni Telegram salvate",
+  "saveError": "Salvataggio impostazioni fallito",
+  "disconnect": {
+    "button": "Disconnetti bot",
+    "title": "Disconnettere il bot Telegram?",
+    "description": "Questo rimuoverà la configurazione del bot e interromperà la ricezione dei messaggi. La cronologia delle conversazioni esistente verrà conservata.",
+    "success": "Bot Telegram disconnesso",
+    "error": "Disconnessione fallita"
+  },
+  "qr": {
+    "title": "Connetti su Telegram",
+    "description": "Scansiona questo codice QR per chattare con noi su Telegram.",
+    "copyLink": "Copia link",
+    "copied": "Link copiato!",
+    "sendEmail": "Invia per email",
+    "emailSubject": "Connettiti con noi su Telegram",
+    "emailBody": "Ciao {name},\n\nOra puoi chattare con noi su Telegram per aggiornamenti sul tuo veicolo.\n\nClicca su questo link: {link}\n\nGrazie!"
+  },
+  "invoiceQr": {
+    "label": "Mostra codice QR sulle fatture",
+    "description": "Visualizza un codice QR Telegram sulle fatture digitali condivise in modo che i clienti possano connettersi."
+  },
+  "helpLink": "Scopri di più",
+  "enable": {
+    "label": "Attiva Telegram",
+    "hint": "Mostra Telegram nella barra laterale e nelle pagine dei clienti.",
+    "disabledInfo": "Telegram è attualmente disattivato. Attivalo per iniziare a inviare e ricevere messaggi tramite Telegram."
+  }
+}

+ 45 - 0
messages/it/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Messaggi Telegram",
+    "featureDescription": "Invia e ricevi messaggi Telegram con i tuoi clienti direttamente da Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram non configurato",
+    "description": "Configura un bot Telegram per inviare e ricevere messaggi con i tuoi clienti. Crea un bot tramite @BotFather e configuralo nelle impostazioni.",
+    "goToSettings": "Vai alle impostazioni Telegram"
+  },
+  "threads": {
+    "title": "Conversazioni",
+    "empty": "Nessuna conversazione Telegram ancora. I clienti possono iniziare a chattare inviando un messaggio al tuo bot.",
+    "loadMore": "Carica altri",
+    "selectConversation": "Seleziona una conversazione",
+    "you": "Tu: ",
+    "deleteConversation": "Elimina conversazione",
+    "deleteTitle": "Eliminare la conversazione?",
+    "deleteDescription": "Questo eliminerà permanentemente tutti i messaggi Telegram con {name}. Questa azione non può essere annullata.",
+    "deleted": "Conversazione eliminata",
+    "deleteError": "Eliminazione conversazione fallita"
+  },
+  "conversation": {
+    "notLinked": "Questo cliente non si è ancora connesso al tuo bot Telegram. Condividi il link del bot per connetterlo.",
+    "loadOlder": "Carica precedenti",
+    "empty": "Nessun messaggio ancora. Invia il primo messaggio a {name}.",
+    "placeholder": "Messaggio a {name}...",
+    "deleteMessage": "Elimina messaggio",
+    "deleteTitle": "Eliminare il messaggio?",
+    "deleteDescription": "Questo eliminerà permanentemente questo messaggio. Questa azione non può essere annullata.",
+    "messageDeleted": "Messaggio eliminato",
+    "deleteError": "Eliminazione messaggio fallita",
+    "sendError": "Invio messaggio fallito"
+  },
+  "sendDialog": {
+    "send": "Invia {label}",
+    "message": "Messaggio",
+    "placeholder": "Scrivi il tuo messaggio...",
+    "characters": "{count} / 4096 caratteri",
+    "sendTelegram": "Invia messaggio",
+    "messageSent": "Messaggio inviato",
+    "sentTo": "Messaggio inviato a {name} tramite Telegram",
+    "sendError": "Invio messaggio fallito"
+  }
+}

+ 3 - 1
messages/nb/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Kjøretøy ({count})",
       "vehicles": "Kjøretøy ({count})",
       "messages": "Meldinger",
       "messages": "Meldinger",
-      "requests": "Forespørsler ({count})"
+      "requests": "Forespørsler ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Registreringsnummer",
       "plate": "Registreringsnummer",

+ 3 - 0
messages/nb/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Utseende",
     "appearance": "Utseende",
     "email": "E-post",
     "email": "E-post",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Om",
     "about": "Om",
     "home": "Hjem",
     "home": "Hjem",
     "newServiceRecord": "Ny serviceregistrering",
     "newServiceRecord": "Ny serviceregistrering",
@@ -62,6 +63,8 @@
     "clients": "Klienter",
     "clients": "Klienter",
     "customers": "Kunder",
     "customers": "Kunder",
     "messages": "Meldinger",
     "messages": "Meldinger",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Verksted",
     "workshop": "Verksted",
     "vehicles": "Kjøretøy",
     "vehicles": "Kjøretøy",
     "reminders": "Påminnelser",
     "reminders": "Påminnelser",

+ 6 - 1
messages/nb/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "SMS-leverandor og meldinger"
         "description": "SMS-leverandor og meldinger"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Kundeportal",
         "title": "Kundeportal",
         "description": "Kundeselvbetjeningsportal"
         "description": "Kundeselvbetjeningsportal"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Diagnostiske notater",
       "diagnostic_notes": "Diagnostiske notater",
       "bank_account": "Bankkonto",
       "bank_account": "Bankkonto",
       "footer": "Bunntekst",
       "footer": "Bunntekst",
-      "general": "Generelt"
+      "general": "Generelt",
+      "telegram_qr": "Telegram QR"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Kundenavn",
       "customer_name": "Kundenavn",

+ 3 - 1
messages/nb/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Drevet av",
     "poweredBy": "Drevet av",
     "termsOfSale": "Salgsbetingelser",
     "termsOfSale": "Salgsbetingelser",
     "portalMessage": "Se kjøretøyene dine, fakturaer og mer i din",
     "portalMessage": "Se kjøretøyene dine, fakturaer og mer i din",
-    "customerPortal": "kundeportal"
+    "customerPortal": "kundeportal",
+    "telegramConnect": "Chat med oss på Telegram",
+    "telegramScan": "Skann denne QR-koden for å sende oss en melding på Telegram for oppdateringer og støtte."
   }
   }
 }
 }

+ 62 - 0
messages/nb/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Telegram-innstillinger",
+  "description": "Konfigurer Telegram-boten din for å sende og motta meldinger med kunder.",
+  "botToken": {
+    "label": "Bot-token",
+    "placeholder": "Skriv inn bot-tokenet fra @BotFather",
+    "help": "Opprett en bot via @BotFather på Telegram for å få tokenet ditt."
+  },
+  "botUsername": {
+    "label": "Bot-brukernavn",
+    "notConnected": "Ikke tilkoblet"
+  },
+  "webhook": {
+    "label": "Webhook-URL",
+    "description": "Denne URL-en konfigureres automatisk når du lagrer bot-tokenet."
+  },
+  "deepLink": {
+    "title": "Kundetilkoblingslenke",
+    "description": "Del denne lenken med kunder slik at de kan koble til boten din. Erstatt kunde-ID-plassholderen med den faktiske kunde-ID-en.",
+    "copied": "Lenke kopiert!"
+  },
+  "test": {
+    "title": "Testmelding",
+    "chatId": "Chat-ID",
+    "chatIdPlaceholder": "Skriv inn en Telegram chat-ID",
+    "message": "Melding",
+    "messagePlaceholder": "Skriv en testmelding...",
+    "send": "Send test",
+    "success": "Testmelding sendt!",
+    "error": "Kunne ikke sende testmelding"
+  },
+  "save": "Lagre innstillinger",
+  "saving": "Lagrer...",
+  "saved": "Telegram-innstillinger lagret",
+  "saveError": "Kunne ikke lagre innstillinger",
+  "disconnect": {
+    "button": "Koble fra bot",
+    "title": "Koble fra Telegram-boten?",
+    "description": "Dette fjerner bot-konfigurasjonen og stopper mottak av meldinger. Eksisterende samtalehistorikk beholdes.",
+    "success": "Telegram-bot frakoblet",
+    "error": "Kunne ikke koble fra"
+  },
+  "qr": {
+    "title": "Koble til på Telegram",
+    "description": "Skann denne QR-koden for å chatte med oss på Telegram.",
+    "copyLink": "Kopier lenke",
+    "copied": "Lenke kopiert!",
+    "sendEmail": "Send på e-post",
+    "emailSubject": "Koble til oss på Telegram",
+    "emailBody": "Hei {name},\n\nDu kan nå chatte med oss på Telegram for oppdateringer om kjøretøyet ditt.\n\nKlikk på denne lenken: {link}\n\nTakk!"
+  },
+  "invoiceQr": {
+    "label": "Vis QR-kode på fakturaer",
+    "description": "Vis en Telegram QR-kode på delte digitale fakturaer slik at kunder kan koble til."
+  },
+  "helpLink": "Les mer",
+  "enable": {
+    "label": "Aktiver Telegram",
+    "hint": "Vis Telegram i sidefeltet og på kundesider.",
+    "disabledInfo": "Telegram er for øyeblikket deaktivert. Aktiver for å begynne å sende og motta meldinger via Telegram."
+  }
+}

+ 45 - 0
messages/nb/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Telegram-meldinger",
+    "featureDescription": "Send og motta Telegram-meldinger med kundene dine direkte fra Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram er ikke konfigurert",
+    "description": "Sett opp en Telegram-bot for å sende og motta meldinger med kundene dine. Opprett en bot via @BotFather og konfigurer i innstillingene.",
+    "goToSettings": "Gå til Telegram-innstillinger"
+  },
+  "threads": {
+    "title": "Samtaler",
+    "empty": "Ingen Telegram-samtaler ennå. Kunder kan starte en chat ved å sende melding til boten din.",
+    "loadMore": "Last inn flere",
+    "selectConversation": "Velg en samtale",
+    "you": "Du: ",
+    "deleteConversation": "Slett samtale",
+    "deleteTitle": "Slette samtale?",
+    "deleteDescription": "Dette vil permanent slette alle Telegram-meldinger med {name}. Denne handlingen kan ikke angres.",
+    "deleted": "Samtale slettet",
+    "deleteError": "Kunne ikke slette samtale"
+  },
+  "conversation": {
+    "notLinked": "Denne kunden har ikke koblet seg til Telegram-boten din ennå. Del bot-lenken for å koble dem til.",
+    "loadOlder": "Last eldre",
+    "empty": "Ingen meldinger ennå. Send den første meldingen til {name}.",
+    "placeholder": "Melding til {name}...",
+    "deleteMessage": "Slett melding",
+    "deleteTitle": "Slette melding?",
+    "deleteDescription": "Dette vil permanent slette denne meldingen. Denne handlingen kan ikke angres.",
+    "messageDeleted": "Melding slettet",
+    "deleteError": "Kunne ikke slette melding",
+    "sendError": "Kunne ikke sende melding"
+  },
+  "sendDialog": {
+    "send": "Send {label}",
+    "message": "Melding",
+    "placeholder": "Skriv meldingen din...",
+    "characters": "{count} / 4096 tegn",
+    "sendTelegram": "Send melding",
+    "messageSent": "Melding sendt",
+    "sentTo": "Melding sendt til {name} via Telegram",
+    "sendError": "Kunne ikke sende melding"
+  }
+}

+ 3 - 1
messages/nl/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Voertuigen ({count})",
       "vehicles": "Voertuigen ({count})",
       "messages": "Berichten",
       "messages": "Berichten",
-      "requests": "Aanvragen ({count})"
+      "requests": "Aanvragen ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Kenteken",
       "plate": "Kenteken",

+ 3 - 0
messages/nl/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Weergave",
     "appearance": "Weergave",
     "email": "E-mail",
     "email": "E-mail",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Over",
     "about": "Over",
     "home": "Home",
     "home": "Home",
     "newServiceRecord": "Nieuw servicerecord",
     "newServiceRecord": "Nieuw servicerecord",
@@ -62,6 +63,8 @@
     "clients": "Klanten",
     "clients": "Klanten",
     "customers": "Klanten",
     "customers": "Klanten",
     "messages": "Berichten",
     "messages": "Berichten",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Werkplaats",
     "workshop": "Werkplaats",
     "vehicles": "Voertuigen",
     "vehicles": "Voertuigen",
     "reminders": "Herinneringen",
     "reminders": "Herinneringen",

+ 6 - 1
messages/nl/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "SMS-provider en berichten"
         "description": "SMS-provider en berichten"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Klantenportaal",
         "title": "Klantenportaal",
         "description": "Selfservice klantenportaal"
         "description": "Selfservice klantenportaal"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Diagnostische notities",
       "diagnostic_notes": "Diagnostische notities",
       "bank_account": "Bankrekening",
       "bank_account": "Bankrekening",
       "footer": "Voettekst",
       "footer": "Voettekst",
-      "general": "Algemeen"
+      "general": "Algemeen",
+      "telegram_qr": "Telegram-QR"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Klantnaam",
       "customer_name": "Klantnaam",

+ 3 - 1
messages/nl/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Mogelijk gemaakt door",
     "poweredBy": "Mogelijk gemaakt door",
     "termsOfSale": "Verkoopvoorwaarden",
     "termsOfSale": "Verkoopvoorwaarden",
     "portalMessage": "Bekijk uw voertuigen, facturen en meer in uw",
     "portalMessage": "Bekijk uw voertuigen, facturen en meer in uw",
-    "customerPortal": "klantenportaal"
+    "customerPortal": "klantenportaal",
+    "telegramConnect": "Chat met ons op Telegram",
+    "telegramScan": "Scan deze QR-code om ons een bericht te sturen op Telegram voor updates en ondersteuning."
   }
   }
 }
 }

+ 62 - 0
messages/nl/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Telegram-instellingen",
+  "description": "Configureer je Telegram-bot om berichten te versturen en ontvangen met klanten.",
+  "botToken": {
+    "label": "Bot-token",
+    "placeholder": "Voer je bot-token van @BotFather in",
+    "help": "Maak een bot aan via @BotFather op Telegram om je token te krijgen."
+  },
+  "botUsername": {
+    "label": "Bot-gebruikersnaam",
+    "notConnected": "Niet verbonden"
+  },
+  "webhook": {
+    "label": "Webhook-URL",
+    "description": "Deze URL wordt automatisch geconfigureerd wanneer je je bot-token opslaat."
+  },
+  "deepLink": {
+    "title": "Klantverbindingslink",
+    "description": "Deel deze link met klanten zodat ze verbinding kunnen maken met je bot. Vervang de klant-ID-plaatshouder door het echte klant-ID.",
+    "copied": "Link gekopieerd!"
+  },
+  "test": {
+    "title": "Testbericht",
+    "chatId": "Chat-ID",
+    "chatIdPlaceholder": "Voer een Telegram chat-ID in",
+    "message": "Bericht",
+    "messagePlaceholder": "Typ een testbericht...",
+    "send": "Test verzenden",
+    "success": "Testbericht verzonden!",
+    "error": "Testbericht verzenden mislukt"
+  },
+  "save": "Instellingen opslaan",
+  "saving": "Opslaan...",
+  "saved": "Telegram-instellingen opgeslagen",
+  "saveError": "Instellingen opslaan mislukt",
+  "disconnect": {
+    "button": "Bot ontkoppelen",
+    "title": "Telegram-bot ontkoppelen?",
+    "description": "Dit verwijdert de botconfiguratie en stopt het ontvangen van berichten. Bestaande gespreksgeschiedenis blijft bewaard.",
+    "success": "Telegram-bot ontkoppeld",
+    "error": "Ontkoppelen mislukt"
+  },
+  "qr": {
+    "title": "Verbinden op Telegram",
+    "description": "Scan deze QR-code om met ons te chatten op Telegram.",
+    "copyLink": "Link kopiëren",
+    "copied": "Link gekopieerd!",
+    "sendEmail": "Per e-mail verzenden",
+    "emailSubject": "Verbind met ons op Telegram",
+    "emailBody": "Hallo {name},\n\nU kunt nu met ons chatten op Telegram voor updates over uw voertuig.\n\nKlik op deze link: {link}\n\nBedankt!"
+  },
+  "invoiceQr": {
+    "label": "QR-code op facturen tonen",
+    "description": "Een Telegram QR-code weergeven op gedeelde digitale facturen zodat klanten verbinding kunnen maken."
+  },
+  "helpLink": "Meer informatie",
+  "enable": {
+    "label": "Telegram inschakelen",
+    "hint": "Telegram tonen in de zijbalk en op klantpagina's.",
+    "disabledInfo": "Telegram is momenteel uitgeschakeld. Schakel het in om berichten via Telegram te verzenden en ontvangen."
+  }
+}

+ 45 - 0
messages/nl/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Telegram-berichten",
+    "featureDescription": "Verstuur en ontvang Telegram-berichten met je klanten rechtstreeks vanuit Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram niet geconfigureerd",
+    "description": "Stel een Telegram-bot in om berichten te versturen en ontvangen met je klanten. Maak een bot aan via @BotFather en configureer in instellingen.",
+    "goToSettings": "Ga naar Telegram-instellingen"
+  },
+  "threads": {
+    "title": "Gesprekken",
+    "empty": "Nog geen Telegram-gesprekken. Klanten kunnen een chat starten door een bericht te sturen naar je bot.",
+    "loadMore": "Meer laden",
+    "selectConversation": "Selecteer een gesprek",
+    "you": "Jij: ",
+    "deleteConversation": "Gesprek verwijderen",
+    "deleteTitle": "Gesprek verwijderen?",
+    "deleteDescription": "Dit verwijdert alle Telegram-berichten met {name} permanent. Deze actie kan niet ongedaan worden gemaakt.",
+    "deleted": "Gesprek verwijderd",
+    "deleteError": "Gesprek verwijderen mislukt"
+  },
+  "conversation": {
+    "notLinked": "Deze klant heeft nog geen verbinding gemaakt met je Telegram-bot. Deel de botlink om ze te verbinden.",
+    "loadOlder": "Oudere laden",
+    "empty": "Nog geen berichten. Stuur het eerste bericht naar {name}.",
+    "placeholder": "Bericht aan {name}...",
+    "deleteMessage": "Bericht verwijderen",
+    "deleteTitle": "Bericht verwijderen?",
+    "deleteDescription": "Dit verwijdert dit bericht permanent. Deze actie kan niet ongedaan worden gemaakt.",
+    "messageDeleted": "Bericht verwijderd",
+    "deleteError": "Bericht verwijderen mislukt",
+    "sendError": "Bericht versturen mislukt"
+  },
+  "sendDialog": {
+    "send": "{label} versturen",
+    "message": "Bericht",
+    "placeholder": "Typ je bericht...",
+    "characters": "{count} / 4096 tekens",
+    "sendTelegram": "Bericht versturen",
+    "messageSent": "Bericht verstuurd",
+    "sentTo": "Bericht verstuurd naar {name} via Telegram",
+    "sendError": "Bericht versturen mislukt"
+  }
+}

+ 3 - 1
messages/pl/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Pojazdy ({count})",
       "vehicles": "Pojazdy ({count})",
       "messages": "Wiadomości",
       "messages": "Wiadomości",
-      "requests": "Zlecenia ({count})"
+      "requests": "Zlecenia ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Tablica",
       "plate": "Tablica",

+ 3 - 0
messages/pl/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Wygląd",
     "appearance": "Wygląd",
     "email": "E-mail",
     "email": "E-mail",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "O programie",
     "about": "O programie",
     "home": "Strona główna",
     "home": "Strona główna",
     "newServiceRecord": "Nowy wpis serwisowy",
     "newServiceRecord": "Nowy wpis serwisowy",
@@ -62,6 +63,8 @@
     "clients": "Klienci",
     "clients": "Klienci",
     "customers": "Klienci",
     "customers": "Klienci",
     "messages": "Wiadomości",
     "messages": "Wiadomości",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Warsztat",
     "workshop": "Warsztat",
     "vehicles": "Pojazdy",
     "vehicles": "Pojazdy",
     "reminders": "Przypomnienia",
     "reminders": "Przypomnienia",

+ 6 - 1
messages/pl/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Dostawca SMS i wiadomosci"
         "description": "Dostawca SMS i wiadomosci"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Portal klienta",
         "title": "Portal klienta",
         "description": "Portal samoobslugowy klienta"
         "description": "Portal samoobslugowy klienta"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Notatki diagnostyczne",
       "diagnostic_notes": "Notatki diagnostyczne",
       "bank_account": "Konto bankowe",
       "bank_account": "Konto bankowe",
       "footer": "Stopka",
       "footer": "Stopka",
-      "general": "Ogólne"
+      "general": "Ogólne",
+      "telegram_qr": "QR Telegrama"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Nazwa klienta",
       "customer_name": "Nazwa klienta",

+ 3 - 1
messages/pl/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Obsługiwane przez",
     "poweredBy": "Obsługiwane przez",
     "termsOfSale": "Warunki sprzedaży",
     "termsOfSale": "Warunki sprzedaży",
     "portalMessage": "Zobacz swoje pojazdy, faktury i więcej w swoim",
     "portalMessage": "Zobacz swoje pojazdy, faktury i więcej w swoim",
-    "customerPortal": "portalu klienta"
+    "customerPortal": "portalu klienta",
+    "telegramConnect": "Porozmawiaj z nami na Telegramie",
+    "telegramScan": "Zeskanuj ten kod QR, aby napisać do nas na Telegramie w sprawie aktualizacji i wsparcia."
   }
   }
 }
 }

+ 62 - 0
messages/pl/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Ustawienia Telegrama",
+  "description": "Skonfiguruj bota Telegrama do wysyłania i odbierania wiadomości z klientami.",
+  "botToken": {
+    "label": "Token bota",
+    "placeholder": "Wprowadź token od @BotFather",
+    "help": "Utwórz bota przez @BotFather na Telegramie, aby uzyskać token."
+  },
+  "botUsername": {
+    "label": "Nazwa użytkownika bota",
+    "notConnected": "Nie połączono"
+  },
+  "webhook": {
+    "label": "URL webhooka",
+    "description": "Ten URL jest konfigurowany automatycznie po zapisaniu tokena bota."
+  },
+  "deepLink": {
+    "title": "Link połączenia klienta",
+    "description": "Udostępnij ten link klientom, aby mogli połączyć się z botem. Zastąp symbol zastępczy ID klienta rzeczywistym ID.",
+    "copied": "Link skopiowany!"
+  },
+  "test": {
+    "title": "Wiadomość testowa",
+    "chatId": "ID czatu",
+    "chatIdPlaceholder": "Wprowadź ID czatu Telegrama",
+    "message": "Wiadomość",
+    "messagePlaceholder": "Wpisz wiadomość testową...",
+    "send": "Wyślij test",
+    "success": "Wiadomość testowa wysłana!",
+    "error": "Nie udało się wysłać wiadomości testowej"
+  },
+  "save": "Zapisz ustawienia",
+  "saving": "Zapisywanie...",
+  "saved": "Ustawienia Telegrama zapisane",
+  "saveError": "Nie udało się zapisać ustawień",
+  "disconnect": {
+    "button": "Odłącz bota",
+    "title": "Odłączyć bota Telegrama?",
+    "description": "Spowoduje to usunięcie konfiguracji bota i zatrzymanie odbierania wiadomości. Istniejąca historia rozmów zostanie zachowana.",
+    "success": "Bot Telegrama odłączony",
+    "error": "Nie udało się odłączyć"
+  },
+  "qr": {
+    "title": "Połącz na Telegramie",
+    "description": "Zeskanuj ten kod QR, aby porozmawiać z nami na Telegramie.",
+    "copyLink": "Kopiuj link",
+    "copied": "Link skopiowany!",
+    "sendEmail": "Wyślij e-mailem",
+    "emailSubject": "Połącz się z nami na Telegramie",
+    "emailBody": "Cześć {name},\n\nTeraz możesz rozmawiać z nami na Telegramie, aby otrzymywać aktualizacje dotyczące Twojego pojazdu.\n\nKliknij ten link: {link}\n\nDziękujemy!"
+  },
+  "invoiceQr": {
+    "label": "Pokaż kod QR na fakturach",
+    "description": "Wyświetl kod QR Telegrama na udostępnionych fakturach cyfrowych, aby klienci mogli się połączyć."
+  },
+  "helpLink": "Dowiedz się więcej",
+  "enable": {
+    "label": "Włącz Telegram",
+    "hint": "Pokaż Telegram na pasku bocznym i na stronach klientów.",
+    "disabledInfo": "Telegram jest obecnie wyłączony. Włącz go, aby zacząć wysyłać i odbierać wiadomości przez Telegram."
+  }
+}

+ 45 - 0
messages/pl/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Wiadomości Telegram",
+    "featureDescription": "Wysyłaj i odbieraj wiadomości Telegram z klientami bezpośrednio z Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram nie skonfigurowany",
+    "description": "Skonfiguruj bota Telegram, aby wysyłać i odbierać wiadomości z klientami. Utwórz bota przez @BotFather i skonfiguruj w ustawieniach.",
+    "goToSettings": "Przejdź do ustawień Telegrama"
+  },
+  "threads": {
+    "title": "Rozmowy",
+    "empty": "Brak rozmów Telegram. Klienci mogą rozpocząć czat, wysyłając wiadomość do Twojego bota.",
+    "loadMore": "Załaduj więcej",
+    "selectConversation": "Wybierz rozmowę",
+    "you": "Ty: ",
+    "deleteConversation": "Usuń rozmowę",
+    "deleteTitle": "Usunąć rozmowę?",
+    "deleteDescription": "Spowoduje to trwałe usunięcie wszystkich wiadomości Telegram z {name}. Tej akcji nie można cofnąć.",
+    "deleted": "Rozmowa usunięta",
+    "deleteError": "Nie udało się usunąć rozmowy"
+  },
+  "conversation": {
+    "notLinked": "Ten klient nie połączył się jeszcze z Twoim botem Telegram. Udostępnij link do bota, aby go połączyć.",
+    "loadOlder": "Załaduj starsze",
+    "empty": "Brak wiadomości. Wyślij pierwszą wiadomość do {name}.",
+    "placeholder": "Wiadomość do {name}...",
+    "deleteMessage": "Usuń wiadomość",
+    "deleteTitle": "Usunąć wiadomość?",
+    "deleteDescription": "Spowoduje to trwałe usunięcie tej wiadomości. Tej akcji nie można cofnąć.",
+    "messageDeleted": "Wiadomość usunięta",
+    "deleteError": "Nie udało się usunąć wiadomości",
+    "sendError": "Nie udało się wysłać wiadomości"
+  },
+  "sendDialog": {
+    "send": "Wyślij {label}",
+    "message": "Wiadomość",
+    "placeholder": "Wpisz wiadomość...",
+    "characters": "{count} / 4096 znaków",
+    "sendTelegram": "Wyślij wiadomość",
+    "messageSent": "Wiadomość wysłana",
+    "sentTo": "Wiadomość wysłana do {name} przez Telegram",
+    "sendError": "Nie udało się wysłać wiadomości"
+  }
+}

+ 3 - 1
messages/pt-BR/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Veículos ({count})",
       "vehicles": "Veículos ({count})",
       "messages": "Mensagens",
       "messages": "Mensagens",
-      "requests": "Solicitações ({count})"
+      "requests": "Solicitações ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Placa",
       "plate": "Placa",

+ 3 - 0
messages/pt-BR/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Aparência",
     "appearance": "Aparência",
     "email": "E-mail",
     "email": "E-mail",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Sobre",
     "about": "Sobre",
     "home": "Início",
     "home": "Início",
     "newServiceRecord": "Novo Registro de Serviço",
     "newServiceRecord": "Novo Registro de Serviço",
@@ -62,6 +63,8 @@
     "clients": "Clientes",
     "clients": "Clientes",
     "customers": "Clientes",
     "customers": "Clientes",
     "messages": "Mensagens",
     "messages": "Mensagens",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Oficina",
     "workshop": "Oficina",
     "vehicles": "Veículos",
     "vehicles": "Veículos",
     "reminders": "Lembretes",
     "reminders": "Lembretes",

+ 6 - 1
messages/pt-BR/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Provedor SMS e mensagens"
         "description": "Provedor SMS e mensagens"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Portal do cliente",
         "title": "Portal do cliente",
         "description": "Portal de autoatendimento do cliente"
         "description": "Portal de autoatendimento do cliente"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Notas de diagnóstico",
       "diagnostic_notes": "Notas de diagnóstico",
       "bank_account": "Conta bancária",
       "bank_account": "Conta bancária",
       "footer": "Rodapé",
       "footer": "Rodapé",
-      "general": "Geral"
+      "general": "Geral",
+      "telegram_qr": "QR do Telegram"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Nome do cliente",
       "customer_name": "Nome do cliente",

+ 3 - 1
messages/pt-BR/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Desenvolvido por",
     "poweredBy": "Desenvolvido por",
     "termsOfSale": "Termos de Venda",
     "termsOfSale": "Termos de Venda",
     "portalMessage": "Veja seus veículos, faturas e muito mais em seu",
     "portalMessage": "Veja seus veículos, faturas e muito mais em seu",
-    "customerPortal": "portal do cliente"
+    "customerPortal": "portal do cliente",
+    "telegramConnect": "Fale conosco no Telegram",
+    "telegramScan": "Escaneie este código QR para nos enviar uma mensagem no Telegram para atualizações e suporte."
   }
   }
 }
 }

+ 62 - 0
messages/pt-BR/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Configurações do Telegram",
+  "description": "Configure seu bot do Telegram para enviar e receber mensagens com clientes.",
+  "botToken": {
+    "label": "Token do bot",
+    "placeholder": "Insira o token do @BotFather",
+    "help": "Crie um bot pelo @BotFather no Telegram para obter seu token."
+  },
+  "botUsername": {
+    "label": "Nome de usuário do bot",
+    "notConnected": "Não conectado"
+  },
+  "webhook": {
+    "label": "URL do webhook",
+    "description": "Esta URL é configurada automaticamente ao salvar o token do bot."
+  },
+  "deepLink": {
+    "title": "Link de conexão do cliente",
+    "description": "Compartilhe este link com os clientes para que se conectem ao seu bot. Substitua o espaço reservado do ID do cliente pelo ID real.",
+    "copied": "Link copiado!"
+  },
+  "test": {
+    "title": "Mensagem de teste",
+    "chatId": "ID do chat",
+    "chatIdPlaceholder": "Insira um ID de chat do Telegram",
+    "message": "Mensagem",
+    "messagePlaceholder": "Digite uma mensagem de teste...",
+    "send": "Enviar teste",
+    "success": "Mensagem de teste enviada!",
+    "error": "Falha ao enviar mensagem de teste"
+  },
+  "save": "Salvar configurações",
+  "saving": "Salvando...",
+  "saved": "Configurações do Telegram salvas",
+  "saveError": "Falha ao salvar configurações",
+  "disconnect": {
+    "button": "Desconectar bot",
+    "title": "Desconectar bot do Telegram?",
+    "description": "Isso removerá a configuração do bot e parará de receber mensagens. O histórico de conversas existente será preservado.",
+    "success": "Bot do Telegram desconectado",
+    "error": "Falha ao desconectar"
+  },
+  "qr": {
+    "title": "Conectar no Telegram",
+    "description": "Escaneie este código QR para conversar conosco no Telegram.",
+    "copyLink": "Copiar link",
+    "copied": "Link copiado!",
+    "sendEmail": "Enviar por e-mail",
+    "emailSubject": "Conecte-se conosco no Telegram",
+    "emailBody": "Olá {name},\n\nAgora você pode conversar conosco no Telegram para receber atualizações sobre seu veículo.\n\nClique neste link: {link}\n\nObrigado!"
+  },
+  "invoiceQr": {
+    "label": "Mostrar código QR nas faturas",
+    "description": "Exibir um código QR do Telegram nas faturas digitais compartilhadas para que os clientes possam se conectar."
+  },
+  "helpLink": "Saiba mais",
+  "enable": {
+    "label": "Ativar Telegram",
+    "hint": "Mostrar Telegram na barra lateral e nas páginas de clientes.",
+    "disabledInfo": "O Telegram está desativado no momento. Ative para começar a enviar e receber mensagens pelo Telegram."
+  }
+}

+ 45 - 0
messages/pt-BR/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Mensagens do Telegram",
+    "featureDescription": "Envie e receba mensagens do Telegram com seus clientes diretamente do Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram não configurado",
+    "description": "Configure um bot do Telegram para enviar e receber mensagens com seus clientes. Crie um bot pelo @BotFather e configure nas configurações.",
+    "goToSettings": "Ir para configurações do Telegram"
+  },
+  "threads": {
+    "title": "Conversas",
+    "empty": "Nenhuma conversa do Telegram ainda. Os clientes podem iniciar um chat enviando uma mensagem ao seu bot.",
+    "loadMore": "Carregar mais",
+    "selectConversation": "Selecionar conversa",
+    "you": "Você: ",
+    "deleteConversation": "Excluir conversa",
+    "deleteTitle": "Excluir conversa?",
+    "deleteDescription": "Isso excluirá permanentemente todas as mensagens do Telegram com {name}. Esta ação não pode ser desfeita.",
+    "deleted": "Conversa excluída",
+    "deleteError": "Falha ao excluir conversa"
+  },
+  "conversation": {
+    "notLinked": "Este cliente ainda não se conectou ao seu bot do Telegram. Compartilhe o link do bot para conectá-lo.",
+    "loadOlder": "Carregar anteriores",
+    "empty": "Nenhuma mensagem ainda. Envie a primeira mensagem para {name}.",
+    "placeholder": "Mensagem para {name}...",
+    "deleteMessage": "Excluir mensagem",
+    "deleteTitle": "Excluir mensagem?",
+    "deleteDescription": "Isso excluirá permanentemente esta mensagem. Esta ação não pode ser desfeita.",
+    "messageDeleted": "Mensagem excluída",
+    "deleteError": "Falha ao excluir mensagem",
+    "sendError": "Falha ao enviar mensagem"
+  },
+  "sendDialog": {
+    "send": "Enviar {label}",
+    "message": "Mensagem",
+    "placeholder": "Digite sua mensagem...",
+    "characters": "{count} / 4096 caracteres",
+    "sendTelegram": "Enviar mensagem",
+    "messageSent": "Mensagem enviada",
+    "sentTo": "Mensagem enviada para {name} via Telegram",
+    "sendError": "Falha ao enviar mensagem"
+  }
+}

+ 3 - 1
messages/ru/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Транспортные средства ({count})",
       "vehicles": "Транспортные средства ({count})",
       "messages": "Сообщения",
       "messages": "Сообщения",
-      "requests": "Заявки ({count})"
+      "requests": "Заявки ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Госномер",
       "plate": "Госномер",

+ 3 - 0
messages/ru/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Внешний вид",
     "appearance": "Внешний вид",
     "email": "Эл. почта",
     "email": "Эл. почта",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "О программе",
     "about": "О программе",
     "home": "Главная",
     "home": "Главная",
     "newServiceRecord": "Новая запись обслуживания",
     "newServiceRecord": "Новая запись обслуживания",
@@ -62,6 +63,8 @@
     "clients": "Клиенты",
     "clients": "Клиенты",
     "customers": "Клиенты",
     "customers": "Клиенты",
     "messages": "Сообщения",
     "messages": "Сообщения",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Мастерская",
     "workshop": "Мастерская",
     "vehicles": "Транспортные средства",
     "vehicles": "Транспортные средства",
     "reminders": "Напоминания",
     "reminders": "Напоминания",

+ 8 - 3
messages/ru/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "Провайдер SMS и сообщения"
         "description": "Провайдер SMS и сообщения"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Портал клиента",
         "title": "Портал клиента",
         "description": "Портал самообслуживания клиентов"
         "description": "Портал самообслуживания клиентов"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Диагностические заметки",
       "diagnostic_notes": "Диагностические заметки",
       "bank_account": "Банковский счёт",
       "bank_account": "Банковский счёт",
       "footer": "Подвал",
       "footer": "Подвал",
-      "general": "Общие"
+      "general": "Общие",
+      "telegram_qr": "QR Telegram"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Имя клиента",
       "customer_name": "Имя клиента",
@@ -683,13 +688,13 @@
     "workDayStart": "Начало рабочего дня",
     "workDayStart": "Начало рабочего дня",
     "workDayEnd": "Конец рабочего дня",
     "workDayEnd": "Конец рабочего дня",
     "weekDays": {
     "weekDays": {
+      "0": "Воскресенье",
       "1": "Понедельник",
       "1": "Понедельник",
       "2": "Вторник",
       "2": "Вторник",
       "3": "Среда",
       "3": "Среда",
       "4": "Четверг",
       "4": "Четверг",
       "5": "Пятница",
       "5": "Пятница",
-      "6": "Суббота",
-      "0": "Воскресенье"
+      "6": "Суббота"
     }
     }
   },
   },
   "maintenance": {
   "maintenance": {

+ 3 - 1
messages/ru/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Работает на",
     "poweredBy": "Работает на",
     "termsOfSale": "Условия продажи",
     "termsOfSale": "Условия продажи",
     "portalMessage": "Просматривайте свои транспортные средства, счета и многое другое на вашем",
     "portalMessage": "Просматривайте свои транспортные средства, счета и многое другое на вашем",
-    "customerPortal": "портале клиента"
+    "customerPortal": "портале клиента",
+    "telegramConnect": "Свяжитесь с нами в Telegram",
+    "telegramScan": "Отсканируйте этот QR-код, чтобы написать нам в Telegram для получения обновлений и поддержки."
   }
   }
 }
 }

+ 62 - 0
messages/ru/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Настройки Telegram",
+  "description": "Настройте бота Telegram для отправки и получения сообщений с клиентами.",
+  "botToken": {
+    "label": "Токен бота",
+    "placeholder": "Введите токен от @BotFather",
+    "help": "Создайте бота через @BotFather в Telegram, чтобы получить токен."
+  },
+  "botUsername": {
+    "label": "Имя пользователя бота",
+    "notConnected": "Не подключен"
+  },
+  "webhook": {
+    "label": "URL вебхука",
+    "description": "Этот URL настраивается автоматически при сохранении токена бота."
+  },
+  "deepLink": {
+    "title": "Ссылка для подключения клиента",
+    "description": "Поделитесь этой ссылкой с клиентами, чтобы они подключились к вашему боту. Замените заполнитель ID клиента на реальный ID.",
+    "copied": "Ссылка скопирована!"
+  },
+  "test": {
+    "title": "Тестовое сообщение",
+    "chatId": "ID чата",
+    "chatIdPlaceholder": "Введите ID чата Telegram",
+    "message": "Сообщение",
+    "messagePlaceholder": "Введите тестовое сообщение...",
+    "send": "Отправить тест",
+    "success": "Тестовое сообщение отправлено!",
+    "error": "Не удалось отправить тестовое сообщение"
+  },
+  "save": "Сохранить настройки",
+  "saving": "Сохранение...",
+  "saved": "Настройки Telegram сохранены",
+  "saveError": "Не удалось сохранить настройки",
+  "disconnect": {
+    "button": "Отключить бота",
+    "title": "Отключить бота Telegram?",
+    "description": "Это удалит конфигурацию бота и прекратит получение сообщений. Существующая история переписки будет сохранена.",
+    "success": "Бот Telegram отключен",
+    "error": "Не удалось отключить"
+  },
+  "qr": {
+    "title": "Подключить Telegram",
+    "description": "Отсканируйте этот QR-код, чтобы общаться с нами в Telegram.",
+    "copyLink": "Копировать ссылку",
+    "copied": "Ссылка скопирована!",
+    "sendEmail": "Отправить по email",
+    "emailSubject": "Свяжитесь с нами в Telegram",
+    "emailBody": "Здравствуйте, {name}!\n\nТеперь вы можете общаться с нами в Telegram для получения обновлений о вашем автомобиле.\n\nНажмите на эту ссылку: {link}\n\nСпасибо!"
+  },
+  "invoiceQr": {
+    "label": "Показывать QR-код на счетах",
+    "description": "Отображать QR-код Telegram на общих цифровых счетах, чтобы клиенты могли подключиться."
+  },
+  "helpLink": "Подробнее",
+  "enable": {
+    "label": "Включить Telegram",
+    "hint": "Показывать Telegram в боковой панели и на страницах клиентов.",
+    "disabledInfo": "Telegram сейчас отключен. Включите, чтобы начать отправлять и получать сообщения через Telegram."
+  }
+}

+ 45 - 0
messages/ru/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Сообщения Telegram",
+    "featureDescription": "Отправляйте и получайте сообщения Telegram с клиентами прямо из Torqvoice."
+  },
+  "notConfigured": {
+    "title": "Telegram не настроен",
+    "description": "Настройте бота Telegram для отправки и получения сообщений с клиентами. Создайте бота через @BotFather и настройте в параметрах.",
+    "goToSettings": "Перейти к настройкам Telegram"
+  },
+  "threads": {
+    "title": "Разговоры",
+    "empty": "Пока нет разговоров в Telegram. Клиенты могут начать чат, отправив сообщение вашему боту.",
+    "loadMore": "Загрузить ещё",
+    "selectConversation": "Выберите разговор",
+    "you": "Вы: ",
+    "deleteConversation": "Удалить разговор",
+    "deleteTitle": "Удалить разговор?",
+    "deleteDescription": "Все сообщения Telegram с {name} будут удалены навсегда. Это действие нельзя отменить.",
+    "deleted": "Разговор удалён",
+    "deleteError": "Не удалось удалить разговор"
+  },
+  "conversation": {
+    "notLinked": "Этот клиент ещё не подключился к вашему боту Telegram. Поделитесь ссылкой на бота для подключения.",
+    "loadOlder": "Загрузить старые",
+    "empty": "Пока нет сообщений. Отправьте первое сообщение {name}.",
+    "placeholder": "Сообщение для {name}...",
+    "deleteMessage": "Удалить сообщение",
+    "deleteTitle": "Удалить сообщение?",
+    "deleteDescription": "Это сообщение будет удалено навсегда. Это действие нельзя отменить.",
+    "messageDeleted": "Сообщение удалено",
+    "deleteError": "Не удалось удалить сообщение",
+    "sendError": "Не удалось отправить сообщение"
+  },
+  "sendDialog": {
+    "send": "Отправить {label}",
+    "message": "Сообщение",
+    "placeholder": "Введите сообщение...",
+    "characters": "{count} / 4096 символов",
+    "sendTelegram": "Отправить сообщение",
+    "messageSent": "Сообщение отправлено",
+    "sentTo": "Сообщение отправлено {name} через Telegram",
+    "sendError": "Не удалось отправить сообщение"
+  }
+}

+ 3 - 1
messages/tr/customers.json

@@ -32,7 +32,9 @@
     "tabs": {
     "tabs": {
       "vehicles": "Araçlar ({count})",
       "vehicles": "Araçlar ({count})",
       "messages": "Mesajlar",
       "messages": "Mesajlar",
-      "requests": "Talepler ({count})"
+      "requests": "Talepler ({count})",
+      "sms": "SMS",
+      "telegram": "Telegram"
     },
     },
     "vehicleTable": {
     "vehicleTable": {
       "plate": "Plaka",
       "plate": "Plaka",

+ 3 - 0
messages/tr/navigation.json

@@ -39,6 +39,7 @@
     "appearance": "Görünüm",
     "appearance": "Görünüm",
     "email": "E-posta",
     "email": "E-posta",
     "sms": "SMS",
     "sms": "SMS",
+    "telegram": "Telegram",
     "about": "Hakkında",
     "about": "Hakkında",
     "home": "Ana Sayfa",
     "home": "Ana Sayfa",
     "newServiceRecord": "Yeni Servis Kaydı",
     "newServiceRecord": "Yeni Servis Kaydı",
@@ -62,6 +63,8 @@
     "clients": "Müşteriler",
     "clients": "Müşteriler",
     "customers": "Müşteriler",
     "customers": "Müşteriler",
     "messages": "Mesajlar",
     "messages": "Mesajlar",
+    "smsMessages": "SMS Messages",
+    "telegram": "Telegram",
     "workshop": "Atölye",
     "workshop": "Atölye",
     "vehicles": "Araçlar",
     "vehicles": "Araçlar",
     "reminders": "Hatırlatmalar",
     "reminders": "Hatırlatmalar",

+ 6 - 1
messages/tr/settings.json

@@ -58,6 +58,10 @@
         "title": "SMS",
         "title": "SMS",
         "description": "SMS sağlayıcı & mesajlaşma"
         "description": "SMS sağlayıcı & mesajlaşma"
       },
       },
+      "telegram": {
+        "title": "Telegram",
+        "description": "Telegram bot & messaging"
+      },
       "customerPortal": {
       "customerPortal": {
         "title": "Müşteri Portalı",
         "title": "Müşteri Portalı",
         "description": "Müşteri self-servis portalı"
         "description": "Müşteri self-servis portalı"
@@ -397,7 +401,8 @@
       "diagnostic_notes": "Teşhis notları",
       "diagnostic_notes": "Teşhis notları",
       "bank_account": "Banka hesabı",
       "bank_account": "Banka hesabı",
       "footer": "Alt bilgi",
       "footer": "Alt bilgi",
-      "general": "Genel"
+      "general": "Genel",
+      "telegram_qr": "Telegram QR"
     },
     },
     "fields": {
     "fields": {
       "customer_name": "Müşteri adı",
       "customer_name": "Müşteri adı",

+ 3 - 1
messages/tr/share.json

@@ -151,6 +151,8 @@
     "poweredBy": "Destekleyen",
     "poweredBy": "Destekleyen",
     "termsOfSale": "Satış Koşulları",
     "termsOfSale": "Satış Koşulları",
     "portalMessage": "Araçlarınızı, faturalarınızı ve daha fazlasını görüntüleyin:",
     "portalMessage": "Araçlarınızı, faturalarınızı ve daha fazlasını görüntüleyin:",
-    "customerPortal": "müşteri portalı"
+    "customerPortal": "müşteri portalı",
+    "telegramConnect": "Telegram'da bizimle sohbet edin",
+    "telegramScan": "Güncellemeler ve destek için Telegram'da bize mesaj göndermek üzere bu QR kodunu tarayın."
   }
   }
 }
 }

+ 62 - 0
messages/tr/telegram.json

@@ -0,0 +1,62 @@
+{
+  "title": "Telegram Ayarları",
+  "description": "Müşterilerle mesaj göndermek ve almak için Telegram botunuzu yapılandırın.",
+  "botToken": {
+    "label": "Bot Token",
+    "placeholder": "@BotFather'dan aldığınız bot tokenini girin",
+    "help": "Tokeninizi almak için Telegram'da @BotFather üzerinden bir bot oluşturun."
+  },
+  "botUsername": {
+    "label": "Bot Kullanıcı Adı",
+    "notConnected": "Bağlı değil"
+  },
+  "webhook": {
+    "label": "Webhook URL'si",
+    "description": "Bu URL, bot tokeninizi kaydettiğinizde otomatik olarak yapılandırılır."
+  },
+  "deepLink": {
+    "title": "Müşteri Bağlantı Linki",
+    "description": "Müşterilerin botunuza bağlanabilmesi için bu linki paylaşın. Müşteri kimliği yer tutucusunu gerçek müşteri kimliğiyle değiştirin.",
+    "copied": "Link kopyalandı!"
+  },
+  "test": {
+    "title": "Test Mesajı",
+    "chatId": "Sohbet Kimliği",
+    "chatIdPlaceholder": "Telegram sohbet kimliği girin",
+    "message": "Mesaj",
+    "messagePlaceholder": "Test mesajı yazın...",
+    "send": "Test gönder",
+    "success": "Test mesajı gönderildi!",
+    "error": "Test mesajı gönderilemedi"
+  },
+  "save": "Ayarları kaydet",
+  "saving": "Kaydediliyor...",
+  "saved": "Telegram ayarları kaydedildi",
+  "saveError": "Ayarlar kaydedilemedi",
+  "disconnect": {
+    "button": "Botu bağlantısını kes",
+    "title": "Telegram botu bağlantısı kesilsin mi?",
+    "description": "Bu, bot yapılandırmasını kaldıracak ve mesaj almayı durduracaktır. Mevcut sohbet geçmişi korunacaktır.",
+    "success": "Telegram botu bağlantısı kesildi",
+    "error": "Bağlantı kesilemedi"
+  },
+  "qr": {
+    "title": "Telegram'da Bağlan",
+    "description": "Telegram'da bizimle sohbet etmek için bu QR kodunu tarayın.",
+    "copyLink": "Bağlantıyı kopyala",
+    "copied": "Bağlantı kopyalandı!",
+    "sendEmail": "E-posta ile gönder",
+    "emailSubject": "Telegram'da bizimle bağlanın",
+    "emailBody": "Merhaba {name},\n\nArtık aracınızla ilgili güncellemeler için Telegram'da bizimle sohbet edebilirsiniz.\n\nBu bağlantıya tıklayın: {link}\n\nTeşekkürler!"
+  },
+  "invoiceQr": {
+    "label": "Faturalarda QR kodu göster",
+    "description": "Müşterilerin bağlanabilmesi için paylaşılan dijital faturalarda bir Telegram QR kodu görüntüleyin."
+  },
+  "helpLink": "Daha fazla bilgi",
+  "enable": {
+    "label": "Telegram'ı Etkinleştir",
+    "hint": "Telegram'ı kenar çubuğunda ve müşteri sayfalarında göster.",
+    "disabledInfo": "Telegram şu anda devre dışı. Telegram üzerinden mesaj gönderip almaya başlamak için etkinleştirin."
+  }
+}

+ 45 - 0
messages/tr/telegramMessages.json

@@ -0,0 +1,45 @@
+{
+  "page": {
+    "featureName": "Telegram Mesajları",
+    "featureDescription": "Torqvoice üzerinden müşterilerinizle doğrudan Telegram mesajları gönderin ve alın."
+  },
+  "notConfigured": {
+    "title": "Telegram yapılandırılmadı",
+    "description": "Müşterilerinizle mesaj göndermek ve almak için bir Telegram botu kurun. @BotFather üzerinden bir bot oluşturun ve ayarlarda yapılandırın.",
+    "goToSettings": "Telegram ayarlarına git"
+  },
+  "threads": {
+    "title": "Sohbetler",
+    "empty": "Henüz Telegram sohbeti yok. Müşteriler botunuza mesaj göndererek sohbet başlatabilir.",
+    "loadMore": "Daha fazla yükle",
+    "selectConversation": "Bir sohbet seçin",
+    "you": "Siz: ",
+    "deleteConversation": "Sohbeti sil",
+    "deleteTitle": "Sohbet silinsin mi?",
+    "deleteDescription": "{name} ile tüm Telegram mesajları kalıcı olarak silinecektir. Bu işlem geri alınamaz.",
+    "deleted": "Sohbet silindi",
+    "deleteError": "Sohbet silinemedi"
+  },
+  "conversation": {
+    "notLinked": "Bu müşteri henüz Telegram botunuza bağlanmadı. Bağlamak için bot linkini paylaşın.",
+    "loadOlder": "Eskileri yükle",
+    "empty": "Henüz mesaj yok. {name} kişisine ilk mesajı gönderin.",
+    "placeholder": "{name} kişisine mesaj...",
+    "deleteMessage": "Mesajı sil",
+    "deleteTitle": "Mesaj silinsin mi?",
+    "deleteDescription": "Bu mesaj kalıcı olarak silinecektir. Bu işlem geri alınamaz.",
+    "messageDeleted": "Mesaj silindi",
+    "deleteError": "Mesaj silinemedi",
+    "sendError": "Mesaj gönderilemedi"
+  },
+  "sendDialog": {
+    "send": "{label} gönder",
+    "message": "Mesaj",
+    "placeholder": "Mesajınızı yazın...",
+    "characters": "{count} / 4096 karakter",
+    "sendTelegram": "Mesaj gönder",
+    "messageSent": "Mesaj gönderildi",
+    "sentTo": "{name} kişisine Telegram ile mesaj gönderildi",
+    "sendError": "Mesaj gönderilemedi"
+  }
+}

+ 218 - 7
package-lock.json

@@ -24,6 +24,7 @@
         "@tiptap/pm": "^3.19.0",
         "@tiptap/pm": "^3.19.0",
         "@tiptap/react": "^3.19.0",
         "@tiptap/react": "^3.19.0",
         "@tiptap/starter-kit": "^3.19.0",
         "@tiptap/starter-kit": "^3.19.0",
+        "@types/qrcode": "^1.5.6",
         "archiver": "^7.0.1",
         "archiver": "^7.0.1",
         "better-auth": "^1.4.18",
         "better-auth": "^1.4.18",
         "bson": "^7.2.0",
         "bson": "^7.2.0",
@@ -46,6 +47,7 @@
         "posthog-js": "^1.360.2",
         "posthog-js": "^1.360.2",
         "postmark": "^4.0.5",
         "postmark": "^4.0.5",
         "prisma": "^6.19.2",
         "prisma": "^6.19.2",
+        "qrcode": "^1.5.4",
         "qrcode.react": "^4.2.0",
         "qrcode.react": "^4.2.0",
         "radix-ui": "^1.4.3",
         "radix-ui": "^1.4.3",
         "react": "19.2.3",
         "react": "19.2.3",
@@ -8529,6 +8531,15 @@
         "@types/node": "*"
         "@types/node": "*"
       }
       }
     },
     },
+    "node_modules/@types/qrcode": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",
+      "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/react": {
     "node_modules/@types/react": {
       "version": "19.2.14",
       "version": "19.2.14",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -9542,6 +9553,15 @@
         "node": ">=6"
         "node": ">=6"
       }
       }
     },
     },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/caniuse-lite": {
     "node_modules/caniuse-lite": {
       "version": "1.0.30001769",
       "version": "1.0.30001769",
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
@@ -10343,6 +10363,15 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/decimal.js": {
     "node_modules/decimal.js": {
       "version": "10.6.0",
       "version": "10.6.0",
       "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
       "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
@@ -10539,6 +10568,12 @@
         "node": ">=0.3.1"
         "node": ">=0.3.1"
       }
       }
     },
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+      "license": "MIT"
+    },
     "node_modules/dom-accessibility-api": {
     "node_modules/dom-accessibility-api": {
       "version": "0.5.16",
       "version": "0.5.16",
       "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
       "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
@@ -11476,7 +11511,6 @@
       "version": "2.0.5",
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true,
       "license": "ISC",
       "license": "ISC",
       "engines": {
       "engines": {
         "node": "6.* || 8.* || >= 10.*"
         "node": "6.* || 8.* || >= 10.*"
@@ -15308,6 +15342,15 @@
         "pathe": "^2.0.3"
         "pathe": "^2.0.3"
       }
       }
     },
     },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/po-parser": {
     "node_modules/po-parser": {
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
@@ -15864,6 +15907,23 @@
         "node": ">=16.0.0"
         "node": ">=16.0.0"
       }
       }
     },
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "license": "MIT",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/qrcode.react": {
     "node_modules/qrcode.react": {
       "version": "4.2.0",
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
       "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
@@ -15873,6 +15933,145 @@
         "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
         "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       }
       }
     },
     },
+    "node_modules/qrcode/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/qrcode/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/qrcode/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+      "license": "ISC"
+    },
+    "node_modules/qrcode/node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/qs": {
     "node_modules/qs": {
       "version": "6.15.0",
       "version": "6.15.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
@@ -16441,7 +16640,6 @@
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
-      "dev": true,
       "license": "MIT",
       "license": "MIT",
       "engines": {
       "engines": {
         "node": ">=0.10.0"
         "node": ">=0.10.0"
@@ -16456,6 +16654,12 @@
         "node": ">=0.10.0"
         "node": ">=0.10.0"
       }
       }
     },
     },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "license": "ISC"
+    },
     "node_modules/reselect": {
     "node_modules/reselect": {
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -16765,6 +16969,12 @@
         "url": "https://opencollective.com/express"
         "url": "https://opencollective.com/express"
       }
       }
     },
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
+    },
     "node_modules/set-cookie-parser": {
     "node_modules/set-cookie-parser": {
       "version": "2.7.2",
       "version": "2.7.2",
       "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
       "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
@@ -18514,6 +18724,12 @@
         "node": ">= 8"
         "node": ">= 8"
       }
       }
     },
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+      "license": "ISC"
+    },
     "node_modules/why-is-node-running": {
     "node_modules/why-is-node-running": {
       "version": "2.3.0",
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
       "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -18535,7 +18751,6 @@
       "version": "6.2.0",
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
       "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
       "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
-      "dev": true,
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "ansi-styles": "^4.0.0",
@@ -18609,7 +18824,6 @@
       "version": "5.0.1",
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "license": "MIT",
       "license": "MIT",
       "engines": {
       "engines": {
         "node": ">=8"
         "node": ">=8"
@@ -18619,14 +18833,12 @@
       "version": "8.0.0",
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
     "node_modules/wrap-ansi/node_modules/string-width": {
     "node_modules/wrap-ansi/node_modules/string-width": {
       "version": "4.2.3",
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "emoji-regex": "^8.0.0",
@@ -18641,7 +18853,6 @@
       "version": "6.0.1",
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "ansi-regex": "^5.0.1"
         "ansi-regex": "^5.0.1"

+ 3 - 1
package.json

@@ -32,6 +32,7 @@
     "@tiptap/pm": "^3.19.0",
     "@tiptap/pm": "^3.19.0",
     "@tiptap/react": "^3.19.0",
     "@tiptap/react": "^3.19.0",
     "@tiptap/starter-kit": "^3.19.0",
     "@tiptap/starter-kit": "^3.19.0",
+    "@types/qrcode": "^1.5.6",
     "archiver": "^7.0.1",
     "archiver": "^7.0.1",
     "better-auth": "^1.4.18",
     "better-auth": "^1.4.18",
     "bson": "^7.2.0",
     "bson": "^7.2.0",
@@ -54,6 +55,7 @@
     "posthog-js": "^1.360.2",
     "posthog-js": "^1.360.2",
     "postmark": "^4.0.5",
     "postmark": "^4.0.5",
     "prisma": "^6.19.2",
     "prisma": "^6.19.2",
+    "qrcode": "^1.5.4",
     "qrcode.react": "^4.2.0",
     "qrcode.react": "^4.2.0",
     "radix-ui": "^1.4.3",
     "radix-ui": "^1.4.3",
     "react": "19.2.3",
     "react": "19.2.3",
@@ -98,4 +100,4 @@
     "fast-xml-parser": ">=5.3.6",
     "fast-xml-parser": ">=5.3.6",
     "qs": ">=6.14.2"
     "qs": ">=6.14.2"
   }
   }
-}
+}

+ 33 - 0
prisma/migrations/20260323160120_telegram_messages/migration.sql

@@ -0,0 +1,33 @@
+-- AlterTable
+ALTER TABLE "customers" ADD COLUMN     "telegramChatId" TEXT;
+
+-- CreateTable
+CREATE TABLE "telegram_messages" (
+    "id" TEXT NOT NULL,
+    "direction" TEXT NOT NULL,
+    "chatId" TEXT NOT NULL,
+    "body" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'queued',
+    "telegramMessageId" TEXT,
+    "errorMessage" TEXT,
+    "relatedEntityType" TEXT,
+    "relatedEntityId" TEXT,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+    "customerId" TEXT,
+
+    CONSTRAINT "telegram_messages_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "telegram_messages_organizationId_customerId_createdAt_idx" ON "telegram_messages"("organizationId", "customerId", "createdAt" DESC);
+
+-- CreateIndex
+CREATE INDEX "telegram_messages_organizationId_chatId_createdAt_idx" ON "telegram_messages"("organizationId", "chatId", "createdAt" DESC);
+
+-- AddForeignKey
+ALTER TABLE "telegram_messages" ADD CONSTRAINT "telegram_messages_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "telegram_messages" ADD CONSTRAINT "telegram_messages_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;

+ 69 - 42
prisma/schema.prisma

@@ -130,27 +130,27 @@ model Passkey {
 }
 }
 
 
 model Vehicle {
 model Vehicle {
-  id                     String    @id @default(cuid())
+  id                     String               @id @default(cuid())
   make                   String
   make                   String
   model                  String
   model                  String
   year                   Int
   year                   Int
   vin                    String?
   vin                    String?
   licensePlate           String?
   licensePlate           String?
   color                  String?
   color                  String?
-  mileage                Int       @default(0)
-  fuelType               String?   @default("gasoline")
-  transmission           String?   @default("automatic")
+  mileage                Int                  @default(0)
+  fuelType               String?              @default("gasoline")
+  transmission           String?              @default("automatic")
   engineSize             String?
   engineSize             String?
   purchaseDate           DateTime?
   purchaseDate           DateTime?
   purchasePrice          Float?
   purchasePrice          Float?
   imageUrl               String?
   imageUrl               String?
-  isArchived             Boolean   @default(false)
+  isArchived             Boolean              @default(false)
   archiveReason          String?
   archiveReason          String?
-  maintenanceDismissed   Boolean   @default(false)
+  maintenanceDismissed   Boolean              @default(false)
   maintenanceDismissedAt DateTime?
   maintenanceDismissedAt DateTime?
-  aiMessages AiGeneratedMessage[]
-  createdAt              DateTime  @default(now())
-  updatedAt              DateTime  @updatedAt
+  aiMessages             AiGeneratedMessage[]
+  createdAt              DateTime             @default(now())
+  updatedAt              DateTime             @updatedAt
 
 
   userId String
   userId String
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -277,15 +277,16 @@ model Reminder {
 }
 }
 
 
 model Customer {
 model Customer {
-  id        String   @id @default(cuid())
-  name      String
-  email     String?
-  phone     String?
-  address   String?
-  company   String?
-  notes     String?
-  createdAt DateTime @default(now())
-  updatedAt DateTime @updatedAt
+  id             String   @id @default(cuid())
+  name           String
+  email          String?
+  phone          String?
+  address        String?
+  company        String?
+  notes          String?
+  telegramChatId String?
+  createdAt      DateTime @default(now())
+  updatedAt      DateTime @updatedAt
 
 
   userId String
   userId String
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -296,6 +297,7 @@ model Customer {
   vehicles         Vehicle[]
   vehicles         Vehicle[]
   quotes           Quote[]
   quotes           Quote[]
   smsMessages      SmsMessage[]
   smsMessages      SmsMessage[]
+  telegramMessages TelegramMessage[]
   customerSessions CustomerSession[]
   customerSessions CustomerSession[]
   serviceRequests  ServiceRequest[]
   serviceRequests  ServiceRequest[]
 
 
@@ -369,15 +371,15 @@ model ServiceLabor {
 }
 }
 
 
 model LaborPreset {
 model LaborPreset {
-  id             String   @id @default(cuid())
+  id             String            @id @default(cuid())
   name           String
   name           String
   description    String?
   description    String?
-  isArchived     Boolean  @default(false)
-  createdAt      DateTime @default(now())
-  updatedAt      DateTime @updatedAt
+  isArchived     Boolean           @default(false)
+  createdAt      DateTime          @default(now())
+  updatedAt      DateTime          @updatedAt
   userId         String
   userId         String
   organizationId String
   organizationId String
-  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+  organization   Organization      @relation(fields: [organizationId], references: [id], onDelete: Cascade)
   items          LaborPresetItem[]
   items          LaborPresetItem[]
 
 
   @@index([organizationId])
   @@index([organizationId])
@@ -385,11 +387,11 @@ model LaborPreset {
 }
 }
 
 
 model LaborPresetItem {
 model LaborPresetItem {
-  id          String @id @default(cuid())
+  id          String      @id @default(cuid())
   description String
   description String
-  hours       Float  @default(0)
-  rate        Float  @default(0)
-  sortOrder   Int    @default(0)
+  hours       Float       @default(0)
+  rate        Float       @default(0)
+  sortOrder   Int         @default(0)
   presetId    String
   presetId    String
   preset      LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
   preset      LaborPreset @relation(fields: [presetId], references: [id], onDelete: Cascade)
 
 
@@ -583,6 +585,7 @@ model Organization {
   invitations            TeamInvitation[]
   invitations            TeamInvitation[]
   inspectionTemplates    InspectionTemplate[]
   inspectionTemplates    InspectionTemplate[]
   smsMessages            SmsMessage[]
   smsMessages            SmsMessage[]
+  telegramMessages       TelegramMessage[]
   technicians            Technician[]
   technicians            Technician[]
   auditLogs              AuditLog[]
   auditLogs              AuditLog[]
 
 
@@ -913,6 +916,30 @@ model SmsMessage {
   @@map("sms_messages")
   @@map("sms_messages")
 }
 }
 
 
+model TelegramMessage {
+  id                String   @id @default(cuid())
+  direction         String // "inbound" | "outbound"
+  chatId            String // Telegram chat ID
+  body              String
+  status            String   @default("queued") // queued | sent | delivered | failed | received
+  telegramMessageId String? // Telegram's message ID for tracking
+  errorMessage      String?
+  relatedEntityType String?
+  relatedEntityId   String?
+  createdAt         DateTime @default(now())
+  updatedAt         DateTime @updatedAt
+
+  organizationId String
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+  customerId String?
+  customer   Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
+
+  @@index([organizationId, customerId, createdAt(sort: Desc)])
+  @@index([organizationId, chatId, createdAt(sort: Desc)])
+  @@map("telegram_messages")
+}
+
 model Technician {
 model Technician {
   id            String  @id @default(cuid())
   id            String  @id @default(cuid())
   name          String
   name          String
@@ -937,21 +964,21 @@ model Technician {
 }
 }
 
 
 model AuditLog {
 model AuditLog {
-  id              String   @id @default(cuid())
-  timestamp       DateTime @default(now())
-  action          String
-  entity          String?
-  entityId        String?
-  message         String?
-  metadata        Json?
-  ip              String?
-  userAgent       String?
+  id        String   @id @default(cuid())
+  timestamp DateTime @default(now())
+  action    String
+  entity    String?
+  entityId  String?
+  message   String?
+  metadata  Json?
+  ip        String?
+  userAgent String?
 
 
-  userId          String?
-  user            User?    @relation(fields: [userId], references: [id], onDelete: SetNull)
+  userId String?
+  user   User?   @relation(fields: [userId], references: [id], onDelete: SetNull)
 
 
-  organizationId  String?
-  organization    Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
+  organizationId String?
+  organization   Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
 
 
   @@index([organizationId, timestamp(sort: Desc)])
   @@index([organizationId, timestamp(sort: Desc)])
   @@index([userId, timestamp(sort: Desc)])
   @@index([userId, timestamp(sort: Desc)])
@@ -1010,7 +1037,7 @@ model ServiceRequest {
 model AiGeneratedMessage {
 model AiGeneratedMessage {
   id        String   @id @default(cuid())
   id        String   @id @default(cuid())
   vehicleId String
   vehicleId String
-  type      String   // "summary" | "common_issues" | etc.
+  type      String // "summary" | "common_issues" | etc.
   content   String
   content   String
   createdAt DateTime @default(now())
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt
   updatedAt DateTime @updatedAt
@@ -1038,7 +1065,7 @@ model AiChat {
 
 
 model AiChatMessage {
 model AiChatMessage {
   id        String   @id @default(cuid())
   id        String   @id @default(cuid())
-  role      String   // "user" | "assistant"
+  role      String // "user" | "assistant"
   content   String
   content   String
   createdAt DateTime @default(now())
   createdAt DateTime @default(now())
 
 

+ 70 - 14
src/app/(authenticated)/customers/[id]/customer-detail-client.tsx

@@ -29,6 +29,7 @@ import {
   Pencil,
   Pencil,
   Phone,
   Phone,
   Plus,
   Plus,
+  Send,
   Users,
   Users,
   Wrench,
   Wrench,
   X,
   X,
@@ -36,9 +37,11 @@ import {
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Textarea } from "@/components/ui/textarea";
 import { Textarea } from "@/components/ui/textarea";
 import { SmsConversation } from "@/features/sms/Components/SmsConversation";
 import { SmsConversation } from "@/features/sms/Components/SmsConversation";
+import { TelegramConversation } from "@/features/telegram/Components/TelegramConversation";
 import { CustomerForm } from "@/features/customers/Components/CustomerForm";
 import { CustomerForm } from "@/features/customers/Components/CustomerForm";
 import { updateServiceRequest, createWorkOrderFromRequest } from "@/features/customers/Actions/customerActions";
 import { updateServiceRequest, createWorkOrderFromRequest } from "@/features/customers/Actions/customerActions";
 import { SendSmsDialog } from "@/features/sms/Components/SendSmsDialog";
 import { SendSmsDialog } from "@/features/sms/Components/SendSmsDialog";
+import { TelegramQrCode } from "@/features/telegram/Components/TelegramQrCode";
 import { toast } from "sonner";
 import { toast } from "sonner";
 
 
 interface ServiceRequestItem {
 interface ServiceRequestItem {
@@ -87,12 +90,22 @@ export function CustomerDetailClient({
   smsEnabled = false,
   smsEnabled = false,
   smsMessages = [],
   smsMessages = [],
   smsNextCursor = null,
   smsNextCursor = null,
+  telegramEnabled = false,
+  telegramBotUsername = null,
+  telegramMessages = [],
+  telegramNextCursor = null,
+  telegramChatId = null,
 }: {
 }: {
   customer: CustomerDetail;
   customer: CustomerDetail;
   unitSystem?: "metric" | "imperial";
   unitSystem?: "metric" | "imperial";
   smsEnabled?: boolean;
   smsEnabled?: boolean;
   smsMessages?: SmsMessage[];
   smsMessages?: SmsMessage[];
   smsNextCursor?: string | null;
   smsNextCursor?: string | null;
+  telegramEnabled?: boolean;
+  telegramBotUsername?: string | null;
+  telegramMessages?: { id: string; direction: string; body: string; status: string; createdAt: string | Date; chatId: string }[];
+  telegramNextCursor?: string | null;
+  telegramChatId?: string | null;
 }) {
 }) {
   const t = useTranslations("customers.detail");
   const t = useTranslations("customers.detail");
   const router = useRouter();
   const router = useRouter();
@@ -101,13 +114,17 @@ export function CustomerDetailClient({
 
 
   const tabParam = searchParams.get("tab");
   const tabParam = searchParams.get("tab");
   const activeTab =
   const activeTab =
-    tabParam === "messages" && smsEnabled
-      ? "messages"
-      : tabParam === "requests"
-        ? "requests"
-        : "vehicles";
+    tabParam === "sms" && smsEnabled
+      ? "sms"
+      : tabParam === "telegram" && telegramEnabled
+        ? "telegram"
+        : tabParam === "messages" && smsEnabled
+          ? "sms"
+          : tabParam === "requests"
+            ? "requests"
+            : "vehicles";
 
 
-  const setActiveTab = (tab: "vehicles" | "messages" | "requests") => {
+  const setActiveTab = (tab: "vehicles" | "sms" | "telegram" | "requests") => {
     const params = new URLSearchParams(searchParams.toString());
     const params = new URLSearchParams(searchParams.toString());
     if (tab === "vehicles") {
     if (tab === "vehicles") {
       params.delete("tab");
       params.delete("tab");
@@ -186,10 +203,20 @@ export function CustomerDetailClient({
               </p>
               </p>
             )}
             )}
           </div>
           </div>
-          <Button variant="outline" size="sm" onClick={() => setShowEditForm(true)}>
-            <Pencil className="mr-1 h-3.5 w-3.5" />
-            {t("editCustomer")}
-          </Button>
+          <div className="flex items-center gap-2 shrink-0">
+            {telegramBotUsername && (
+              <TelegramQrCode
+                botUsername={telegramBotUsername}
+                customerId={customer.id}
+                customerName={customer.name}
+                customerEmail={customer.email}
+              />
+            )}
+            <Button variant="outline" size="sm" onClick={() => setShowEditForm(true)}>
+              <Pencil className="mr-1 h-3.5 w-3.5" />
+              {t("editCustomer")}
+            </Button>
+          </div>
         </div>
         </div>
       </div>
       </div>
 
 
@@ -212,16 +239,31 @@ export function CustomerDetailClient({
           {smsEnabled && (
           {smsEnabled && (
             <button
             <button
               type="button"
               type="button"
-              onClick={() => setActiveTab("messages")}
+              onClick={() => setActiveTab("sms")}
               className={cn(
               className={cn(
                 "flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors",
                 "flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors",
-                activeTab === "messages"
+                activeTab === "sms"
                   ? "border-primary text-primary"
                   ? "border-primary text-primary"
                   : "border-transparent text-muted-foreground hover:text-foreground",
                   : "border-transparent text-muted-foreground hover:text-foreground",
               )}
               )}
             >
             >
               <MessageSquare className="h-4 w-4" />
               <MessageSquare className="h-4 w-4" />
-              {t("tabs.messages")}
+              {t("tabs.sms")}
+            </button>
+          )}
+          {telegramEnabled && (
+            <button
+              type="button"
+              onClick={() => setActiveTab("telegram")}
+              className={cn(
+                "flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors",
+                activeTab === "telegram"
+                  ? "border-primary text-primary"
+                  : "border-transparent text-muted-foreground hover:text-foreground",
+              )}
+            >
+              <Send className="h-4 w-4" />
+              {t("tabs.telegram")}
             </button>
             </button>
           )}
           )}
           {(customer.serviceRequests?.length ?? 0) > 0 && (
           {(customer.serviceRequests?.length ?? 0) > 0 && (
@@ -289,7 +331,7 @@ export function CustomerDetailClient({
           </>
           </>
         )}
         )}
 
 
-        {activeTab === "messages" && smsEnabled && (
+        {activeTab === "sms" && smsEnabled && (
           <Card>
           <Card>
             <CardContent className="p-0">
             <CardContent className="p-0">
               <SmsConversation
               <SmsConversation
@@ -303,6 +345,20 @@ export function CustomerDetailClient({
           </Card>
           </Card>
         )}
         )}
 
 
+        {activeTab === "telegram" && telegramEnabled && (
+          <Card>
+            <CardContent className="p-0">
+              <TelegramConversation
+                customerId={customer.id}
+                customerName={customer.name}
+                telegramChatId={telegramChatId}
+                initialMessages={telegramMessages}
+                initialNextCursor={telegramNextCursor}
+              />
+            </CardContent>
+          </Card>
+        )}
+
         {activeTab === "requests" && customer.serviceRequests && (
         {activeTab === "requests" && customer.serviceRequests && (
           <div className="space-y-3">
           <div className="space-y-3">
             {customer.serviceRequests.map((req) => (
             {customer.serviceRequests.map((req) => (

+ 30 - 1
src/app/(authenticated)/customers/[id]/page.tsx

@@ -3,8 +3,11 @@ import { getCustomer } from "@/features/customers/Actions/customerActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { getConversation } from "@/features/sms/Actions/smsActions";
 import { getConversation } from "@/features/sms/Actions/smsActions";
+import { getTelegramConversation } from "@/features/telegram/Actions/telegramActions";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
+import { getOrgTelegramBotUsername } from "@/lib/telegram";
+import { db } from "@/lib/db";
 import { CustomerDetailClient } from "./customer-detail-client";
 import { CustomerDetailClient } from "./customer-detail-client";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
 
 
@@ -34,15 +37,27 @@ export default async function CustomerDetailPage({
     );
     );
   }
   }
 
 
-  // Load SMS features and conversation if SMS is enabled
+  // Load SMS/Telegram features and conversation if enabled
   let smsEnabled = false;
   let smsEnabled = false;
+  let telegramBotUsername: string | null = null;
+  let telegramEnabled = false;
   let smsMessages: { id: string; direction: string; body: string; status: string; createdAt: string | Date; fromNumber: string; toNumber: string }[] = [];
   let smsMessages: { id: string; direction: string; body: string; status: string; createdAt: string | Date; fromNumber: string; toNumber: string }[] = [];
   let smsNextCursor: string | null = null;
   let smsNextCursor: string | null = null;
+  let telegramMessages: { id: string; direction: string; body: string; status: string; createdAt: string | Date; chatId: string }[] = [];
+  let telegramNextCursor: string | null = null;
 
 
   if (layoutData.status === "ok") {
   if (layoutData.status === "ok") {
     const features = await getFeatures(layoutData.organizationId);
     const features = await getFeatures(layoutData.organizationId);
     smsEnabled = features.sms;
     smsEnabled = features.sms;
 
 
+    if (features.telegram) {
+      const tgEnabledSetting = await db.appSetting.findUnique({
+        where: { organizationId_key: { organizationId: layoutData.organizationId, key: "telegram.enabled" } },
+        select: { value: true },
+      });
+      telegramEnabled = tgEnabledSetting?.value === "true";
+    }
+
     if (smsEnabled && result.data.phone) {
     if (smsEnabled && result.data.phone) {
       const convResult = await getConversation(id);
       const convResult = await getConversation(id);
       if (convResult.success && convResult.data) {
       if (convResult.success && convResult.data) {
@@ -50,6 +65,15 @@ export default async function CustomerDetailPage({
         smsNextCursor = convResult.data.nextCursor;
         smsNextCursor = convResult.data.nextCursor;
       }
       }
     }
     }
+
+    if (telegramEnabled) {
+      telegramBotUsername = await getOrgTelegramBotUsername(layoutData.organizationId);
+      const tgResult = await getTelegramConversation(id);
+      if (tgResult.success && tgResult.data) {
+        telegramMessages = tgResult.data.messages;
+        telegramNextCursor = tgResult.data.nextCursor;
+      }
+    }
   }
   }
 
 
   return (
   return (
@@ -62,6 +86,11 @@ export default async function CustomerDetailPage({
           smsEnabled={smsEnabled}
           smsEnabled={smsEnabled}
           smsMessages={smsMessages}
           smsMessages={smsMessages}
           smsNextCursor={smsNextCursor}
           smsNextCursor={smsNextCursor}
+          telegramEnabled={telegramEnabled}
+          telegramBotUsername={telegramBotUsername}
+          telegramMessages={telegramMessages}
+          telegramNextCursor={telegramNextCursor}
+          telegramChatId={result.data.telegramChatId ?? null}
         />
         />
       </div>
       </div>
     </>
     </>

+ 20 - 0
src/app/(authenticated)/layout.tsx

@@ -38,6 +38,24 @@ export default async function DashboardLayout({
   const features = await getFeatures(data.organizationId);
   const features = await getFeatures(data.organizationId);
   const showWhiteLabelCta = !isCloudMode() && !features.brandingRemoved;
   const showWhiteLabelCta = !isCloudMode() && !features.brandingRemoved;
 
 
+  // Check user-level messaging enabled settings
+  let smsEnabled = false;
+  let telegramEnabled = false;
+  if (features.sms) {
+    const smsSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: data.organizationId, key: "sms.provider" } },
+      select: { value: true },
+    });
+    smsEnabled = !!smsSetting?.value;
+  }
+  if (features.telegram) {
+    const tgSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: data.organizationId, key: "telegram.enabled" } },
+      select: { value: true },
+    });
+    telegramEnabled = tgSetting?.value === "true";
+  }
+
   // Determine which subjects the user can access (for sidebar visibility)
   // Determine which subjects the user can access (for sidebar visibility)
   const isOwnerOrAdmin = data.role === "owner" || data.role === "admin" || data.role === "super_admin";
   const isOwnerOrAdmin = data.role === "owner" || data.role === "admin" || data.role === "super_admin";
   const allSubjects = Object.values(PermissionSubject);
   const allSubjects = Object.values(PermissionSubject);
@@ -78,6 +96,8 @@ export default async function DashboardLayout({
           activeOrgId={data.organizationId}
           activeOrgId={data.organizationId}
           isSuperAdmin={data.isSuperAdmin}
           isSuperAdmin={data.isSuperAdmin}
           features={features}
           features={features}
+          smsEnabled={smsEnabled}
+          telegramEnabled={telegramEnabled}
           visibleSubjects={visibleSubjects}
           visibleSubjects={visibleSubjects}
           isAdminOrOwner={isOwnerOrAdmin}
           isAdminOrOwner={isOwnerOrAdmin}
         />
         />

+ 16 - 2
src/app/(authenticated)/settings/invoice/invoice-settings.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useState, useCallback } from 'react'
+import { useState, useCallback, useMemo } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
 import { useRouter, useSearchParams } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -47,6 +47,7 @@ interface InvoiceSettingsProps {
   initialQuoteLayout?: InvoiceLayoutConfig
   initialQuoteLayout?: InvoiceLayoutConfig
   customFields: FieldDef[]
   customFields: FieldDef[]
   customFieldsEnabled: boolean
   customFieldsEnabled: boolean
+  telegramEnabled?: boolean
 }
 }
 
 
 export function InvoiceSettings({
 export function InvoiceSettings({
@@ -55,6 +56,7 @@ export function InvoiceSettings({
   initialQuoteLayout,
   initialQuoteLayout,
   customFields,
   customFields,
   customFieldsEnabled,
   customFieldsEnabled,
+  telegramEnabled = false,
 }: InvoiceSettingsProps) {
 }: InvoiceSettingsProps) {
   const router = useRouter()
   const router = useRouter()
   const searchParams = useSearchParams()
   const searchParams = useSearchParams()
@@ -86,6 +88,13 @@ export function InvoiceSettings({
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || '14')
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || '14')
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || '')
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || '')
 
 
+  // Sections to hide from layout editor when features are disabled
+  const hiddenLayoutSections = useMemo(() => {
+    const hidden = new Set<string>()
+    if (!telegramEnabled) hidden.add('telegram_qr')
+    return hidden
+  }, [telegramEnabled])
+
   // Layout tab state
   // Layout tab state
   const [invoiceLayout, setInvoiceLayout] = useState(
   const [invoiceLayout, setInvoiceLayout] = useState(
     initialInvoiceLayout ?? getDefaultInvoiceLayout()
     initialInvoiceLayout ?? getDefaultInvoiceLayout()
@@ -336,6 +345,7 @@ export function InvoiceSettings({
                   onChange={layoutDocType === 'invoice' ? setInvoiceLayout : setQuoteLayout}
                   onChange={layoutDocType === 'invoice' ? setInvoiceLayout : setQuoteLayout}
                   documentType={layoutDocType}
                   documentType={layoutDocType}
                   customFields={customFields}
                   customFields={customFields}
+                  hiddenSections={hiddenLayoutSections}
                 />
                 />
                 <div className="hidden xl:block pr-2">
                 <div className="hidden xl:block pr-2">
                   <div className="sticky top-6 space-y-2">
                   <div className="sticky top-6 space-y-2">
@@ -343,7 +353,11 @@ export function InvoiceSettings({
                       {t('invoice.preview')}
                       {t('invoice.preview')}
                     </p>
                     </p>
                     <InvoiceLayoutPreview
                     <InvoiceLayoutPreview
-                      config={layoutDocType === 'invoice' ? invoiceLayout : quoteLayout}
+                      config={{
+                        sections: (layoutDocType === 'invoice' ? invoiceLayout : quoteLayout).sections.map(
+                          (s) => hiddenLayoutSections.has(s.id) ? { ...s, visible: false } : s
+                        ),
+                      }}
                       documentType={layoutDocType}
                       documentType={layoutDocType}
                       customFields={customFields}
                       customFields={customFields}
                       template={layoutDocType === 'invoice' ? invoiceTemplate : quoteTemplate}
                       template={layoutDocType === 'invoice' ? invoiceTemplate : quoteTemplate}

+ 12 - 0
src/app/(authenticated)/settings/invoice/page.tsx

@@ -1,6 +1,7 @@
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
+import { db } from "@/lib/db";
 import { getInvoiceLayoutConfig, getQuoteLayoutConfig } from "@/features/settings/Actions/invoiceLayoutActions";
 import { getInvoiceLayoutConfig, getQuoteLayoutConfig } from "@/features/settings/Actions/invoiceLayoutActions";
 import { getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
 import { getFieldDefinitions } from "@/features/custom-fields/Actions/customFieldActions";
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
@@ -24,6 +25,16 @@ export default async function InvoiceSettingsPage() {
   const settings = result.success && result.data ? result.data : {};
   const settings = result.success && result.data ? result.data : {};
   const customFields = customFieldsResult.success && customFieldsResult.data ? customFieldsResult.data : [];
   const customFields = customFieldsResult.success && customFieldsResult.data ? customFieldsResult.data : [];
 
 
+  // Check if Telegram is enabled (plan feature + user setting)
+  let telegramEnabled = false;
+  if (features.telegram) {
+    const tgSetting = await db.appSetting.findUnique({
+      where: { organizationId_key: { organizationId: data.organizationId, key: "telegram.enabled" } },
+      select: { value: true },
+    });
+    telegramEnabled = tgSetting?.value === "true";
+  }
+
   return (
   return (
     <InvoiceSettings
     <InvoiceSettings
       settings={settings}
       settings={settings}
@@ -31,6 +42,7 @@ export default async function InvoiceSettingsPage() {
       initialQuoteLayout={quoteLayoutResult.success ? quoteLayoutResult.data : undefined}
       initialQuoteLayout={quoteLayoutResult.success ? quoteLayoutResult.data : undefined}
       customFields={customFields}
       customFields={customFields}
       customFieldsEnabled={features.customFields ?? false}
       customFieldsEnabled={features.customFields ?? false}
+      telegramEnabled={telegramEnabled}
     />
     />
   );
   );
 }
 }

+ 2 - 0
src/app/(authenticated)/settings/settings-nav.tsx

@@ -25,6 +25,7 @@ import {
   Layout,
   Layout,
   Mail,
   Mail,
   MessageSquare,
   MessageSquare,
+  Send,
   Sparkles,
   Sparkles,
   UserCog,
   UserCog,
   UsersRound,
   UsersRound,
@@ -69,6 +70,7 @@ const settingsCategories: SettingsCategory[] = [
     items: [
     items: [
       { key: 'email', href: '/settings/email', icon: Mail, gate: 'smtp' },
       { key: 'email', href: '/settings/email', icon: Mail, gate: 'smtp' },
       { key: 'sms', href: '/settings/sms', icon: MessageSquare, gate: 'sms' },
       { key: 'sms', href: '/settings/sms', icon: MessageSquare, gate: 'sms' },
+      { key: 'telegram', href: '/settings/telegram', icon: Send, gate: 'telegram' },
       {
       {
         key: 'customerPortal',
         key: 'customerPortal',
         href: '/settings/customer-portal',
         href: '/settings/customer-portal',

+ 37 - 0
src/app/(authenticated)/settings/telegram/page.tsx

@@ -0,0 +1,37 @@
+import { getTelegramSettings } from "@/features/telegram/Actions/telegramSettingsActions";
+import { getLayoutData } from "@/lib/get-layout-data";
+import { getFeatures, isCloudMode } from "@/lib/features";
+import { TelegramSettingsForm } from "@/features/telegram/Components/TelegramSettingsForm";
+import { FeatureLockedMessage } from "../feature-locked-message";
+import { redirect } from "next/navigation";
+
+export default async function TelegramSettingsPage() {
+  const data = await getLayoutData();
+
+  if (data.status === "unauthenticated") redirect("/auth/sign-in");
+  if (data.status === "no-organization") redirect("/onboarding");
+
+  const features = await getFeatures(data.organizationId);
+
+  if (!features.telegram) {
+    return (
+      <FeatureLockedMessage
+        feature="Telegram Messaging"
+        description="Send and receive Telegram messages with your customers directly from Torqvoice."
+        isCloud={isCloudMode()}
+      />
+    );
+  }
+
+  const result = await getTelegramSettings();
+  const settings = result.success && result.data ? result.data : {};
+  const telegramEnabled = settings["telegram.enabled"] === "true";
+
+  const appUrl =
+    process.env.NEXT_PUBLIC_APP_URL ||
+    (process.env.VERCEL_URL
+      ? `https://${process.env.VERCEL_URL}`
+      : "http://localhost:3000");
+
+  return <TelegramSettingsForm initial={settings} appUrl={appUrl} initialEnabled={telegramEnabled} />;
+}

+ 74 - 0
src/app/(authenticated)/telegram/page.tsx

@@ -0,0 +1,74 @@
+import { Suspense } from "react";
+import { getAuthContext } from "@/lib/get-auth-context";
+import { getFeatures, isCloudMode } from "@/lib/features";
+import { redirect } from "next/navigation";
+import { getTranslations } from "next-intl/server";
+import { getRecentTelegramThreads } from "@/features/telegram/Actions/telegramThreadActions";
+import { getTelegramSettings } from "@/features/telegram/Actions/telegramSettingsActions";
+import { TelegramMessagesClient } from "@/features/telegram/Components/TelegramMessagesClient";
+import { PageHeader } from "@/components/page-header";
+import { FeatureLockedMessage } from "../settings/feature-locked-message";
+import { TelegramNotConfiguredMessage } from "./telegram-not-configured-message";
+
+export default async function TelegramPage() {
+  const ctx = await getAuthContext();
+  if (!ctx) redirect("/auth/sign-in");
+
+  const features = await getFeatures(ctx.organizationId);
+
+  if (!features.telegram) {
+    const t = await getTranslations("telegramMessages.page");
+    return (
+      <>
+        <PageHeader />
+        <div className="flex flex-1 flex-col p-4 pt-0">
+          <FeatureLockedMessage
+            feature={t("featureName")}
+            description={t("featureDescription")}
+            isCloud={isCloudMode()}
+          />
+        </div>
+      </>
+    );
+  }
+
+  const settingsResult = await getTelegramSettings();
+  const settings =
+    settingsResult.success && settingsResult.data ? settingsResult.data : {};
+  const hasBot = !!settings["telegram.botToken"];
+
+  if (!hasBot) {
+    return (
+      <>
+        <PageHeader />
+        <div className="flex flex-1 flex-col p-4 pt-0">
+          <TelegramNotConfiguredMessage />
+        </div>
+      </>
+    );
+  }
+
+  const threadsResult = await getRecentTelegramThreads();
+  const threads =
+    threadsResult.success && threadsResult.data
+      ? threadsResult.data.threads
+      : [];
+  const hasMore =
+    threadsResult.success && threadsResult.data
+      ? threadsResult.data.hasMore
+      : false;
+
+  return (
+    <>
+      <PageHeader />
+      <div className="flex flex-1 flex-col p-4 pt-0">
+        <Suspense>
+          <TelegramMessagesClient
+            initialThreads={threads}
+            initialHasMore={hasMore}
+          />
+        </Suspense>
+      </div>
+    </>
+  );
+}

+ 28 - 0
src/app/(authenticated)/telegram/telegram-not-configured-message.tsx

@@ -0,0 +1,28 @@
+"use client";
+
+import Link from "next/link";
+import { Send, ArrowRight } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useTranslations } from "next-intl";
+
+export function TelegramNotConfiguredMessage() {
+  const t = useTranslations("telegramMessages.notConfigured");
+
+  return (
+    <div className="flex flex-col items-center justify-center py-16 text-center">
+      <div className="flex h-12 w-12 items-center justify-center rounded-full bg-blue-500/10">
+        <Send className="h-6 w-6 text-blue-600" />
+      </div>
+      <h2 className="mt-4 text-lg font-semibold">{t("title")}</h2>
+      <p className="mt-1 max-w-sm text-sm text-muted-foreground">
+        {t("description")}
+      </p>
+      <Button asChild className="mt-4" size="sm">
+        <Link href="/settings/telegram">
+          {t("goToSettings")}
+          <ArrowRight className="ml-1.5 h-3.5 w-3.5" />
+        </Link>
+      </Button>
+    </div>
+  );
+}

+ 15 - 0
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -13,6 +13,7 @@ import {
   Paperclip,
   Paperclip,
   X,
   X,
 } from 'lucide-react'
 } from 'lucide-react'
+import { QRCodeSVG } from 'qrcode.react'
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from '@/lib/format'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
@@ -190,6 +191,7 @@ export function InvoiceView({
   portalUrl,
   portalUrl,
   layoutConfig,
   layoutConfig,
   customFields = [],
   customFields = [],
+  telegramBotLink,
 }: {
 }: {
   record: InvoiceRecord
   record: InvoiceRecord
   workshop: { name: string; address: string; phone: string; email: string }
   workshop: { name: string; address: string; phone: string; email: string }
@@ -211,6 +213,7 @@ export function InvoiceView({
   portalUrl?: string
   portalUrl?: string
   layoutConfig?: InvoiceLayoutConfig
   layoutConfig?: InvoiceLayoutConfig
   customFields?: CustomField[]
   customFields?: CustomField[]
+  telegramBotLink?: string
 }) {
 }) {
   const t = useTranslations('share.invoice')
   const t = useTranslations('share.invoice')
   const tc = useTranslations('share.common')
   const tc = useTranslations('share.common')
@@ -1012,6 +1015,18 @@ export function InvoiceView({
               )
               )
             }
             }
 
 
+            case 'telegram_qr':
+              if (!telegramBotLink) return null
+              return (
+                <div key="telegram_qr" className="mt-4 border-t pt-4 flex flex-col items-center gap-2">
+                  <p className="text-sm font-medium text-foreground">{tc('telegramConnect')}</p>
+                  <div className="rounded-lg bg-white p-2">
+                    <QRCodeSVG value={telegramBotLink} size={100} />
+                  </div>
+                  <p className="text-xs text-muted-foreground text-center">{tc('telegramScan')}</p>
+                </div>
+              )
+
             case 'footer':
             case 'footer':
               if (!invoiceSettings?.footerNote) return null
               if (!invoiceSettings?.footerNote) return null
               return (
               return (

+ 5 - 0
src/app/(public)/share/invoice/[orgId]/[token]/page.tsx

@@ -97,6 +97,7 @@ export default async function PublicInvoicePage({
             "workshop.timezone",
             "workshop.timezone",
             "portal.enabled",
             "portal.enabled",
             "invoice.layoutConfig",
             "invoice.layoutConfig",
+            "telegram.botUsername",
           ],
           ],
         },
         },
       },
       },
@@ -183,6 +184,9 @@ export default async function PublicInvoicePage({
     ? `${appUrl}/portal/${portalSlug || orgId}`
     ? `${appUrl}/portal/${portalSlug || orgId}`
     : undefined;
     : undefined;
 
 
+  const telegramBotUsername = settingsMap["telegram.botUsername"] || "";
+  const telegramBotLink = telegramBotUsername ? `https://t.me/${telegramBotUsername}` : undefined;
+
   return (
   return (
     <InvoiceView
     <InvoiceView
       record={publicRecord}
       record={publicRecord}
@@ -205,6 +209,7 @@ export default async function PublicInvoicePage({
       portalUrl={portalUrl}
       portalUrl={portalUrl}
       layoutConfig={layoutConfig}
       layoutConfig={layoutConfig}
       customFields={customFields}
       customFields={customFields}
+      telegramBotLink={telegramBotLink}
     />
     />
   );
   );
 }
 }

+ 13 - 0
src/app/api/protected/services/[id]/pdf/route.ts

@@ -223,6 +223,17 @@ export async function GET(
       ? `${appUrl}/portal/${portalSlug || ctx.organizationId}`
       ? `${appUrl}/portal/${portalSlug || ctx.organizationId}`
       : undefined;
       : undefined;
 
 
+    // Generate Telegram QR if the telegram_qr section is visible in layout
+    let telegramQrDataUri: string | undefined;
+    const telegramBotUsername = settingsMap["telegram.botUsername"];
+    const telegramQrVisible = layoutConfig.sections.some(
+      (s) => s.id === "telegram_qr" && s.visible
+    );
+    if (telegramBotUsername && telegramQrVisible) {
+      const { generateQrDataUri } = await import("@/lib/qr");
+      telegramQrDataUri = await generateQrDataUri(`https://t.me/${telegramBotUsername}`, 200);
+    }
+
     const element = React.createElement(InvoicePDF, {
     const element = React.createElement(InvoicePDF, {
       data: { ...record, customFields },
       data: { ...record, customFields },
       workshop: {
       workshop: {
@@ -240,6 +251,8 @@ export async function GET(
       template,
       template,
       torqvoiceLogoDataUri,
       torqvoiceLogoDataUri,
       portalUrl,
       portalUrl,
+      telegramQrDataUri,
+      telegramLabel: labels?.telegramConnect || "Chat with us on Telegram",
       labels,
       labels,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     }) as any;

+ 13 - 0
src/app/api/public/share/invoice/[orgId]/[token]/pdf/route.ts

@@ -182,6 +182,17 @@ export async function GET(
       ? `${appUrl}/portal/${portalSlug || orgId}`
       ? `${appUrl}/portal/${portalSlug || orgId}`
       : undefined;
       : undefined;
 
 
+    // Generate Telegram QR if the telegram_qr section is visible in layout
+    let telegramQrDataUri: string | undefined;
+    const telegramBotUsername = settingsMap["telegram.botUsername"];
+    const telegramQrVisible = layoutConfig.sections.some(
+      (s: { id: string; visible: boolean }) => s.id === "telegram_qr" && s.visible
+    );
+    if (telegramBotUsername && telegramQrVisible) {
+      const { generateQrDataUri } = await import("@/lib/qr");
+      telegramQrDataUri = await generateQrDataUri(`https://t.me/${telegramBotUsername}`, 200);
+    }
+
     const element = React.createElement(InvoicePDF, {
     const element = React.createElement(InvoicePDF, {
       data: { ...record, customFields },
       data: { ...record, customFields },
       workshop: {
       workshop: {
@@ -196,6 +207,8 @@ export async function GET(
       template,
       template,
       torqvoiceLogoDataUri,
       torqvoiceLogoDataUri,
       portalUrl,
       portalUrl,
+      telegramQrDataUri,
+      telegramLabel: labels?.telegramConnect || "Chat with us on Telegram",
       labels,
       labels,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     }) as any;

+ 148 - 0
src/app/api/webhooks/telegram/[organizationId]/route.ts

@@ -0,0 +1,148 @@
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { ORG_TELEGRAM_KEYS } from "@/features/telegram/Schema/telegramSettingsSchema";
+import { sendTelegramMessage } from "@/lib/telegram";
+import { notify } from "@/lib/notify";
+
+interface TelegramUpdate {
+  message?: {
+    message_id: number;
+    chat: { id: number; first_name?: string; username?: string };
+    from?: { id: number; first_name?: string; username?: string };
+    text?: string;
+  };
+}
+
+export async function POST(
+  request: Request,
+  { params }: { params: Promise<{ organizationId: string }> },
+) {
+  const { organizationId } = await params;
+
+  try {
+    // Validate webhook secret from header
+    const secretHeader = request.headers.get(
+      "x-telegram-bot-api-secret-token",
+    );
+
+    if (!secretHeader) {
+      // Return 200 to prevent Telegram retries
+      return NextResponse.json({ ok: true });
+    }
+
+    const secretSetting = await db.appSetting.findUnique({
+      where: {
+        organizationId_key: {
+          organizationId,
+          key: ORG_TELEGRAM_KEYS.TELEGRAM_WEBHOOK_SECRET,
+        },
+      },
+    });
+
+    if (!secretSetting?.value || secretSetting.value !== secretHeader) {
+      return NextResponse.json({ ok: true });
+    }
+
+    // Parse the Telegram Update
+    const update = (await request.json()) as TelegramUpdate;
+    const msg = update.message;
+
+    if (!msg?.text || !msg.chat?.id) {
+      return NextResponse.json({ ok: true });
+    }
+
+    const chatId = String(msg.chat.id);
+    const text = msg.text;
+    const telegramMessageId = String(msg.message_id);
+
+    // Handle /start deep-link command: /start {customerId}
+    if (text.startsWith("/start ")) {
+      const customerId = text.slice(7).trim();
+      if (customerId) {
+        await handleStartCommand(
+          organizationId,
+          chatId,
+          customerId,
+          msg.chat.first_name,
+        );
+      }
+      return NextResponse.json({ ok: true });
+    }
+
+    // Regular message: find customer by telegramChatId
+    const customer = await db.customer.findFirst({
+      where: { organizationId, telegramChatId: chatId },
+      select: { id: true, name: true },
+    });
+
+    // Create inbound message record
+    const message = await db.telegramMessage.create({
+      data: {
+        direction: "inbound",
+        chatId,
+        body: text,
+        status: "received",
+        telegramMessageId,
+        organizationId,
+        customerId: customer?.id,
+      },
+    });
+
+    // Send in-app notification
+    const senderName =
+      msg.from?.first_name || msg.chat.first_name || "Unknown";
+
+    await notify({
+      organizationId,
+      type: "telegram_inbound",
+      title: "New Telegram message",
+      message: customer
+        ? `${customer.name}: ${text.slice(0, 100)}`
+        : `${senderName}: ${text.slice(0, 100)}`,
+      entityType: "telegram_message",
+      entityId: message.id,
+      entityUrl: customer
+        ? `/telegram?customerId=${customer.id}`
+        : "/settings/telegram",
+    });
+
+    return NextResponse.json({ ok: true });
+  } catch (error) {
+    console.error("[webhook/telegram] Error:", error);
+    // Always return 200 to prevent Telegram retries
+    return NextResponse.json({ ok: true });
+  }
+}
+
+async function handleStartCommand(
+  organizationId: string,
+  chatId: string,
+  customerId: string,
+  firstName?: string,
+) {
+  // Verify customer belongs to this organization
+  const customer = await db.customer.findFirst({
+    where: { id: customerId, organizationId },
+    select: { id: true, name: true },
+  });
+
+  if (!customer) {
+    return;
+  }
+
+  // Link the Telegram chat ID to the customer
+  await db.customer.update({
+    where: { id: customer.id },
+    data: { telegramChatId: chatId },
+  });
+
+  // Send confirmation message back
+  try {
+    await sendTelegramMessage(organizationId, {
+      chatId,
+      text: `Hi ${firstName || customer.name}! Your Telegram is now linked to your account. You will receive messages here.`,
+    });
+  } catch (error) {
+    console.error("[webhook/telegram] Failed to send confirmation:", error);
+  }
+}

+ 7 - 1
src/components/app-sidebar.tsx

@@ -52,6 +52,7 @@ import {
   Globe,
   Globe,
   Layers,
   Layers,
   MessageSquare,
   MessageSquare,
+  Send,
   LayoutDashboard,
   LayoutDashboard,
   Loader2,
   Loader2,
   LogOut,
   LogOut,
@@ -82,6 +83,8 @@ export function AppSidebar({
   activeOrgId,
   activeOrgId,
   isSuperAdmin,
   isSuperAdmin,
   features,
   features,
+  smsEnabled = false,
+  telegramEnabled = false,
   isAdminOrOwner = false,
   isAdminOrOwner = false,
   visibleSubjects,
   visibleSubjects,
   ...props
   ...props
@@ -91,6 +94,8 @@ export function AppSidebar({
   activeOrgId?: string
   activeOrgId?: string
   isSuperAdmin?: boolean
   isSuperAdmin?: boolean
   features?: PlanFeatures
   features?: PlanFeatures
+  smsEnabled?: boolean
+  telegramEnabled?: boolean
   isAdminOrOwner?: boolean
   isAdminOrOwner?: boolean
   visibleSubjects?: string[]
   visibleSubjects?: string[]
 }) {
 }) {
@@ -109,7 +114,8 @@ export function AppSidebar({
 
 
   const clientItems = [
   const clientItems = [
     { titleKey: 'sidebar.customers' as const, url: '/customers', icon: Users, subject: 'customers' },
     { titleKey: 'sidebar.customers' as const, url: '/customers', icon: Users, subject: 'customers' },
-    { titleKey: 'sidebar.messages' as const, url: '/messages', icon: MessageSquare, subject: 'customers' },
+    ...(smsEnabled ? [{ titleKey: 'sidebar.smsMessages' as const, url: '/messages', icon: MessageSquare, subject: 'customers' }] : []),
+    ...(telegramEnabled ? [{ titleKey: 'sidebar.telegram' as const, url: '/telegram', icon: Send, subject: 'customers' }] : []),
   ].filter((item) => canAccess(item.subject))
   ].filter((item) => canAccess(item.subject))
 
 
   const workshopItems = [
   const workshopItems = [

+ 6 - 2
src/features/settings/Components/InvoiceLayoutEditor.tsx

@@ -75,6 +75,7 @@ interface InvoiceLayoutEditorProps {
   onChange: (config: InvoiceLayoutConfig) => void;
   onChange: (config: InvoiceLayoutConfig) => void;
   documentType: "invoice" | "quote";
   documentType: "invoice" | "quote";
   customFields?: FieldDef[];
   customFields?: FieldDef[];
+  hiddenSections?: Set<string>;
 }
 }
 
 
 // ---------------------------------------------------------------------------
 // ---------------------------------------------------------------------------
@@ -544,6 +545,7 @@ export function InvoiceLayoutEditor({
   onChange,
   onChange,
   documentType,
   documentType,
   customFields,
   customFields,
+  hiddenSections,
 }: InvoiceLayoutEditorProps) {
 }: InvoiceLayoutEditorProps) {
   const t = useTranslations('settings');
   const t = useTranslations('settings');
   const [expandedSections, setExpandedSections] = useState<Set<string>>(
   const [expandedSections, setExpandedSections] = useState<Set<string>>(
@@ -558,8 +560,10 @@ export function InvoiceLayoutEditor({
   );
   );
 
 
   const sortedSections = useMemo(
   const sortedSections = useMemo(
-    () => [...config.sections].sort((a, b) => a.order - b.order),
-    [config.sections],
+    () => [...config.sections]
+      .filter((s) => !hiddenSections?.has(s.id))
+      .sort((a, b) => a.order - b.order),
+    [config.sections, hiddenSections],
   );
   );
 
 
   const availableCustomFields = useMemo(() => {
   const availableCustomFields = useMemo(() => {

+ 17 - 1
src/features/settings/Components/InvoiceLayoutPreviewRenderer.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import { useMemo } from "react";
+import { useMemo, useState, useEffect } from "react";
 import { useMessages } from "next-intl";
 import { useMessages } from "next-intl";
 import { PDFViewer } from "@react-pdf/renderer";
 import { PDFViewer } from "@react-pdf/renderer";
 import "@/features/vehicles/Components/invoice-pdf/fonts.client";
 import "@/features/vehicles/Components/invoice-pdf/fonts.client";
@@ -241,6 +241,20 @@ export function InvoiceLayoutPreviewRenderer({
 
 
   const isQuote = documentType === "quote";
   const isQuote = documentType === "quote";
 
 
+  // Generate dummy Telegram QR for preview if section is visible
+  const telegramQrVisible = config.sections.some(
+    (s) => s.id === "telegram_qr" && s.visible
+  );
+  const [telegramQrDataUri, setTelegramQrDataUri] = useState<string | undefined>();
+  useEffect(() => {
+    if (!telegramQrVisible) { setTelegramQrDataUri(undefined); return; }
+    import("qrcode").then((QRCode) => {
+      QRCode.toDataURL("https://t.me/example_bot", { width: 200, margin: 1 }).then(
+        (uri) => setTelegramQrDataUri(uri)
+      );
+    });
+  }, [telegramQrVisible]);
+
   return (
   return (
     <div className="rounded-lg shadow-sm overflow-hidden bg-gray-100">
     <div className="rounded-lg shadow-sm overflow-hidden bg-gray-100">
       <PDFViewer
       <PDFViewer
@@ -268,6 +282,8 @@ export function InvoiceLayoutPreviewRenderer({
             invoiceSettings={DUMMY_INVOICE_SETTINGS}
             invoiceSettings={DUMMY_INVOICE_SETTINGS}
             logoDataUri={logoUrl || undefined}
             logoDataUri={logoUrl || undefined}
             template={templateConfig}
             template={templateConfig}
+            telegramQrDataUri={telegramQrDataUri}
+            telegramLabel="Chat with us on Telegram"
             labels={labels}
             labels={labels}
           />
           />
         )}
         )}

+ 3 - 1
src/features/settings/Schema/invoiceLayoutSchema.ts

@@ -65,6 +65,7 @@ export const BUILTIN_SECTIONS = [
   { id: "diagnostic_notes", name: "Diagnostic Notes" },
   { id: "diagnostic_notes", name: "Diagnostic Notes" },
   { id: "bank_account", name: "Bank Account" },
   { id: "bank_account", name: "Bank Account" },
   { id: "footer", name: "Footer" },
   { id: "footer", name: "Footer" },
+  { id: "telegram_qr", name: "Telegram QR" },
   { id: "general", name: "General" },
   { id: "general", name: "General" },
 ] as const;
 ] as const;
 
 
@@ -146,6 +147,7 @@ export const FULL_WIDTH_ONLY_SECTIONS = new Set<string>([
   "labor_table",
   "labor_table",
   "totals",
   "totals",
   "footer",
   "footer",
+  "telegram_qr",
 ]);
 ]);
 
 
 /** Default column assignment for column-eligible sections */
 /** Default column assignment for column-eligible sections */
@@ -185,7 +187,7 @@ export function getDefaultInvoiceLayout(): InvoiceLayoutConfig {
       const column = DEFAULT_COLUMN[s.id];
       const column = DEFAULT_COLUMN[s.id];
       return {
       return {
         id: s.id,
         id: s.id,
-        visible: s.id !== "general", // general hidden by default (no fields)
+        visible: s.id !== "general" && s.id !== "telegram_qr", // general and telegram_qr hidden by default
         order: index,
         order: index,
         ...(column ? { column } : {}),
         ...(column ? { column } : {}),
         ...(fields ? { fields } : {}),
         ...(fields ? { fields } : {}),

+ 7 - 0
src/features/settings/Schema/settingsSchema.ts

@@ -70,6 +70,13 @@ export const SETTING_KEYS = {
   SMS_TEMPLATE_STATUS_READY: "sms.template.statusReady",
   SMS_TEMPLATE_STATUS_READY: "sms.template.statusReady",
   SMS_TEMPLATE_STATUS_COMPLETED: "sms.template.statusCompleted",
   SMS_TEMPLATE_STATUS_COMPLETED: "sms.template.statusCompleted",
   SMS_TEMPLATE_PAYMENT_RECEIVED: "sms.template.paymentReceived",
   SMS_TEMPLATE_PAYMENT_RECEIVED: "sms.template.paymentReceived",
+  // Telegram templates
+  TELEGRAM_TEMPLATE_INVOICE_READY: "telegram.template.invoiceReady",
+  TELEGRAM_TEMPLATE_QUOTE_READY: "telegram.template.quoteReady",
+  TELEGRAM_TEMPLATE_STATUS_IN_PROGRESS: "telegram.template.statusInProgress",
+  TELEGRAM_TEMPLATE_STATUS_COMPLETED: "telegram.template.statusCompleted",
+  TELEGRAM_TEMPLATE_PAYMENT_RECEIVED: "telegram.template.paymentReceived",
+  TELEGRAM_SHOW_QR_ON_INVOICE: "telegram.showQrOnInvoice",
   PORTAL_ENABLED: "portal.enabled",
   PORTAL_ENABLED: "portal.enabled",
   WORKBOARD_WEEK_START_DAY: "workboard.weekStartDay",
   WORKBOARD_WEEK_START_DAY: "workboard.weekStartDay",
   WORKBOARD_WORK_DAY_START: "workboard.workDayStart",
   WORKBOARD_WORK_DAY_START: "workboard.workDayStart",

+ 184 - 0
src/features/telegram/Actions/telegramActions.ts

@@ -0,0 +1,184 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { sendTelegramMessage } from "@/lib/telegram";
+import { requireFeature } from "@/lib/features";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function sendTelegramToCustomer(input: {
+  customerId: string;
+  body: string;
+  relatedEntityType?: string;
+  relatedEntityId?: string;
+}) {
+  return withAuth(
+    async ({ organizationId }) => {
+      await requireFeature(organizationId, "telegram");
+
+      const customer = await db.customer.findFirst({
+        where: { id: input.customerId, organizationId },
+        select: { id: true, telegramChatId: true, name: true },
+      });
+
+      if (!customer) throw new Error("Customer not found");
+      if (!customer.telegramChatId) {
+        throw new Error("Customer has no Telegram chat linked");
+      }
+
+      // Create the message record first
+      const message = await db.telegramMessage.create({
+        data: {
+          direction: "outbound",
+          chatId: customer.telegramChatId,
+          body: input.body,
+          status: "queued",
+          organizationId,
+          customerId: customer.id,
+          relatedEntityType: input.relatedEntityType,
+          relatedEntityId: input.relatedEntityId,
+        },
+      });
+
+      try {
+        const result = await sendTelegramMessage(organizationId, {
+          chatId: customer.telegramChatId,
+          text: input.body,
+        });
+
+        await db.telegramMessage.update({
+          where: { id: message.id },
+          data: {
+            status: "sent",
+            telegramMessageId: String(result.messageId),
+          },
+        });
+
+        return {
+          id: message.id,
+          status: "sent" as const,
+          customerName: customer.name,
+        };
+      } catch (error) {
+        const errorMessage =
+          error instanceof Error ? error.message : "Unknown error";
+
+        await db.telegramMessage.update({
+          where: { id: message.id },
+          data: { status: "failed", errorMessage },
+        });
+
+        throw new Error(`Failed to send Telegram message: ${errorMessage}`);
+      }
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.CUSTOMERS,
+        },
+      ],
+      audit: ({ result }) => ({
+        action: "telegram.send",
+        entity: "TelegramMessage",
+        entityId: result.id,
+        message: `Sent Telegram message to ${result.customerName}`,
+        metadata: { messageId: result.id },
+      }),
+    },
+  );
+}
+
+export async function getTelegramConversation(
+  customerId: string,
+  cursor?: string,
+  limit: number = 50,
+) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const messages = await db.telegramMessage.findMany({
+        where: {
+          organizationId,
+          customerId,
+        },
+        orderBy: { createdAt: "desc" },
+        take: limit + 1,
+        ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
+        select: {
+          id: true,
+          direction: true,
+          body: true,
+          status: true,
+          createdAt: true,
+          chatId: true,
+        },
+      });
+
+      const hasMore = messages.length > limit;
+      const items = hasMore ? messages.slice(0, limit) : messages;
+
+      return {
+        messages: items.reverse(),
+        nextCursor: hasMore ? items[items.length - 1]?.id : null,
+      };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.READ,
+          subject: PermissionSubject.CUSTOMERS,
+        },
+      ],
+    },
+  );
+}
+
+export async function deleteTelegramMessage(messageId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const message = await db.telegramMessage.findFirst({
+        where: { id: messageId, organizationId },
+      });
+      if (!message) throw new Error("Message not found");
+
+      await db.telegramMessage.delete({ where: { id: messageId } });
+      return { deleted: true };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.DELETE,
+          subject: PermissionSubject.CUSTOMERS,
+        },
+      ],
+    },
+  );
+}
+
+export async function deleteTelegramConversation(customerId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const customer = await db.customer.findFirst({
+        where: { id: customerId, organizationId },
+        select: { id: true },
+      });
+      if (!customer) throw new Error("Customer not found");
+
+      const { count } = await db.telegramMessage.deleteMany({
+        where: { organizationId, customerId },
+      });
+
+      return { deleted: count };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.DELETE,
+          subject: PermissionSubject.CUSTOMERS,
+        },
+      ],
+    },
+  );
+}
+
+// getRecentTelegramThreads is in telegramThreadActions.ts to keep this file under 200 lines

+ 165 - 0
src/features/telegram/Actions/telegramSettingsActions.ts

@@ -0,0 +1,165 @@
+"use server";
+
+import crypto from "crypto";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { revalidatePath } from "next/cache";
+import {
+  ALL_ORG_TELEGRAM_KEYS,
+  ORG_TELEGRAM_KEYS,
+} from "../Schema/telegramSettingsSchema";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import {
+  getTelegramBotInfo,
+  setTelegramWebhook,
+  deleteTelegramWebhook,
+  sendTelegramMessage,
+} from "@/lib/telegram";
+import { requireFeature } from "@/lib/features";
+
+export async function getTelegramSettings() {
+  return withAuth(
+    async ({ organizationId }) => {
+      const settings = await db.appSetting.findMany({
+        where: { organizationId, key: { in: ALL_ORG_TELEGRAM_KEYS } },
+      });
+      const map: Record<string, string> = {};
+      for (const s of settings) {
+        map[s.key] = s.value;
+      }
+      return map;
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.SETTINGS },
+      ],
+    },
+  );
+}
+
+export async function setTelegramSettings(settings: { botToken: string }) {
+  return withAuth(
+    async ({ userId, organizationId }) => {
+      await requireFeature(organizationId, "telegram");
+
+      // Validate bot token by calling getMe
+      const botInfo = await getTelegramBotInfo(settings.botToken);
+
+      // Generate webhook secret
+      const webhookSecret = crypto.randomUUID();
+
+      // Build webhook URL
+      const appUrl =
+        process.env.NEXT_PUBLIC_APP_URL || process.env.VERCEL_URL || "";
+      const webhookUrl = `${appUrl}/api/webhooks/telegram/${organizationId}?secret=${webhookSecret}`;
+
+      // Register webhook with Telegram
+      await setTelegramWebhook(settings.botToken, webhookUrl, webhookSecret);
+
+      // Save all settings via upsert (auto-enable when connecting)
+      const entries: Record<string, string> = {
+        [ORG_TELEGRAM_KEYS.TELEGRAM_ENABLED]: "true",
+        [ORG_TELEGRAM_KEYS.TELEGRAM_BOT_TOKEN]: settings.botToken,
+        [ORG_TELEGRAM_KEYS.TELEGRAM_BOT_USERNAME]: botInfo.username,
+        [ORG_TELEGRAM_KEYS.TELEGRAM_WEBHOOK_SECRET]: webhookSecret,
+      };
+
+      await db.$transaction(
+        Object.entries(entries).map(([key, value]) =>
+          db.appSetting.upsert({
+            where: { organizationId_key: { organizationId, key } },
+            update: { value },
+            create: { userId, organizationId, key, value },
+          }),
+        ),
+      );
+
+      revalidatePath("/settings/telegram");
+      return { botUsername: botInfo.username };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.SETTINGS,
+        },
+      ],
+    },
+  );
+}
+
+export async function testTelegramSend(input: {
+  chatId: string;
+  message: string;
+}) {
+  return withAuth(
+    async ({ organizationId }) => {
+      await requireFeature(organizationId, "telegram");
+
+      if (!input.chatId?.trim()) {
+        throw new Error("Please enter a chat ID to send the test message to");
+      }
+
+      await sendTelegramMessage(organizationId, {
+        chatId: input.chatId.trim(),
+        text:
+          input.message?.trim() ||
+          "Test message from Torqvoice -- your Telegram bot is configured correctly.",
+      });
+
+      return { sentTo: input.chatId.trim() };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.SETTINGS,
+        },
+      ],
+    },
+  );
+}
+
+export async function disconnectTelegram() {
+  return withAuth(
+    async ({ organizationId }) => {
+      // Get current bot token to delete webhook
+      const tokenSetting = await db.appSetting.findUnique({
+        where: {
+          organizationId_key: {
+            organizationId,
+            key: ORG_TELEGRAM_KEYS.TELEGRAM_BOT_TOKEN,
+          },
+        },
+      });
+
+      if (tokenSetting?.value) {
+        try {
+          await deleteTelegramWebhook(tokenSetting.value);
+        } catch (error) {
+          // Log but continue — we still want to remove local settings
+          console.error("[disconnectTelegram] Failed to delete webhook:", error);
+        }
+      }
+
+      // Remove all telegram settings
+      await db.appSetting.deleteMany({
+        where: {
+          organizationId,
+          key: { in: ALL_ORG_TELEGRAM_KEYS },
+        },
+      });
+
+      revalidatePath("/settings/telegram");
+      return { disconnected: true };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.UPDATE,
+          subject: PermissionSubject.SETTINGS,
+        },
+      ],
+    },
+  );
+}

+ 71 - 0
src/features/telegram/Actions/telegramThreadActions.ts

@@ -0,0 +1,71 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getRecentTelegramThreads(
+  offset: number = 0,
+  limit: number = 50,
+) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const customers = await db.customer.findMany({
+        where: {
+          organizationId,
+          telegramMessages: { some: {} },
+        },
+        select: {
+          id: true,
+          name: true,
+          telegramChatId: true,
+          telegramMessages: {
+            orderBy: { createdAt: "desc" },
+            take: 1,
+            select: {
+              id: true,
+              body: true,
+              direction: true,
+              status: true,
+              createdAt: true,
+            },
+          },
+        },
+        orderBy: {
+          telegramMessages: { _count: "desc" },
+        },
+        skip: offset,
+        take: limit + 1,
+      });
+
+      const filtered = customers.filter(
+        (c) => c.telegramMessages.length > 0,
+      );
+      const hasMore = filtered.length > limit;
+      const items = hasMore ? filtered.slice(0, limit) : filtered;
+
+      const threads = items
+        .map((c) => ({
+          customerId: c.id,
+          customerName: c.name,
+          telegramChatId: c.telegramChatId,
+          lastMessage: c.telegramMessages[0],
+        }))
+        .sort(
+          (a, b) =>
+            new Date(b.lastMessage.createdAt).getTime() -
+            new Date(a.lastMessage.createdAt).getTime(),
+        );
+
+      return { threads, hasMore };
+    },
+    {
+      requiredPermissions: [
+        {
+          action: PermissionAction.READ,
+          subject: PermissionSubject.CUSTOMERS,
+        },
+      ],
+    },
+  );
+}

+ 122 - 0
src/features/telegram/Components/SendTelegramDialog.tsx

@@ -0,0 +1,122 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Loader2, Send } from "lucide-react";
+import { useGlassModal } from "@/components/glass-modal";
+import { sendTelegramToCustomer } from "../Actions/telegramActions";
+
+interface SendTelegramDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  customerId: string;
+  customerName: string;
+  entityLabel?: string;
+  defaultMessage?: string;
+  relatedEntityType?: string;
+  relatedEntityId?: string;
+}
+
+export function SendTelegramDialog({
+  open,
+  onOpenChange,
+  customerId,
+  customerName,
+  entityLabel = "Telegram",
+  defaultMessage = "",
+  relatedEntityType,
+  relatedEntityId,
+}: SendTelegramDialogProps) {
+  const modal = useGlassModal();
+  const t = useTranslations("telegramMessages.sendDialog");
+  const tc = useTranslations("common.buttons");
+  const te = useTranslations("common.errors");
+  const [sending, setSending] = useState(false);
+  const [message, setMessage] = useState(defaultMessage);
+
+  const handleSend = async (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!message.trim()) return;
+    setSending(true);
+
+    const result = await sendTelegramToCustomer({
+      customerId,
+      body: message.trim(),
+      relatedEntityType,
+      relatedEntityId,
+    });
+
+    if (result.success) {
+      modal.open(
+        "success",
+        t("messageSent"),
+        t("sentTo", { name: customerName }),
+      );
+      onOpenChange(false);
+      setMessage("");
+    } else {
+      modal.open("error", te("error"), result.error || t("sendError"));
+    }
+    setSending(false);
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-md">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <Send className="h-5 w-5" />
+            {t("send", { label: entityLabel })}
+          </DialogTitle>
+        </DialogHeader>
+        <form onSubmit={handleSend} className="space-y-4">
+          <div className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
+            <span className="text-sm font-medium">{customerName}</span>
+            <span className="text-sm text-muted-foreground">Telegram</span>
+          </div>
+          <div className="space-y-2">
+            <Label htmlFor="telegram-message">{t("message")}</Label>
+            <Textarea
+              id="telegram-message"
+              placeholder={t("placeholder")}
+              value={message}
+              onChange={(e) => setMessage(e.target.value)}
+              rows={4}
+              maxLength={4096}
+              required
+            />
+            <p className="text-xs text-muted-foreground">
+              {t("characters", { count: message.length })}
+            </p>
+          </div>
+          <div className="flex justify-end gap-2">
+            <Button
+              type="button"
+              variant="ghost"
+              onClick={() => onOpenChange(false)}
+            >
+              {tc("cancel")}
+            </Button>
+            <Button type="submit" disabled={sending || !message.trim()}>
+              {sending ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Send className="mr-2 h-4 w-4" />
+              )}
+              {t("sendTelegram")}
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 200 - 0
src/features/telegram/Components/TelegramConversation.tsx

@@ -0,0 +1,200 @@
+"use client";
+
+import { useState, useTransition, useRef, useEffect, useCallback } from "react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { Loader2, Send, ChevronUp } from "lucide-react";
+import { cn } from "@/lib/utils";
+import {
+  sendTelegramToCustomer,
+  getTelegramConversation,
+  deleteTelegramMessage,
+} from "../Actions/telegramActions";
+import { useNotificationStore } from "@/features/notifications/store/notificationStore";
+import { toast } from "sonner";
+import type { TelegramMessage } from "./TelegramMessagesClient";
+import { TelegramMessageBubble } from "./TelegramMessageBubble";
+
+interface TelegramConversationProps {
+  customerId: string;
+  customerName: string;
+  telegramChatId: string | null;
+  initialMessages: TelegramMessage[];
+  initialNextCursor: string | null;
+  className?: string;
+}
+
+export function TelegramConversation({
+  customerId,
+  customerName,
+  telegramChatId,
+  initialMessages,
+  initialNextCursor,
+  className,
+}: TelegramConversationProps) {
+  const t = useTranslations("telegramMessages.conversation");
+  const tc = useTranslations("common.buttons");
+  const [messages, setMessages] = useState<TelegramMessage[]>(initialMessages);
+  const [nextCursor, setNextCursor] = useState<string | null>(initialNextCursor);
+  const [newMessage, setNewMessage] = useState("");
+  const [mounted, setMounted] = useState(false);
+  const [deleteTarget, setDeleteTarget] = useState<TelegramMessage | null>(null);
+  const [isDeleting, setIsDeleting] = useState(false);
+  const [isSending, startSend] = useTransition();
+  const [isLoadingMore, startLoadMore] = useTransition();
+  const scrollAreaRef = useRef<HTMLDivElement>(null);
+  const bottomRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => { setMounted(true); }, []);
+
+  const initialScrollDone = useRef(false);
+  useEffect(() => {
+    const el = scrollAreaRef.current;
+    if (!el) return;
+    if (!initialScrollDone.current) {
+      initialScrollDone.current = true;
+      requestAnimationFrame(() => {
+        requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; });
+      });
+    } else {
+      el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
+    }
+  }, [messages.length]);
+
+  const refreshConversation = useCallback(async () => {
+    const conv = await getTelegramConversation(customerId);
+    if (conv.success && conv.data) {
+      setMessages(conv.data.messages.map((m: TelegramMessage) => ({
+        ...m,
+        createdAt: typeof m.createdAt === "string" ? m.createdAt : new Date(m.createdAt).toISOString(),
+      })));
+      setNextCursor(conv.data.nextCursor);
+    }
+  }, [customerId]);
+
+  useEffect(() => {
+    let lastCount = useNotificationStore.getState().notifications.length;
+    const unsub = useNotificationStore.subscribe((state) => {
+      const { notifications } = state;
+      if (notifications.length <= lastCount) { lastCount = notifications.length; return; }
+      lastCount = notifications.length;
+      const latest = notifications[0];
+      if (latest?.type === "telegram_inbound" && latest.entityUrl === `/telegram?customerId=${customerId}`) {
+        refreshConversation();
+      }
+    });
+    return unsub;
+  }, [customerId, refreshConversation]);
+
+  const handleSend = () => {
+    if (!newMessage.trim() || !telegramChatId) return;
+    const body = newMessage.trim();
+    setNewMessage("");
+    startSend(async () => {
+      const result = await sendTelegramToCustomer({ customerId, body });
+      if (result.success) { await refreshConversation(); }
+      else { toast.error(result.error ?? t("sendError")); setNewMessage(body); }
+    });
+  };
+
+  const handleLoadOlder = () => {
+    if (!nextCursor) return;
+    startLoadMore(async () => {
+      const result = await getTelegramConversation(customerId, nextCursor);
+      if (result.success && result.data) {
+        const older = result.data.messages.map((m: TelegramMessage) => ({
+          ...m,
+          createdAt: typeof m.createdAt === "string" ? m.createdAt : new Date(m.createdAt).toISOString(),
+        }));
+        setMessages((prev) => [...older, ...prev]);
+        setNextCursor(result.data.nextCursor);
+      }
+    });
+  };
+
+  const handleDeleteMessage = async () => {
+    if (!deleteTarget) return;
+    setIsDeleting(true);
+    const result = await deleteTelegramMessage(deleteTarget.id);
+    if (result.success) {
+      setMessages((prev) => prev.filter((m) => m.id !== deleteTarget.id));
+      toast.success(t("messageDeleted"));
+    } else {
+      toast.error(result.error || t("deleteError"));
+    }
+    setIsDeleting(false);
+    setDeleteTarget(null);
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); }
+  };
+
+  if (!telegramChatId) {
+    return (
+      <div className="flex flex-col items-center justify-center py-12">
+        <Send className="mb-3 h-10 w-10 text-muted-foreground/40" />
+        <p className="text-sm text-muted-foreground">{t("notLinked")}</p>
+      </div>
+    );
+  }
+
+  return (
+    <div className={cn("flex h-[500px] flex-col", className)}>
+      <div ref={scrollAreaRef} className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
+        {nextCursor && (
+          <div className="flex justify-center">
+            <Button variant="ghost" size="sm" onClick={handleLoadOlder} disabled={isLoadingMore}>
+              {isLoadingMore ? <Loader2 className="mr-1 h-3 w-3 animate-spin" /> : <ChevronUp className="mr-1 h-3 w-3" />}
+              {t("loadOlder")}
+            </Button>
+          </div>
+        )}
+        {messages.length === 0 && (
+          <div className="flex flex-col items-center justify-center py-12">
+            <Send className="mb-3 h-10 w-10 text-muted-foreground/40" />
+            <p className="text-sm text-muted-foreground">{t("empty", { name: customerName })}</p>
+          </div>
+        )}
+        {messages.map((msg) => (
+          <TelegramMessageBubble key={msg.id} msg={msg} mounted={mounted} deleteLabel={t("deleteMessage")} onDelete={setDeleteTarget} />
+        ))}
+        <div ref={bottomRef} />
+      </div>
+      <div className="shrink-0 border-t bg-background p-4">
+        <div className="flex items-end gap-2">
+          <Textarea placeholder={t("placeholder", { name: customerName })} value={newMessage} onChange={(e) => setNewMessage(e.target.value)} onKeyDown={handleKeyDown} rows={2} className="resize-none" />
+          <Button onClick={handleSend} disabled={isSending || !newMessage.trim()} size="icon">
+            {isSending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
+          </Button>
+        </div>
+      </div>
+      <AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>{t("deleteTitle")}</AlertDialogTitle>
+            <AlertDialogDescription>{t("deleteDescription")}</AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel disabled={isDeleting}>{tc("cancel")}</AlertDialogCancel>
+            <AlertDialogAction onClick={handleDeleteMessage} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
+              {isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {tc("delete")}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  );
+}

+ 118 - 0
src/features/telegram/Components/TelegramConversationPanel.tsx

@@ -0,0 +1,118 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { ArrowLeft, MoreVertical, Send, Trash2, User } from "lucide-react";
+import { TelegramConversation } from "./TelegramConversation";
+import type { TelegramThread, TelegramMessage } from "./TelegramMessagesClient";
+
+export function TelegramConversationPanel({
+  selectedCustomerId,
+  conversation,
+  loadingConversation,
+  threads,
+  onBack,
+  onDeleteThread,
+  t,
+}: {
+  selectedCustomerId: string | null;
+  conversation: {
+    messages: TelegramMessage[];
+    nextCursor: string | null;
+    customerName: string;
+    telegramChatId: string | null;
+  } | null;
+  loadingConversation: boolean;
+  threads: TelegramThread[];
+  onBack: () => void;
+  onDeleteThread: (thread: TelegramThread) => void;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  t: any;
+}) {
+  return (
+    <div
+      className={cn(
+        "flex min-w-0 flex-1 flex-col",
+        selectedCustomerId ? "flex" : "hidden sm:flex",
+      )}
+    >
+      {selectedCustomerId && conversation && !loadingConversation ? (
+        <>
+          <div className="flex shrink-0 items-center gap-3 border-b px-4 py-3">
+            <Button
+              variant="ghost"
+              size="icon"
+              className="h-8 w-8 shrink-0 sm:hidden"
+              onClick={onBack}
+            >
+              <ArrowLeft className="h-4 w-4" />
+            </Button>
+            <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
+              <User className="h-4 w-4" />
+            </div>
+            <div className="min-w-0 flex-1">
+              <p className="truncate text-sm font-semibold">
+                {conversation.customerName}
+              </p>
+              {conversation.telegramChatId && (
+                <p className="flex items-center gap-1 text-xs text-muted-foreground">
+                  <Send className="h-3 w-3" />
+                  Telegram
+                </p>
+              )}
+            </div>
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Button
+                  variant="ghost"
+                  size="icon"
+                  className="h-8 w-8 shrink-0"
+                >
+                  <MoreVertical className="h-4 w-4" />
+                </Button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="end">
+                <DropdownMenuItem
+                  className="text-destructive"
+                  onClick={() => {
+                    const thread = threads.find(
+                      (th) => th.customerId === selectedCustomerId,
+                    );
+                    if (thread) onDeleteThread(thread);
+                  }}
+                >
+                  <Trash2 className="mr-2 h-4 w-4" />
+                  {t("deleteConversation")}
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </div>
+          <TelegramConversation
+            key={selectedCustomerId}
+            customerId={selectedCustomerId}
+            customerName={conversation.customerName}
+            telegramChatId={conversation.telegramChatId}
+            initialMessages={conversation.messages}
+            initialNextCursor={conversation.nextCursor}
+            className="h-auto min-h-0 flex-1"
+          />
+        </>
+      ) : loadingConversation ? (
+        <div className="flex flex-1 items-center justify-center">
+          <div className="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
+        </div>
+      ) : (
+        <div className="flex flex-1 flex-col items-center justify-center text-muted-foreground">
+          <Send className="mb-3 h-10 w-10 text-muted-foreground/40" />
+          <p className="text-sm">{t("selectConversation")}</p>
+        </div>
+      )}
+    </div>
+  );
+}

+ 72 - 0
src/features/telegram/Components/TelegramDisconnectButton.tsx

@@ -0,0 +1,72 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { Button } from "@/components/ui/button";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { Loader2 } from "lucide-react";
+import { disconnectTelegram } from "../Actions/telegramSettingsActions";
+
+export function TelegramDisconnectButton() {
+  const t = useTranslations("telegram");
+  const tc = useTranslations("common.buttons");
+  const router = useRouter();
+  const [isDisconnecting, setIsDisconnecting] = useState(false);
+
+  const handleDisconnect = async () => {
+    setIsDisconnecting(true);
+    const result = await disconnectTelegram();
+    if (result.success) {
+      toast.success(t("disconnect.success"));
+      router.refresh();
+    } else {
+      toast.error(result.error ?? t("disconnect.error"));
+    }
+    setIsDisconnecting(false);
+  };
+
+  return (
+    <AlertDialog>
+      <AlertDialogTrigger asChild>
+        <Button type="button" variant="destructive">
+          {t("disconnect.button")}
+        </Button>
+      </AlertDialogTrigger>
+      <AlertDialogContent>
+        <AlertDialogHeader>
+          <AlertDialogTitle>{t("disconnect.title")}</AlertDialogTitle>
+          <AlertDialogDescription>
+            {t("disconnect.description")}
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel disabled={isDisconnecting}>
+            {tc("cancel")}
+          </AlertDialogCancel>
+          <AlertDialogAction
+            onClick={handleDisconnect}
+            disabled={isDisconnecting}
+            className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+          >
+            {isDisconnecting && (
+              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+            )}
+            {t("disconnect.button")}
+          </AlertDialogAction>
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  );
+}

+ 76 - 0
src/features/telegram/Components/TelegramMessageBubble.tsx

@@ -0,0 +1,76 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import { Trash2 } from "lucide-react";
+import type { TelegramMessage } from "./TelegramMessagesClient";
+
+export function TelegramMessageBubble({
+  msg,
+  mounted,
+  deleteLabel,
+  onDelete,
+}: {
+  msg: TelegramMessage;
+  mounted: boolean;
+  deleteLabel: string;
+  onDelete: (msg: TelegramMessage) => void;
+}) {
+  const isOutbound = msg.direction === "outbound";
+  const time = mounted
+    ? new Date(msg.createdAt).toLocaleTimeString([], {
+        hour: "2-digit",
+        minute: "2-digit",
+      })
+    : "";
+
+  return (
+    <div
+      className={cn(
+        "group flex",
+        isOutbound ? "justify-end" : "justify-start",
+      )}
+    >
+      {isOutbound && (
+        <button
+          type="button"
+          onClick={() => onDelete(msg)}
+          className="mr-2 self-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100"
+          title={deleteLabel}
+        >
+          <Trash2 className="h-3.5 w-3.5" />
+        </button>
+      )}
+      <div
+        className={cn(
+          "max-w-[75%] rounded-2xl px-4 py-2",
+          isOutbound
+            ? "bg-primary text-primary-foreground"
+            : "bg-muted",
+        )}
+      >
+        <p className="whitespace-pre-wrap text-sm">{msg.body}</p>
+        <div
+          className={cn(
+            "mt-1 flex items-center gap-1.5 text-[10px]",
+            isOutbound
+              ? "justify-end text-primary-foreground/70"
+              : "text-muted-foreground",
+          )}
+        >
+          <span>{time}</span>
+          {isOutbound && <span className="capitalize">{msg.status}</span>}
+        </div>
+      </div>
+      {!isOutbound && (
+        <button
+          type="button"
+          onClick={() => onDelete(msg)}
+          className="ml-2 self-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive group-hover:opacity-100"
+          title={deleteLabel}
+        >
+          <Trash2 className="h-3.5 w-3.5" />
+        </button>
+      )}
+    </div>
+  );
+}

+ 160 - 0
src/features/telegram/Components/TelegramMessagesClient.tsx

@@ -0,0 +1,160 @@
+"use client";
+
+import { useState, useEffect, useCallback, useTransition, useRef } from "react";
+import { useSearchParams, useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import { getTelegramConversation, deleteTelegramConversation } from "../Actions/telegramActions";
+import { getRecentTelegramThreads } from "../Actions/telegramThreadActions";
+import { TelegramThreadList } from "./TelegramThreadList";
+import { TelegramConversationPanel } from "./TelegramConversationPanel";
+import { useNotificationStore } from "@/features/notifications/store/notificationStore";
+
+export interface TelegramThread {
+  customerId: string;
+  customerName: string;
+  telegramChatId: string | null;
+  lastMessage: { id: string; body: string; direction: string; createdAt: string | Date };
+}
+
+export interface TelegramMessage {
+  id: string;
+  direction: string;
+  body: string;
+  status: string;
+  createdAt: string | Date;
+  chatId: string;
+}
+
+const PAGE_SIZE = 50;
+
+function formatRelativeTime(date: string | Date) {
+  const diffMs = Date.now() - new Date(date).getTime();
+  const mins = Math.floor(diffMs / 60000);
+  if (mins < 1) return "now";
+  if (mins < 60) return `${mins}m`;
+  const hrs = Math.floor(diffMs / 3600000);
+  if (hrs < 24) return `${hrs}h`;
+  const days = Math.floor(diffMs / 86400000);
+  if (days < 7) return `${days}d`;
+  return new Date(date).toLocaleDateString([], { month: "short", day: "numeric" });
+}
+
+export function TelegramMessagesClient({ initialThreads, initialHasMore = false }: { initialThreads: TelegramThread[]; initialHasMore?: boolean }) {
+  const searchParams = useSearchParams();
+  const router = useRouter();
+  const t = useTranslations("telegramMessages.threads");
+  const tc = useTranslations("common.buttons");
+  const [threads, setThreads] = useState<TelegramThread[]>(initialThreads);
+  const [hasMore, setHasMore] = useState(initialHasMore);
+  const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>(searchParams.get("customerId"));
+  const [conversation, setConversation] = useState<{ messages: TelegramMessage[]; nextCursor: string | null; customerName: string; telegramChatId: string | null } | null>(null);
+  const [loadingConversation, setLoadingConversation] = useState(false);
+  const [isLoadingMore, startLoadMore] = useTransition();
+  const [deleteTarget, setDeleteTarget] = useState<TelegramThread | null>(null);
+  const [isDeleting, setIsDeleting] = useState(false);
+
+  const normalizeMessages = (msgs: TelegramMessage[]) =>
+    msgs.map((m) => ({ ...m, createdAt: typeof m.createdAt === "string" ? m.createdAt : new Date(m.createdAt).toISOString() }));
+
+  const loadConversation = useCallback(async (cid: string) => {
+    setLoadingConversation(true);
+    const result = await getTelegramConversation(cid);
+    if (result.success && result.data) {
+      const thread = threads.find((th) => th.customerId === cid);
+      setConversation({ messages: normalizeMessages(result.data.messages), nextCursor: result.data.nextCursor, customerName: thread?.customerName || "", telegramChatId: thread?.telegramChatId || null });
+    }
+    setLoadingConversation(false);
+  }, [threads]);
+
+  const initialLoadDone = useRef(false);
+  useEffect(() => {
+    if (initialLoadDone.current) return;
+    initialLoadDone.current = true;
+    const cid = searchParams.get("customerId");
+    if (cid) loadConversation(cid);
+  }, [searchParams, loadConversation]);
+
+  const updateUrl = (key: string, value: string | null) => {
+    const params = new URLSearchParams(searchParams.toString());
+    if (value) params.set(key, value); else params.delete(key);
+    router.replace(`?${params.toString()}`, { scroll: false });
+  };
+
+  const handleSelectThread = (cid: string) => {
+    setSelectedCustomerId(cid);
+    loadConversation(cid);
+    updateUrl("customerId", cid);
+  };
+
+  const handleBack = () => {
+    setSelectedCustomerId(null);
+    setConversation(null);
+    updateUrl("customerId", null);
+  };
+
+  const handleLoadMore = () => {
+    startLoadMore(async () => {
+      const result = await getRecentTelegramThreads(threads.length, PAGE_SIZE);
+      if (result.success && result.data) {
+        setThreads((prev) => [...prev, ...result.data!.threads]);
+        setHasMore(result.data.hasMore);
+      }
+    });
+  };
+
+  const refreshThreads = useCallback(async () => {
+    const result = await getRecentTelegramThreads(0, threads.length || PAGE_SIZE);
+    if (result.success && result.data) {
+      setThreads(result.data.threads);
+      setHasMore(result.data.hasMore);
+    }
+  }, [threads.length]);
+
+  const handleDeleteConversation = async () => {
+    if (!deleteTarget) return;
+    setIsDeleting(true);
+    const result = await deleteTelegramConversation(deleteTarget.customerId);
+    if (result.success) {
+      toast.success(t("deleted"));
+      setThreads((prev) => prev.filter((th) => th.customerId !== deleteTarget.customerId));
+      if (selectedCustomerId === deleteTarget.customerId) { setSelectedCustomerId(null); setConversation(null); updateUrl("customerId", null); }
+    } else { toast.error(result.error || t("deleteError")); }
+    setIsDeleting(false);
+    setDeleteTarget(null);
+  };
+
+  useEffect(() => {
+    let lastCount = useNotificationStore.getState().notifications.length;
+    const unsub = useNotificationStore.subscribe((state) => {
+      if (state.notifications.length <= lastCount) { lastCount = state.notifications.length; return; }
+      lastCount = state.notifications.length;
+      if (state.notifications[0]?.type === "telegram_inbound") refreshThreads();
+    });
+    return unsub;
+  }, [refreshThreads]);
+
+  return (
+    <div className="flex h-[calc(100vh-8rem)] overflow-hidden rounded-lg border bg-background">
+      <TelegramThreadList threads={threads} hasMore={hasMore} selectedCustomerId={selectedCustomerId} isLoadingMore={isLoadingMore} onSelectThread={handleSelectThread} onLoadMore={handleLoadMore} formatRelativeTime={formatRelativeTime} t={t} />
+      <TelegramConversationPanel selectedCustomerId={selectedCustomerId} conversation={conversation} loadingConversation={loadingConversation} threads={threads} onBack={handleBack} onDeleteThread={setDeleteTarget} t={t} />
+      <AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>{t("deleteTitle")}</AlertDialogTitle>
+            <AlertDialogDescription>{t("deleteDescription", { name: deleteTarget?.customerName ?? "" })}</AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel disabled={isDeleting}>{tc("cancel")}</AlertDialogCancel>
+            <AlertDialogAction onClick={handleDeleteConversation} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
+              {isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {tc("delete")}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  );
+}

+ 79 - 0
src/features/telegram/Components/TelegramQrCode.tsx

@@ -0,0 +1,79 @@
+"use client";
+
+import { useState } from "react";
+import { QRCodeSVG } from "qrcode.react";
+import { useTranslations } from "next-intl";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Check, Copy, Mail, Send } from "lucide-react";
+import { toast } from "sonner";
+
+export function TelegramQrCode({
+  botUsername,
+  customerId,
+  customerName,
+  customerEmail,
+}: {
+  botUsername: string;
+  customerId: string;
+  customerName: string;
+  customerEmail?: string | null;
+}) {
+  const t = useTranslations("telegram.qr");
+  const [open, setOpen] = useState(false);
+  const [copied, setCopied] = useState(false);
+
+  const deepLink = `https://t.me/${botUsername}?start=${customerId}`;
+
+  const handleCopy = () => {
+    navigator.clipboard.writeText(deepLink);
+    setCopied(true);
+    toast.success(t("copied"));
+    setTimeout(() => setCopied(false), 2000);
+  };
+
+  return (
+    <>
+      <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
+        <Send className="mr-1.5 h-3.5 w-3.5" />
+        {t("title")}
+      </Button>
+
+      <Dialog open={open} onOpenChange={setOpen}>
+        <DialogContent className="sm:max-w-sm">
+          <DialogHeader>
+            <DialogTitle className="text-center">{t("title")}</DialogTitle>
+          </DialogHeader>
+          <div className="flex flex-col items-center gap-4 py-4">
+            <div className="rounded-xl bg-white p-4">
+              <QRCodeSVG value={deepLink} size={200} />
+            </div>
+            <p className="text-center text-sm text-muted-foreground">
+              {t("description", { name: customerName })}
+            </p>
+            <div className="flex w-full gap-2">
+              <Button variant="outline" onClick={handleCopy} className="flex-1">
+                {copied ? <Check className="mr-2 h-4 w-4" /> : <Copy className="mr-2 h-4 w-4" />}
+                {copied ? t("copied") : t("copyLink")}
+              </Button>
+              {customerEmail && (
+                <Button
+                  variant="outline"
+                  className="flex-1"
+                  onClick={() => {
+                    const subject = encodeURIComponent(t("emailSubject"));
+                    const body = encodeURIComponent(t("emailBody", { name: customerName, link: deepLink }));
+                    window.open(`mailto:${customerEmail}?subject=${subject}&body=${body}`);
+                  }}
+                >
+                  <Mail className="mr-2 h-4 w-4" />
+                  {t("sendEmail")}
+                </Button>
+              )}
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </>
+  );
+}

+ 164 - 0
src/features/telegram/Components/TelegramSettingsForm.tsx

@@ -0,0 +1,164 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Switch } from "@/components/ui/switch";
+import { ExternalLink, Info, Loader2, Copy, Check, Eye, EyeOff } from "lucide-react";
+import { setTelegramSettings } from "../Actions/telegramSettingsActions";
+import { setSettings } from "@/features/settings/Actions/settingsActions";
+import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "@/app/(authenticated)/settings/read-only-guard";
+import { TelegramDisconnectButton } from "./TelegramDisconnectButton";
+import { TelegramTestMessage } from "./TelegramTestMessage";
+
+export function TelegramSettingsForm({
+  initial,
+  appUrl,
+  initialEnabled = false,
+}: {
+  initial: Record<string, string>;
+  appUrl: string;
+  initialEnabled?: boolean;
+}) {
+  const t = useTranslations("telegram");
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+  const [showToken, setShowToken] = useState(false);
+  const [enabled, setEnabled] = useState(initialEnabled);
+  const [botToken, setBotToken] = useState(initial["telegram.botToken"] || "");
+  const botUsername = initial["telegram.botUsername"] || "";
+  const isConnected = !!botUsername;
+  const [copied, setCopied] = useState(false);
+  const deepLink = botUsername ? `https://t.me/${botUsername}?start={customerId}` : "";
+
+  const handleCopyLink = () => {
+    if (!deepLink) return;
+    navigator.clipboard.writeText(deepLink);
+    setCopied(true);
+    toast.success(t("deepLink.copied"));
+    setTimeout(() => setCopied(false), 2000);
+  };
+
+  const handleToggleEnabled = (checked: boolean) => {
+    setEnabled(checked);
+    startTransition(async () => {
+      const result = await setSettings({ "telegram.enabled": checked ? "true" : "false" });
+      if (result.success) { toast.success(t("saved")); router.refresh(); }
+      else { toast.error(result.error ?? t("saveError")); }
+    });
+  };
+
+  const handleSave = () => {
+    startTransition(async () => {
+      const result = await setTelegramSettings({ botToken });
+      if (result.success) { toast.success(t("saved")); router.refresh(); }
+      else { toast.error(result.error ?? t("saveError")); }
+    });
+  };
+
+  return (
+    <div className="space-y-6">
+      <ReadOnlyBanner />
+      <ReadOnlyWrapper>
+        <Card>
+          <CardHeader>
+            <CardTitle>{t("title")}</CardTitle>
+            <CardDescription>
+              {t("description")}{" "}
+              <a
+                href="https://torqvoice.com/docs/configuration#telegram"
+                target="_blank"
+                rel="noopener noreferrer"
+                className="inline-flex items-center gap-1 font-medium text-primary hover:underline"
+              >
+                {t("helpLink")}
+                <ExternalLink className="h-3 w-3" />
+              </a>
+            </CardDescription>
+          </CardHeader>
+          <CardContent className="space-y-6">
+            <div className="flex items-center justify-between">
+              <div className="space-y-0.5">
+                <Label htmlFor="enable-telegram">{t("enable.label")}</Label>
+                <p className="text-xs text-muted-foreground">{t("enable.hint")}</p>
+              </div>
+              <Switch id="enable-telegram" checked={enabled} onCheckedChange={handleToggleEnabled} />
+            </div>
+
+            {!enabled && (
+              <div className="flex items-start gap-3 rounded-lg border bg-muted/50 p-4">
+                <Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
+                <p className="text-sm text-muted-foreground">{t("enable.disabledInfo")}</p>
+              </div>
+            )}
+
+            {enabled && (
+              <>
+                <div className="space-y-2">
+                  <Label htmlFor="bot-token">{t("botToken.label")}</Label>
+                  <div className="flex items-center gap-2">
+                    <Input id="bot-token" type={showToken ? "text" : "password"} placeholder={t("botToken.placeholder")} value={botToken} onChange={(e) => setBotToken(e.target.value)} />
+                    <Button type="button" variant="outline" size="icon" onClick={() => setShowToken(!showToken)}>
+                      {showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
+                    </Button>
+                  </div>
+                  <p className="text-xs text-muted-foreground">{t("botToken.help")}</p>
+                </div>
+
+                {isConnected && (
+                  <div className="space-y-2">
+                    <Label>{t("botUsername.label")}</Label>
+                    <Input readOnly value={`@${botUsername}`} className="font-mono text-sm" />
+                  </div>
+                )}
+
+                {isConnected && (
+                  <div className="space-y-2">
+                    <Label>{t("webhook.label")}</Label>
+                    <Input readOnly value={`${appUrl}/api/webhooks/telegram`} className="font-mono text-xs" />
+                    <p className="text-xs text-muted-foreground">{t("webhook.description")}</p>
+                  </div>
+                )}
+
+                {isConnected && (
+                  <div className="space-y-2">
+                    <Label>{t("deepLink.title")}</Label>
+                    <p className="text-xs text-muted-foreground">{t("deepLink.description")}</p>
+                    <div className="flex items-center gap-2">
+                      <Input readOnly value={deepLink} className="font-mono text-xs" />
+                      <Button type="button" variant="outline" size="icon" onClick={handleCopyLink}>
+                        {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
+                      </Button>
+                    </div>
+                  </div>
+                )}
+
+                {isConnected && <TelegramTestMessage />}
+
+              </>
+            )}
+          </CardContent>
+        </Card>
+
+        {enabled && (
+          <SaveButton>
+            <div className="flex items-center justify-between">
+              {isConnected && <TelegramDisconnectButton />}
+              <div className="ml-auto">
+                <Button onClick={handleSave} disabled={isPending || !botToken.trim()}>
+                  {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                  {isPending ? t("saving") : t("save")}
+                </Button>
+              </div>
+            </div>
+          </SaveButton>
+        )}
+      </ReadOnlyWrapper>
+    </div>
+  );
+}

Some files were not shown because too many files changed in this diff