Skip to content

Commit 94dde74

Browse files
committed
feat: add public entry points and Twig templates
- Add index.php for message inbox - Add conversation.php for thread view - Add settings.php for configuration - Create Twig templates with Bootstrap 5 styling - Include CSRF tokens on all forms
1 parent 4e7142e commit 94dde74

File tree

6 files changed

+385
-0
lines changed

6 files changed

+385
-0
lines changed

public/conversation.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/**
4+
* Conversation thread view
5+
*
6+
* @package OpenCoreEMR
7+
* @link http://www.open-emr.org
8+
* @author Michael A. Smith <michael@opencoreemr.com>
9+
* @copyright Copyright (c) 2025 OpenCoreEMR Inc
10+
* @license GNU General Public License 3
11+
*/
12+
13+
require_once __DIR__ . '/../../../../globals.php';
14+
15+
use OpenCoreEMR\Modules\SinchConversations\Bootstrap;
16+
use OpenCoreEMR\Modules\SinchConversations\GlobalsAccessor;
17+
use OpenCoreEMR\Sinch\Conversation\Exception\ExceptionInterface;
18+
use Symfony\Component\HttpFoundation\Response;
19+
20+
$globalsAccessor = new GlobalsAccessor();
21+
$kernel = $globalsAccessor->get('kernel');
22+
$bootstrap = new Bootstrap($kernel->getEventDispatcher(), $kernel, $globalsAccessor);
23+
24+
$controller = $bootstrap->getConversationController();
25+
26+
$action = $_GET['action'] ?? $_POST['action'] ?? 'view';
27+
28+
try {
29+
$response = $controller->dispatch($action);
30+
$response->send();
31+
} catch (ExceptionInterface $e) {
32+
error_log("Sinch Conversations error: " . $e->getMessage());
33+
34+
$response = new Response(
35+
"Error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
36+
$e->getStatusCode()
37+
);
38+
$response->send();
39+
} catch (\Throwable $e) {
40+
error_log("Unexpected error in Sinch Conversations: " . $e->getMessage());
41+
42+
$response = new Response(
43+
"Error: An unexpected error occurred",
44+
500
45+
);
46+
$response->send();
47+
}

public/index.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/**
4+
* Main entry point for Sinch Conversations inbox
5+
*
6+
* @package OpenCoreEMR
7+
* @link http://www.open-emr.org
8+
* @author Michael A. Smith <michael@opencoreemr.com>
9+
* @copyright Copyright (c) 2025 OpenCoreEMR Inc
10+
* @license GNU General Public License 3
11+
*/
12+
13+
require_once __DIR__ . '/../../../../globals.php';
14+
15+
use OpenCoreEMR\Modules\SinchConversations\Bootstrap;
16+
use OpenCoreEMR\Modules\SinchConversations\GlobalsAccessor;
17+
use OpenCoreEMR\Sinch\Conversation\Exception\ExceptionInterface;
18+
use Symfony\Component\HttpFoundation\Response;
19+
20+
$globalsAccessor = new GlobalsAccessor();
21+
$kernel = $globalsAccessor->get('kernel');
22+
$bootstrap = new Bootstrap($kernel->getEventDispatcher(), $kernel, $globalsAccessor);
23+
24+
$controller = $bootstrap->getInboxController();
25+
26+
$action = $_GET['action'] ?? $_POST['action'] ?? 'list';
27+
28+
try {
29+
$response = $controller->dispatch($action);
30+
$response->send();
31+
} catch (ExceptionInterface $e) {
32+
error_log("Sinch Conversations error: " . $e->getMessage());
33+
34+
$response = new Response(
35+
"Error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
36+
$e->getStatusCode()
37+
);
38+
$response->send();
39+
} catch (\Throwable $e) {
40+
error_log("Unexpected error in Sinch Conversations: " . $e->getMessage());
41+
42+
$response = new Response(
43+
"Error: An unexpected error occurred",
44+
500
45+
);
46+
$response->send();
47+
}

public/settings.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/**
4+
* Settings and configuration
5+
*
6+
* @package OpenCoreEMR
7+
* @link http://www.open-emr.org
8+
* @author Michael A. Smith <michael@opencoreemr.com>
9+
* @copyright Copyright (c) 2025 OpenCoreEMR Inc
10+
* @license GNU General Public License 3
11+
*/
12+
13+
require_once __DIR__ . '/../../../../globals.php';
14+
15+
use OpenCoreEMR\Modules\SinchConversations\Bootstrap;
16+
use OpenCoreEMR\Modules\SinchConversations\GlobalsAccessor;
17+
use OpenCoreEMR\Sinch\Conversation\Exception\ExceptionInterface;
18+
use Symfony\Component\HttpFoundation\Response;
19+
20+
$globalsAccessor = new GlobalsAccessor();
21+
$kernel = $globalsAccessor->get('kernel');
22+
$bootstrap = new Bootstrap($kernel->getEventDispatcher(), $kernel, $globalsAccessor);
23+
24+
$controller = $bootstrap->getSettingsController();
25+
26+
$action = $_GET['action'] ?? $_POST['action'] ?? 'show';
27+
28+
try {
29+
$response = $controller->dispatch($action);
30+
$response->send();
31+
} catch (ExceptionInterface $e) {
32+
error_log("Sinch Conversations error: " . $e->getMessage());
33+
34+
$response = new Response(
35+
"Error: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'),
36+
$e->getStatusCode()
37+
);
38+
$response->send();
39+
} catch (\Throwable $e) {
40+
error_log("Unexpected error in Sinch Conversations: " . $e->getMessage());
41+
42+
$response = new Response(
43+
"Error: An unexpected error occurred",
44+
500
45+
);
46+
$response->send();
47+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>{{ 'Conversation'|xlt }}</title>
7+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
8+
<style>
9+
body { padding: 20px; background: #f8f9fa; }
10+
.message-thread { max-height: 500px; overflow-y: auto; padding: 15px; background: white; border: 1px solid #dee2e6; border-radius: 4px; }
11+
.message { margin-bottom: 15px; padding: 10px; border-radius: 8px; max-width: 70%; }
12+
.message-inbound { background: #e3f2fd; margin-right: auto; }
13+
.message-outbound { background: #c8e6c9; margin-left: auto; }
14+
.message-meta { font-size: 0.85em; color: #666; margin-top: 5px; }
15+
</style>
16+
</head>
17+
<body>
18+
<div class="container-fluid">
19+
<div class="mb-4">
20+
<a href="index.php" class="btn btn-secondary mb-3">
21+
&larr; {{ 'Back to Inbox'|xlt }}
22+
</a>
23+
<h2>{{ 'Conversation with'|xlt }} {{ patient.fname|text }} {{ patient.lname|text }}</h2>
24+
<p class="text-muted">{{ 'Phone'|xlt }}: {{ patient.phone_cell|text }}</p>
25+
</div>
26+
27+
<div class="message-thread mb-4">
28+
{% if messages|length == 0 %}
29+
<p class="text-muted text-center">{{ 'No messages in this conversation yet.'|xlt }}</p>
30+
{% else %}
31+
{% for message in messages %}
32+
<div class="message message-{{ message.direction }}">
33+
<div>{{ message.body|text }}</div>
34+
<div class="message-meta">
35+
{{ message.sent_at|text }} - {{ message.status|text }}
36+
</div>
37+
</div>
38+
{% endfor %}
39+
{% endif %}
40+
</div>
41+
42+
<div class="card">
43+
<div class="card-body">
44+
<h5 class="card-title">{{ 'Send Reply'|xlt }}</h5>
45+
<form method="post" action="conversation.php?action=reply">
46+
<input type="hidden" name="csrf_token" value="{{ csrf_token|attr }}">
47+
<input type="hidden" name="conversation_id" value="{{ conversation.conversation_id|attr }}">
48+
49+
<div class="mb-3">
50+
<label for="message" class="form-label">{{ 'Message'|xlt }}</label>
51+
<textarea name="message" id="message" class="form-control" rows="3" required></textarea>
52+
<div class="form-text">
53+
{{ 'Enter your message to the patient. Avoid including PHI in SMS.'|xlt }}
54+
</div>
55+
</div>
56+
57+
<button type="submit" class="btn btn-primary">
58+
{{ 'Send Message'|xlt }}
59+
</button>
60+
</form>
61+
</div>
62+
</div>
63+
</div>
64+
65+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
66+
</body>
67+
</html>

templates/inbox/list.html.twig

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>{{ 'Message Inbox'|xlt }}</title>
7+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
8+
<style>
9+
body { padding: 20px; background: #f8f9fa; }
10+
.conversation-row { cursor: pointer; }
11+
.conversation-row:hover { background-color: #f8f9fa; }
12+
.unread-badge { background-color: #dc3545; }
13+
</style>
14+
</head>
15+
<body>
16+
<div class="container-fluid">
17+
<div class="d-flex justify-content-between align-items-center mb-4">
18+
<h2>{{ 'Message Inbox'|xlt }}</h2>
19+
<a href="?action=refresh&csrf_token={{ csrf_token|attr }}" class="btn btn-primary">
20+
{{ 'Refresh Messages'|xlt }}
21+
</a>
22+
</div>
23+
24+
{% if success_message %}
25+
<div class="alert alert-info alert-dismissible fade show" role="alert">
26+
{{ success_message|text }}
27+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
28+
</div>
29+
{% endif %}
30+
31+
{% if conversations|length == 0 %}
32+
<div class="alert alert-info">
33+
{{ 'No conversations found. Messages will appear here when patients reply.'|xlt }}
34+
</div>
35+
{% else %}
36+
<div class="card">
37+
<div class="card-body p-0">
38+
<table class="table table-hover mb-0">
39+
<thead>
40+
<tr>
41+
<th>{{ 'Patient'|xlt }}</th>
42+
<th>{{ 'Channel'|xlt }}</th>
43+
<th>{{ 'Unread'|xlt }}</th>
44+
<th>{{ 'Last Activity'|xlt }}</th>
45+
<th>{{ 'Actions'|xlt }}</th>
46+
</tr>
47+
</thead>
48+
<tbody>
49+
{% for conversation in conversations %}
50+
<tr class="conversation-row">
51+
<td>{{ conversation.patient_name|text }}</td>
52+
<td>
53+
<span class="badge bg-info">
54+
{{ conversation.channel|text }}
55+
</span>
56+
</td>
57+
<td>
58+
{% if conversation.unread_count > 0 %}
59+
<span class="badge unread-badge">
60+
{{ conversation.unread_count }}
61+
</span>
62+
{% else %}
63+
<span class="text-muted">-</span>
64+
{% endif %}
65+
</td>
66+
<td>{{ conversation.last_activity|text }}</td>
67+
<td>
68+
<a href="conversation.php?action=view&conversation_id={{ conversation.conversation_id|attr }}"
69+
class="btn btn-sm btn-primary">
70+
{{ 'View'|xlt }}
71+
</a>
72+
</td>
73+
</tr>
74+
{% endfor %}
75+
</tbody>
76+
</table>
77+
</div>
78+
</div>
79+
{% endif %}
80+
</div>
81+
82+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
83+
</body>
84+
</html>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>{{ 'Sinch Conversations Settings'|xlt }}</title>
7+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
8+
<style>
9+
body { padding: 20px; background: #f8f9fa; }
10+
</style>
11+
</head>
12+
<body>
13+
<div class="container">
14+
<h2 class="mb-4">{{ 'Sinch Conversations Settings'|xlt }}</h2>
15+
16+
{% if success_message %}
17+
<div class="alert alert-success alert-dismissible fade show" role="alert">
18+
{{ success_message|text }}
19+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
20+
</div>
21+
{% endif %}
22+
23+
<div class="card mb-4">
24+
<div class="card-body">
25+
<h5 class="card-title">{{ 'API Configuration'|xlt }}</h5>
26+
<p class="text-muted">{{ 'Configure settings in Administration > Globals > Conversations'|xlt }}</p>
27+
28+
<div class="mb-3">
29+
<label class="form-label">{{ 'Sinch Project ID'|xlt }}</label>
30+
<input type="text" class="form-control" value="{{ settings.project_id|attr }}" disabled>
31+
</div>
32+
33+
<div class="mb-3">
34+
<label class="form-label">{{ 'Sinch App ID'|xlt }}</label>
35+
<input type="text" class="form-control" value="{{ settings.app_id|attr }}" disabled>
36+
</div>
37+
38+
<div class="mb-3">
39+
<label class="form-label">{{ 'API Region'|xlt }}</label>
40+
<input type="text" class="form-control" value="{{ settings.region|attr }}" disabled>
41+
</div>
42+
43+
<div class="mb-3">
44+
<label class="form-label">{{ 'Default Channel'|xlt }}</label>
45+
<input type="text" class="form-control" value="{{ settings.default_channel|attr }}" disabled>
46+
</div>
47+
48+
<div class="mb-3">
49+
<label class="form-label">{{ 'Clinic Name'|xlt }}</label>
50+
<input type="text" class="form-control" value="{{ settings.clinic_name|attr }}" disabled>
51+
</div>
52+
53+
<div class="mb-3">
54+
<label class="form-label">{{ 'Clinic Phone'|xlt }}</label>
55+
<input type="text" class="form-control" value="{{ settings.clinic_phone|attr }}" disabled>
56+
</div>
57+
</div>
58+
</div>
59+
60+
<div class="card">
61+
<div class="card-body">
62+
<h5 class="card-title">{{ 'API Connection Test'|xlt }}</h5>
63+
<p>{{ 'Test your Sinch API configuration to ensure connectivity.'|xlt }}</p>
64+
<button type="button" class="btn btn-primary" onclick="testConnection()">
65+
{{ 'Test Connection'|xlt }}
66+
</button>
67+
<div id="testResult" class="mt-3"></div>
68+
</div>
69+
</div>
70+
</div>
71+
72+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
73+
<script>
74+
function testConnection() {
75+
const resultDiv = document.getElementById('testResult');
76+
resultDiv.innerHTML = '<div class="alert alert-info">Testing connection...</div>';
77+
78+
fetch('settings.php?action=test&csrf_token={{ csrf_token|attr }}')
79+
.then(response => response.json())
80+
.then(data => {
81+
if (data.success) {
82+
resultDiv.innerHTML = '<div class="alert alert-success">' + data.message + '</div>';
83+
} else {
84+
resultDiv.innerHTML = '<div class="alert alert-danger">Error: ' + data.message + '</div>';
85+
}
86+
})
87+
.catch(error => {
88+
resultDiv.innerHTML = '<div class="alert alert-danger">Connection failed: ' + error + '</div>';
89+
});
90+
}
91+
</script>
92+
</body>
93+
</html>

0 commit comments

Comments
 (0)