Додані повідомлення та перепрацьована структура застосунку та api

This commit is contained in:
2026-03-15 00:25:10 +02:00
parent 85483b85bb
commit 4bc9c11512
101 changed files with 5763 additions and 2546 deletions

View File

@@ -1,6 +1,5 @@
const CONFIG = {
"web": "https://test.sheep-service.com/",
"api": "https://test.sheep-service.com/api/",
"wss": "wss://test.sheep-service.com/ws",
"metrics": "wss://test.sheep-service.com/metrics"
"wss": "wss://test.sheep-service.com/ws"
}

View File

@@ -1,4 +1,42 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto/Roboto-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto/Roboto-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto/Roboto-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto/Roboto-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Roboto';
src: url('../fonts/Roboto/Roboto-Black.ttf') format('truetype');
font-weight: 900;
font-style: normal;
font-display: swap;
}
:root {
--FontSize1: 12px;
@@ -28,13 +66,14 @@
/* BGColor */
--ColorThemes0: #fbfbfb;
--ColorThemes1: #f3f3f3;
--ColorThemes2: #dbdbd1;
--ColorThemes2: #e7e7e1;
/* TextColor */
--ColorThemes3: #313131;
--ColorAnimation: linear-gradient(90deg, #f3f3f3, #efefef, #f3f3f3);
--shadow-l1: 0px 2px 4px rgba(0, 0, 0, 0.02), 0px 0px 2px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04);
/* --shadow-l1: 0px 2px 4px rgba(0, 0, 0, 0.02), 0px 0px 2px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04); */
--shadow-l1: 0;
--border-radius: 15px;
@@ -51,13 +90,14 @@
/* BGColor */
--ColorThemes0: #1c1c19;
--ColorThemes1: #21221d;
--ColorThemes2: #525151;
--ColorThemes2: #3d3c3c;
/* TextColor */
--ColorThemes3: #f3f3f3;
--ColorAnimation: linear-gradient(90deg, #21221d, #242520, #21221d);
--shadow-l1: 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04);
/* --shadow-l1: 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04); */
--shadow-l1: 0;
--border-radius: 15px;
@@ -81,6 +121,16 @@
cursor: no-drop !important;
}
@media(hover: hover) {
a:hover,
button:hover,
select:hover,
input:hover,
textarea:hover {
opacity: 0.8;
}
}
@media (min-width: 800px) {
* {
scroll-snap-type: none !important;
@@ -238,314 +288,6 @@ body.modal-open {
overflow: hidden;
}
/* Банер з прохання встановлення PWA */
#blur-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
z-index: 9998;
}
.pwa-overlay {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.pwa-overlay>.popup {
background: var(--ColorThemes0);
padding: 24px 32px;
border-radius: var(--border-radius);
max-width: 90%;
width: 320px;
text-align: center;
font-family: sans-serif;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
animation: fadeIn 0.3s ease-out;
display: flex;
flex-direction: column;
align-items: center;
}
.pwa-overlay>.popup h2 {
margin-bottom: 12px;
color: var(--ColorThemes3);
opacity: 0.8;
}
.pwa-overlay>.popup p {
margin-bottom: 10px;
color: var(--ColorThemes3);
opacity: 0.6;
}
.pwa-overlay>.popup ol {
text-align: justify;
font-size: var(--FontSize4);
margin-bottom: 10px;
max-width: 290px;
}
.pwa-overlay>.popup li {
list-style-type: none;
font-size: var(--FontSize3);
}
.pwa-overlay>.popup li span {
vertical-align: middle;
display: inline-block;
width: 22px;
height: 22px;
}
.pwa-overlay>.popup li span svg {
fill: var(--PrimaryColor);
}
.pwa-overlay>.popup>div {
margin-top: 10px;
display: flex;
justify-content: center;
gap: 10px;
}
.pwa-overlay>.popup>div>button {
padding: 8px 16px;
border: none;
border-radius: calc(var(--border-radius) - 8px);
cursor: pointer;
font-size: var(--FontSize3);
}
#pwa-install-button {
background-color: var(--PrimaryColor);
color: var(--PrimaryColorText);
}
#pwa-close-button,
#pwa-ios-close-button {
background-color: #ccc;
color: #333;
}
.pwa-hidden {
display: none;
}
@media (max-width: 450px) {
.pwa-overlay>.popup {
padding: 17px 10px;
}
.pwa-overlay>.popup h2 {
font-size: 22px;
}
.pwa-overlay>.popup p {
font-size: var(--FontSize4);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
#swipe_updater {
position: absolute;
top: 0px;
width: 100%;
}
#swipe_block {
width: calc(100% - 252px);
margin-left: 252px;
height: 50px;
display: flex;
justify-content: center;
align-items: flex-end;
position: relative;
}
#swipe_icon {
width: 20px;
fill: var(--ColorThemes3);
transform: rotate(0deg);
position: absolute;
margin-top: -45px;
top: -45px;
background: var(--ColorThemes2);
border: 2px solid var(--ColorThemes3);
border-radius: 50%;
padding: 10px;
display: flex;
overflow: hidden;
height: 0;
opacity: 0;
transition: height 0ms 400ms, opacity 400ms 0ms;
}
#swipe_icon[data-state="active"] {
height: 20px;
opacity: 1;
transition: height 0ms 0ms, opacity 400ms 0ms;
}
@media (max-width: 1100px) {
#swipe_block {
width: calc(100% - 122px);
margin-left: 122px;
}
}
@media (max-width: 700px) {
#swipe_block {
width: 100%;
margin-left: 0;
}
}
/* Стили для меню */
#navigation {
position: fixed;
width: 230px;
height: calc(100vh - 60px);
min-height: 510px;
background: var(--ColorThemes2);
padding: 36px 10px;
-webkit-transition: width .2s ease 0s;
-o-transition: width .2s ease 0s;
transition: width .2s ease 0s;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#navigation>nav {
display: flex;
flex-direction: column;
height: 290px;
align-items: center;
justify-content: flex-start;
}
#navigation>nav>li {
width: 180px;
height: 50px;
list-style-type: none;
position: relative;
-webkit-transition: all .2s ease 0s;
-o-transition: all .2s ease 0s;
transition: all .2s ease 0s;
z-index: 1;
margin: 5px;
}
#navigation>nav>li>div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
height: 50px;
padding: 0 10px;
border-radius: var(--border-radius);
-webkit-transition: all .2s ease 0s;
-o-transition: all .2s ease 0s;
transition: all .2s ease 0s;
opacity: 0.8;
cursor: pointer;
border: 2px;
border: 2px solid var(--ColorThemes2);
overflow: hidden;
}
#navigation>nav>li>div[data-state="active"] {
background: var(--ColorThemes3);
border: 2px solid var(--ColorThemes3);
box-shadow: var(--shadow-l1);
}
#navigation>nav>li:has(div[data-state="active"]) {
z-index: 10;
}
#navigation>nav>li>div>svg {
width: 25px;
height: 25px;
min-width: 25px;
min-height: 25px;
fill: var(--ColorThemes3);
}
#navigation>nav>li>div[data-state="active"] svg {
fill: var(--ColorThemes2);
}
#navigation>nav>li>div>b {
margin-left: 15px;
font-size: var(--FontSize3);
font-weight: 300;
color: var(--ColorThemes3);
white-space: nowrap;
}
#navigation>nav>li>div[data-state="active"] b {
color: var(--ColorThemes2);
}
#navigation>nav>li>a {
position: absolute;
width: 100%;
height: 100%;
top: 2px;
}
@media (hover: hover) {
#navigation>nav>li:hover {
transform: scale(1.01);
}
#navigation>nav>li:hover>div {
border: 2px solid var(--ColorThemes3);
}
}
@media (max-width: 1100px) {
#navigation {
width: 100px;
}
#navigation>nav>li {
width: 50px;
}
#navigation>nav>li>div {
width: 30px;
justify-content: center;
}
#navigation>nav>li>div>b {
display: none;
}
}
#app {
background: var(--ColorThemes0);
position: absolute;
@@ -659,7 +401,7 @@ body.modal-open {
}
.leaflet-popup-content {
margin: 8px 10px !important;
margin: 10px 10px !important;
padding: 0 !important;
}

View File

@@ -1,94 +0,0 @@
Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public),
with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,96 +0,0 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

View File

@@ -41,12 +41,16 @@
<!-- Конфигурация SW -->
<script src="/sw.js"></script>
<!-- Кастомні елементи -->
<script src="/lib/customElements/notification.js" defer></script>
<script src="/lib/customElements/territoryCard.js" defer></script>
<link rel="stylesheet" href="/css/main.css" />
<!-- Кастомні елементи -->
<script src="/lib/customElements/notifManager.js" defer></script>
<script src="/lib/customElements/pwaInstallBanner.js" defer></script>
<script src="/lib/customElements/territoryCard.js" defer></script>
<script src="/lib/customElements/swipeUpdater.js" defer></script>
<script src="/lib/customElements/menuContainer.js" defer></script>
<script src="/lib/customElements/smartSelect.js" defer></script>
<script src="/config.js" defer></script>
<script src="/lib/router/router.js" defer></script>
@@ -63,12 +67,11 @@
<script src="/lib/components/cloud.js" defer></script>
<script src="/lib/components/metrics.js" defer></script>
<!-- <script src="/lib/components/metrics.js" defer></script> -->
<script src="/lib/components/clipboard.js" defer></script>
<script src="/lib/components/colorGroup.js" defer></script>
<script src="/lib/components/makeid.js" defer></script>
<script src="/lib/components/swipeUpdater.js" defer></script>
<script src="/lib/components/detectBrowser.js" defer></script>
<script src="/lib/components/detectOS.js" defer></script>
<script src="/lib/components/formattedDate.js" defer></script>
@@ -130,148 +133,28 @@
</head>
<body>
<!-- Банер з прохання встановлення PWA -->
<div id="blur-backdrop" class="pwa-hidden"></div>
<div id="pwa-install-overlay" class="pwa-overlay pwa-hidden">
<div class="popup">
<h2>Встановити застосунок?</h2>
<p>Додайте його на головний екран для швидкого доступу.</p>
<div>
<button id="pwa-install-button">Встановити</button>
<button id="pwa-close-button">Пізніше</button>
</div>
</div>
</div>
<div id="pwa-ios-overlay" class="pwa-overlay pwa-hidden">
<div class="popup">
<h2>Встановлення застосунку</h2>
<p>Щоб встановити застосунок, виконайте наступні кроки:</p>
<pwa-install-banner></pwa-install-banner>
<ol>
<li>1. Відкрийте посилання в браузері Safari.</li>
<li>
2. Натисніть кнопку
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M 14.984375 1 A 1.0001 1.0001 0 0 0 14.292969 1.2929688 L 10.292969 5.2929688 A 1.0001 1.0001 0 1 0 11.707031 6.7070312 L 14 4.4140625 L 14 17 A 1.0001 1.0001 0 1 0 16 17 L 16 4.4140625 L 18.292969 6.7070312 A 1.0001 1.0001 0 1 0 19.707031 5.2929688 L 15.707031 1.2929688 A 1.0001 1.0001 0 0 0 14.984375 1 z M 9 9 C 7.3550302 9 6 10.35503 6 12 L 6 24 C 6 25.64497 7.3550302 27 9 27 L 21 27 C 22.64497 27 24 25.64497 24 24 L 24 12 C 24 10.35503 22.64497 9 21 9 L 19 9 L 19 11 L 21 11 C 21.56503 11 22 11.43497 22 12 L 22 24 C 22 24.56503 21.56503 25 21 25 L 9 25 C 8.4349698 25 8 24.56503 8 24 L 8 12 C 8 11.43497 8.4349698 11 9 11 L 11 11 L 11 9 L 9 9 z"
/>
</svg>
</span>
в нижній частині екрана Safari.
</li>
<li>
3. У меню, що з’явиться, виберіть
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M 6 3 C 4.3550302 3 3 4.3550302 3 6 L 3 18 C 3 19.64497 4.3550302 21 6 21 L 18 21 C 19.64497 21 21 19.64497 21 18 L 21 6 C 21 4.3550302 19.64497 3 18 3 L 6 3 z M 6 5 L 18 5 C 18.56503 5 19 5.4349698 19 6 L 19 18 C 19 18.56503 18.56503 19 18 19 L 6 19 C 5.4349698 19 5 18.56503 5 18 L 5 6 C 5 5.4349698 5.4349698 5 6 5 z M 11.984375 6.9863281 A 1.0001 1.0001 0 0 0 11 8 L 11 11 L 8 11 A 1.0001 1.0001 0 1 0 8 13 L 11 13 L 11 16 A 1.0001 1.0001 0 1 0 13 16 L 13 13 L 16 13 A 1.0001 1.0001 0 1 0 16 11 L 13 11 L 13 8 A 1.0001 1.0001 0 0 0 11.984375 6.9863281 z"
/>
</svg>
</span>
«На Початковий екран».
</li>
</ol>
<div>
<button id="pwa-ios-close-button">Зрозуміло</button>
</div>
</div>
</div>
<app-notification-container
id="notif-manager"
position="top-right"
max-visible="5"
timeout="4000">
</app-notification-container>
<notification-container
id="notif-manager"
position="top-right"
max-visible="5"
timeout="4000"
>
</notification-container>
<!-- Анімація оновлення сторінки свайпом -->
<div id="swipe_updater">
<div id="swipe_block">
<svg
id="swipe_icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
data-state="active"
>
<path
d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z"
></path>
</svg>
</div>
</div>
<swipe-updater></swipe-updater>
<!-- Меню застосунку -->
<div id="navigation">
<nav>
<li>
<div id="nav-home">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M9 2H4C2.897 2 2 2.897 2 4v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C11 2.897 10.103 2 9 2zM20 2h-5c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C22 2.897 21.103 2 20 2zM9 15H4c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-3C11 15.897 10.103 15 9 15zM20 11h-5c-1.103 0-2 .897-2 2v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-7C22 11.897 21.103 11 20 11z"
/>
</svg>
<b>Головна</b>
</div>
<a href="/" data-route></a>
</li>
<li id="li-territory" style="display: none">
<div id="nav-territory">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"
/>
</svg>
<b>Території</b>
</div>
<a href="/territory" data-route></a>
</li>
<li id="li-sheeps" style="display: none">
<div id="nav-sheeps">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z"
/>
</svg>
<b>Вісники</b>
</div>
<a href="/sheeps" data-route></a>
</li>
<li id="li-schedule" style="display: none">
<div id="nav-schedule">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"
/>
</svg>
<b>Графік зібрань</b>
</div>
<a href="/schedule" data-route></a>
</li>
<li id="li-stand" style="display: none">
<div id="nav-stand">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"
/>
</svg>
<b>Графік стенду</b>
</div>
<a href="/stand" data-route></a>
</li>
<li id="li-options" style="display: none">
<div id="nav-options">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172">
<path
d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"
></path>
</svg>
<b>Опції</b>
</div>
<a href="/options" data-route></a>
</li>
</nav>
</div>
<navigation-container id="main-nav" data-os="ios">
<nav-item
id="menu-home"
title="Головна"
icon='<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M9 2H4C2.897 2 2 2.897 2 4v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C11 2.897 10.103 2 9 2zM20 2h-5c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C22 2.897 21.103 2 20 2zM9 15H4c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-3C11 15.897 10.103 15 9 15zM20 11h-5c-1.103 0-2 .897-2 2v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-7C22 11.897 21.103 11 20 11z" /> </svg>'
href="/"
></nav-item>
</navigation-container>
<!-- Блок контенту застосунка -->
<div id="app"></div>

View File

@@ -4,6 +4,7 @@ let swRegistration = null;
// Реєструємо CustomElements
const Notifier = document.getElementById('notif-manager');
const Updater = document.querySelector('swipe-updater');
// Определение ID главного блока
let app = document.getElementById('app');
@@ -13,32 +14,23 @@ Router.config({ mode: 'history' });
async function appReload() {
location.reload();
// location.reload();
// Router.navigate(window.location.pathname, false).check();
// Router.check().listen().delegateLinks();
// // Закрытие старого соединения WebSocket
// if (socket) socket.close(1000, "Перезапуск соединения");
// if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання");
// Cloud.start();
// listEntrances = []
// listApartment = []
}
// Перевизначення функції, яка викликається при "свайпі"
// Updater.setReloadFunction(() => appReload());
// Функция загрузки приложения
window.addEventListener('load', async function () {
console.log('[OS] ', detectOS());
if (window.matchMedia('(display-mode: standalone)').matches) {
if (detectOS() == 'Android') {
document.getElementById('navigation').dataset.state = '';
} else if (detectOS() == 'iOS') {
document.getElementById('navigation').dataset.state = 'ios';
localStorage.setItem('backToTop', 'false');
} else if (detectOS() == 'MacOS') {
document.getElementById('navigation').dataset.state = 'ios';
localStorage.setItem('backToTop', 'false');
} else {
document.getElementById('navigation').dataset.state = '';
}
}
if (Router.getParams().uuid) {
localStorage.setItem("uuid", Router.getParams().uuid)
@@ -65,14 +57,47 @@ window.addEventListener('load', async function () {
console.log("[APP] USER Info: ", USER);
if (USER.possibilities.can_view_sheeps) document.getElementById("li-sheeps").style.display = "";
if (USER.possibilities.can_view_schedule) document.getElementById("li-schedule").style.display = "";
if (USER.possibilities.can_manager_territory) document.getElementById("li-territory").style.display = "";
if (USER.possibilities.can_view_stand) document.getElementById("li-stand").style.display = "";
document.getElementById("li-options").style.display = "";
if (USER.possibilities.can_view_sheeps) await Sheeps.sheeps_list.loadAPI();
if (USER.possibilities.can_view_stand) {
newMenuItems({
id: 'menu-stand',
title: 'Графік стенду',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"/></svg>`,
href: '/stand'
});
}
if (USER.possibilities.can_view_schedule) {
newMenuItems({
id: 'menu-schedule',
title: 'Графіки зібрань',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"/></svg>`,
href: '/schedule'
});
}
if (USER.possibilities.can_view_sheeps) {
newMenuItems({
id: 'menu-sheeps',
title: 'Вісники',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z" /></svg>`,
href: '/sheeps',
hidden: true
});
await Sheeps.sheeps_list.loadAPI();
}
if (USER.possibilities.can_manager_territory) {
newMenuItems({
id: 'menu-territory',
title: 'Території',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"/></svg>`,
href: '/territory',
hidden: true
});
}
newMenuItems({
id: 'menu-options',
title: 'Опції',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172"><path d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"/></svg>`,
href: '/options'
});
if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання");
Cloud.start();
@@ -80,11 +105,19 @@ window.addEventListener('load', async function () {
editFontStyle();
Router.check().listen().delegateLinks();
setupFrontendMetrics();
}
});
function newMenuItems({ id, title, icon, href, hidden=false }) {
const newItem = document.createElement('nav-item');
newItem.setAttribute('id', id);
newItem.setAttribute('title', title);
newItem.setAttribute('icon', icon);
newItem.setAttribute('href', href);
newItem.setAttribute('data-hidden', hidden);
document.querySelector('navigation-container').appendChild(newItem);
}
let offlineNode = null;
window.addEventListener("offline", () => {
console.log("[APP] Інтернет зник");
@@ -104,9 +137,6 @@ window.addEventListener("online", () => {
title: 'Онлайн',
text: 'Інтернет знову працює'
}, { timeout: 3000 });
if (Cloud.socket) Cloud.socket.close(1000, "Перезапуск з'єднання");
Cloud.start();
});
function editFontStyle() {
@@ -154,52 +184,6 @@ function applyFontMode(mode) {
});
}
// Банер з прохання встановлення PWA
let deferredPrompt;
const isInStandaloneMode = () =>
('standalone' in window.navigator && window.navigator.standalone === true);
if (detectOS() == 'iOS' && !isInStandaloneMode()) {
setTimeout(() => {
document.getElementById('blur-backdrop').classList.remove('pwa-hidden');
document.getElementById('pwa-ios-overlay').classList.remove('pwa-hidden');
document.body.classList.add('modal-open');
}, 1000);
}
window.addEventListener("beforeinstallprompt", (e) => {
if (localStorage.getItem('modal') != "false") {
e.preventDefault();
deferredPrompt = e;
document.getElementById("blur-backdrop").classList.remove("pwa-hidden");
document.getElementById("pwa-install-overlay").classList.remove("pwa-hidden");
document.body.classList.add("modal-open");
}
});
document.getElementById("pwa-install-button").addEventListener("click", async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`[APP] Результат встановлення PWA: ${outcome}`);
closePopup();
});
document.getElementById("pwa-close-button").addEventListener("click", closePopup);
document.getElementById('pwa-ios-close-button').addEventListener('click', closePopup);
function closePopup() {
document.getElementById("pwa-install-overlay").classList.add("pwa-hidden");
document.getElementById("blur-backdrop").classList.add("pwa-hidden");
document.getElementById('pwa-ios-overlay').classList.add('pwa-hidden');
document.body.classList.remove("modal-open");
deferredPrompt = null;
}
if ('serviceWorker' in navigator) {
let refreshing = false;
let updateNode = null;

View File

@@ -1,5 +1,5 @@
clipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => alert("Посилання скопійовано!"))
.then(() => Notifier.success("Посилання скопійовано!", {timeout: 2000}))
.catch(err => console.error(err))
}

View File

@@ -9,8 +9,6 @@ const Cloud = {
Cloud.status = 'sync';
const uuid = localStorage.getItem("uuid");
if(!navigator.onLine) alert("[APP] Інтернет з'єднання відсутнє!")
if (Cloud.socket && Cloud.socket.readyState <= 1) return;
const ws = new WebSocket(CONFIG.wss, uuid);
@@ -79,13 +77,18 @@ const Cloud = {
} else {
Cloud.reconnecting = false;
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Cloud.reconnecting = true;
Cloud.reconnectAttempts = 0;
Cloud.start();
} else {
console.warn("[WebSocket] Перепідключення відмінено користувачем");
}
Notifier.click({
title: `З'єднання розірвано!`,
text: `Натисніть, щоб перепідключитись!`
}, {
type: 'warn',
f: () => {
Cloud.reconnecting = true;
Cloud.reconnectAttempts = 0;
Cloud.start();
},
timeout: 0
});
}
};

View File

@@ -3,7 +3,6 @@ const RECONNECT_INTERVAL = 3000;
let isConnectedMetrics = false;
function setupFrontendMetrics() {
console.log("[Metrics] Спроба підключення до метрик...");
mws = new WebSocket(CONFIG.metrics);
mws.onopen = () => {

View File

@@ -1,40 +0,0 @@
// Скрипт перезагрузки страници свайпом.
let app_scroll = false;
let animID = document.getElementById('swipe_updater');
let animIconID = document.getElementById('swipe_icon');
window.addEventListener('scroll', function(e) {
if (window.matchMedia('(display-mode: standalone)').matches) {
let a = window.scrollY;
let b = 50;
let c = 125;
a = -a;
a = a;
animIconID.style.top = a/1.5;
console.log(window.scrollY);
if(window.scrollY <= -10){
animID.style.zIndex = 115;
} else {
animID.style.zIndex = 0;
}
if(window.scrollY <= -120){
if(app_scroll == false){
app_scroll = true;
animIconID.style.transform = 'rotate(180deg)';
animIconID.setAttribute('data-state', '')
}
} else if(window.scrollY >= 0){
if(app_scroll == true){
appReload();
app_scroll = false;
animIconID.style.transform = 'rotate(0deg)';
animIconID.setAttribute('data-state', 'active')
}
}
}
});

View File

@@ -0,0 +1,511 @@
// =========================================================
// Клас 1: Пункт Меню (nav-item)
// =========================================================
class NavigationItem extends HTMLElement {
// 1. Відстежувані атрибути
static get observedAttributes() {
return ['title', 'icon', 'href', 'click'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Створюємо базову структуру елементів
this.renderBaseStructure();
}
/**
* Створює базовий DOM та стилі.
*/
renderBaseStructure() {
const shadow = this.shadowRoot;
shadow.innerHTML = '';
const style = document.createElement('style');
style.textContent = `
.item-wrapper{
width: 184px;
height: 54px;
list-style-type: none;
position: relative;
-webkit-transition: width .2s ease 0s;
-o-transition: width .2s ease 0s;
transition: width .2s ease 0s;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-item {
width: 100%;
height: 50px;
padding: 0 12px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
border-radius: var(--border-radius, 15px);
-webkit-transition: width .2s ease 0s;
-o-transition: width .2s ease 0s;
transition: width .2s ease 0s;
opacity: 0.8;
cursor: pointer;
border: 2px;
border: 2px solid var(--ColorThemes2, #525151);
color: var(--ColorThemes3, #f3f3f3);
overflow: hidden;
text-decoration: none;
gap: 15px;
}
.nav-icon-img, .nav-icon-wrapper {
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
}
.nav-icon-wrapper svg {
width: 25px;
height: 25px;
min-width: 25px;
min-height: 25px;
fill: currentColor;
}
.nav-title {
font-size: var(--FontSize3, 14px);
font-weight: 300;
white-space: nowrap;
}
:host([data-state="active"]) .nav-item {
color: var(--ColorThemes2, #525151);
background: var(--ColorThemes3, #f3f3f3);
border: 2px solid var(--ColorThemes3, #f3f3f3);
box-shadow: var(--shadow-l1, 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04));
}
@media (hover: hover) {
.nav-item:hover {
border: 2px solid var(--ColorThemes3);
}
}
@media (max-width: 1100px) {
.item-wrapper{
width: 54px;
}
.nav-item{
width: 50px;
}
.nav-title {
display: none;
}
}
@media (max-width: 700px), (max-height: 540px) {
.item-wrapper{
width: 40px;
height: 40px;
}
.nav-item {
width: 40px;
height: 40px;
padding: 0;
border: 0;
justify-content: center;
background: transparent;
color: var(--ColorThemes0, #1c1c19);
border-radius: 50%;
}
:host([data-state="active"]) .nav-item {
color: var(--PrimaryColor, #cb9e44);
background: transparent;
border: 0;
box-shadow: none;
}
.nav-title {
display: none;
}
@media (hover: hover) {
.nav-item:hover {
border: 0;
}
}
}
`;
shadow.appendChild(style);
// Створюємо порожній контейнер, який буде замінено на <div> або <a>
this.containerWrapper = document.createElement('div');
this.containerWrapper.setAttribute('class', 'item-wrapper');
shadow.appendChild(this.containerWrapper);
// Первинне заповнення контентом
this.updateContent();
}
connectedCallback() {
// Обробник кліку додається після того, як this.itemElement створено в updateContent
this.containerWrapper.addEventListener('click', this.handleItemClick.bind(this));
}
disconnectedCallback() {
this.containerWrapper.removeEventListener('click', this.handleItemClick.bind(this));
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.updateContent();
}
}
/**
* Зчитує атрибути та оновлює внутрішній HTML і тег-контейнер.
*/
updateContent() {
const title = this.getAttribute('title') || 'Без назви';
const iconContent = this.getAttribute('icon');
const href = this.getAttribute('href');
// 1. Формуємо HTML для іконки та заголовка
let iconHTML = '';
if (iconContent) {
const trimmedIcon = iconContent.trim();
const isSVG = trimmedIcon.startsWith('<svg') && trimmedIcon.endsWith('</svg>');
if (isSVG) {
iconHTML = `<span class="nav-icon-wrapper">${trimmedIcon}</span>`;
} else {
iconHTML = `<img src="${iconContent}" alt="${title} icon" class="nav-icon-img">`;
}
}
const innerContent = `${iconHTML}<span class="nav-title">${title}</span>`;
// 2. Визначаємо, який тег використовувати (<a> чи <div>)
const currentTag = this.containerWrapper.firstChild ? this.containerWrapper.firstChild.tagName.toLowerCase() : null;
const requiredTag = href ? 'a' : 'div';
// Якщо тип тега потрібно змінити, створюємо новий елемент
if (currentTag !== requiredTag) {
// Створюємо новий елемент <a> або <div>
this.itemElement = document.createElement(requiredTag);
this.itemElement.setAttribute('class', 'nav-item');
// Замінюємо старий елемент новим
this.containerWrapper.innerHTML = '';
this.containerWrapper.appendChild(this.itemElement);
} else {
// Елемент вже правильний, використовуємо його
this.itemElement = this.containerWrapper.firstChild;
}
// 3. Встановлюємо атрибути
this.itemElement.innerHTML = innerContent;
if (href) {
// Це посилання: встановлюємо href та data-route
this.itemElement.setAttribute('href', href);
this.itemElement.setAttribute('data-route', href); // <-- ДОДАНО data-route
this.itemElement.removeAttribute('role');
this.itemElement.removeAttribute('tabindex');
} else {
// Це кнопка: видаляємо посилальні атрибути та встановлюємо роль
this.itemElement.removeAttribute('href');
this.itemElement.removeAttribute('data-route');
this.itemElement.setAttribute('role', 'button');
this.itemElement.setAttribute('tabindex', '0');
}
}
/**
* Обробляє клік, виконуючи код з атрибута 'click' для не-посилань.
*/
handleItemClick(event) {
const clickAction = this.getAttribute('click');
const href = this.getAttribute('href');
if (href) {
// Якщо це тег <a>, дозволяємо браузеру обробляти клік (або JS-роутеру)
return;
}
if (clickAction) {
try {
event.preventDefault(); // Запобігаємо стандартній дії (якщо була встановлена роль кнопки)
console.log(`Executing click action: ${clickAction}`);
eval(clickAction);
} catch (e) {
console.error(`Error executing click action "${clickAction}":`, e);
}
}
}
}
// =========================================================
// Клас 2: Контейнер Меню (navigation-container)
// =========================================================
class NavigationContainer extends HTMLElement {
/**
* Обробляє клік на документі, щоб приховати меню, якщо клік був за його межами.
*/
handleOutsideClick = (event) => {
// Перевіряємо, чи містить наш компонент елемент, на який клікнули
// (this.shadowRoot.host - це сам <navigation-container>)
// або this.menuContainer.contains(event.target)
if (!this.contains(event.target) && !this.shadowRoot.contains(event.target)) {
// Клік був за межами компонента
this.hideHiddenMenu();
}
}
handleScroll = () => {
this.hideHiddenMenu();
}
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
this.standalone = window.matchMedia('(display-mode: standalone)').matches;
this.os = this.detectOS();
// Стилі контейнера
const style = document.createElement('style');
style.textContent = `
.navigation-menu{
position: fixed;
width: 230px;
height: calc(100vh - 60px);
min-height: 510px;
background: var(--ColorThemes2, #525151);
margin: 0;
padding: 40px 10px;
-webkit-transition: width .2s ease 0s;
-o-transition: width .2s ease 0s;
transition: width .2s ease 0s;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.navigation-items,
.navigation-items-hidden {
position: relative;
display: flex;
flex-direction: column;
min-height: 55px;
align-items: center;
justify-content: flex-start;
gap: 6px;
border-radius: 30px;
}
.navigation-items-hidden {
margin-top: 5px;
}
.more-button-item {
display: none;
}
@media (max-width: 1100px) {
.navigation-menu {
width: 100px;
}
}
@media (max-width: 700px), (max-height: 540px) {
.navigation-menu {
width: calc(100% - 30px);
height: 60px;
min-height: 60px;
padding: 0;
z-index: 9991;
bottom: 0px;
background: transparent;
left: 15px;
border: 0;
margin: 0;
bottom: 0px;
}
.navigation-items {
display: flex;
flex-direction: row;
height: 100%;
justify-content: space-around;
align-items: center;
z-index: 9998;
bottom: 10px;
box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2);
}
.navigation-items::before {
content: "";
position: absolute;
inset: 0;
background: var(--ColorThemes2, #525151);
background: var(--ColorThemes3, #f3f3f3);
opacity: 0.97;
z-index: 0;
border-radius: 30px;
}
.navigation-items-hidden{
flex-direction: row;
height: fit-content;
justify-content: space-around;
align-items: center;
position: absolute;
bottom: 12px;
left: 2px;
width: calc(100% - 4px);
margin: 0;
-webkit-transition: .2s ease 0s;
-o-transition: .2s ease 0s;
transition: .2s ease 0s;
z-index: 9992;
opacity: 0;
}
.navigation-items-hidden::before {
content: "";
position: absolute;
inset: 0;
background: var(--ColorThemes2, #525151);
background: var(--ColorThemes3, #f3f3f3);
opacity: 0.98;
z-index: 0;
border-radius: 30px;
}
.more-button-item {
display: flex;
}
.navigation-menu.expanded .navigation-items-hidden {
bottom: 75px;
opacity: 1;
box-shadow: 0px -4px 10px rgba(0, 0, 0, 0.2);
}
.navigation-menu[data-os="iOS"] .navigation-items {
bottom: 15px;
}
.navigation-menu[data-os="iOS"] .navigation-items-hidden {
bottom: 17px;
}
.navigation-menu[data-os="iOS"].expanded .navigation-items-hidden {
bottom: 80px;
opacity: 1;
}
}
@media (max-width: 700px) {
.navigation-menu {
-webkit-transition: 0s ease 0s;
-o-transition: 0s ease 0s;
transition: 0s ease 0s;
}
}
`;
shadow.appendChild(style);
this.menuContainer = document.createElement('menu');
this.menuContainer.setAttribute('class', 'navigation-menu');
if (this.standalone) this.menuContainer.setAttribute('data-os', this.os);
this.itemsContainer = document.createElement('items');
this.itemsContainer.setAttribute('class', 'navigation-items');
this.itemsHiddenContainer = document.createElement('items-hidden');
this.itemsHiddenContainer.setAttribute('class', 'navigation-items-hidden');
// Слот дозволяє відображати дочірні елементи <nav-item>
const slot = document.createElement('slot');
this.itemsContainer.appendChild(slot);
this.menuContainer.appendChild(this.itemsContainer);
this.menuContainer.appendChild(this.itemsHiddenContainer);
shadow.appendChild(this.menuContainer);
// MutationObserver для відстеження динамічного додавання/видалення пунктів
this.observer = new MutationObserver(this.handleMutations.bind(this));
// Спостерігаємо за зміною дочірніх елементів
this.observer.observe(this, { childList: true, subtree: false });
}
connectedCallback() {
// Додаємо обробник кліків до документа
document.addEventListener('click', this.handleOutsideClick);
// Додаємо обробник прокручування до вікна
window.addEventListener('scroll', this.handleScroll);
// Повторна перевірка елементів на випадок, якщо вони вже були в DOM до реєстрації
this.reassignItems();
}
disconnectedCallback() {
this.observer.disconnect();
// Видаляємо обробник кліків з документа
document.removeEventListener('click', this.handleOutsideClick);
// Видаляємо обробник прокручування з вікна
window.removeEventListener('scroll', this.handleScroll);
}
handleMutations(mutationsList, observer) {
// Логіка оновлення при додаванні/видаленні дочірніх елементів (наприклад, для логування)
this.reassignItems();
}
reassignItems() {
// Отримуємо всі дочірні елементи (які можуть бути nav-item)
const allItems = Array.from(this.children);
allItems.forEach(item => {
// Перевіряємо, чи має елемент атрибут data-hidden
if (item.getAttribute('data-hidden') == 'true') {
this.itemsHiddenContainer.appendChild(item);
this.createMoreButton();
} else if (item.parentNode !== this) {
this.appendChild(item);
}
});
}
// --- Утиліти ---
hideHiddenMenu() {
this.menuContainer.classList.remove('expanded');
}
toggleHiddenMenu() {
this.menuContainer.classList.toggle('expanded');
}
createMoreButton() {
let moreButton = this.itemsContainer.querySelector('.more-button-item');
if (!moreButton) {
const button = document.createElement('nav-item');
button.setAttribute('class', 'more-button-item');
button.setAttribute('title', 'Більше');
button.setAttribute('icon',
`<svg viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>`
);
button.addEventListener('click', this.toggleHiddenMenu.bind(this));
this.itemsContainer.appendChild(button);
return button;
}
}
detectOS() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
return 'iOS';
}
return 'Other';
}
}
// =========================================================
// Реєстрація компонентів у браузері
// =========================================================
customElements.define('nav-item', NavigationItem);
customElements.define('navigation-container', NavigationContainer);

View File

@@ -1,20 +1,28 @@
class AppNotificationContainer extends HTMLElement {
/**
* Клас NotificationContainer
* Веб-компонент для відображення системних сповіщень.
* Використовує Shadow DOM для інкапсуляції стилів.
*/
class NotificationContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Настройки по умолчанию
this._timeout = 4000;
this._maxVisible = 5;
this._position = 'top-right';
this._mobileBottomEnabled = false;
// *** Вдосконалення: Відстежуємо ноди, що видаляються ***
this._removingNodes = new Set();
this._container = document.createElement('div');
this._container.className = 'app-notification-container';
this.shadowRoot.appendChild(this._container);
this._insertStyles();
// SVG іконки для різних типів сповіщень (залишаємо як є)
this._icons = {
info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 15 3 C 13.895 3 13 3.895 13 5 L 13 5.2929688 C 10.109011 6.1538292 8 8.8293311 8 12 L 8 14.757812 C 8 17.474812 6.921 20.079 5 22 A 1 1 0 0 0 4 23 A 1 1 0 0 0 5 24 L 25 24 A 1 1 0 0 0 26 23 A 1 1 0 0 0 25 22 C 23.079 20.079 22 17.474812 22 14.757812 L 22 12 C 22 8.8293311 19.890989 6.1538292 17 5.2929688 L 17 5 C 17 3.895 16.105 3 15 3 z M 3.9550781 7.9882812 A 1.0001 1.0001 0 0 0 3.1054688 8.5527344 C 3.1054688 8.5527344 2 10.666667 2 13 C 2 15.333333 3.1054687 17.447266 3.1054688 17.447266 A 1.0001165 1.0001165 0 0 0 4.8945312 16.552734 C 4.8945312 16.552734 4 14.666667 4 13 C 4 11.333333 4.8945313 9.4472656 4.8945312 9.4472656 A 1.0001 1.0001 0 0 0 3.9550781 7.9882812 z M 26.015625 7.9882812 A 1.0001 1.0001 0 0 0 25.105469 9.4472656 C 25.105469 9.4472656 26 11.333333 26 13 C 26 14.666667 25.105469 16.552734 25.105469 16.552734 A 1.0001163 1.0001163 0 1 0 26.894531 17.447266 C 26.894531 17.447266 28 15.333333 28 13 C 28 10.666667 26.894531 8.5527344 26.894531 8.5527344 A 1.0001 1.0001 0 0 0 26.015625 7.9882812 z M 12 26 C 12 27.657 13.343 29 15 29 C 16.657 29 18 27.657 18 26 L 12 26 z"/></svg>`,
success: `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8' style="width: 17px;height: 17px;"><path d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/></svg>`,
@@ -43,7 +51,6 @@ class AppNotificationContainer extends HTMLElement {
this._timeout = parseInt(this.getAttribute('timeout')) || 4000;
const mobilePosAttr = this.getAttribute('mobile-position');
// Если атрибут установлен в 'bottom' или присутствует (как пустая строка, если это булевый атрибут)
this._mobileBottomEnabled = mobilePosAttr === 'bottom' || mobilePosAttr === '';
this._container.setAttribute('data-position', this._position);
@@ -51,9 +58,6 @@ class AppNotificationContainer extends HTMLElement {
this._applyMobileStyles();
}
/**
* Динамически применяет класс, который активирует мобильные стили "только снизу".
*/
_applyMobileStyles() {
if (this._mobileBottomEnabled) {
this._container.classList.add('mobile-bottom');
@@ -62,15 +66,14 @@ class AppNotificationContainer extends HTMLElement {
}
}
/**
* Публичный метод для изменения настройки мобильной позиции во время выполнения.
* @param {boolean} enable - true, чтобы принудительно устанавливать позицию снизу на мобильных, false, чтобы использовать обычные @media стили.
*/
setMobileBottom(enable) {
this._mobileBottomEnabled = !!enable;
this._applyMobileStyles();
}
/**
* Показує нове сповіщення.
*/
show(message, options = {}) {
const {
type = 'info',
@@ -84,15 +87,28 @@ class AppNotificationContainer extends HTMLElement {
? { title: title || '', text: message }
: message;
while (this._container.children.length >= this._maxVisible) {
const first = this._container.firstElementChild;
if (first) first.remove();
else break;
}
// **Оновлена логіка обмеження кількості:**
// Визначаємо кількість "видимих" нод (які не перебувають у процесі видалення)
const totalChildren = this._container.children.length;
const visibleNodesCount = totalChildren - this._removingNodes.size;
while (visibleNodesCount >= this._maxVisible) {
// Шукаємо найстаріший елемент, який ще НЕ видаляється
const first = Array.from(this._container.children).find(n => !this._removingNodes.has(n));
if (first) {
this._removeNode(first);
} else {
// Якщо всі елементи в процесі видалення, виходимо
break;
}
}
// Кінець оновленої логіки обмеження
// Створення DOM елементів (залишаємо як є)
const node = document.createElement('div');
node.className = `app-notification ${type}`;
if (onClick) node.style.cursor = "pointer"
if (onClick) node.style.cursor = "pointer";
const icon = document.createElement('div');
icon.className = 'icon';
@@ -114,6 +130,7 @@ class AppNotificationContainer extends HTMLElement {
node.appendChild(icon);
node.appendChild(body);
// Додаємо кнопку закриття
if (!onClick && !lock) {
const closeDiv = document.createElement('div');
closeDiv.className = 'blockClose';
@@ -121,28 +138,35 @@ class AppNotificationContainer extends HTMLElement {
const closeBtn = document.createElement('button');
closeBtn.className = 'close';
closeBtn.setAttribute('aria-label', 'Закрыть уведомление');
closeBtn.setAttribute('aria-label', 'Закрити повідомлення');
closeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg>';
closeDiv.appendChild(closeBtn);
closeBtn.addEventListener('click', () => this._removeNode(node));
closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Запобігаємо спрацьовуванню onClick на самій ноді
this._removeNode(node);
});
}
this._container.appendChild(node);
// Запускаємо анімацію появи через requestAnimationFrame
requestAnimationFrame(() => node.classList.add('show'));
let timer = null;
const startTimer = () => {
if (timeout === 0) return;
if (timeout === 0 || lock) return;
timer = setTimeout(() => this._removeNode(node), timeout);
};
const clearTimer = () => { if (timer) { clearTimeout(timer); timer = null; } };
// Зупинка таймауту при наведенні
node.addEventListener('mouseenter', clearTimer);
node.addEventListener('mouseleave', startTimer);
// Обробка кліку на сповіщенні
if (typeof onClick === 'function') {
node.addEventListener('click', () => {
try { onClick(); } catch (e) { }
clearTimer(); // Зупиняємо таймаут, якщо він був
try { onClick(); } catch (e) { console.error(e); }
this._removeNode(node);
});
}
@@ -152,29 +176,50 @@ class AppNotificationContainer extends HTMLElement {
return node;
}
/**
* Приватний метод для видалення ноди з анімацією.
* **Тепер перевіряє, чи нода вже видаляється.**
*/
_removeNode(node) {
if (!node || !node.parentElement) return;
if (!node || !node.parentElement || this._removingNodes.has(node)) return;
this._removingNodes.add(node); // Позначаємо як ту, що видаляється
node.classList.remove('show');
// Чекаємо завершення анімації зникнення (200мс)
setTimeout(() => {
if (node && node.parentElement) node.parentElement.removeChild(node);
if (node && node.parentElement) {
node.parentElement.removeChild(node);
}
this._removingNodes.delete(node); // Видаляємо зі списку після фізичного видалення
}, 200);
}
/**
* Видаляє всі видимі сповіщення.
*/
clearAll() {
if (!this._container) return;
// Використовуємо _removeNode, який тепер безпечно обробляє повторні виклики
Array.from(this._container.children).forEach(n => this._removeNode(n));
}
// Допоміжні методи з фіксованим типом сповіщення
info(message, opts = {}) { return this.show(message, { ...opts, type: 'info' }); }
success(message, opts = {}) { return this.show(message, { ...opts, type: 'success' }); }
warn(message, opts = {}) { return this.show(message, { ...opts, type: 'warn' }); }
error(message, opts = {}) { return this.show(message, { ...opts, type: 'error' }); }
// Метод для сповіщень, що реагують на клік (псевдонім 'click' для 'show' з 'onClick')
click(message, opts = {}) { return this.show(message, { ...opts, onClick: opts.f }); }
/**
* Вставляє необхідні CSS стилі в Shadow DOM (залишаємо як є).
*/
_insertStyles() {
const style = document.createElement('style');
style.textContent = `
/* Контейнер */
.app-notification-container {
position: fixed;
z-index: 9999;
@@ -184,11 +229,13 @@ class AppNotificationContainer extends HTMLElement {
gap: 10px;
padding: 12px;
}
/* Позиціонування контейнера */
.app-notification-container[data-position="top-right"] { top: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="top-left"] { top: 8px; left: 8px; align-items: flex-start; }
.app-notification-container[data-position="bottom-right"] { bottom: 8px; right: 8px; align-items: flex-end; }
.app-notification-container[data-position="bottom-left"] { bottom: 8px; left: 8px; align-items: flex-start; }
/* Одне сповіщення */
.app-notification {
pointer-events: auto;
min-width: 220px;
@@ -230,6 +277,8 @@ class AppNotificationContainer extends HTMLElement {
}
.app-notification .body { flex:1; }
.app-notification .title { font-weight: 600; margin-bottom: 4px; font-size: 13px; }
/* Кнопка закриття */
.app-notification .blockClose {
width: 20px;
height: 20px;
@@ -253,6 +302,7 @@ class AppNotificationContainer extends HTMLElement {
display: block;
}
/* Стилі за типами */
.app-notification.info {
background: var(--ColorThemes3, #2196F3);
color: var(--ColorThemes0, #ffffff);
@@ -273,6 +323,7 @@ class AppNotificationContainer extends HTMLElement {
.app-notification.error .icon { background: #c45050; }
.app-notification.error .close svg{fill: #fff;}
/* Адаптивність для мобільних пристроїв */
@media (max-width: 700px) {
.app-notification-container {
left: 0;
@@ -281,10 +332,10 @@ class AppNotificationContainer extends HTMLElement {
align-items: center !important;
}
.app-notification-container .app-notification {
max-width: 95%;
min-width: 95%;
max-width: calc(100% - 30px);
min-width: calc(100% - 30px);
}
/* Спеціальна мобільна позиція знизу */
.app-notification-container.mobile-bottom {
top: auto;
bottom: 0;
@@ -295,32 +346,62 @@ class AppNotificationContainer extends HTMLElement {
}
}
customElements.define('app-notification-container', AppNotificationContainer);
// Реєструємо веб-компонент у браузері
customElements.define('notification-container', NotificationContainer);
/* <app-notification-container
/*
============================
ПРИКЛАД ВИКОРИСТАННЯ
============================
*/
/*
1. Додайте цей елемент у свій HTML:
<notification-container
id="notif-manager"
position="top-right"
max-visible="5"
timeout="4000"
mobile-position>
</app-notification-container> */
// const Notifier = document.getElementById('notif-manager');
</notification-container>
// 💡 Включить принудительную позицию снизу для мобильных
// Notifier.setMobileBottom(true);
2. Отримайте посилання на компонент у JS:
const Notifier = document.getElementById('notif-manager');
// 💡 Отключить принудительную позицию снизу (вернется к поведению @media или position)
// Notifier.setMobileBottom(false);
3. Приклади викликів:
💡 Базові сповіщення
Notifier.info('Налаштування мобільної позиції змінено.');
Notifier.success('Успішна операція.');
Notifier.warn('Увага: низький рівень заряду батареї.');
Notifier.error('Критична помилка!');
// Пример использования
💡 Сповіщення із заголовком
Notifier.info('Це повідомлення має чіткий заголовок.', {
title: 'Важлива інформація'
});
// Notifier.info('Настройки мобильной позиции изменены.');
// Notifier.info('Привет! Это ваше первое уведомление через Web Component.', {
// title: 'Успешная инициализация',
// onClick: () => alert('Вы кликнули!'),
// lock: false
// });
// Notifier.success('Успешная операция.');
// Notifier.error('Критическая ошибка!', { timeout: 0, lock: true });
// Notifier.warn({ title: `Metrics`, text: `З'єднання встановлено` }, { timeout: 0 });
💡 Сповіщення з об'єктом (заголовок та текст)
Notifier.warn({
title: `Metrics`,
text: `З'єднання встановлено`
});
💡 Сповіщення, яке не зникає (timeout: 0 або lock: true)
Notifier.error('Критична помилка! Необхідне втручання.', {
timeout: 0,
lock: true
});
💡 Сповіщення з обробником кліку (автоматично закривається після кліку)
Notifier.info('Натисніть тут, щоб побачити деталі.', {
onClick: () => alert('Ви клацнули! Дякую.'),
lock: false
});
💡 Програмне керування
Notifier.setMobileBottom(true); // Включити примусову позицію знизу для мобільних
Notifier.setMobileBottom(false); // Вимкнути примусову позицію знизу
Notifier.clearAll(); // Видалити всі сповіщення
*/

View File

@@ -0,0 +1,301 @@
class PwaInstallBanner extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.deferredPrompt = null;
this.STORAGE_KEY = 'PwaInstallBanner'; // Визначаємо ключ localStorage
this.isInStandaloneMode = () => ('standalone' in window.navigator && window.navigator.standalone === true);
this.os = this.detectOS();
}
connectedCallback() {
// Додаємо стилі та розмітку до Shadow DOM
this.shadowRoot.innerHTML = this.getStyles() + this.getTemplate();
this.elements = {
backdrop: this.shadowRoot.getElementById('blur-backdrop'),
installOverlay: this.shadowRoot.getElementById('pwa-install-overlay'),
iosOverlay: this.shadowRoot.getElementById('pwa-ios-overlay'),
installButton: this.shadowRoot.getElementById('pwa-install-button'),
closeButton: this.shadowRoot.getElementById('pwa-close-button'),
iosCloseButton: this.shadowRoot.getElementById('pwa-ios-close-button'),
};
this.setupListeners();
this.checkInitialDisplay();
}
// --- Утиліти ---
detectOS() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
return 'iOS';
}
// ... (можна додати Android, Windows, але для PWA нас цікавить в першу чергу iOS)
return 'Other';
}
shouldShowBanner() {
return localStorage.getItem(this.STORAGE_KEY) !== 'false';
}
checkInitialDisplay() {
if (!this.shouldShowBanner()) {
return; // Не показуємо, якщо localStorage = 'false'
}
// Логіка для iOS
if (this.os === 'iOS' && !this.isInStandaloneMode()) {
// Затримка відображення, як у вихідному коді
setTimeout(() => {
this.openPopup(this.elements.iosOverlay);
}, 1000);
}
}
openPopup(overlayElement) {
this.elements.backdrop.classList.remove('pwa-hidden');
overlayElement.classList.remove('pwa-hidden');
document.body.classList.add('modal-open');
}
closePopup = () => {
this.elements.installOverlay.classList.add('pwa-hidden');
this.elements.iosOverlay.classList.add('pwa-hidden');
this.elements.backdrop.classList.add('pwa-hidden');
document.body.classList.remove('modal-open');
this.deferredPrompt = null;
}
// --- Обробники подій ---
setupListeners() {
window.addEventListener("beforeinstallprompt", this.handleBeforeInstallPrompt);
// Обробники кнопок
this.elements.installButton.addEventListener("click", this.handleInstallClick);
this.elements.closeButton.addEventListener("click", this.closePopup);
this.elements.iosCloseButton.addEventListener('click', this.closePopup);
}
handleBeforeInstallPrompt = (e) => {
// Вихідний код перевіряв localStorage, але для простоти прикладу я її пропускаю.
if (!this.shouldShowBanner()) {
return; // Не показуємо, якщо localStorage = 'false'
}
e.preventDefault();
this.deferredPrompt = e;
// Показуємо стандартний банер, якщо доступно і не в режимі iOS
if (this.os !== 'iOS') {
this.openPopup(this.elements.installOverlay);
}
}
handleInstallClick = async () => {
if (!this.deferredPrompt) return;
this.deferredPrompt.prompt();
const { outcome } = await this.deferredPrompt.userChoice;
console.log(`[APP] Результат встановлення PWA: ${outcome}`);
this.closePopup();
}
// --- Шаблон (Template) та Стилі (Styles) ---
getTemplate() {
// HTML розмітка з вихідного коду
return `
<div id="blur-backdrop" class="pwa-hidden"></div>
<div id="pwa-install-overlay" class="pwa-overlay pwa-hidden">
<div class="popup">
<h2>Встановити застосунок?</h2>
<p>Додайте його на головний екран для швидкого доступу.</p>
<div>
<button id="pwa-install-button">Встановити</button>
<button id="pwa-close-button">Пізніше</button>
</div>
</div>
</div>
<div id="pwa-ios-overlay" class="pwa-overlay pwa-hidden">
<div class="popup">
<h2>Встановлення застосунку</h2>
<p>Щоб встановити застосунок, виконайте наступні кроки:</p>
<ol>
<li>1. Відкрийте посилання в браузері Safari.</li>
<li>
2. Натисніть кнопку
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M 14.984375 1 A 1.0001 1.0001 0 0 0 14.292969 1.2929688 L 10.292969 5.2929688 A 1.0001 1.0001 0 1 0 11.707031 6.7070312 L 14 4.4140625 L 14 17 A 1.0001 1.0001 0 1 0 16 17 L 16 4.4140625 L 18.292969 6.7070312 A 1.0001 1.0001 0 1 0 19.707031 5.2929688 L 15.707031 1.2929688 A 1.0001 1.0001 0 0 0 14.984375 1 z M 9 9 C 7.3550302 9 6 10.35503 6 12 L 6 24 C 6 25.64497 7.3550302 27 9 27 L 21 27 C 22.64497 27 24 25.64497 24 24 L 24 12 C 24 10.35503 22.64497 9 21 9 L 19 9 L 19 11 L 21 11 C 21.56503 11 22 11.43497 22 12 L 22 24 C 22 24.56503 21.56503 25 21 25 L 9 25 C 8.4349698 25 8 24.56503 8 24 L 8 12 C 8 11.43497 8.4349698 11 9 11 L 11 11 L 11 9 L 9 9 z"
/>
</svg>
</span>
в нижній частині екрана Safari.
</li>
<li>
3. У меню, що з’явиться, виберіть
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M 6 3 C 4.3550302 3 3 4.3550302 3 6 L 3 18 C 3 19.64497 4.3550302 21 6 21 L 18 21 C 19.64497 21 21 19.64497 21 18 L 21 6 C 21 4.3550302 19.64497 3 18 3 L 6 3 z M 6 5 L 18 5 C 18.56503 5 19 5.4349698 19 6 L 19 18 C 19 18.56503 18.56503 19 18 19 L 6 19 C 5.4349698 19 5 18.56503 5 18 L 5 6 C 5 5.4349698 5.4349698 5 6 5 z M 11.984375 6.9863281 A 1.0001 1.0001 0 0 0 11 8 L 11 11 L 8 11 A 1.0001 1.0001 0 1 0 8 13 L 11 13 L 11 16 A 1.0001 1.0001 0 1 0 13 16 L 13 13 L 16 13 A 1.0001 1.0001 0 1 0 16 11 L 13 11 L 13 8 A 1.0001 1.0001 0 0 0 11.984375 6.9863281 z"
/>
</svg>
</span>
«На Початковий екран».
</li>
</ol>
<div>
<button id="pwa-ios-close-button">Зрозуміло</button>
</div>
</div>
</div>
`;
}
getStyles() {
// CSS стилі, які були у вихідному коді, але адаптовані для Shadow DOM
// Примітки:
// 1. Змінні CSS (наприклад, --ColorThemes0) мають бути визначені в основному документі
// або передані через властивості, інакше вони не працюватимуть в Shadow DOM.
// Я залишаю їх як є, припускаючи, що вони глобально доступні.
// 2. Стилі для body.modal-open потрібно додати в основний CSS.
return `
<style>
#blur-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
z-index: 9998;
}
.pwa-overlay {
position: fixed;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.pwa-overlay>.popup {
background: var(--ColorThemes0, #ffffff); /* Fallback */
padding: 24px 32px;
border-radius: var(--border-radius, 15px);
max-width: 90%;
width: 320px;
text-align: center;
font-family: sans-serif;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
animation: fadeIn 0.3s ease-out;
display: flex;
flex-direction: column;
align-items: center;
}
.pwa-overlay>.popup h2 {
margin-bottom: 12px;
color: var(--ColorThemes3, #333);
opacity: 0.8;
}
.pwa-overlay>.popup p {
margin-bottom: 10px;
color: var(--ColorThemes3, #333);
opacity: 0.6;
}
.pwa-overlay>.popup ol {
text-align: justify;
font-size: var(--FontSize4, 15px);
margin-bottom: 10px;
max-width: 290px;
padding-left: 0; /* Виправлення відступу списку */
}
.pwa-overlay>.popup li {
list-style-type: none;
font-size: var(--FontSize3, 14px);
margin-bottom: 8px;
}
.pwa-overlay>.popup li span {
vertical-align: middle;
display: inline-block;
width: 22px;
height: 22px;
}
.pwa-overlay>.popup li span svg {
fill: var(--PrimaryColor, #007bff);
}
.pwa-overlay>.popup>div {
margin-top: 10px;
display: flex;
justify-content: center;
gap: 10px;
}
.pwa-overlay>.popup>div>button {
padding: 8px 16px;
border: none;
border-radius: calc(var(--border-radius, 15px) - 8px);
cursor: pointer;
font-size: var(--FontSize3, 14px);
}
#pwa-install-button {
background-color: var(--PrimaryColor, #007bff);
color: var(--PrimaryColorText, #ffffff);
}
#pwa-close-button,
#pwa-ios-close-button {
background-color: #ccc;
color: #333;
}
.pwa-hidden {
display: none !important; /* Важливо для скриптів */
}
@media (max-width: 450px) {
.pwa-overlay>.popup {
padding: 17px 10px;
}
.pwa-overlay>.popup h2 {
font-size: 22px;
}
.pwa-overlay>.popup p {
font-size: var(--FontSize4, 15px);
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>
`;
}
}
// Реєстрація веб-компонента
customElements.define('pwa-install-banner', PwaInstallBanner);

View File

@@ -0,0 +1,292 @@
const SMART_SELECT_STYLES_CSS = `
:host { display: block; position: relative; width: 100%; font-family: system-ui, sans-serif; }
:host ::-webkit-scrollbar { height: 5px; width: 8px; }
:host ::-webkit-scrollbar-track { background: transparent; }
:host ::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; }
@media (hover: hover) {
:host ::-webkit-scrollbar-thumb:hover { opacity: 0.7; }
}
/* Стили для скролла */
.trigger::-webkit-scrollbar { height: 4px; }
.trigger::-webkit-scrollbar-thumb { background: var(--smart-select-chip-background, #475569); border-radius: 4px; }
.wrapper {
min-height: 35px;
border: 1px solid var(--smart-select-border-color, #ccc);
border-radius: var(--smart-select-border-radius-1, 6px);
display: flex;
padding: 0 6px;
flex-direction: column;
justify-content: center;
background: var(--smart-select-background, #fff);
}
.trigger {
display: flex;
gap: 6px;
width: 100%;
overflow-x: auto;
align-items: center;
cursor: pointer;
}
.placeholder-text {
color: var(--smart-select-chip-color);
opacity: 0.4;
pointer-events: none;
user-select: none;
font-size: var(--smart-select-font-size-2);
}
.chip {
background: var(--smart-select-chip-background, #dbe3ea);
color: var(--smart-select-chip-color, #000);
padding: 4px 6px;
border-radius: var(--smart-select-border-radius-2, 4px);
font-size: var(--smart-select-font-size-1, 12px);
display: flex;
align-items: center;
gap: 3px;
white-space: nowrap;
cursor: pointer;
}
.chip button {
display: flex;
position: relative;
background: none;
border: none;
cursor: pointer;
padding: 0;
width: 20px;
height: 15px;
justify-content: flex-end;
fill: var(--smart-select-chip-fill, #e91e63);
}
.chip button svg{ width: 15px; height: 15px; }
.dropdown {
display: none; position: absolute; top: 100%; left: 0; right: 0;
background: var(--smart-select-background, #ffffff);
border: 1px solid var(--smart-select-border-color, #ccc);
border-radius: var(--smart-select-border-radius-1, 6px); z-index: 9999; margin-top: 4px;
flex-direction: column; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
}
:host([open]) .dropdown { display: flex; }
input[type="search"] {
margin: 10px;
padding: 8px 10px;
border-radius: var(--smart-select-border-radius-2, 4px);
border: 1px solid var(--smart-select-search-border, #ccc);
background: transparent;
color: var(--smart-select-search-color, #000);
}
.options-list {max-height: 200px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 8px; }
::slotted([slot="option"]) {
padding: 8px 12px !important;
cursor: pointer;
border-radius: var(--smart-select-border-radius-2, 4px);
display: block;
color: var(--smart-select-option-color, #000);
font-size: var(--smart-select-font-size-2, 14px);
}
@media (hover: hover) {
::slotted([slot="option"]:hover) {
background: var(--smart-select-hover-background, #475569);
color: var(--smart-select-hover-color, #fff);
}
}
::slotted([slot="option"].selected) {
background: var(--smart-select-selected-background, #dbe3eb) !important;
color: var(--smart-select-selected-color, #000) !important;
}
`;
// Створення об'єкта CSSStyleSheet (якщо підтримується)
let SmartSelectStyles = null;
if (typeof CSSStyleSheet !== 'undefined' && CSSStyleSheet.prototype.replaceSync) {
SmartSelectStyles = new CSSStyleSheet(); // (2) Визначення об'єкта тут
SmartSelectStyles.replaceSync(SMART_SELECT_STYLES_CSS);
}
class SmartSelect extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._selectedValues = new Set();
// Додаємо стилі в конструкторі, якщо це adoptable
if (this.shadowRoot.adoptedStyleSheets) {
this.shadowRoot.adoptedStyleSheets = [SmartSelectStyles];
} else {
// FALLBACK для старих браузерів (наприклад, iOS < 16.4)
const style = document.createElement('style');
style.textContent = SMART_SELECT_STYLES_CSS;
this.shadowRoot.appendChild(style);
}
}
connectedCallback() {
this.render();
const searchInput = this.shadowRoot.querySelector('input[type="search"]');
searchInput.addEventListener('input', (e) => this.handleSearch(e));
const wrapper = this.shadowRoot.querySelector('.wrapper');
wrapper.addEventListener('click', (e) => {
if (e.target.closest('button')) return;
this.toggleAttribute('open');
if (this.hasAttribute('open')) {
setTimeout(() => searchInput.focus(), 50);
}
});
// Слушаем клики по элементам в слоте
this.addEventListener('click', (e) => {
const opt = e.target.closest('[slot="option"]');
if (opt) {
this.toggleValue(opt.getAttribute('data-value'), opt);
}
});
// Слушаем изменение слота для инициализации (фикс Safari)
this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => {
this.syncOptions();
});
this.syncOptions();
}
_formatValue(val) {
const isNumber = this.getAttribute('type') === 'number';
return isNumber ? Number(val) : String(val);
}
syncOptions() {
const options = Array.from(this.querySelectorAll('[slot="option"]'));
options.forEach(opt => {
// Используем data-selected вместо атрибута selected
if (opt.hasAttribute('data-selected')) {
// Зберігаємо вже у потрібному форматі
const val = this._formatValue(opt.getAttribute('data-value'));
this._selectedValues.add(val);
}
});
this.updateDisplay();
}
handleSearch(e) {
const term = e.target.value.toLowerCase().trim();
const options = this.querySelectorAll('[slot="option"]');
options.forEach(opt => {
const text = opt.textContent.toLowerCase();
opt.style.display = text.includes(term) ? '' : 'none';
});
}
toggleValue(val) {
const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null;
const formattedVal = this._formatValue(val);
if (this._selectedValues.has(formattedVal)) {
this._selectedValues.delete(formattedVal);
this._click = {
value: formattedVal,
state: "delete"
};
} else {
if (max && this._selectedValues.size >= max) return;
this._selectedValues.add(formattedVal);
this._click = {
value: formattedVal,
state: "add"
};
}
this.updateDisplay();
this.dispatchEvent(new Event('change', { bubbles: true }));
}
updateDisplay() {
const container = this.shadowRoot.getElementById('tags');
const placeholder = this.shadowRoot.getElementById('placeholder');
const optionsElements = this.querySelectorAll('[slot="option"]');
// Керування видимістю плейсхолдера
if (this._selectedValues.size > 0) {
placeholder.style.display = 'none';
} else {
placeholder.style.display = 'block';
}
container.innerHTML = '';
this._selectedValues.forEach(val => {
// Важливо: при пошуку елемента в DOM атрибут data-value завжди рядок,
// тому використовуємо == для порівняння числа з рядком
const opt = Array.from(optionsElements).find(o => o.getAttribute('data-value') == val);
if (opt) {
const chip = document.createElement('div');
chip.className = 'chip';
chip.innerHTML = `${opt.textContent} <button><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26"><path d="M 6.65625 4 C 6.367188 4 6.105469 4.113281 5.90625 4.3125 L 4.3125 5.90625 C 3.914063 6.304688 3.914063 7 4.3125 7.5 L 9.8125 13 L 4.3125 18.5 C 3.914063 19 3.914063 19.695313 4.3125 20.09375 L 5.90625 21.6875 C 6.40625 22.085938 7.101563 22.085938 7.5 21.6875 L 13 16.1875 L 18.5 21.6875 C 19 22.085938 19.695313 22.085938 20.09375 21.6875 L 21.6875 20.09375 C 22.085938 19.59375 22.085938 18.898438 21.6875 18.5 L 16.1875 13 L 21.6875 7.5 C 22.085938 7 22.085938 6.304688 21.6875 5.90625 L 20.09375 4.3125 C 19.59375 3.914063 18.898438 3.914063 18.5 4.3125 L 13 9.8125 L 7.5 4.3125 C 7.25 4.113281 6.945313 4 6.65625 4 Z"></path></svg></button>`;
chip.querySelector('button').onclick = (e) => {
e.stopPropagation();
this.toggleValue(val);
};
container.appendChild(chip);
}
});
const max = this.getAttribute('max') ? parseInt(this.getAttribute('max')) : null;
const isFull = max && this._selectedValues.size >= max;
optionsElements.forEach(opt => {
const optVal = this._formatValue(opt.getAttribute('data-value'));
const isSelected = this._selectedValues.has(optVal);
opt.classList.toggle('selected', isSelected);
// Если лимит исчерпан, делаем невыбранные опции полупрозрачными
if (isFull && !isSelected) {
opt.style.opacity = '0.5';
opt.style.cursor = 'not-allowed';
} else {
opt.style.opacity = '1';
opt.style.cursor = 'pointer';
}
});
}
get value() {
return Array.from(this._selectedValues);
}
get getClick() {
return this._click;
}
render() {
this.shadowRoot.innerHTML = `
<div class="wrapper">
<div class="trigger" id="tags"></div>
<div id="placeholder" class="placeholder-text">
${this.getAttribute('placeholder') || 'Оберіть значення...'}
</div>
</div>
<div class="dropdown">
<input type="search" placeholder="Пошук...">
<div class="options-list">
<slot name="option"></slot>
</div>
</div>
`;
}
}
customElements.define('smart-select', SmartSelect);

View File

@@ -0,0 +1,234 @@
/**
* Вебкомпонент для ініціації оновлення сторінки (Pull-to-Refresh)
* за допомогою свайпу вниз на пристроях з iOS/iPadOS у режимі PWA.
*/
class SwipeUpdater extends HTMLElement {
constructor() {
super();
// 1. Створення Shadow DOM
// Використовуємо тіньовий DOM для інкапсуляції стилів та структури
const shadow = this.attachShadow({ mode: 'open' });
// 2. Внутрішня функція оновлення за замовчуванням
this._appReload = () => {
console.log('Стандартна функція: Перезавантаження сторінки');
// Стандартна дія - перезавантаження сторінки
window.location.reload();
};
// 3. Внутрішній стан
this._isReadyToReload = false; // Прапорець, що вказує на готовність до оновлення
// 4. Створення елементів (Внутрішній HTML)
shadow.innerHTML = `
<div id="swipe_updater">
<div id="swipe_block">
<svg id="swipe_icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" data-state="active">
<path d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z"></path>
</svg>
</div>
</div>
<style>
:host {
display: block; /* Важливо для позиціонування кореневого елемента */
}
#swipe_updater {
position: absolute;
top: 0px;
width: 100%;
z-index: 0; /* Базовий z-index */
/* Використання CSS-змінних для кастомізації кольорів */
--swipe-color-theme1: var(--ColorThemes2, #525151); /* Колір фону іконки */
--swipe-color-theme2: var(--ColorThemes3, #f3f3f3); /* Колір іконки та рамки */
}
#swipe_block {
/* Розрахунок ширини та відступу для центрифікації в певних макетах */
width: calc(100% - 252px);
margin-left: 252px;
height: 50px;
display: flex;
justify-content: center;
align-items: flex-end;
position: relative;
}
#swipe_icon {
width: 20px;
fill: var(--swipe-color-theme2);
transform: rotate(0deg); /* Початковий стан: стрілка вниз */
position: absolute;
/* Початкове приховане позиціонування */
margin-top: -30px;
top: -30px;
background: var(--swipe-color-theme1);
border: 2px solid var(--swipe-color-theme2);
border-radius: 50%;
padding: 10px;
display: flex;
overflow: hidden;
height: 0;
opacity: 0;
/* Анімація: прихована іконка плавно з'являється (активується/деактивується) */
transition: height 0ms 450ms, opacity 450ms 0ms, transform 450ms;
}
#swipe_icon[data-state="active"] {
height: 20px;
margin-top: -45px;
top: -45px;
opacity: 1;
/* Анімація: активна іконка видима */
transition: height 0ms 0ms, opacity 450ms 0ms, transform 450ms;
}
/* Адаптивні стилі для зміни центрифікації на різних екранах */
@media (max-width: 1100px){
#swipe_block {
width: calc(100% - 122px);
margin-left: 122px;
}
}
@media (max-width: 700px), (max-height: 540px) {
#swipe_block {
width: 100%;
margin-left: 0;
}
/* Зміна кольорів для менших екранів */
#swipe_updater {
--swipe-color-theme1: var(--ColorThemes0, #525151);
--swipe-color-theme2: var(--ColorThemes3, #f3f3f3);;
}
}
</style>
`;
// 5. Збереження посилань на елементи Shadow DOM
this._animID = shadow.getElementById('swipe_updater');
this._animIconID = shadow.getElementById('swipe_icon');
// 6. Прив'язка контексту `this` для обробника подій (важливо для коректної роботи `this.handleScroll`)
this.handleScroll = this.handleScroll.bind(this);
}
/**
* Метод для встановлення користувацької функції оновлення.
* Замінює стандартне перезавантаження сторінки.
* @param {function} func - Користувацька функція, що буде викликана при свайпі.
*/
setReloadFunction(func) {
if (typeof func === 'function') {
this._appReload = func;
} else {
console.error('setReloadFunction вимагає передати функцію.');
}
}
/**
* Обробник події скролу (головна логіка Pull-to-Refresh).
* Відстежує прокручування вище верхньої межі сторінки (`window.scrollY < 0`).
*/
handleScroll() {
// Перевірка на режим Standalone (PWA) - функціональність актуальна переважно тут
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone) {
let scrollY = window.scrollY;
// 1. Анімація іконки під час прокручування за верхню межу (scrollY < 0)
if (scrollY <= -10) {
// Зміщення іконки разом із прокруткою
this._animIconID.style.top = `${scrollY / 1.5}px`;
this._animID.style.zIndex = '115'; // Піднімаємо z-index для видимості над контентом
} else {
this._animID.style.zIndex = '0'; // Повертаємо базовий z-index
}
const threshold = -125; // Поріг прокрутки (наприклад, -125px) для активації оновлення
// 2. Логіка активації "готовий до оновлення"
if (scrollY <= threshold) {
if (!this._isReadyToReload) {
this._isReadyToReload = true;
// Поворот іконки на 180 градусів
this._animIconID.style.transform = 'rotate(180deg)';
this._animIconID.setAttribute('data-state', ''); // Деактивація стану "active"
}
}
// 3. Логіка виклику оновлення та скидання стану
// Якщо користувач відпускає свайп (scrollY повертається до >= 0) І був готовий до оновлення
else if (scrollY >= 0) {
if (this._isReadyToReload) {
// Виклик користувацької функції (або стандартного window.location.reload())
this._appReload();
// Скидання стану та анімації
this._isReadyToReload = false;
this._animIconID.style.transform = 'rotate(0deg)';
this._animIconID.setAttribute('data-state', 'active');
}
}
}
}
/**
* Lifecycle hook: викликається при додаванні елемента в DOM.
* Додаємо обробник події прокручування.
*/
connectedCallback() {
// Прослуховування глобальної події скролу
window.addEventListener('scroll', this.handleScroll);
}
/**
* Lifecycle hook: викликається при видаленні елемента з DOM.
* Видаляємо обробник події прокручування для запобігання витоку пам'яті.
*/
disconnectedCallback() {
window.removeEventListener('scroll', this.handleScroll);
}
}
// Реєстрація веб-компонента
customElements.define('swipe-updater', SwipeUpdater);
/*
============================
ПРИКЛАД ВИКОРИСТАННЯ
============================
*/
/*
1. Додайте цей елемент у свій HTML:
<swipe-updater id="swipe-updater"></swipe-updater>
2. Отримайте посилання на компонент у JS:
const Updater = document.querySelector('swipe-updater');
3. Користувацька функція оновлення
function customReload() {
const now = new Date().toLocaleTimeString();
console.log(`Користувацьке оновлення: оновлено о ${now}`);
document.querySelector('h1').textContent = `Сторінка оновлена о ${now}`;
// Тут можна виконати AJAX-запит, оновити DOM тощо.
}
4. Перевизначення функції оновлення компонента
if (Updater && Updater.setReloadFunction) {
Updater.setReloadFunction(customReload);
} else {
console.error('Компонент SwipeUpdater не знайдено або не готовий.');
}
💡 Приклад стандартного використання (якщо не викликати setReloadFunction):
<swipe-updater></swipe-updater>
При свайпі буде викликано window.location.reload();
*/

View File

@@ -1,5 +1,5 @@
const appTerritoryCardStyles = new CSSStyleSheet();
appTerritoryCardStyles.replaceSync(`
// Заміна вмісту таблиці стилів на надані CSS-правила.
const CARD_STYLES_CSS = `
:host {
display: inline-block;
box-sizing: border-box;
@@ -54,14 +54,15 @@ appTerritoryCardStyles.replaceSync(`
width: 100%;
height: 100%;
object-fit: cover;
position: relative;
z-index: 1;
filter: blur(3px);
border-radius: calc(var(--border-radius, 15px) - 5px);
}
.contents {
position: absolute;
top: 0;
left: 0;
z-index: 1;
filter: blur(3px);
}
.contents {
position: relative;
z-index: 2;
background: rgb(64 64 64 / 0.7);
width: 100%;
@@ -89,7 +90,7 @@ appTerritoryCardStyles.replaceSync(`
/* Стили для режима 'sheep' */
/* Стилі для режиму 'sheep' */
.sheep {
margin: 10px;
max-height: 50px;
@@ -116,7 +117,7 @@ appTerritoryCardStyles.replaceSync(`
}
/* Стили для режима 'info' (прогресс) */
/* Стилі для режиму 'info' (прогресс) */
.info {
margin: 10px;
}
@@ -133,7 +134,7 @@ appTerritoryCardStyles.replaceSync(`
}
.info span {
z-index: 2;
font-size: var(--FontSize1, 12px);
font-size: var(--FontSize3, 14px);
color: var(--ColorThemes3, #f3f3f3);
}
.info p {
@@ -158,24 +159,45 @@ appTerritoryCardStyles.replaceSync(`
width: 100%;
height: 100%;
z-index: 10;
border-radius: calc(var(--border-radius, 15px) - 5px);
}
`);
`;
// Створення об'єкта CSSStyleSheet (якщо підтримується)
let appTerritoryCardStyles = null;
if (typeof CSSStyleSheet !== 'undefined' && CSSStyleSheet.prototype.replaceSync) {
appTerritoryCardStyles = new CSSStyleSheet(); // (2) Визначення об'єкта тут
appTerritoryCardStyles.replaceSync(CARD_STYLES_CSS);
}
/**
* Веб-компонент AppTerritoryCard.
* Відображає картку території з фоновим зображенням та різними режимами відображення.
*/
class AppTerritoryCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Додаємо стилі в конструкторі, якщо це adoptable
if (this.shadowRoot.adoptedStyleSheets) {
this.shadowRoot.adoptedStyleSheets = [appTerritoryCardStyles];
} else {
// FALLBACK для старих браузерів (наприклад, iOS < 16.4)
const style = document.createElement('style');
style.textContent = CARD_STYLES_CSS;
this.shadowRoot.appendChild(style);
}
}
// Определяем, какие атрибуты будем отслеживать
// Вказуємо, які атрибути ми хочемо відстежувати
static get observedAttributes() {
return ['image', 'address', 'sheep', 'link', 'atWork', 'quantity'];
return ['image', 'address', 'sheep', 'link', 'atWork', 'quantity', 'overdue'];
}
// Геттери та Сеттери для атрибутів
// Вони спрощують роботу з атрибутами як з властивостями DOM-елемента
get image() {
return this.getAttribute('image');
}
@@ -198,6 +220,11 @@ class AppTerritoryCard extends HTMLElement {
}
}
/** * Атрибут 'sheep' може приймати три стани:
* 1. null / відсутній: відключення блоку sheep та info
* 2. порожній рядок ('') / присутній без значення: Режим "Територія не опрацьовується"
* 3. рядок зі значенням: Режим "Територію опрацьовує: [значення]"
*/
get sheep() {
return this.getAttribute('sheep');
}
@@ -228,6 +255,7 @@ class AppTerritoryCard extends HTMLElement {
if (newValue === null) {
this.removeAttribute('atWork');
} else {
// Приводимо до рядка, оскільки атрибути завжди є рядками
this.setAttribute('atWork', String(newValue));
}
}
@@ -239,41 +267,74 @@ class AppTerritoryCard extends HTMLElement {
if (newValue === null) {
this.removeAttribute('quantity');
} else {
// Приводимо до рядка
this.setAttribute('quantity', String(newValue));
}
}
// Вызывается при добавлении элемента в DOM
get overdue() {
return this.getAttribute('address');
}
set overdue(newValue) {
if (newValue === null) {
this.removeAttribute('overdue');
} else {
this.setAttribute('overdue', newValue);
}
}
/**
* connectedCallback викликається, коли елемент додається в DOM.
* Тут ми викликаємо початкове рендеринг.
*/
connectedCallback() {
this.render();
}
// Вызывается при изменении одного из отслеживаемых атрибутов
/**
* attributeChangedCallback викликається при зміні одного зі спостережуваних атрибутів.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
this.render(); // Перерендеринг при зміні атрибута
}
}
/**
* Логіка рендерингу (відображення) вмісту компонента.
*/
render() {
const image = this.getAttribute('image') || '';
const address = this.getAttribute('address') || '';
const sheep = this.getAttribute('sheep'); // Может быть null или ""
const sheep = this.getAttribute('sheep');
const link = this.getAttribute('link') || '#';
const atWork = this.getAttribute('atWork'); // Может быть null
const quantity = this.getAttribute('quantity'); // Может быть null
const atWork = this.getAttribute('atWork');
const quantity = this.getAttribute('quantity');
const overdue = this.getAttribute('overdue') == 'true' ? true : false;
this.shadowRoot.innerHTML = ``;
// Додаємо стилі для старих браузерів
if (!this.shadowRoot.adoptedStyleSheets) {
const style = document.createElement('style');
style.textContent = CARD_STYLES_CSS;
this.shadowRoot.appendChild(style);
}
// --- Логика определения контента ---
let contentHTML = '';
// Перевіряємо, чи має бути увімкнений режим прогресу ('info'):
// обидва атрибути 'atWork' та 'quantity' присутні і є коректними числами.
const isProgressMode = atWork !== null && quantity !== null && !isNaN(parseInt(atWork)) && !isNaN(parseInt(quantity));
const hasSheep = sheep !== null && sheep !== '';
if (isProgressMode) {
// Режим прогресса (свободные подъезды)
// Режим прогресу (вільні під'їзди)
const atWorkNum = parseInt(atWork);
const quantityNum = parseInt(quantity);
const free = quantityNum - atWorkNum;
// Обчислення відсотка прогресу. Уникнення ділення на нуль.
const progressPercent = quantityNum > 0 ? (atWorkNum / quantityNum) * 100 : 100;
contentHTML = `
@@ -286,15 +347,15 @@ class AppTerritoryCard extends HTMLElement {
</div>
`;
} else if (sheep !== null && sheep !== '') {
// Режим ответственного
// Режим опрацювання (значення атрибута 'sheep' є ім'ям опрацювача)
contentHTML = `
<div class="sheep">
<div class="sheep" ${overdue ? `style="background: #bb4444;"` : ``}>
<span>Територію опрацьовує:</span>
<p>${sheep}</p>
</div>
`;
} else if (sheep !== null) {
// Режим "не опрацьовується"
} else if (sheep !== null && sheep === '') {
// Режим "не опрацьовується" (атрибут 'sheep' присутній, але порожній)
contentHTML = `
<div class="sheep">
<span>Територія не опрацьовується</span>
@@ -302,9 +363,9 @@ class AppTerritoryCard extends HTMLElement {
`;
}
// --- Сборка всего шаблона ---
this.shadowRoot.innerHTML = `
<div class="card">
// --- Складання всього шаблону ---
this.shadowRoot.innerHTML += `
<div class="card" ${overdue ? `title="Термін опрацювання минув!"` : ``}>
<img src="${image}" alt="${address}" />
<div class="contents">
<h1 class="address">${address}</h1>
@@ -316,8 +377,42 @@ class AppTerritoryCard extends HTMLElement {
}
}
// Регистрируем веб-компонент
// Реєструємо веб-компонент у браузері
customElements.define('app-territory-card', AppTerritoryCard);
// document.getElementById('app-territory-card-1').setAttribute('sheep', 'test')
/*
============================
ПРИКЛАД ВИКОРИСТАННЯ
============================
*/
/*
<app-territory-card
address="Вул. Прикладна, 15А"
image="https://example.com/images/territory-1.jpg"
link="/territory/15a"
atWork="12"
quantity="20"
></app-territory-card>
<app-territory-card
address="Просп. Науковий, 5"
image="https://example.com/images/territory-2.jpg"
link="/territory/naukovyi-5"
sheep="Іван Петренко"
></app-territory-card>
<app-territory-card
address="Майдан Свободи, 1"
image="https://example.com/images/territory-3.jpg"
link="/territory/svobody-1"
sheep=""
></app-territory-card>
<app-territory-card
address="Вул. Безіменна, 99"
image="https://example.com/images/territory-4.jpg"
link="/territory/bezymenna-99"
></app-territory-card>
*/

View File

@@ -2,7 +2,7 @@
<form id="page-auth-form">
<div>
<input
type="text"
type="password"
name="uuid"
id="auth-forms-uuid"
placeholder="UUID"

View File

@@ -6,12 +6,10 @@ const Auth = {
document.getElementById("page-auth-form").addEventListener("submit", async function (event) {
event.preventDefault();
let uuid = document.getElementById("auth-forms-uuid").value;
uuid = uuid.replace("https://sheep-service.com/?uuid=", "");
uuid = uuid.replace("https://sheep-service.com?uuid=", "");
uuid = uuid.replace("https://sheep-service.com?/hash=", "");
uuid = uuid.replace("https://sheep-service.com?hash=", "");
const uuid = document
.getElementById("auth-forms-uuid")
.value
.replace(/^https?:\/\/sheep-service\.com\/?\?(uuid|hash)=/, "");
console.log(uuid);
@@ -38,11 +36,47 @@ const Auth = {
console.log("USER Info: ", USER);
if (USER.possibilities.can_view_sheeps) document.getElementById("li-sheeps").style.display = "";
if (USER.possibilities.can_add_schedule) document.getElementById("li-schedule").style.display = "";
if (USER.possibilities.can_manager_territory) document.getElementById("li-territory").style.display = "";
if (USER.possibilities.can_view_stand) document.getElementById("li-stand").style.display = "";
document.getElementById("li-options").style.display = "";
if (USER.possibilities.can_view_stand) {
newMenuItems({
id: 'menu-stand',
title: 'Графік стенду',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"/></svg>`,
href: '/stand'
});
}
if (USER.possibilities.can_view_schedule) {
newMenuItems({
id: 'menu-schedule',
title: 'Графіки зібрань',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"/></svg>`,
href: '/schedule'
});
}
if (USER.possibilities.can_view_sheeps) {
newMenuItems({
id: 'menu-sheeps',
title: 'Вісники',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z" /></svg>`,
href: '/sheeps',
hidden: true
});
await Sheeps.sheeps_list.loadAPI();
}
if (USER.possibilities.can_manager_territory) {
newMenuItems({
id: 'menu-territory',
title: 'Території',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"/></svg>`,
href: '/territory',
hidden: true
});
}
newMenuItems({
id: 'menu-options',
title: 'Опції',
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172"><path d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"/></svg>`,
href: '/options'
});
});
}
}

View File

@@ -28,4 +28,12 @@
<div id="home-group-territory-list"></div>
</details>
<details id="details-joint-territory" open style="display: none">
<summary>
<span>Тимчасові території</span>
</summary>
<div id="home-joint-territory-list"></div>
</details>
</div>

View File

@@ -9,6 +9,8 @@ const Home = {
Home.group.house.setHTML();
Home.group.homestead.setHTML();
Home.joint.homestead.setHTML();
}
},
personal: {
@@ -119,6 +121,34 @@ const Home = {
}
}
},
joint: {
homestead: {
list: [],
loadAPI: async () => {
const uuid = localStorage.getItem("uuid");
const URL = `${CONFIG.api}homestead/list?mode=joint`;
const res = await fetch(URL, {
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
Home.joint.homestead.list = await res.json();
return Home.joint.homestead.list;
},
setHTML: async () => {
const list = Home.joint.homestead.list.length > 0
? Home.joint.homestead.list
: await Home.joint.homestead.loadAPI();
if (USER.possibilities.can_view_territory && list.length)
document.getElementById('details-joint-territory').style.display = "";
list.sort((a, b) => b.id - a.id);
Home.renderCards(list, "homestead", "joint");
}
}
},
renderCards: (list, type, block) => {
const container = document.getElementById(`home-${block}-territory-list`);
const fragment = document.createDocumentFragment();
@@ -126,7 +156,7 @@ const Home = {
for (const el of list) {
const card = document.createElement('app-territory-card');
card.image = `${CONFIG.web}cards/${type}/${type === "house" ? "T" : "H"}${el.id}.webp`;
card.address = `${el.title} ${el.number})`;
card.address = `${el.title} ${el.number}`;
card.link = `/territory/card/${type}/${el.id}`;
fragment.appendChild(card);
}

View File

@@ -91,8 +91,9 @@
.page-home #home-personal-territory-list,
.page-home #home-group-territory-list {
width: 100%;
.page-home #home-group-territory-list,
.page-home #home-joint-territory-list {
width: calc(100% - 20px);
margin: 0;
display: flex;
flex-wrap: wrap;

View File

@@ -1,3 +1,3 @@
<div class="page-schedule-constructor">
</div>
</div>

View File

@@ -3,6 +3,12 @@ const Schedule_constructor = {
let html = await fetch('/lib/pages/schedule/constructor/index.html').then((response) => response.text());
app.innerHTML = html;
const newItem = document.createElement('nav-item');
const uniqueId = `nav-dynamic-${Date.now()}`;
newItem.setAttribute('id', uniqueId);
newItem.setAttribute('title', 'Динамічний Пункт');
newItem.setAttribute('icon', '/img/0.svg');
newItem.setAttribute('click', `alert('${uniqueId} clicked!')`);
document.querySelector('navigation-container').appendChild(newItem);
}
}

View File

@@ -121,7 +121,7 @@
id="sheep-editor-can_view_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_view_sheeps"> View Sheeps </label>
<label for="sheep-editor-can_view_sheeps"> Перегляд списку вісників </label>
</div>
<div class="checkbox">
<input
@@ -130,7 +130,16 @@
id="sheep-editor-can_add_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_add_sheeps"> Create Sheeps </label>
<label for="sheep-editor-can_add_sheeps"> Додавання вісників </label>
</div>
<div class="checkbox">
<input
name="can_manager_sheeps"
class="custom-checkbox"
id="sheep-editor-can_manager_sheeps"
type="checkbox"
/>
<label for="sheep-editor-can_manager_sheeps"> Керування дозволами вісників </label>
</div>
<div class="checkbox">
<input
@@ -140,7 +149,7 @@
type="checkbox"
/>
<label for="sheep-editor-can_add_territory">
Create Territory
Створення територій
</label>
</div>
<div class="checkbox">
@@ -151,7 +160,18 @@
type="checkbox"
/>
<label for="sheep-editor-can_manager_territory">
Manager Territory
Керування територіями
</label>
</div>
<div class="checkbox">
<input
name="can_joint_territory"
class="custom-checkbox"
id="sheep-editor-can_joint_territory"
type="checkbox"
/>
<label for="sheep-editor-can_joint_territory">
Спільний доступ до території
</label>
</div>
<div class="checkbox">
@@ -161,7 +181,7 @@
id="sheep-editor-can_add_stand"
type="checkbox"
/>
<label for="sheep-editor-can_add_stand"> Create Stand </label>
<label for="sheep-editor-can_add_stand"> Створення стендів </label>
</div>
<div class="checkbox">
<input
@@ -170,7 +190,7 @@
id="sheep-editor-can_manager_stand"
type="checkbox"
/>
<label for="sheep-editor-can_manager_stand"> Manager Stand </label>
<label for="sheep-editor-can_manager_stand"> Керування стендами </label>
</div>
<div class="checkbox">
<input
@@ -179,7 +199,7 @@
id="sheep-editor-can_add_schedule"
type="checkbox"
/>
<label for="sheep-editor-can_add_schedule"> Create Schedule </label>
<label for="sheep-editor-can_add_schedule"> Створення розкладу зібрань </label>
</div>
</div>
</div>
@@ -194,7 +214,7 @@
id="sheep-editor-can_view_schedule"
type="checkbox"
/>
<label for="sheep-editor-can_view_schedule"> View Schedule </label>
<label for="sheep-editor-can_view_schedule"> Перегляд розкладу зібрань </label>
</div>
<div class="checkbox">
<input
@@ -203,7 +223,7 @@
id="sheep-editor-can_view_stand"
type="checkbox"
/>
<label for="sheep-editor-can_view_stand"> View Stand </label>
<label for="sheep-editor-can_view_stand"> Перегляд стендів </label>
</div>
<div class="checkbox">
<input
@@ -213,7 +233,7 @@
type="checkbox"
/>
<label for="sheep-editor-can_view_territory">
View Territory
Перегляд територій
</label>
</div>
</div>

View File

@@ -40,6 +40,8 @@ const SheepsEvents = {
const sheepEditorButton = document.getElementById('sheep-editor-button');
const form = event.target;
const formData = new FormData(form);
console.log(formData);
const uuidValue = form.elements["uuid"].value;
const sheep = Sheeps.sheeps_list.list.find(item => item.uuid === uuidValue);
@@ -50,7 +52,7 @@ const SheepsEvents = {
sheep.name = form.elements["name"].value;
sheep.group_id = Number(formData.get("group_id"));
sheep.mode = formData.get("mode");
sheep.mode = formData.get("mode") || sheep.mode;
sheep.mode_title = ["Користувач", "Модератор", "Адміністратор"][sheep.mode] || "Користувач";
const permKeys = [
@@ -59,8 +61,10 @@ const SheepsEvents = {
"can_view_stand",
"can_view_territory",
"can_add_sheeps",
"can_manager_sheeps",
"can_add_territory",
"can_manager_territory",
"can_joint_territory",
"can_add_stand",
"can_manager_stand",
"can_add_schedule"
@@ -73,8 +77,7 @@ const SheepsEvents = {
try {
const uuid = localStorage.getItem('uuid');
const URL = `${CONFIG.api}sheep`;
const response = await fetch(URL, {
const response = await fetch(`${CONFIG.api}sheep`, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
@@ -84,26 +87,23 @@ const SheepsEvents = {
});
if (response.ok) {
sheepEditorButton.innerText = "Успішно збережено";
sheepEditorButton.innerText = "Зберегти";
Notifier.success("Успішно збережено!", {timeout: 2000})
const data = await response.json();
console.log(data);
Sheeps.sheeps_list.list = [];
await Sheeps.sheeps_list.setHTML();
setTimeout(() => {
sheepEditorButton.innerText = "Зберегти";
}, 3000);
} else {
sheepEditorButton.innerText = "Зберегти";
console.error('Помилка збереження');
sheepEditorButton.innerText = "Помилка збереження";
Notifier.error("Помилка збереження!", {timeout: 3000});
}
} catch (err) {
console.error(err);
sheepEditorButton.innerText = "Помилка збереження";
Notifier.error("Помилка збереження!", {timeout: 3000});
}
// тот же код, что был в _onSheepEditorSubmit, но обращаемся к editorForm
return;
}
@@ -120,8 +120,7 @@ const SheepsEvents = {
try {
const uuid = localStorage.getItem('uuid');
const URL = `${CONFIG.api}sheep`;
const response = await fetch(URL, {
const response = await fetch(`${CONFIG.api}sheep`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
@@ -131,7 +130,7 @@ const SheepsEvents = {
});
if (response.ok) {
sheepAddedsButton.innerText = "Вісника додано";
sheepAddedsButton.innerText = "Додати";
const data = await response.json();
console.log(data);
@@ -141,17 +140,14 @@ const SheepsEvents = {
Sheeps.addeds.close();
await Sheeps.editor.setHTML(data.id, randomNumber);
setTimeout(() => {
sheepAddedsButton.innerText = "Додати";
}, 3000);
} else {
sheepEditorButton.innerText = "Додати";
console.error('Помилка додавання');
sheepAddedsButton.innerText = "Помилка додавання";
Notifier.error("Помилка додавання!", {timeout: 3000});
}
} catch (err) {
console.error(err);
sheepAddedsButton.innerText = "Помилка додавання";
Notifier.error("Помилка додавання!", {timeout: 3000});
}
return;
}
@@ -176,9 +172,7 @@ const Sheeps = {
loadAPI: async () => {
let uuid = localStorage.getItem("uuid");
const URL = `${CONFIG.api}sheeps/list`;
Sheeps.sheeps_list.list = await fetch(URL, {
Sheeps.sheeps_list.list = await fetch(`${CONFIG.api}sheeps/list`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
@@ -238,9 +232,11 @@ const Sheeps = {
if (p.can_view_territory) perms.push("View Territory");
if (p.can_add_sheeps) perms.push("Create Sheeps");
if (p.can_add_territory) perms.push("Create Territory");
if (p.can_manager_sheeps) perms.push("Manager Sheeps");
if (p.can_manager_territory) perms.push("Manager Territory");
if (p.can_add_stand) perms.push("Create Stand");
if (p.can_manager_stand) perms.push("Manager Stand");
if (p.can_joint_territory) perms.push("Joint Territory");
if (p.can_add_stand) perms.push("Create Stand");
if (p.can_add_schedule) perms.push("Create Schedule");
return perms.map(p => `<b>${p}</b>`).join('');
};
@@ -274,8 +270,7 @@ const Sheeps = {
loadAPI: async (id) => {
let uuid = localStorage.getItem("uuid");
const URL = `${CONFIG.api}sheep?id=${id}`;
return await fetch(URL, {
return await fetch(`${CONFIG.api}sheep?id=${id}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
@@ -327,8 +322,11 @@ const Sheeps = {
let sheep_editor_can_view_sheeps = document.getElementById('sheep-editor-can_view_sheeps');
let sheep_editor_can_add_sheeps = document.getElementById('sheep-editor-can_add_sheeps');
let sheep_editor_can_manager_sheeps = document.getElementById('sheep-editor-can_manager_sheeps');
let sheep_editor_can_add_territory = document.getElementById('sheep-editor-can_add_territory');
let sheep_editor_can_manager_territory = document.getElementById('sheep-editor-can_manager_territory');
let sheep_editor_can_joint_territory = document.getElementById('sheep-editor-can_joint_territory');
let sheep_editor_can_add_stand = document.getElementById('sheep-editor-can_add_stand');
let sheep_editor_can_manager_stand = document.getElementById('sheep-editor-can_manager_stand');
let sheep_editor_can_add_schedule = document.getElementById('sheep-editor-can_add_schedule');
@@ -367,8 +365,10 @@ const Sheeps = {
sheep_editor_can_view_sheeps.checked = sheep.possibilities.can_view_sheeps;
sheep_editor_can_add_sheeps.checked = sheep.possibilities.can_add_sheeps;
sheep_editor_can_manager_sheeps.checked = sheep.possibilities.can_manager_sheeps;
sheep_editor_can_add_territory.checked = sheep.possibilities.can_add_territory;
sheep_editor_can_manager_territory.checked = sheep.possibilities.can_manager_territory;
sheep_editor_can_joint_territory.checked = sheep.possibilities.can_joint_territory;
sheep_editor_can_add_stand.checked = sheep.possibilities.can_add_stand;
sheep_editor_can_manager_stand.checked = sheep.possibilities.can_manager_stand;
sheep_editor_can_add_schedule.checked = sheep.possibilities.can_add_schedule;
@@ -387,6 +387,9 @@ const Sheeps = {
if (USER.mode == 2) {
document.getElementById('sheep-editor-button').style.display = "";
sheep_editor_mode.disabled = false;
} else if (USER.possibilities.can_manager_sheeps) {
document.getElementById('sheep-editor-button').style.display = "";
sheep_editor_mode.disabled = true;
} else {
sheep_editor_mode.disabled = true;
}
@@ -458,9 +461,9 @@ const Sheeps = {
}
},
territory: {
async loadAPI(URL) {
async loadAPI(url) {
const uuid = localStorage.getItem("uuid");
const res = await fetch(URL, {
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"Authorization": uuid
@@ -470,8 +473,7 @@ const Sheeps = {
},
async house(id) {
const URL = `${CONFIG.api}house/list?mode=admin&sheep_id=${id}`;
const list = await Sheeps.territory.loadAPI(URL);
const list = await Sheeps.territory.loadAPI(`${CONFIG.api}house/list?mode=admin&sheep_id=${id}`);
if ((USER.possibilities.can_view_territory || USER.mode == 2) && list.length > 0) {
document.getElementById('editor-blocks-territory').style.display = "";
@@ -480,8 +482,7 @@ const Sheeps = {
},
async homestead(id) {
const URL = `${CONFIG.api}homestead/list?mode=admin&sheep_id=${id}`;
const list = await Sheeps.territory.loadAPI(URL);
const list = await Sheeps.territory.loadAPI(`${CONFIG.api}homestead/list?mode=admin&sheep_id=${id}`);
if ((USER.possibilities.can_view_territory || USER.mode == 2) && list.length > 0) {
document.getElementById('editor-blocks-territory').style.display = "";

View File

@@ -54,15 +54,15 @@
}
#stand-info div span {
opacity: 0.8;
font-weight: 400;
font-size: var(--FontSize2);
font-weight: 500;
font-size: var(--FontSize3);
margin-right: 5px;
}
#stand-info div p {
font-weight: 300;
font-size: var(--FontSize4);
opacity: 0.9;
font-weight: 400;
font-size: var(--FontSize3);
}
#stand-info img {
@@ -143,11 +143,11 @@
}
#stand-schedule>.block-day h3 {
font-size: var(--FontSize5);
font-size: var(--FontSize6);
font-weight: 500;
padding: 10px;
padding: 20px;
text-transform: capitalize;
width: calc(100% - 20px);
width: calc(100% - 40px);
text-align: center;
}
@@ -181,6 +181,7 @@
#stand-schedule>.block-day div select:disabled {
opacity: 0.9 !important;
background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%2217%2010%2038%2050%22%3E%3Cpath%20fill%3D%22%23F2BD53%22%20d%3D%22M%2036%2010%20C%2028.28%2010%2022%2016.28%2022%2024%20L%2022%2028.587891%20C%2019.069798%2029.775473%2017%2032.643974%2017%2036%20L%2017%2052%20C%2017%2056.418%2020.582%2060%2025%2060%20L%2047%2060%20C%2051.418%2060%2055%2056.418%2055%2052%20L%2055%2036%20C%2055%2032.643974%2052.930202%2029.775473%2050%2028.587891%20L%2050%2024%20C%2050%2016.28%2043.72%2010%2036%2010%20z%20M%2036%2018%20C%2039.309%2018%2042%2020.691%2042%2024%20L%2042%2028%20L%2030%2028%20L%2030%2024%20C%2030%2020.691%2032.691%2018%2036%2018%20z%22%2F%3E%3C%2Fsvg%3E);
}
#stand-schedule>.block-day div:nth-child(2n) {

View File

@@ -52,6 +52,9 @@ const Stand_constructor = {
button.innerText = "Стенд додано";
Notifier.success('Стенд створено');
Stand_list.list = [];
Stand_list.loadAPI();
return response.json()
} else {
console.log('err');

View File

@@ -85,6 +85,9 @@ const Stand_editor = {
button.innerText = "Стенд відредаговано";
Notifier.success('Стенд відредаговано');
Stand_list.list = [];
Stand_list.loadAPI();
return response.json()
} else {
console.log('err');

View File

@@ -1,206 +1,11 @@
<style>
*[disabled] {
opacity: 0.5;
}
select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
/* Arrow */
appearance: none;
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%237a899d%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
background-repeat: no-repeat;
background-position: right 0.3rem top 50%;
background-size: 0.55rem auto;
}
#list {
display: flex;
flex-direction: column;
align-items: center;
}
details {
color: var(--ColorThemes3);
width: 100%;
min-width: 320px;
background: var(--ColorThemes1);
margin: 20px 0px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.04),
0px 0px 1px rgba(0, 0, 0, 0.04);
}
@media (min-width: 900px) {
#list {
align-items: flex-start;
justify-content: space-between;
flex-direction: row;
flex-wrap: wrap;
}
details {
width: calc(50% - 10px);
}
}
details[disabled] summary,
details.disabled summary {
pointer-events: none;
user-select: none;
}
details summary::-webkit-details-marker,
details summary::marker {
display: none;
content: "";
}
summary {
cursor: pointer;
background: #7a8a9d;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
height: 45px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: space-between;
}
summary p {
padding: 0 10px;
color: var(--PrimaryColorText);
font-size: var(--FontSize3);
font-weight: 500;
}
summary svg {
width: 25px;
height: 25px;
padding: 0 10px;
fill: var(--PrimaryColorText);
}
.apartments_list {
padding: 10px;
border-bottom: 2px solid var(--PrimaryColor);
border-left: 2px solid var(--PrimaryColor);
border-right: 2px solid var(--PrimaryColor);
border-radius: 0 0 10px 10px;
}
@media (max-width: 500px) {
.apartments_list {
padding: 1px;
}
}
.apartments_list > p {
font-size: var(--FontSize5);
text-align: center;
color: var(--ColorThemes3);
padding: 10px;
}
.card_info {
display: flex;
font-size: var(--FontSize3);
border-radius: 8px;
margin: 10px 10px 15px 10px;
flex-direction: column;
align-items: stretch;
border: 1px solid var(--ColorThemes3);
background: var(--ColorThemes2);
}
.card_info_homestead {
width: 100%;
margin: 0;
}
.card_info > .info {
display: flex;
font-size: var(--FontSize3);
align-items: center;
justify-content: space-between;
border-radius: 8px;
}
.card_info > .info > span {
min-width: 40px;
font-size: var(--FontSize1);
position: relative;
margin: 5px;
}
.card_info > .info > select {
color: #3d3d3d;
border-radius: 6px;
border: 1px solid #eaebef;
margin: 5px;
background-color: var(--ColorThemes3);
min-width: 110px;
width: 100%;
padding: 4px;
height: 30px;
}
.card_info > .info > input,
.card_info > .info > button {
font-size: var(--FontSize4);
font-weight: 400;
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 6px;
margin: 5px;
background-color: var(--ColorThemes0);
border: 1px solid var(--ColorThemes1);
color: var(--ColorThemes3);
width: 100%;
max-width: 170px;
min-width: 70px;
padding: 0 4px;
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.card_info > .info > button > svg {
width: 15px;
height: 15px;
fill: var(--ColorThemes3);
}
.card_info > textarea {
border-radius: 6px;
font-size: var(--FontSize3);
margin: 5px;
background-color: var(--ColorThemes0);
border: 1px solid var(--ColorThemes1);
color: var(--ColorThemes3);
width: calc(100% - 22px);
min-width: 70px;
padding: 5px;
min-height: 40px;
appearance: none;
resize: vertical;
-webkit-appearance: none;
}
.card_info > textarea::placeholder {
color: var(--ColorThemes3);
opacity: 0.5;
}
.card_info > textarea::-webkit-input-placeholder {
color: var(--ColorThemes3);
opacity: 0.5;
}
#map_card {
display: none;
width: 100%;
height: calc(100vh - 100px);
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
border-radius: var(--border-radius);
}
</style>
<div class="page-card">
<div class="list-controls" id="page-card-controls" style="display: none">
<div id="page-card-sort">
<button id="sort_1" onclick="Territory_card.sort('2')" data-state="active">
<button
id="sort_1"
onclick="Territory_card.sort('2')"
data-state="active"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 32.476562 5.9785156 A 1.50015 1.50015 0 0 0 31 7.5 L 31 37.878906 L 26.560547 33.439453 A 1.50015 1.50015 0 1 0 24.439453 35.560547 L 31.439453 42.560547 A 1.50015 1.50015 0 0 0 33.560547 42.560547 L 40.560547 35.560547 A 1.50015 1.50015 0 1 0 38.439453 33.439453 L 34 37.878906 L 34 7.5 A 1.50015 1.50015 0 0 0 32.476562 5.9785156 z M 14.375 8.0058594 C 14.257547 8.01575 14.139641 8.0379219 14.025391 8.0761719 L 11.025391 9.0761719 C 10.239391 9.3381719 9.8141719 10.188609 10.076172 10.974609 C 10.338172 11.760609 11.190609 12.188828 11.974609 11.923828 L 13 11.580078 L 13 20.5 C 13 21.329 13.671 22 14.5 22 C 15.329 22 16 21.329 16 20.5 L 16 9.5 C 16 9.018 15.767953 8.5652031 15.376953 8.2832031 C 15.082953 8.0717031 14.727359 7.9761875 14.375 8.0058594 z M 14 27 C 11.344 27 9.387625 28.682109 9.015625 31.287109 C 8.898625 32.107109 9.4671094 32.867375 10.287109 32.984375 C 11.106109 33.102375 11.867375 32.533891 11.984375 31.712891 C 12.096375 30.931891 12.537 30 14 30 C 15.103 30 16 30.897 16 32 C 16 33.103 15.103 34 14 34 C 11.592 34 9 35.721 9 39.5 C 9 40.329 9.672 41 10.5 41 L 17.5 41 C 18.329 41 19 40.329 19 39.5 C 19 38.671 18.329 38 17.5 38 L 12.308594 38 C 12.781594 37.093 13.664 37 14 37 C 16.757 37 19 34.757 19 32 C 19 29.243 16.757 27 14 27 z"
@@ -394,6 +199,8 @@
</div>
</div>
<div id="page-card-info" style="display: none"></div>
<div id="list"></div>
<div id="map_card"></div>
</div>
@@ -403,4 +210,4 @@
<button id="card-new-date-button">Оновити дату</button>
<input type="datetime-local" id="card-new-date-input" placeholder="Дата" />
</div>
</div>
</div>

View File

@@ -32,7 +32,6 @@ const Territory_card = {
// Застосовуємо режим сортування
this.sort(localStorage.getItem('territory_card_sort'), false);
this.getEntrances({ update: false });
} else if (type === "homestead") {
this.getHomestead.map({});
}
@@ -40,7 +39,7 @@ const Territory_card = {
const ids = ['cloud_1', 'cloud_2', 'cloud_3'];
ids.forEach((id, idx) => {
const el = document.getElementById(id);
if(!el) return;
if (!el) return;
el.setAttribute('data-state', ['sync', 'ok', 'err'].indexOf(Cloud.status) === idx ? 'active' : '');
});
@@ -134,10 +133,18 @@ const Territory_card = {
if (navigator.onLine && Cloud.socket?.readyState === WebSocket.OPEN) {
Cloud.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Cloud.start();
Territory_card.getEntrances({ update: true });
}
Notifier.click({
title: `Запис не додано!`,
text: `Натисніть, щоб перепідключитись!`
}, {
type: 'warn',
f: () => {
Cloud.reconnecting = true;
Cloud.reconnectAttempts = 0;
Cloud.start();
},
timeout: 0
});
}
},
@@ -181,9 +188,18 @@ const Territory_card = {
if (navigator.onLine && Cloud.socket?.readyState === WebSocket.OPEN) {
Cloud.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.cloud.start();
}
Notifier.click({
title: `Запис не додано!`,
text: `Натисніть, щоб перепідключитись!`
}, {
type: 'warn',
f: () => {
Cloud.reconnecting = true;
Cloud.reconnectAttempts = 0;
Cloud.start();
},
timeout: 0
});
}
}
},
@@ -211,12 +227,14 @@ const Territory_card = {
return;
}
Territory_card.info(data);
const fragment = document.createDocumentFragment();
const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory);
for (const element of data) {
const { id, entrance_number, title, history, working } = element;
const { id, entrance_number, title, history, working, address } = element;
const isMy = ((history.name === "Групова" && history.group_id == USER.group_id) || history.name === USER.name);
const show = (isMy && working) ? "open" : canManage ? "close" : null;
@@ -310,8 +328,9 @@ const Territory_card = {
div.style = style;
div.innerHTML = `
<span>Квартира ${apt.title}</span>
<hr>
<div class="info">
<span>кв.${apt.title}</span>
<select id="status_${apt.id}" onchange="Territory_card.cloud.messApartment({number:${number},id:${apt.id},update:true})" style="${style}">
${statusOptions(apt.status)}
</select>
@@ -350,9 +369,6 @@ const Territory_card = {
async map({ homestead_id = Territory_card.id }) {
let data = await this.loadAPI({ url: `${CONFIG.api}homestead/${homestead_id}` });
console.log(data);
let lat = data.geo?.lat ?? data.points?.[0]?.[0]?.[0]?.lat ?? 49.5629016;
let lng = data.geo?.lng ?? data.points?.[0]?.[0]?.[0]?.lng ?? 25.6145625;
let zoom = 15;
@@ -483,10 +499,116 @@ const Territory_card = {
Territory_card.getHomestead.markers[element.id] = marker; // сохраним ссылку на маркер
}
if((USER.possibilities.can_joint_territory && data.history.sheep_id == USER.id) || USER.mode == 2){
this.joint.setHTML(homestead_id);
}
},
joint: {
async setHTML(homestead_id){
let lest = await this.getJoint(homestead_id);
let block_info = document.getElementById('page-card-info');
block_info.style.display = "flex";
block_info.innerHTML = `
<h2>Надати спільний доступ:</h2>
<smart-select type="number" id="joint-${homestead_id}" onchange="Territory_card.getHomestead.joint.setJoint('${homestead_id}')" max="30" placeholder="Оберіть вісників..." title="Оберіть вісників, з якими хочете поділитись територією">
${Sheeps.sheeps_list.list.map(p => {
const isSelected = lest.some(item => item.sheep_id === p.id);
if(USER.id === Number(p.id) && USER.mode != 2) return
return `<div
slot="option"
data-value="${Number(p.id)}"
${isSelected ? 'data-selected' : ''}>
${p.name}
</div>`;
}).join('')}
</smart-select>
`;
},
setJoint(homestead_id){
const select = document.getElementById(`joint-${homestead_id}`);
if (!select) return;
console.log(select.getClick);
if(select.getClick.state == "add"){
this.addSheep(homestead_id, select.getClick.value);
} else if(select.getClick.state == "delete"){
this.delSheep(homestead_id, select.getClick.value);
}
},
async getJoint(homestead_id){
let uuid = localStorage.getItem("uuid");
return await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
}).then((response) => response.json());
},
async addSheep(homestead_id, sheep_id){
const uuid = localStorage.getItem('uuid');
if (!homestead_id) {
console.warn("Невірні дані для наданя доступу.");
return;
}
try {
const response = await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify({"sheep_id": sheep_id})
});
if (!response.ok) throw new Error("Failed to assign");
Notifier.success('Віснику успішно надано доступ.');
} catch (err) {
console.error('❌ Error:', err);
Notifier.error('Помилка надання доступу.');
}
},
async delSheep(homestead_id, sheep_id){
const uuid = localStorage.getItem('uuid');
if (!homestead_id) {
console.warn("Невірні дані для відкликання доступу.");
return;
}
try {
const response = await fetch(`${CONFIG.api}homestead/joint/${homestead_id}`, {
method: 'DELETE',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify({"sheep_id": sheep_id})
});
if (!response.ok) throw new Error("Failed to assign");
Notifier.success('Доступ успішно відкликанно.');
} catch (err) {
console.error('❌ Error:', err);
Notifier.error('Помилка при відкликанні доступу.');
}
}
}
},
async reload(){
async reload() {
Territory_card.getEntrances({ update: true });
},
@@ -547,5 +669,39 @@ const Territory_card = {
}
this.close();
}
},
info(data) {
let block_info = document.getElementById('page-card-info');
block_info.style.display = "flex";
block_info.innerHTML = `
<a href="https://www.google.com/maps?q=${data[0].address.points_number.lat},${data[0].address.points_number.lng}">${data[0].address.title} ${data[0].address.number}</a>
<hr>
<h2>Терміни опрацювання:</h2>
`
for (let index = 0; index < data.length; index++) {
const element = data[index];
const canManage = USER.mode === 2 || (USER.mode === 1 && USER.possibilities.can_manager_territory);
const isMy = ((element.history.name === "Групова" && element.history.group_id == USER.group_id) || element.history.name === USER.name);
let date_start = element.history.date.start;
let date_end = date_start + (1000 * 2629743 * 4);
let red = () => {
if(Date.now() > date_end) return `color: #ec2d2d;`
return
}
if (element.working && (isMy || canManage)) {
block_info.innerHTML += `
<div>
<h3>${element.title}</h3>
<h4>${formattedDate(date_start)} — <span style="${red()}">${formattedDate(date_end)}</span></h4>
</div>
`;
}
}
}
}

View File

@@ -88,6 +88,48 @@
}
#page-card-info {
padding: 10px;
margin: 0px 0 10px 0;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
border-radius: var(--border-radius);
/* overflow: auto; */
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 10px;
}
#page-card-info>a {
font-size: var(--FontSize5);
font-weight: 500;
}
#page-card-info>h2 {
font-size: var(--FontSize4);
}
#page-card-info>div {
display: flex;
align-items: flex-end;
gap: 10px;
}
#page-card-info>div>h3 {
font-size: var(--FontSize3);
font-weight: 500;
opacity: 0.9;
}
#page-card-info>div>h4 {
font-size: var(--FontSize2);
font-weight: 400;
opacity: 0.8;
}
#card-new-date {
display: flex;
width: 100%;
@@ -149,4 +191,221 @@
font-weight: 400;
-webkit-appearance: none;
-moz-appearance: none;
}
.page-card *[disabled] {
opacity: 0.5;
}
.page-card #list {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.page-card details {
color: var(--ColorThemes3);
width: 100%;
min-width: 320px;
background: var(--ColorThemes1);
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
}
@media (min-width: 900px) {
.page-card #list {
align-items: flex-start;
justify-content: space-between;
flex-direction: row;
flex-wrap: wrap;
}
.page-card details {
width: calc(50% - 10px);
}
}
.page-card details[disabled] .page-card details summary,
.page-card details.disabled .page-card details summary {
pointer-events: none;
user-select: none;
}
.page-card details .page-card details summary::-webkit-.page-card details-marker,
.page-card details .page-card details summary::marker {
display: none;
content: "";
}
.page-card details summary {
cursor: pointer;
height: 45px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-card details summary p {
padding: 0 10px;
font-size: var(--FontSize3);
font-weight: 500;
}
.page-card details summary svg {
width: 25px;
height: 25px;
padding: 0 10px;
fill: currentColor;
}
.page-card .apartments_list {
padding: 10px;
gap: 15px;
display: flex;
flex-direction: column;
}
.page-card .apartments_list>p {
font-size: var(--FontSize5);
text-align: center;
color: var(--ColorThemes3);
padding: 10px;
}
.page-card .card_info {
display: flex;
font-size: var(--FontSize3);
border-radius: 8px;
flex-direction: column;
align-items: stretch;
border: 1px solid var(--ColorThemes3);
background-color: var(--ColorThemes2);
gap: 6px;
padding: 6px;
}
.card_info_homestead {
border-radius: 6px !important;
width: calc(100% - 15px);
margin: 0;
}
.page-card .card_info>span {
font-size: var(--FontSize3);
font-weight: 500;
position: relative;
}
.page-card .card_info>hr {
height: 1px;
background-color: currentColor;
opacity: 0.1;
margin-bottom: 4px;
}
.page-card .card_info>.info {
display: flex;
font-size: var(--FontSize3);
align-items: center;
justify-content: space-between;
border-radius: 8px;
gap: 5px;
}
.page-card .card_info>.info>select {
color: #3d3d3d;
border-radius: 6px;
border: 1px solid #eaebef;
background-color: var(--ColorThemes3);
min-width: 110px;
width: 50%;
padding: 4px;
height: 30px;
}
.page-card .card_info>.info>input,
.page-card .card_info>.info>button {
font-size: var(--FontSize2);
font-weight: 400;
-webkit-appearance: none;
-moz-appearance: none;
border-radius: 6px;
background-color: var(--ColorThemes0);
border: 1px solid var(--ColorThemes1);
color: var(--ColorThemes3);
width: 50%;
min-width: 70px;
padding: 0 4px;
height: 30px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.page-card .card_info>.info>button>svg {
width: 15px;
height: 15px;
fill: var(--ColorThemes3);
}
.page-card .card_info>textarea {
border-radius: 6px;
font-size: var(--FontSize3);
background-color: var(--ColorThemes0);
border: 0;
color: var(--ColorThemes3);
width: calc(100% - 10px);
min-width: 70px;
padding: 5px;
min-height: 40px;
appearance: none;
resize: vertical;
-webkit-appearance: none;
}
.page-card .card_info>textarea::placeholder {
color: var(--ColorThemes3);
opacity: 0.5;
}
.page-card .card_info>textarea::-webkit-input-placeholder {
color: var(--ColorThemes3);
opacity: 0.5;
}
.page-card #map_card {
display: none;
width: 100%;
height: calc(100vh - 100px);
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
border-radius: var(--border-radius);
margin-bottom: 15px;
}
smart-select {
--smart-select-option-color: var(--ColorThemes3);
--smart-select-border-color: var(--ColorThemes2);
--smart-select-background: var(--ColorThemes0);
--smart-select-color: var(--PrimaryColor);
--smart-select-chip-fill: var(--PrimaryColorText);
--smart-select-chip-background: var(--PrimaryColor);
--smart-select-chip-color: var(--ColorThemes3);
--smart-select-search-color: var(--ColorThemes3);
--smart-select-hover-background: var(--ColorThemes2);
--smart-select-hover-color: var(--ColorThemes3);
--smart-select-selected-background: var(--PrimaryColor);
--smart-select-selected-color: var(--PrimaryColorText);
--smart-select-font-size-1: var(--FontSize1);
--smart-select-font-size-2: var(--FontSize3);
--smart-select-border-radius-1: 8px;
--smart-select-border-radius-2: 4px;
}

View File

@@ -13,12 +13,20 @@
name="address"
required
value=""
onchange="Territory_editor.info.title=this.value"
/>
</div>
<div>
<label for="info-number">Номер будинку / частини</label>
<input type="text" id="info-number" name="number" required value="" />
<input
type="text"
id="info-number"
name="number"
required
value=""
onchange="Territory_editor.info.number=this.value"
/>
</div>
<div>
@@ -29,6 +37,7 @@
name="settlement"
required
value="Тернопіль"
onchange="Territory_editor.info.settlement=this.value"
/>
</div>
</form>
@@ -66,14 +75,18 @@
</div>
</div>
<span>або</span>
<button onclick="Territory_editor.osm.newPoligon()">Обрати на карті</button>
<button onclick="Territory_editor.osm.newPoligon()">
Обрати на карті
</button>
</div>
<div class="block-map">
<div id="map"></div>
</div>
<button type="button" id="part-2-button" onclick="Territory_editor.save()">Зберегти</button>
<button type="button" id="part-2-button" onclick="Territory_editor.save()">
Зберегти
</button>
</div>
<div id="part-3" class="part_block" style="display: none">

View File

@@ -40,10 +40,11 @@ const Territory_list = {
},
house: {
url: null,
list: [],
loadAPI: async function (url) {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
const response = await fetch(url ?? Territory_list.house.url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
@@ -60,6 +61,7 @@ const Territory_list = {
const territory_list_filter = Number(localStorage.getItem("territory_list_filter") ?? 0);
const url = `${CONFIG.api}houses/list${territory_entrances ? '/entrances' : ''}`;
Territory_list.house.url = url;
let list = this.list.length > 0 ? this.list : await this.loadAPI(url);
const isEnd = territory_list_filter === "2";
@@ -94,11 +96,13 @@ const Territory_list = {
const person = working
? `${element.history.name === 'Групова' ? 'Група ' + element.history.group_id : element.history.name}`
: ``;
const overdue = working && (element.history.date.start + (1000 * 2629743 * 4)) <= Date.now();
card.image = `${CONFIG.web}cards/house/T${element.house.id}.webp`;
card.address = `${element.house.title} ${element.house.number} (${element.title})`;
card.link = `/territory/manager/house/${element.house.id}`;
card.sheep = person;
card.overdue = overdue;
block.appendChild(card);
} else {
const qty = element.entrance.quantity;
@@ -154,8 +158,8 @@ const Territory_list = {
}
});
this.list = await response.json();
return this.list;
Territory_list.homestead.list = await response.json();
return Territory_list.homestead.list;
},
setHTML: async function () {
const block = document.getElementById('list-homestead');
@@ -186,12 +190,15 @@ const Territory_list = {
const person = working
? `${element.history.name === 'Групова' ? 'Група ' + element.history.group_id : element.history.name}`
: ``;
const overdue = working && (element.history.date.start + (1000 * 2629743 * 4)) <= Date.now();
const card = document.createElement('app-territory-card');
card.image = `${CONFIG.web}cards/homestead/H${element.id}.webp`;
card.address = `${element.title} ${element.number}`;
card.link = `/territory/manager/homestead/${element.id}`;
card.sheep = person;
card.overdue = overdue;
block.appendChild(card);
}
}

View File

@@ -358,6 +358,11 @@ const Territory_Manager = {
Territory_Manager.mess.close();
Territory_Manager.entrances.list = [];
await Territory_Manager.entrances.setHTML(type, id);
Territory_list.house.list = [];
Territory_list.homestead.list = [];
Territory_list.house.loadAPI();
Territory_list.homestead.loadAPI();
} catch (err) {
console.error('❌ Error:', err);
Notifier.error('Помилка призначення території');
@@ -389,6 +394,11 @@ const Territory_Manager = {
Territory_Manager.entrances.list = [];
await Territory_Manager.entrances.setHTML(type, id);
Territory_list.house.list = [];
Territory_list.homestead.list = [];
Territory_list.house.loadAPI();
Territory_list.homestead.loadAPI();
Notifier.success('Територія забрана успішно');
} catch (error) {
console.error("❌ Помилка зняття призначення:", error);

View File

@@ -1,7 +1,7 @@
let map_all;
let map_all, free_entrance, free_homesteads;
const Territory_Map = {
init: async () => {
async init() {
let html = await fetch('/lib/pages/territory/map/index.html').then((response) => response.text());
app.innerHTML = html;
@@ -9,7 +9,7 @@ const Territory_Map = {
Territory_Map.info.setHTML();
},
info: {
loadAPI: async (url) => {
async loadAPI(url) {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
@@ -23,22 +23,22 @@ const Territory_Map = {
return await response.json();
},
setHTML: async () => {
async setHTML() {
const houses = await Territory_Map.info.loadAPI(`${CONFIG.api}houses/list`);
const homestead = await Territory_Map.info.loadAPI(`${CONFIG.api}homestead/list`);
Territory_Map.map.added({ type: "houses", data: houses });
Territory_Map.map.added({ type: "house", data: houses });
Territory_Map.map.added({ type: "homestead", data: homestead });
}
},
map: {
polygons: [],
init: () => {
init() {
if (map_all && map_all.remove) map_all.remove();
const mapElement = document.getElementById('map');
if (!mapElement) return;
let firstLocate = true;
let googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 20,
@@ -55,6 +55,9 @@ const Territory_Map = {
tms: true
});
free_entrance = new L.FeatureGroup();
free_homesteads = new L.FeatureGroup();
map_all = L.map(mapElement, {
renderer: L.canvas(),
center: [49.5629016, 25.6145625],
@@ -63,22 +66,72 @@ const Territory_Map = {
layers: [
googleHybrid,
osm,
mytile
mytile,
free_entrance,
free_homesteads
]
});
map_all.locate({
setView: true, // 🔥 сразу центрирует карту
maxZoom: 16
});
let baseMaps = {
"Google Hybrid": googleHybrid,
"OpenStreetMap": osm,
"Sheep Service Map": mytile,
};
let layerControl = L.control.layers(baseMaps, [], { position: 'bottomright' }).addTo(map_all);
let baseLayer = {
"Вільні під'їзди": free_entrance,
"Вільні райони": free_homesteads
};
L.control.layers(baseMaps, baseLayer, { position: 'bottomright' }).addTo(map_all);
map_all.pm.setLang("ua");
map_all.on('zoomend', () => {
const z = map_all.getZoom();
if (z <= 15) {
map_all.removeLayer(free_homesteads);
} else {
map_all.addLayer(free_homesteads);
}
if (z <= 16) {
map_all.removeLayer(free_entrance);
} else {
map_all.addLayer(free_entrance);
}
});
// слежение в реальном времени
map_all.locate({ setView: false, watch: true, enableHighAccuracy: true });
map_all.on('locationfound', (e) => {
if (firstLocate) map_all.setView(e.latlng, 16);
if (!map_all._userMarker) {
map_all._userMarker = L.marker(e.latlng).addTo(map_all).bindPopup("");
map_all._userMarker.on("popupopen", () => {
const div = document.createElement("div");
div.className = 'marker_popup'
div.innerHTML = `<p>Ви тут!</p>`;
map_all._userMarker.setPopupContent(div);
});
} else {
map_all._userMarker.setLatLng(e.latlng);
}
firstLocate = false;
});
},
added: ({ type, data }) => {
added({ type, data }) {
for (let index = 0; index < data.length; index++) {
const element = data[index];
let posPersonal, posGroup;
@@ -108,6 +161,13 @@ const Territory_Map = {
}
}
this.marker({
id: element.id,
type: 'homestead',
free: element.working ? 0 : 1,
geo: element.geo
})
} else {
posPersonal = Home.personal.house.list.map(e => e.id).indexOf(element.id);
posGroup = Home.group.house.list.map(e => e.id).indexOf(element.id);
@@ -119,6 +179,13 @@ const Territory_Map = {
fillOpacity: 0.8
}
}
this.marker({
id: element.id,
type: 'house',
free: element.entrance.quantity - element.entrance.working,
geo: element.geo
})
}
const polygon = L.polygon(element.points, polygonOptions).addTo(map_all);
@@ -129,13 +196,14 @@ const Territory_Map = {
polygon.on("popupopen", () => {
const div = document.createElement("div");
let text = () => {
if (posPersonal != -1) return "<span>Моя територія</span>"
else if (posGroup != -1) return "<span>Групова територія</span>"
return ""
if (posPersonal != -1) return `<span>Моя територія</span> <p>${element.title} ${element.number}</p> <a href="/territory/card/${type}/${element.id}" data-route>Перейти до території</a>`
else if (posGroup != -1) return `<span>Групова територія</span> <p>${element.title} ${element.number}</p> <a href="/territory/card/${type}/${element.id}" data-route>Перейти до території</a>`
return `<p>${element.title} ${element.number}</p> `
}
div.innerHTML = `${text()} ${element.title} ${element.number}`;
div.className = "leaflet_drop"
div.className = 'marker_popup'
div.innerHTML = `${text()}`;
if (USER.possibilities.can_manager_territory || USER.mode == 2) div.innerHTML += `<a href="/territory/manager/${type}/${element.id}" data-route>Керування</a>`;
polygon.setPopupContent(div);
});
@@ -144,40 +212,29 @@ const Territory_Map = {
}
},
marker: ({ data, personal = false, group = false }) => {
console.log(data);
marker({ id, type, free, geo }) {
if (!USER.possibilities.can_manager_territory || USER.mode != 2) return;
if (free <= 0) return;
for (let index = 0; index < data.length; index++) {
const element = data[index];
const redDot = L.divIcon({
className: "marker",
html: `${free}`,
iconSize: [30, 30],
iconAnchor: [15, 15]
});
// создаём маркер
const marker = L.marker([geo.lat, geo.lng], { icon: redDot }).addTo(type == 'homestead' ? free_homesteads : free_entrance);
marker.bindPopup("");
console.log(element);
// при открытии popup генерим div заново
marker.on("popupopen", () => {
const div = document.createElement("div");
div.className = 'marker_popup'
div.innerHTML = `<a href="/territory/manager/${type}/${id}" data-route>Перейти до території</a>`;
const redDot = L.divIcon({
className: "leaflet_drop",
html: `<div id="redDot_${element.id}"></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
// создаём маркер
const marker = L.marker([element.geo.lat, element.geo.lng], { icon: redDot }).addTo(map_all);
marker.bindPopup("");
// при открытии popup генерим div заново
marker.on("popupopen", () => {
const div = document.createElement("div");
let text = () => {
if (personal) return "Моя територія"
else if (group) return "Групова територія"
return ""
}
div.innerHTML = text();
marker.setPopupContent(div);
});
}
marker.setPopupContent(div);
});
}
}
}

View File

@@ -1,11 +1,53 @@
.page-territory_map {
width: 100%;
width: 100%;
display: flex;
position: relative;
}
.page-territory_map>#map {
margin: 20px;
width: calc(100% - 40px);
height: calc(100% - 40px);
border-radius: calc(var(--border-radius) - 5px);
}
.page-territory_map .marker {
background: var(--ColorThemes2);
color: var(--ColorThemes3);
font-size: var(--FontSize1);
border-radius: var(--border-radius);
border: 2px solid var(--ColorThemes3);
display: flex;
align-items: center;
justify-content: center;
}
.page-territory_map .marker_popup {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin: 0 2px;
}
.page-territory_map .marker_popup>p {
margin: 0;
}
.page-territory_map .marker_popup>span {
margin: 0;
}
.page-territory_map .marker_popup>a {
color: var(--ColorThemes3);
cursor: pointer;
border-radius: calc(var(--border-radius) - 8px);
padding: 5px 10px;
min-width: fit-content;
background: var(--PrimaryColor);
display: flex;
align-items: center;
justify-content: center;
font-weight: 400;
font-size: var(--FontSize1);
min-width: calc(100% - 15px);
}

View File

@@ -94,12 +94,55 @@ Router
pageActive();
})
function pageActive(element) {
const active = document.querySelector("nav li [data-state='active']");
if (active) active.setAttribute('data-state', '');
// function pageActive(element) {
// const active = document.querySelector("nav-item[data-state='active']");
// if (active) active.setAttribute('data-state', '');
// if (element) {
// const target = document.getElementById(`menu-${element}`);
// if (target) target.setAttribute('data-state', 'active');
// }
// }
function pageActive(element) {
// 1. Знаходимо контейнер меню
const navContainer = document.querySelector('navigation-container');
if (!navContainer) {
console.warn('Компонент <navigation-container> не знайдено.');
return;
}
// 2. Видаляємо активний стан у всіх елементів.
// Шукаємо як у Light DOM (через querySelectorAll на документі),
// так і у Shadow DOM (через shadowRoot).
const activeInLight = document.querySelector("nav-item[data-state='active']");
if (activeInLight) {
activeInLight.setAttribute('data-state', '');
}
const activeInShadow = navContainer.shadowRoot.querySelector("nav-item[data-state='active']");
if (activeInShadow) {
activeInShadow.setAttribute('data-state', '');
}
// 3. Знаходимо цільовий елемент
if (element) {
const target = document.getElementById(`nav-${element}`);
if (target) target.setAttribute('data-state', 'active');
const targetId = `menu-${element}`;
let target = null;
// Спробуємо знайти в основному DOM
target = document.getElementById(targetId);
// Якщо не знайдено, шукаємо у Shadow DOM контейнера
if (!target) {
// Використовуємо querySelector для пошуку по всьому shadowRoot
// Якщо елементи переміщені у itemsHiddenContainer, вони будуть тут
target = navContainer.shadowRoot.querySelector(`#${targetId}`);
}
// 4. Встановлюємо активний стан, якщо знайдено
if (target) {
target.setAttribute('data-state', 'active');
}
}
}

View File

@@ -1,4 +1,4 @@
const STATIC_CACHE_NAME = 'v2.0.103';
const STATIC_CACHE_NAME = 'v2.2.1';
const FILES_TO_CACHE = [
'/',
@@ -7,7 +7,12 @@ const FILES_TO_CACHE = [
"/lib/router/router.js",
"/lib/router/routes.js",
"/lib/customElements/notification.js",
"/lib/customElements/notifManager.js",
"/lib/customElements/pwaInstallBanner.js",
"/lib/customElements/swipeUpdater.js",
"/lib/customElements/menuContainer.js",
"/lib/customElements/territoryCard.js",
"/lib/customElements/smartSelect.js",
"/lib/components/leaflet/leaflet.css",
"/lib/components/leaflet/leaflet.js",
@@ -19,12 +24,9 @@ const FILES_TO_CACHE = [
"/lib/components/cloud.js",
"/lib/components/metrics.js",
"/lib/components/clipboard.js",
"/lib/components/colorGroup.js",
"/lib/components/makeid.js",
"/lib/components/swipeUpdater.js",
"/lib/components/detectBrowser.js",
"/lib/components/detectOS.js",
"/lib/components/formattedDate.js",
@@ -141,7 +143,7 @@ self.addEventListener("push", event => {
try { data = event.data.json(); } catch { data = { title: "Повідомлення", body: event.data?.text() }; }
console.log('[ServiceWorker] ', data);
const title = data.title || "Повідомлення";
const options = {
@@ -157,7 +159,7 @@ self.addEventListener("push", event => {
self.addEventListener("notificationclick", event => {
event.notification.close();
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true }).then(clientList => {
clients.matchAll({ type: "window", includeUncontrolled: true }).then(clientList => {
for (const client of clientList) {
if (client.url === event.notification.data && "focus" in client) return client.focus();
}