Skip to content

Commit ca033be

Browse files
authored
feat/qg-157: сайт сделан PWA-приложением (#283)
2 parents b4c33ca + dfa6de5 commit ca033be

File tree

32 files changed

+306
-172
lines changed

32 files changed

+306
-172
lines changed

app/src/main/kotlin/pro/qyoga/app/infra/WebSecurityConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class WebSecurityConfig(
5353
.requestMatchers(
5454
HttpMethod.GET,
5555
"/",
56+
"/offline.html",
57+
"/manifest.json",
5658
"/register",
5759
"/components/**",
5860
"/styles/**",

app/src/main/kotlin/pro/qyoga/app/publc/surverys/ProcessSurveyOp.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package pro.qyoga.app.publc.surverys
22

3+
import org.slf4j.LoggerFactory
34
import org.springframework.stereotype.Component
45
import org.springframework.transaction.annotation.Transactional
56
import pro.qyoga.core.clients.cards.ClientsRepo
67
import pro.qyoga.core.clients.cards.findByPhone
78
import pro.qyoga.core.clients.cards.model.PhoneNumber
9+
import pro.qyoga.core.clients.cards.model.toLogString
810
import pro.qyoga.core.survey_forms.settings.model.SurveyFormsSettingsRepo
911
import pro.qyoga.core.survey_forms.settings.model.findByYandexAdminEmail
1012
import java.time.LocalDate
@@ -16,15 +18,24 @@ class ProcessSurveyOp(
1618
private val surveyFormsSettingsRepo: SurveyFormsSettingsRepo
1719
) : (SurveyRq) -> Unit {
1820

21+
private val log = LoggerFactory.getLogger(javaClass)
22+
1923
@Transactional
2024
override operator fun invoke(surveyRq: SurveyRq) {
25+
log.debug("Processing survey: {}", surveyRq)
2126
val therapistRef = surveyFormsSettingsRepo.findByYandexAdminEmail(surveyRq.yandexAdminEmail)
2227
?.therapistRef
2328
?: throw InvalidSurveyException.surveySettingsNotFoundForAdminEmail()
2429

2530
var client = clientsRepo.findByPhone(therapistRef, PhoneNumber.of(surveyRq.survey.phone))
2631
?: surveyRq.survey.toClient(therapistRef)
2732

33+
if (client.version > 0) {
34+
log.info("Updating client: {} from survey", client.toLogString())
35+
} else {
36+
log.info("Creating client: {} from survey", client.toLogString())
37+
}
38+
2839
val today = LocalDate.now()
2940
client = client
3041
.prependComplaints(formatComplaintsEntry(surveyRq, today))

app/src/main/kotlin/pro/qyoga/core/clients/cards/model/Client.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,7 @@ private fun prependTextBlock(value: String?, base: String?): String? {
6363
}
6464

6565
return value.trimEnd('\n') + "\n\n" + base
66-
}
66+
}
67+
68+
fun Client.toLogString() =
69+
"$lastName $firstName ($id)"
53.1 KB
Loading
282 KB
Loading
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
if ("serviceWorker" in navigator) {
2+
navigator.serviceWorker.register("/js/pwa/sw.js").then(
3+
(registration) => {
4+
console.log("Service worker registration successful:", registration);
5+
},
6+
(error) => {
7+
console.error(`Service worker registration failed: ${error}`);
8+
},
9+
);
10+
} else {
11+
console.error("Service workers are not supported.");
12+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const CACHE_NAME = 'trainer-advisor-v1';
2+
const OFFLINE_URL = '/offline.html';
3+
4+
const urlsToCache = [
5+
'/',
6+
OFFLINE_URL,
7+
'/styles/styles-qyoga.css',
8+
'/vendor/bootstrap/css/bootstrap.css',
9+
'/vendor/bootstrap/js/bootstrap.bundle.min.js',
10+
'/img/icon.png',
11+
'/img/icon-192x192.png',
12+
'/img/icon-512x512.png',
13+
'/manifest.json'
14+
];
15+
16+
// Install service worker and cache assets
17+
self.addEventListener('install', event => {
18+
event.waitUntil(
19+
caches.open(CACHE_NAME)
20+
.then(cache => {
21+
console.log('Opened cache');
22+
return cache.addAll(urlsToCache);
23+
})
24+
);
25+
});
26+
27+
// Serve cached content when offline
28+
self.addEventListener('fetch', event => {
29+
// For all other requests, go to the cache first, and then the network.
30+
event.respondWith(
31+
(async () => {
32+
const cache = await caches.open(CACHE_NAME);
33+
const cachedResponse = await cache.match(event.request.url);
34+
if (cachedResponse) {
35+
// Return the cached response if it's available.
36+
return cachedResponse;
37+
}
38+
// If resource isn't in the cache, return a 404.
39+
return new Response(null, {status: 404});
40+
})(),
41+
);
42+
});
43+
44+
// Clean up old caches
45+
self.addEventListener('activate', event => {
46+
const cacheWhitelist = [CACHE_NAME];
47+
event.waitUntil(
48+
caches.keys().then(cacheNames => {
49+
return Promise.all(
50+
cacheNames.map(cacheName => {
51+
if (cacheWhitelist.indexOf(cacheName) === -1) {
52+
return caches.delete(cacheName);
53+
}
54+
})
55+
);
56+
})
57+
);
58+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "Trainer Advisor",
3+
"short_name": "TrainerAdvisor",
4+
"description": "Приложение для управления терапевтическими программами",
5+
"start_url": "/",
6+
"scope": "/",
7+
"display": "standalone",
8+
"background_color": "#e6e6e6",
9+
"theme_color": "#e6e6e6",
10+
"icons": [
11+
{
12+
"src": "/img/icon-192x192.png",
13+
"sizes": "192x192",
14+
"type": "image/png",
15+
"purpose": "any maskable"
16+
},
17+
{
18+
"src": "/img/icon-512x512.png",
19+
"sizes": "512x512",
20+
"type": "image/png",
21+
"purpose": "any maskable"
22+
}
23+
]
24+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE html>
2+
<html lang="ru">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta content="width=device-width, initial-scale=1.0" name="viewport">
6+
<title>Trainer Advisor - Оффлайн</title>
7+
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"/>
8+
9+
<script defer src="/vendor/fontawesome-6.5.2/js/fontawesome.min.js"></script>
10+
<link href="/vendor/fontawesome-6.5.2/css/all.min.css" rel="stylesheet">
11+
12+
<script src="/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
13+
<link href="/vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
14+
<style>
15+
body {
16+
display: flex;
17+
justify-content: center;
18+
align-items: center;
19+
height: 100vh;
20+
background-color: #f8f9fc;
21+
text-align: center;
22+
padding: 20px;
23+
}
24+
25+
.offline-container {
26+
max-width: 500px;
27+
padding: 2rem;
28+
border-radius: 10px;
29+
background: white;
30+
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
31+
}
32+
</style>
33+
</head>
34+
<body>
35+
<div class="offline-container">
36+
<div class="offline-icon">
37+
<i class="bi bi-wifi-off"></i>
38+
</div>
39+
<h1>Вы находитесь оффлайн</h1>
40+
<p>К сожалению, эта страница недоступна без подключения к интернету. Пожалуйста, проверьте ваше интернет-соединение
41+
и попробуйте снова.</p>
42+
<a class="btn btn-primary" onclick="window.location.href='/';">Вернуться на главную</a>
43+
</div>
44+
45+
</body>
46+
</html>

app/src/main/resources/static/styles/styles-qyoga.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,14 @@ input[type=number] {
124124
.w-sm-auto {
125125
width: auto !important;
126126
}
127+
}
128+
129+
/* "мобилизация" табов в карточке клиента */
130+
.nav-tabs .nav-link.active {
131+
border: 0;
132+
border-bottom: 2px solid #20c997 !important;
133+
}
134+
135+
.nav-tabs .nav-link:hover {
136+
border-color: transparent;
127137
}

0 commit comments

Comments
 (0)