Initial Ladill Queue release — enterprise QMS standalone app.
Deploy Ladill Queue / deploy (push) Successful in 56s

Phases 1–6: tickets, counters, displays, appointments, workflows, rules, analytics, reports, feedback, admin, device API, and Gitea deploy workflow for queue.ladill.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-29 20:19:52 +00:00
co-authored by Cursor
commit cca98eefd2
297 changed files with 27263 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Services\Comms;
use App\Mail\ContactMessageMail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
/**
* Outbound email to contacts. Uses the app's configured mailer (set MAIL_* to
* a Ladill Bird SMTP credential in production so mail leaves as the account's
* sender). Failures are swallowed + logged; the caller decides what to record.
*/
class EmailService
{
public function send(string $to, string $subject, string $body, ?string $fromName = null, ?string $replyTo = null): bool
{
if (! filter_var($to, FILTER_VALIDATE_EMAIL)) {
return false;
}
try {
Mail::to($to)->send(new ContactMessageMail($subject, $body, $fromName, $replyTo));
return true;
} catch (\Throwable $e) {
Log::warning('CRM email send failed', ['to' => $to, 'error' => $e->getMessage()]);
return false;
}
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace App\Services\Comms;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* Outbound SMS to contacts via Arkesel (the platform SMS provider). Low-volume,
* best-effort transactional send mirrors the platform's direct Arkesel calls;
* uses the shared ARKESEL_API_KEY. Failures are swallowed + logged.
*/
class SmsService
{
public function send(string $to, string $message): bool
{
$apiKey = (string) config('arkesel.api_key', '');
$sender = (string) config('arkesel.sender_id', 'Ladill');
$msisdn = $this->normalise($to);
if ($apiKey === '' || $msisdn === null) {
return false;
}
try {
$res = Http::timeout(20)
->withHeaders(['api-key' => $apiKey])
->acceptJson()
->post(rtrim((string) config('arkesel.base_url', 'https://sms.arkesel.com'), '/').'/api/v2/sms/send', [
'sender' => $sender,
'message' => $message,
'recipients' => [$msisdn],
]);
return $res->successful() && ($res->json('status') === 'success');
} catch (\Throwable $e) {
Log::warning('CRM SMS send failed', ['to' => $msisdn, 'error' => $e->getMessage()]);
return false;
}
}
/** Normalise to E.164-without-plus (e.g. 233XXXXXXXXX). */
private function normalise(string $to): ?string
{
$digits = preg_replace('/\D+/', '', $to) ?? '';
$country = (string) config('arkesel.default_country_code', '233');
if (strlen($digits) < 9) {
return null;
}
if (str_starts_with($digits, $country)) {
return $digits;
}
if (str_starts_with($digits, '0')) {
return $country.substr($digits, 1);
}
if (strlen($digits) === 9) {
return $country.$digits;
}
return $digits;
}
}