Skip to content

Commit 4374e32

Browse files
committed
feat/qg-290: реализована динамическая загрузка фрагмента настроек нотификаций и отображение корректных дня и времени оповещения о заполнении расписания
1 parent c3f8962 commit 4374e32

File tree

12 files changed

+362
-167
lines changed

12 files changed

+362
-167
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package pro.qyoga.app.therapist.appointments.core.schedule.settings
2+
3+
import org.springframework.security.core.annotation.AuthenticationPrincipal
4+
import org.springframework.stereotype.Controller
5+
import org.springframework.web.bind.annotation.GetMapping
6+
import org.springframework.web.servlet.ModelAndView
7+
import pro.qyoga.core.appointments.notifications.fill_schedule.FillScheduleNotificationsSettings
8+
import pro.qyoga.core.appointments.notifications.fill_schedule.FillScheduleNotificationsSettingsRepo
9+
import pro.qyoga.core.users.auth.dtos.QyogaUserDetails
10+
import pro.qyoga.core.users.therapists.ref
11+
import pro.qyoga.l10n.russianTimeFormat
12+
import pro.qyoga.l10n.systemLocale
13+
import java.time.format.TextStyle
14+
15+
16+
data class NotificationsSettingsModel(
17+
val settings: FillScheduleNotificationsSettings
18+
) : ModelAndView(
19+
"therapist/appointments/notifications-settings-component",
20+
mapOf(
21+
"fillScheduleRemainderDay" to settings.dayOfWeek.getDisplayName(TextStyle.FULL, systemLocale),
22+
"fillScheduleRemainderTime" to settings.scheduledTime.format(russianTimeFormat)
23+
)
24+
)
25+
26+
@Controller
27+
class NotificationsSettingsController(
28+
private val fillScheduleNotificationsSettingsRepo: FillScheduleNotificationsSettingsRepo
29+
) {
30+
31+
@GetMapping(PATH)
32+
fun getNotificationsSettingsComponent(
33+
@AuthenticationPrincipal therapist: QyogaUserDetails
34+
): NotificationsSettingsModel {
35+
val settings = fillScheduleNotificationsSettingsRepo.findForTherapist(therapist.ref)
36+
?: FillScheduleNotificationsSettings.defaultSettingsFor(therapist.ref)
37+
return NotificationsSettingsModel(settings)
38+
}
39+
40+
companion object {
41+
const val PATH = "/therapist/schedule/settings/notifications"
42+
}
43+
44+
}
Lines changed: 153 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,123 @@
1-
<div class="mb-3"
2-
id="notifications-settings"
3-
x-cloak
4-
x-data="notificationsSettings()"
5-
x-init="init()">
6-
<h6>Настройки напоминаний</h6>
7-
8-
<div class="border rounded-3 p-3 bg-white mb-4"
9-
x-show="!isActive"
10-
>
11-
<div class="d-flex align-items-center justify-content-between mb-2">
12-
<span class="badge bg-secondary">Отключено</span>
13-
</div>
14-
<div class="text-muted small mb-3" style="height: 2.5em">
15-
Напоминания не активированы на этом устройстве.
16-
</div>
17-
<button @click="enable()"
18-
class="btn btn-outline-secondary w-100">
19-
Включить напоминания
20-
</button>
21-
</div>
22-
23-
<div class="border rounded-3 p-3 bg-white mb-4"
24-
x-show="isActive"
25-
>
26-
<div class="d-flex justify-content-between align-items-center mb-2">
27-
<span class="badge bg-success">Активно</span>
28-
</div>
29-
<div class="text-muted small mb-3" style="height: 2.5em">
30-
Напоминания о планировании приёмов: каждый понедельник, 09:00 утра
31-
</div>
32-
<button @click="disable()"
33-
class="btn btn-outline-danger w-100">
34-
Отключить напоминания
35-
</button>
36-
</div>
1+
<div id="notifications-settings">
372

383
<script>
39-
document.addEventListener('alpine:init', () => {
40-
Alpine.data('notificationsSettings', () => ({
41-
isActive: false,
42-
43-
async init() {
44-
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
45-
console.warn("Push isn't supported by the browser");
46-
return;
47-
}
48-
49-
const existing = await this.getSubscription();
50-
if (!existing) {
51-
this.isActive = false;
52-
return;
53-
}
54-
55-
this.isActive = true;
56-
console.info("Notification settings initialized")
57-
},
58-
59-
async enable() {
60-
console.info("Enabling notifications")
61-
const perm = await this.ensurePermission();
62-
console.debug('enable(): permission', perm);
63-
if (perm !== 'granted') {
64-
return;
65-
}
66-
67-
const sub = await this.subscribe();
68-
console.debug('enable(): sub', sub);
69-
if (!sub) {
70-
return;
71-
}
72-
73-
const saved = await this.saveSubscription(sub);
74-
console.debug('enable(): savedSub', saved);
75-
if (!saved) {
76-
return;
77-
}
78-
79-
this.isActive = true;
80-
},
81-
82-
async disable() {
83-
const swReg = await navigator.serviceWorker.ready;
84-
const sub = await swReg.pushManager.getSubscription();
85-
if (!sub) {
86-
this.isActive = false;
87-
return;
88-
}
89-
90-
const {keys} = sub.toJSON();
91-
const p256dh = keys?.p256dh;
92-
await fetch(`/pushes/web/subscriptions/${p256dh}`, {
93-
method: 'DELETE',
94-
});
4+
Alpine.data('notificationsSettings', () => ({
5+
isActive: false,
956

96-
await sub.unsubscribe();
7+
async init() {
8+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
9+
console.warn("Push isn't supported by the browser");
10+
return;
11+
}
12+
13+
const existing = await this.getSubscription();
14+
if (!existing) {
9715
this.isActive = false;
98-
},
99-
100-
async ensurePermission() {
101-
if (Notification.permission === 'denied') {
102-
console.info('Notifications are denied');
103-
return 'denied';
104-
}
105-
if (Notification.permission === 'default') {
106-
return await Notification.requestPermission();
107-
}
108-
return Notification.permission;
109-
},
110-
111-
async subscribe() {
112-
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
113-
console.debug('subscribe(): PushManager unavailable');
114-
return null;
115-
}
116-
117-
try {
118-
console.debug('subscribe(): fetching VAPID key');
119-
let resp = await fetch('/pushes/web/public-key');
120-
const vapidKey = (await resp.text()).trim();
121-
console.debug('subscribe(): fetch status', resp.status, resp.redirected);
122-
123-
const swReg = await navigator.serviceWorker.ready;
124-
console.debug('subscribe(): service worker scope', swReg.scope);
125-
126-
let subscription = await swReg.pushManager.subscribe({
127-
userVisibleOnly: true,
128-
applicationServerKey: urlBase64ToUint8Array(vapidKey)
129-
});
130-
console.debug('subscribe(): success');
131-
132-
return subscription;
133-
} catch (e) {
134-
console.error('Push subscription failed', e);
135-
return null;
136-
}
137-
},
138-
139-
async saveSubscription(sub) {
140-
const response = await fetch('/pushes/web/subscriptions', {
141-
method: 'POST',
142-
headers: {'Content-Type': 'application/json'},
143-
body: JSON.stringify(sub)
144-
});
145-
if (!response.ok || response.redirected) {
146-
return false;
147-
}
148-
return true;
149-
},
16+
return;
17+
}
18+
19+
this.isActive = true;
20+
console.info("Notification settings initialized")
21+
},
22+
23+
async enable() {
24+
console.info("Enabling notifications")
25+
const perm = await this.ensurePermission();
26+
console.debug('enable(): permission', perm);
27+
if (perm !== 'granted') {
28+
return;
29+
}
30+
31+
const sub = await this.subscribe();
32+
console.debug('enable(): sub', sub);
33+
if (!sub) {
34+
return;
35+
}
36+
37+
const saved = await this.saveSubscription(sub);
38+
console.debug('enable(): savedSub', saved);
39+
if (!saved) {
40+
return;
41+
}
42+
43+
this.isActive = true;
44+
},
45+
46+
async disable() {
47+
const swReg = await navigator.serviceWorker.ready;
48+
const sub = await swReg.pushManager.getSubscription();
49+
if (!sub) {
50+
this.isActive = false;
51+
return;
52+
}
53+
54+
const {keys} = sub.toJSON();
55+
const p256dh = keys?.p256dh;
56+
await fetch(`/pushes/web/subscriptions/${p256dh}`, {
57+
method: 'DELETE',
58+
});
59+
60+
await sub.unsubscribe();
61+
this.isActive = false;
62+
},
63+
64+
async ensurePermission() {
65+
if (Notification.permission === 'denied') {
66+
console.info('Notifications are denied');
67+
return 'denied';
68+
}
69+
if (Notification.permission === 'default') {
70+
return await Notification.requestPermission();
71+
}
72+
return Notification.permission;
73+
},
74+
75+
async subscribe() {
76+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
77+
console.debug('subscribe(): PushManager unavailable');
78+
return null;
79+
}
80+
81+
try {
82+
console.debug('subscribe(): fetching VAPID key');
83+
let resp = await fetch('/pushes/web/public-key');
84+
const vapidKey = (await resp.text()).trim();
85+
console.debug('subscribe(): fetch status', resp.status, resp.redirected);
15086

151-
async getSubscription() {
15287
const swReg = await navigator.serviceWorker.ready;
153-
return swReg.pushManager.getSubscription();
154-
},
88+
console.debug('subscribe(): service worker scope', swReg.scope);
15589

156-
}));
157-
});
90+
let subscription = await swReg.pushManager.subscribe({
91+
userVisibleOnly: true,
92+
applicationServerKey: urlBase64ToUint8Array(vapidKey)
93+
});
94+
console.debug('subscribe(): success');
95+
96+
return subscription;
97+
} catch (e) {
98+
console.error('Push subscription failed', e);
99+
return null;
100+
}
101+
},
102+
103+
async saveSubscription(sub) {
104+
const response = await fetch('/pushes/web/subscriptions', {
105+
method: 'POST',
106+
headers: {'Content-Type': 'application/json'},
107+
body: JSON.stringify(sub)
108+
});
109+
if (!response.ok || response.redirected) {
110+
return false;
111+
}
112+
return true;
113+
},
114+
115+
async getSubscription() {
116+
const swReg = await navigator.serviceWorker.ready;
117+
return swReg.pushManager.getSubscription();
118+
},
119+
120+
}));
158121

159122
function urlBase64ToUint8Array(base64String) {
160123
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
@@ -167,4 +130,44 @@ <h6>Настройки напоминаний</h6>
167130
return outputArray;
168131
}
169132
</script>
133+
<div class="mb-3"
134+
x-cloak
135+
x-data="notificationsSettings()"
136+
x-init="init()">
137+
<h6>Настройки напоминаний</h6>
138+
139+
<div class="border rounded-3 p-3 bg-white mb-4"
140+
x-show="!isActive"
141+
>
142+
<div class="d-flex align-items-center justify-content-between mb-2">
143+
<span class="badge bg-secondary">Отключено</span>
144+
</div>
145+
<div class="text-muted small mb-3" style="height: 2.5em">
146+
Напоминания не активированы на этом устройстве.
147+
</div>
148+
<button @click="enable()"
149+
class="btn btn-outline-secondary w-100">
150+
Включить напоминания
151+
</button>
152+
</div>
153+
154+
<div class="border rounded-3 p-3 bg-white mb-4"
155+
x-show="isActive"
156+
>
157+
<div class="d-flex justify-content-between align-items-center mb-2">
158+
<span class="badge bg-success">Активно</span>
159+
</div>
160+
<div class="text-muted small mb-3" style="height: 2.5em">
161+
Напоминания о планировании приёмов: каждый
162+
<span id="fill-sched-remainder-day" th:text="${fillScheduleRemainderDay}">понедельник</span>
163+
в
164+
<span id="fill-sched-remainder-time" th:text="${fillScheduleRemainderTime}">09:00</span>
165+
</div>
166+
<button @click="disable()"
167+
class="btn btn-outline-danger w-100">
168+
Отключить напоминания
169+
</button>
170+
</div>
171+
172+
</div>
170173
</div>

app/src/main/resources/templates/therapist/appointments/schedule.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,16 @@ <h5 class="modal-title" id="settingsModalLabel">Настройки календ
190190
</div>
191191
<div class="modal-body">
192192

193-
<div th:replace="~{therapist/appointments/notifications-settings-component.html}"></div>
193+
<div hx-get="/therapist/schedule/settings/notifications"
194+
hx-swap="innerHTML"
195+
hx-trigger="intersect"
196+
id="notifications-settings-container">
197+
<div class="text-center py-3">
198+
<div class="mt-2">
199+
<small class="text-muted">Загрузка настроек напоминаний...</small>
200+
</div>
201+
</div>
202+
</div>
194203

195204
<hr/>
196205

0 commit comments

Comments
 (0)