Add per-customer SMS and Bird Integrations for patient messaging.
Deploy Ladill Care / deploy (push) Successful in 37s
Deploy Ladill Care / deploy (push) Successful in 37s
Patient SMS/email now use encrypted tenant keys via the platform relay instead of shared platform Care API keys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
<x-app-layout title="Integrations">
|
||||
<x-settings.page title="Messaging integrations">
|
||||
<p class="text-sm text-slate-600">
|
||||
Connect your own <a href="https://sms.ladill.com" target="_blank" rel="noopener" class="text-teal-700 underline">Ladill SMS</a>
|
||||
and <a href="https://bird.ladill.com" target="_blank" rel="noopener" class="text-teal-700 underline">Ladill Bird</a>
|
||||
API keys so patient messages bill your wallets and send under your brand.
|
||||
</p>
|
||||
|
||||
@if (! $canManage)
|
||||
<p class="mt-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">You can view integrations, but only admins can change credentials.</p>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 space-y-6">
|
||||
<x-settings.card title="Ladill SMS" description="Paste an SMS API key (lsk_sms_live_…) and an approved sender ID from sms.ladill.com.">
|
||||
@if ($credential->hasValidSms())
|
||||
<div class="mb-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
|
||||
Connected · key {{ $credential->sms_api_key_prefix }}… · sender <strong>{{ $credential->sms_sender_id }}</strong>
|
||||
@if ($credential->sms_validated_at)
|
||||
· validated {{ $credential->sms_validated_at->diffForHumans() }}
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($credential->sms_status === \App\Models\MessagingCredential::STATUS_INVALID)
|
||||
<div class="mb-4 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-800">
|
||||
{{ $credential->sms_last_error ?: 'SMS credentials are invalid.' }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($canManage)
|
||||
<form method="POST" action="{{ route('care.integrations.sms.save') }}" class="space-y-4">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">SMS API key</label>
|
||||
<input type="password" name="sms_api_key" autocomplete="off" required
|
||||
placeholder="lsk_sms_live_…"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Sender ID</label>
|
||||
<input type="text" name="sms_sender_id" maxlength="11" required
|
||||
value="{{ old('sms_sender_id', $credential->sms_sender_id) }}"
|
||||
placeholder="Approved sender ID"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<p class="mt-1 text-xs text-slate-500">Must match an approved sender ID on your SMS service (or the platform default).</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="submit" class="btn-primary">Validate & save</button>
|
||||
</div>
|
||||
</form>
|
||||
@if ($credential->sms_api_key_encrypted || $credential->hasValidSms())
|
||||
<form method="POST" action="{{ route('care.integrations.sms.disconnect') }}" class="mt-3" onsubmit="return confirm('Disconnect Ladill SMS?')">
|
||||
@csrf
|
||||
<button type="submit" class="text-sm text-rose-700 hover:underline">Disconnect SMS</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</x-settings.card>
|
||||
|
||||
<x-settings.card title="Ladill Bird" description="Paste a Bird API key (lsk_live_…) plus a from address on a verified domain from bird.ladill.com.">
|
||||
@if ($credential->hasValidBird())
|
||||
<div class="mb-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">
|
||||
Connected · key {{ $credential->bird_api_key_prefix }}… · from
|
||||
<strong>{{ $credential->bird_from_name ? $credential->bird_from_name.' <'.$credential->bird_from_email.'>' : $credential->bird_from_email }}</strong>
|
||||
@if ($credential->bird_validated_at)
|
||||
· validated {{ $credential->bird_validated_at->diffForHumans() }}
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($credential->bird_status === \App\Models\MessagingCredential::STATUS_INVALID)
|
||||
<div class="mb-4 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-800">
|
||||
{{ $credential->bird_last_error ?: 'Bird credentials are invalid.' }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($canManage)
|
||||
<form method="POST" action="{{ route('care.integrations.bird.save') }}" class="space-y-4">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Bird API key</label>
|
||||
<input type="password" name="bird_api_key" autocomplete="off" required
|
||||
placeholder="lsk_live_…"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 font-mono text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">From email</label>
|
||||
<input type="email" name="bird_from_email" required
|
||||
value="{{ old('bird_from_email', $credential->bird_from_email) }}"
|
||||
placeholder="clinic@yourdomain.com"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">From name</label>
|
||||
<input type="text" name="bird_from_name" maxlength="100"
|
||||
value="{{ old('bird_from_name', $credential->bird_from_name) }}"
|
||||
placeholder="{{ $organization->name }}"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="submit" class="btn-primary">Validate & save</button>
|
||||
</div>
|
||||
</form>
|
||||
@if ($credential->bird_api_key_encrypted || $credential->hasValidBird())
|
||||
<form method="POST" action="{{ route('care.integrations.bird.disconnect') }}" class="mt-3" onsubmit="return confirm('Disconnect Ladill Bird?')">
|
||||
@csrf
|
||||
<button type="submit" class="text-sm text-rose-700 hover:underline">Disconnect Bird</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</x-settings.card>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-sm text-slate-500">
|
||||
<a href="{{ route('care.settings') }}" class="text-teal-700 underline">← Back to facility settings</a>
|
||||
</p>
|
||||
</x-settings.page>
|
||||
</x-app-layout>
|
||||
@@ -137,7 +137,18 @@
|
||||
@if ($canManage)
|
||||
<section class="rounded-2xl border border-slate-200 bg-white p-6" x-data="{ tab: 'sms' }">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Message patient</h2>
|
||||
<p class="mt-1 text-xs text-slate-500">SMS uses Ladill SMS credits. Email uses Ladill Bird credits.</p>
|
||||
<p class="mt-1 text-xs text-slate-500">Uses your connected Ladill SMS / Bird credentials. <a href="{{ route('care.integrations') }}" class="text-teal-700 underline">Manage integrations</a></p>
|
||||
@if (! ($messagingSmsReady ?? false) || ! ($messagingEmailReady ?? false))
|
||||
<p class="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||
@if (! ($messagingSmsReady ?? false) && ! ($messagingEmailReady ?? false))
|
||||
Connect SMS and Bird in Integrations before messaging patients.
|
||||
@elseif (! ($messagingSmsReady ?? false))
|
||||
Connect Ladill SMS in Integrations to enable SMS.
|
||||
@else
|
||||
Connect Ladill Bird in Integrations to enable email.
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
<div class="mt-4 flex gap-1 rounded-xl bg-slate-100 p-1 text-sm">
|
||||
<button type="button" @click="tab = 'sms'" :class="tab === 'sms' ? 'bg-white shadow-sm text-slate-900' : 'text-slate-500'" class="flex-1 rounded-lg px-3 py-1.5 font-medium">SMS</button>
|
||||
<button type="button" @click="tab = 'email'" :class="tab === 'email' ? 'bg-white shadow-sm text-slate-900' : 'text-slate-500'" class="flex-1 rounded-lg px-3 py-1.5 font-medium">Email</button>
|
||||
@@ -148,7 +159,7 @@
|
||||
<p class="text-xs text-slate-500">To: {{ $patient->phone ?: 'no phone on file' }}</p>
|
||||
<textarea name="message" rows="3" maxlength="480" placeholder="Text message" required class="block w-full rounded-xl border-slate-300 text-sm focus:border-sky-500 focus:ring-sky-500">{{ old('message') }}</textarea>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn-primary" @if(! $patient->phone) disabled @endif>Send SMS</button>
|
||||
<button type="submit" class="btn-primary" @if(! $patient->phone || ! ($messagingSmsReady ?? false)) disabled @endif>Send SMS</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -158,7 +169,7 @@
|
||||
<input type="text" name="subject" value="{{ old('subject') }}" placeholder="Subject" required class="block w-full rounded-xl border-slate-300 text-sm focus:border-sky-500 focus:ring-sky-500">
|
||||
<textarea name="body" rows="4" placeholder="Message" required class="block w-full rounded-xl border-slate-300 text-sm focus:border-sky-500 focus:ring-sky-500">{{ old('body') }}</textarea>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn-primary" @if(! $patient->email) disabled @endif>Send email</button>
|
||||
<button type="submit" class="btn-primary" @if(! $patient->email || ! ($messagingEmailReady ?? false)) disabled @endif>Send email</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
</x-settings.card>
|
||||
|
||||
@if ($canManage)
|
||||
<x-settings.card title="Messaging" description="Connect your Ladill SMS and Bird API keys for patient messaging.">
|
||||
<a href="{{ route('care.integrations') }}" class="text-sm font-medium text-teal-700 underline">Open messaging integrations →</a>
|
||||
</x-settings.card>
|
||||
|
||||
<x-settings.card title="Integrations" description="Manage service queues and counters in Care. You must also enable Care integration in Ladill Queue settings.">
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" name="queue_integration_enabled" value="1" @checked(data_get($organization->settings, 'queue_integration_enabled'))>
|
||||
|
||||
Reference in New Issue
Block a user