Переработаны роутеры приложения

Переписано APi WebSocket для работы с новыми роутерами
This commit is contained in:
2025-10-03 17:11:31 +03:00
parent d75fb7ec3d
commit 6ec6523d71
54 changed files with 2593 additions and 3749 deletions

View File

@@ -0,0 +1,406 @@
<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">
<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"
></path>
</svg>
</button>
<button id="sort_2" onclick="Territory_card.sort('3')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 31.478516 6 A 1.50015 1.50015 0 0 0 30.439453 6.4394531 L 23.439453 13.439453 A 1.50015 1.50015 0 1 0 25.560547 15.560547 L 30 11.121094 L 30 41.5 A 1.50015 1.50015 0 1 0 33 41.5 L 33 11.121094 L 37.439453 15.560547 A 1.50015 1.50015 0 1 0 39.560547 13.439453 L 32.560547 6.4394531 A 1.50015 1.50015 0 0 0 31.478516 6 z M 13.375 8.0058594 C 13.257547 8.01575 13.139641 8.0379219 13.025391 8.0761719 L 10.025391 9.0761719 C 9.2393906 9.3381719 8.8141719 10.188609 9.0761719 10.974609 C 9.3381719 11.760609 10.190609 12.188828 10.974609 11.923828 L 12 11.580078 L 12 20.5 C 12 21.329 12.671 22 13.5 22 C 14.329 22 15 21.329 15 20.5 L 15 9.5 C 15 9.018 14.767953 8.5652031 14.376953 8.2832031 C 14.082953 8.0717031 13.727359 7.9761875 13.375 8.0058594 z M 13 27 C 10.344 27 8.387625 28.682109 8.015625 31.287109 C 7.898625 32.107109 8.4671094 32.867375 9.2871094 32.984375 C 10.106109 33.102375 10.867375 32.533891 10.984375 31.712891 C 11.096375 30.931891 11.537 30 13 30 C 14.103 30 15 30.897 15 32 C 15 33.103 14.103 34 13 34 C 10.592 34 8 35.721 8 39.5 C 8 40.329 8.672 41 9.5 41 L 16.5 41 C 17.329 41 18 40.329 18 39.5 C 18 38.671 17.329 38 16.5 38 L 11.308594 38 C 11.781594 37.093 12.664 37 13 37 C 15.757 37 18 34.757 18 32 C 18 29.243 15.757 27 13 27 z"
></path>
</svg>
</button>
<button id="sort_3" onclick="Territory_card.sort('4')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
transform="rotate(180 32.5 24.5106)"
id="svg_1"
d="m32.47852,6a1.50015,1.50015 0 0 0 -1.03907,0.43945l-7,7a1.50015,1.50015 0 1 0 2.1211,2.1211l4.43945,-4.43946l0,30.37891a1.50015,1.50015 0 1 0 3,0l0,-30.37891l4.43945,4.43946a1.50015,1.50015 0 1 0 2.1211,-2.1211l-7,-7a1.50015,1.50015 0 0 0 -1.08203,-0.43945z"
></path>
<path
d="m16,6c-5.511,0 -10,4.489 -10,10c0,5.511 4.489,10 10,10c5.511,0 10,-4.489 10,-10c0,-5.511 -4.489,-10 -10,-10zm0,2c4.43012,0 8,3.56988 8,8c0,4.43012 -3.56988,8 -8,8c-4.43012,0 -8,-3.56988 -8,-8c0,-4.43012 3.56988,-8 8,-8zm-1,2l0,6.41406l4.29297,4.29297l1.41406,-1.41406l-3.70703,-3.70703l0,-5.58594l-2,0z"
></path>
</svg>
</button>
<button id="sort_4" onclick="Territory_card.sort('1')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
id="svg_1"
d="m32.47852,6a1.50015,1.50015 0 0 0 -1.03907,0.43945l-7,7a1.50015,1.50015 0 1 0 2.1211,2.1211l4.43945,-4.43946l0,30.37891a1.50015,1.50015 0 1 0 3,0l0,-30.37891l4.43945,4.43946a1.50015,1.50015 0 1 0 2.1211,-2.1211l-7,-7a1.50015,1.50015 0 0 0 -1.08203,-0.43945z"
></path>
<path
d="m16,22.85262c-5.511,0 -10,4.489 -10,10c0,5.511 4.489,10 10,10c5.511,0 10,-4.489 10,-10c0,-5.511 -4.489,-10 -10,-10zm0,2c4.43012,0 8,3.56988 8,8c0,4.43012 -3.56988,8 -8,8c-4.43012,0 -8,-3.56988 -8,-8c0,-4.43012 3.56988,-8 8,-8zm-1,2l0,6.41406l4.29297,4.29297l1.41406,-1.41406l-3.70703,-3.70703l0,-5.58594l-2,0z"
></path>
</svg>
</button>
</div>
<div id="page-card-status">
<div id="cloud_1" data-state="active" title="Йде синхронізація...">
<svg
xmlns:svg="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
version="1.1"
>
<g>
<g transform="matrix(1, 0, 0, 1, 0, 0)" opacity="1">
<g transform="translate(24 29)">
<g transform="rotate(0)">
<g transform="scale(1 1)">
<g transform="translate(-24 -29)">
<g opacity="1">
<g
transform="matrix(1, 0, 0, 1, 18.772, 33.991)"
opacity="1"
stroke-width="3"
stroke-opacity="1"
stroke="var(--ColorThemes3)"
>
<path
d="M -2.708,2.491 C -2.708,2.491 -2.273,-2.49 -2.273,-2.49 -2.273,-2.49 2.708,-2.055 2.708,-2.055"
style="
fill: none;
stroke-dasharray: none;
stroke-linecap: round;
stroke-linejoin: round;
"
>
<animate
calcMode="spline"
repeatCount="indefinite"
attributeName="d"
dur="1.166667"
begin="0.000000"
keyTimes="0; 0.107143; 0.178571; 0.821429; 0.892857; 1"
keySplines="0 0 1 1; 0.333000 0.000000 0.667000 1.000000; 0.167000 0.167000 0.833000 0.833000; 0.333000 0.000000 0.667000 1.000000; 0.000000 0.000000 0.000000 0.000000"
values="M -2.708,2.491 C -2.708,2.491 -2.273,-2.49 -2.273,-2.49 -2.273,-2.49 2.708,-2.055 2.708,-2.055; M -2.708,2.491 C -2.708,2.491 -2.273,-2.49 -2.273,-2.49 -2.273,-2.49 2.708,-2.055 2.708,-2.055; M -1.771,-2.28 C -1.771,-2.28 -1.761,-2.279 -1.761,-2.279 -1.761,-2.279 -1.78,-2.283 -1.78,-2.283; M -1.768,-2.281 C -1.768,-2.281 -1.758,-2.28 -1.758,-2.28 -1.758,-2.28 -1.778,-2.284 -1.778,-2.284; M -2.708,2.491 C -2.708,2.491 -2.273,-2.49 -2.273,-2.49 -2.273,-2.49 2.708,-2.055 2.708,-2.055; M -2.708,2.491 C -2.708,2.491 -2.273,-2.49 -2.273,-2.49 -2.273,-2.49 2.708,-2.055 2.708,-2.055"
></animate>
</path>
</g>
<g
transform="matrix(1, 0, 0, 1, 24.037, 34)"
opacity="1"
stroke-width="3"
stroke-opacity="1"
stroke="var(--ColorThemes3)"
>
<path
d="M 7.036,-2.5 C 6.006,0.413 3.228,2.5 -0.036,2.5 -3.227,2.5 -5.953,0.507 -7.036,-2.303"
style="
fill: none;
stroke-dasharray: none;
stroke-linecap: round;
stroke-linejoin: round;
"
></path>
</g>
<g
transform="matrix(1, 0, 0, 1, 29.227, 24.009)"
opacity="1"
stroke="var(--ColorThemes3)"
stroke-width="3"
stroke-opacity="1"
>
<path
d="M 2.708,-2.49 C 2.708,-2.49 2.273,2.491 2.273,2.491 2.273,2.491 -2.708,2.055 -2.708,2.055"
style="
fill: none;
stroke-dasharray: none;
stroke-linecap: round;
stroke-linejoin: round;
"
>
<animate
calcMode="spline"
repeatCount="indefinite"
attributeName="d"
dur="1.166667"
begin="0.000000"
keyTimes="0; 0.107143; 0.178571; 0.821429; 0.892857; 1"
keySplines="0 0 1 1; 0.333000 0.000000 0.667000 1.000000; 0.167000 0.167000 0.833000 0.833000; 0.333000 0.000000 0.667000 1.000000; 0.000000 0.000000 0.000000 0.000000"
values="M 2.708,-2.49 C 2.708,-2.49 2.273,2.491 2.273,2.491 2.273,2.491 -2.708,2.055 -2.708,2.055; M 2.708,-2.49 C 2.708,-2.49 2.273,2.491 2.273,2.491 2.273,2.491 -2.708,2.055 -2.708,2.055; M 1.775,2.301 C 1.775,2.301 1.775,2.298 1.775,2.298 1.775,2.298 1.777,2.29 1.777,2.29; M 1.767,2.292 C 1.767,2.292 1.767,2.289 1.767,2.289 1.767,2.289 1.768,2.281 1.768,2.281; M 2.708,-2.49 C 2.708,-2.49 2.273,2.491 2.273,2.491 2.273,2.491 -2.708,2.055 -2.708,2.055; M 2.708,-2.49 C 2.708,-2.49 2.273,2.491 2.273,2.491 2.273,2.491 -2.708,2.055 -2.708,2.055"
></animate>
</path>
</g>
<g
transform="matrix(1, 0, 0, 1, 23.963, 24)"
opacity="1"
stroke="var(--ColorThemes3)"
stroke-width="3"
stroke-opacity="1"
>
<path
d="M -7.036,2.5 C -6.006,-0.413 -3.229,-2.5 0.036,-2.5 3.228,-2.5 5.953,-0.507 7.036,2.303"
style="
fill: none;
stroke-dasharray: none;
stroke-linecap: round;
stroke-linejoin: round;
"
></path>
</g>
</g>
</g>
</g>
<animateTransform
calcMode="spline"
repeatCount="indefinite"
attributeName="transform"
dur="1.166667"
begin="0.000000"
type="rotate"
keyTimes="0; 0.107143; 0.821429; 0.892857; 1"
keySplines="0 0 1 1; 0.333000 0.000000 0.667000 1.000000; 0.333000 0.000000 0.667000 1.000000; 0.000000 0.000000 0.000000 0.000000"
values="0; 0; 380; 360; 360"
></animateTransform>
</g>
</g>
</g>
<g transform="matrix(1, 0, 0, 1, 0, 0)" opacity="1">
<g
fill-opacity="1"
transform="matrix(1, 0, 0, 1, 24, 22.5)"
opacity="1"
fill="var(--ColorThemes3)"
>
<path
sodipodi:nodetypes="ccccccccccccccccccccccccc"
d="M 15,-2.5 C 15,-2.5 14.42,-2.5 14.42,-2.5 13.67,-9.79 7.49,-15.5 0,-15.5 -7.49,-15.5 -13.67,-9.79 -14.42,-2.5 -14.42,-2.5 -15,-2.5 -15,-2.5 -19.96,-2.5 -24,1.54 -24,6.5 -24,11.46 -19.96,15.5 -15,15.5 -15,15.5 -12.17,15.5 -12.17,15.5 -12.38,14.91 -12.48,14.26 -12.42,13.59 -12.42,13.59 -12.32,12.5 -12.32,12.5 -12.32,12.5 -15,12.5 -15,12.5 -18.31,12.5 -21,9.81 -21,6.5 -21,3.19 -18.31,0.5 -15,0.5 -15,0.5 -13,0.5 -13,0.5 -12.17,0.5 -11.5,-0.17 -11.5,-1 -11.5,-7.34 -6.34,-12.5 0,-12.5 6.34,-12.5 11.5,-7.34 11.5,-1 11.5,-0.17 12.17,0.5 13,0.5 13,0.5 15,0.5 15,0.5 18.31,0.5 21,3.19 21,6.5 21,9.81 18.31,12.5 15,12.5 15,12.5 10.38,12.5 10.38,12.5 9.73,13.64 8.89,14.66 7.92,15.5 7.92,15.5 15,15.5 15,15.5 19.96,15.5 24,11.46 24,6.5 24,1.54 19.96,-2.5 15,-2.5 Z"
style=""
></path>
</g>
</g>
</g>
</svg>
</div>
<div id="cloud_2" data-state="" title="Синхронізовано">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 24 7 C 16.51 7 10.330078 12.71 9.5800781 20 L 9 20 C 4.04 20 0 24.04 0 29 C 0 33.96 4.04 38 9 38 L 16.640625 38 L 13.640625 35 L 9 35 C 5.69 35 3 32.31 3 29 C 3 25.69 5.69 23 9 23 L 11 23 C 11.83 23 12.5 22.33 12.5 21.5 C 12.5 15.16 17.66 10 24 10 C 30.34 10 35.5 15.16 35.5 21.5 C 35.5 22.33 36.17 23 37 23 L 39 23 C 42.31 23 45 25.69 45 29 C 45 32.31 42.31 35 39 35 L 29.839844 35 L 26.539062 38 L 39 38 C 43.96 38 48 33.96 48 29 C 48 24.04 43.96 20 39 20 L 38.419922 20 C 37.669922 12.71 31.49 7 24 7 z M 32.542969 24.986328 A 1.50015 1.50015 0 0 0 31.490234 25.390625 L 21.548828 34.427734 L 16.560547 29.439453 A 1.50015 1.50015 0 1 0 14.439453 31.560547 L 20.439453 37.560547 A 1.50015 1.50015 0 0 0 22.509766 37.609375 L 33.509766 27.609375 A 1.50015 1.50015 0 0 0 32.542969 24.986328 z"
></path>
</svg>
</div>
<div id="cloud_3" data-state="" title="Помилка синхронізації">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 24 8 C 16.51 8 10.330078 13.71 9.5800781 21 L 9 21 C 4.04 21 0 25.04 0 30 C 0 34.96 4.04 39 9 39 L 19.099609 39 C 18.899609 38 18.999922 36.95 19.419922 36 L 9 36 C 5.69 36 3 33.31 3 30 C 3 26.69 5.69 24 9 24 L 11 24 C 11.83 24 12.5 23.33 12.5 22.5 C 12.5 16.16 17.66 11 24 11 C 30.34 11 35.5 16.16 35.5 22.5 C 35.5 23.33 36.17 24 37 24 L 39 24 C 42.31 24 45 26.69 45 30 C 45 33.31 42.31 36 39 36 L 28.580078 36 C 29.000078 36.95 29.100391 38 28.900391 39 L 39 39 C 43.96 39 48 34.96 48 30 C 48 25.04 43.96 21 39 21 L 38.419922 21 C 37.669922 13.71 31.49 8 24 8 z M 23.976562 20.978516 A 1.50015 1.50015 0 0 0 22.5 22.5 L 22.5 31.5 A 1.50015 1.50015 0 1 0 25.5 31.5 L 25.5 22.5 A 1.50015 1.50015 0 0 0 23.976562 20.978516 z M 24 36 A 2 2 0 0 0 24 40 A 2 2 0 0 0 24 36 z"
></path>
</svg>
</div>
</div>
</div>
<div id="list"></div>
<div id="map_card"></div>
</div>
<div id="card-new-date" style="display: none; opacity: 0">
<div class="mess">
<button id="card-new-date-button">Оновити дату</button>
<input type="datetime-local" id="card-new-date-input" placeholder="Дата" />
</div>
</div>

View File

@@ -0,0 +1,624 @@
let map_card;
const Territory_card = {
// Глобальні змінні стану
id: null,
socket: null,
reconnectTimeout: null,
reconnectAttempts: 0,
listEntrances: [],
listApartment: [],
listBuilding: [],
// Кольори статусів квартир
color_status: [
["var(--ColorThemes2)", "var(--ColorThemes3)"],
["#fbf1e0", "#ff8300"],
["#fce3e2", "#ff0000"],
["#d7ddec", "#2919bd"],
["#d5e9dd", "#11a568"],
["#d7ebfa", "#3fb4fc"],
["#e8dbf5", "#b381eb"]
],
// Ініціалізація сторінки
async init(type, Id) {
// Завантажуємо HTML
const html = await fetch('/lib/pages/territory/card/index.html').then(r => r.text());
app.innerHTML = html;
Territory_card.id = Id;
// Закриваємо старий WebSocket
if (this.socket) this.socket.close(1000, "Перезапуск з'єднання");
// this.cloud.start(makeid(6));
this.cloud.start()
// Якщо це сторінка будинку, отримуємо під’їзди та стартуємо WebSocket
if (type === "house") {
const controls = document.getElementById('page-card-controls');
controls.style.display = "flex";
// Застосовуємо режим сортування
this.sort(localStorage.getItem('sort_mode'), false);
this.getEntrances({ update: false });
} else if (type === "homestead") {
this.getHomestead.map({});
}
// Додаємо обробник закриття попапу
const popup = document.getElementById('card-new-date');
if (!popup.dataset.listenerAdded) {
popup.addEventListener('click', (e) => {
if (!popup.querySelector('.mess').contains(e.target)) {
this.dateEditor.close();
}
});
popup.dataset.listenerAdded = 'true';
}
},
// Робота з WebSocket
cloud: {
start() {
const uuid = localStorage.getItem("uuid");
const ws = new WebSocket(CONFIG.wss, uuid);
Territory_card.socket = ws;
ws.onopen = () => {
console.log("[WebSocket] З'єднання встановлено");
Territory_card.cloud.setStatus('ok');
ws.send(JSON.stringify({
event: 'connection',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
uuid,
user: {
name: USER.name,
id: USER.id
},
data: {}
}));
Territory_card.reconnectAttempts = 0;
clearTimeout(Territory_card.reconnectTimeout);
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.event === 'connection' && data.user.id !== USER.id) {
console.log(`Новий користувач: ${data.user}`);
}
if (data.event === 'message') {
Territory_card.cloud.update(data);
}
};
ws.onclose = () => {
console.warn("[WebSocket] З'єднання розірвано");
Territory_card.cloud.setStatus('err');
Territory_card.reconnectAttempts++;
if (Territory_card.reconnectAttempts <= 5) {
Territory_card.reconnectTimeout = setTimeout(() => {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}, 1000);
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.reconnectAttempts = 0;
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
};
ws.onerror = (err) => {
console.error("[WebSocket] Помилка", err);
Territory_card.cloud.setStatus('err');
};
},
setStatus(mode) {
const ids = ['cloud_1', 'cloud_2', 'cloud_3'];
ids.forEach((id, idx) => {
const el = document.getElementById(id);
el.setAttribute('data-state', ['sync', 'ok', 'err'].indexOf(mode) === idx ? 'active' : '');
});
},
update(msg) {
if (msg.type !== "apartment" && msg.type !== "building") return;
const [bg, color] = Territory_card.color_status[msg.data.status];
const id = msg.data.id;
const el = document.getElementById(`status_${id}`);
const redDot = document.getElementById(`redDot_${id}`);
if (msg.type === "building") {
redDot.style = `background:${bg};border:2px solid ${color}`;
const apt = Territory_card.listBuilding.find(e => e.id === id);
if (!apt) return;
apt.status = msg.data.status;
apt.description = msg.data.description;
apt.updated_at = msg.data.updated_at;
if (!el) return;
let date = new Date(msg.data.updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
document.getElementById(`date_${id}`).value = date.toISOString().slice(0, 16);
} else if (msg.type === "apartment") {
if (!el) return;
document.getElementById(`date_text_${id}`).innerText = formattedDateTime(msg.data.updated_at);
}
if (!el) return;
document.getElementById(`card_${id}`).style = `background:${bg};color:${color};border:1px solid ${color}`;
el.value = msg.data.status;
el.style = `background:${bg};color:${color};border:1px solid ${color}`;
document.getElementById(`description_${id}`).value = msg.data.description;
},
messApartment({ number, id, update, time }) {
const apt = Territory_card.listApartment[number]?.find(e => e.id === id);
if (!apt) return;
const statusEl = document.getElementById(`status_${id}`);
const descEl = document.getElementById(`description_${id}`);
let date = () => {
if (!update && !time) {
return apt.updated_at;
} else if (update && !time) {
return getTimeInSeconds();
} else if (update && time) {
return getTimeInSeconds(time);
}
}
apt.status = Number(statusEl.value);
apt.description = descEl.value;
apt.updated_at = date();
const [bg, color] = Territory_card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
user: {
name: USER.name,
id: USER.id
},
type: "apartment",
data: {
...apt,
sheep_id: USER.id
}
};
if (Territory_card.socket?.readyState === WebSocket.OPEN) {
Territory_card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
},
messBuildings({ id, update, time }) {
const apt = Territory_card.listBuilding.find(e => e.id === id);
if (!apt) return;
const statusEl = document.getElementById(`status_${id}`);
const descEl = document.getElementById(`description_${id}`);
const dateEl = document.getElementById(`date_text_${id}`);
let date = () => {
if (!update && !time) {
return apt.updated_at;
} else if (update && !time) {
return getTimeInSeconds();
} else if (update && time) {
const ts = new Date(time).getTime();
return getTimeInSeconds(ts);
}
}
apt.status = Number(statusEl.value);
apt.description = descEl.value;
apt.updated_at = date();
const [bg, color] = Territory_card.color_status[apt.status];
statusEl.style = `background:${bg};color:${color};border:1px solid ${color}`;
const message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
user: {
name: USER.name,
id: USER.id
},
type: "building",
data: {
...apt,
sheep_id: USER.id
}
};
if (Territory_card.socket?.readyState === WebSocket.OPEN) {
Territory_card.socket.send(JSON.stringify(message));
} else {
if (confirm("З'єднання розірвано! Перепідключитись?")) {
Territory_card.getEntrances({ update: true });
Territory_card.cloud.start();
}
}
}
},
// Отримання під’їздів
async getEntrances({ house_id = Territory_card.id, update = false }) {
const uuid = localStorage.getItem("uuid");
const res = await fetch(`${CONFIG.api}/house/${house_id}/entrances`, {
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
const data = await res.json();
this.listEntrances = data;
const container = document.getElementById('list');
if (!update) container.innerHTML = "";
if (update) {
for (const { id, entrance_number } of data) {
this.getApartment({ id, number: entrance_number, update: true });
}
return;
}
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 isMy = ((history.name === "Групова" && history.group_id == USER.group_id) || history.name === USER.name);
const show = (isMy && working) ? "open" : canManage ? "close" : null;
if (!show) continue;
const icon = isMy && working
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 12 1 C 9.1277778 1 6.7189086 3.0461453 6.1230469 5.7871094 L 8.078125 6.2128906 C 8.4822632 4.3538547 10.072222 3 12 3 C 14.27619 3 16 4.7238095 16 7 L 16 8 L 6 8 C 4.9069372 8 4 8.9069372 4 10 L 4 20 C 4 21.093063 4.9069372 22 6 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 10 C 20 8.9069372 19.093063 8 18 8 L 18 7 C 18 3.6761905 15.32381 1 12 1 z M 6 10 L 18 10 L 18 20 L 6 20 L 6 10 z M 12 13 C 10.9 13 10 13.9 10 15 C 10 16.1 10.9 17 12 17 C 13.1 17 14 16.1 14 15 C 14 13.9 13.1 13 12 13 z"/></svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 12 1 C 8.6761905 1 6 3.6761905 6 7 L 6 8 C 4.9069372 8 4 8.9069372 4 10 L 4 20 C 4 21.093063 4.9069372 22 6 22 L 18 22 C 19.093063 22 20 21.093063 20 20 L 20 10 C 20 8.9069372 19.093063 8 18 8 L 18 7 C 18 3.6761905 15.32381 1 12 1 z M 12 3 C 14.27619 3 16 4.7238095 16 7 L 16 8 L 8 8 L 8 7 C 8 4.7238095 9.7238095 3 12 3 z M 6 10 L 18 10 L 18 20 L 6 20 L 6 10 z M 12 13 C 10.9 13 10 13.9 10 15 C 10 16.1 10.9 17 12 17 C 13.1 17 14 16.1 14 15 C 14 13.9 13.1 13 12 13 z"/></svg>`;
const details = document.createElement('details');
if (show === "open") details.setAttribute('open', '');
details.innerHTML = `
<summary><p>${title}</p>${icon}</summary>
<div id="apartments_${id}" class="apartments_list"></div>
`;
fragment.appendChild(details);
this.getApartment({ id, number: entrance_number, update: false });
}
container.appendChild(fragment);
},
async getApartment({ id, number, update }) {
const uuid = localStorage.getItem('uuid');
const res = await fetch(`${CONFIG.api}/apartment/${id}`, {
headers: { "Authorization": uuid }
});
const data = await res.json();
this.listApartment[number] = data;
const sort_mode = localStorage.getItem('sort_mode') ?? "1";
const sorters = {
"1": (a, b) => a.apartment_number - b.apartment_number,
"2": (a, b) => b.apartment_number - a.apartment_number,
"3": (a, b) => a.updated_at - b.updated_at,
"4": (a, b) => b.updated_at - a.updated_at,
};
data.sort(sorters[sort_mode] || sorters["1"]);
const container = document.getElementById(`apartments_${id}`);
if (!update) container.innerHTML = "";
const statusOptions = (selected) => {
const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"];
return labels.map((txt, i) => `<option value="${i}" ${i === selected ? "selected" : ""}>${txt}</option>`).join("");
};
if (update) {
for (const apt of data) {
const [bg, color] = this.color_status[apt.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const dateEl = document.getElementById(`date_${apt.id}`);
const cardEl = document.getElementById(`card_${apt.id}`);
const statusEl = document.getElementById(`status_${apt.id}`);
const dateTextEl = document.getElementById(`date_text_${apt.id}`);
const descEl = document.getElementById(`description_${apt.id}`);
if (cardEl) cardEl.style = style;
if (statusEl) {
statusEl.value = apt.status;
statusEl.style = style;
}
if (dateEl) dateEl.setAttribute('onclick', `Territory_card.dateEditor.open({id: ${apt.id}, number: ${number}, updated_at: ${apt.updated_at}})`);
if (dateTextEl) dateTextEl.innerText = formattedDateTime(apt.updated_at);
if (descEl) descEl.innerText = apt.description ?? "";
}
} else {
const fragment = document.createDocumentFragment();
if (data.length == 0) {
const p = document.createElement('p');
p.innerHTML = `
Інформація про цей під'їзд відсутня. Надайте інформацію відповідальному за території.
`;
return container.appendChild(p);
}
for (const apt of data) {
const [bg, color] = this.color_status[apt.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const div = document.createElement('div');
div.className = `card_info`;
div.id = `card_${apt.id}`;
div.style = style;
div.innerHTML = `
<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>
<button id="date_${apt.id}" onclick="Territory_card.dateEditor.open({id:${apt.id},number:${number},updated_at:${apt.updated_at}})">
<p id="date_text_${apt.id}">${formattedDateTime(apt.updated_at)}</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z"></path></svg>
</button>
</div>
<textarea id="description_${apt.id}" onchange="Territory_card.cloud.messApartment({number:${number},id:${apt.id}})" placeholder="Коротка нотатка.">${apt.description || ""}</textarea>
`;
fragment.appendChild(div);
}
container.appendChild(fragment);
}
},
getHomestead: {
markers: {},
async loadAPI({ url }) {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
return await response.json();
},
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;
if (map_card && map_Territory_card.remove) {
map_Territory_card.stopLocate();
map_Territory_card.remove();
}
const mapElement = document.getElementById('map_card');
mapElement.style.display = "flex";
if (!mapElement) return;
let googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 20,
minZoom: 15,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
});
let osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
});
let mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', {
maxZoom: 20,
minZoom: 15,
tms: true
});
map_card = L.map(mapElement, {
renderer: L.canvas(),
center: [lat, lng],
zoom,
zoomControl: false,
layers: [
googleHybrid,
osm,
mytile
]
});
// слежение в реальном времени
map_Territory_card.locate({ setView: false, watch: true, enableHighAccuracy: true });
map_Territory_card.on('locationfound', (e) => {
if (!map_Territory_card._userMarker) {
map_Territory_card._userMarker = L.marker(e.latlng).addTo(map_card)
.bindPopup("Ви тут!");
} else {
map_Territory_card._userMarker.setLatLng(e.latlng);
}
});
let baseMaps = {
"Google Hybrid": googleHybrid,
"OpenStreetMap": osm,
"Sheep Service Map": mytile,
};
let layerControl = L.control.layers(baseMaps, [], { position: 'bottomright' }).addTo(map_card);
map_Territory_card.pm.setLang("ua");
const polygonOptions = {
color: "#f2bd53",
radius: 500,
fillOpacity: 0.3,
dashArray: '20,15',
dashOffset: '20',
};
L.polygon(data.points, polygonOptions).addTo(map_card);
map_Territory_card.setZoom(data.zoom);
// map_Territory_card.getZoom()
// console.log(data.zoom);
Territory_card.listBuilding = await this.loadAPI({ url: `${CONFIG.api}building/${homestead_id}` });
const statusOptions = (selected) => {
const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"];
return labels.map((txt, i) => `<option value="${i}" ${i === selected ? "selected" : ""}>${txt}</option>`).join("");
};
for (let i = 0; i < Territory_card.listBuilding.length; i++) {
const element = Territory_card.listBuilding[i];
const [bg, color] = Territory_card.color_status[element.status];
const redDot = L.divIcon({
className: "leaflet_drop",
html: `<div id="redDot_${element.id}" style='background:${bg};border:2px solid ${color}'></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
// создаём маркер
const marker = L.marker(element.geo, { icon: redDot }).addTo(map_card);
marker.bindPopup("");
// при открытии popup генерим div заново
marker.on("popupopen", () => {
const el = Territory_card.listBuilding.find(e => e.id === element.id);
const [bg, color] = Territory_card.color_status[el.status];
const style = `background:${bg};color:${color};border:1px solid ${color}`;
const div = document.createElement("div");
div.className = "card_info card_info_homestead";
div.id = `card_${el.id}`;
div.style = style;
let date = new Date(el.updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
div.innerHTML = `
<div class="info">
<select id="status_${element.id}" onchange="Territory_card.cloud.messBuildings({id:${element.id},update:true})" style="${style}">
${statusOptions(element.status)}
</select>
<input type="datetime-local" id="date_${element.id}" placeholder="Дата" onchange="Territory_card.cloud.messBuildings({id:${element.id},update:true, time: this.value })" value="${date.toISOString().slice(0, 16)}">
</div>
<textarea id="description_${element.id}" onchange="Territory_card.cloud.messBuildings({id:${element.id}})" placeholder="Коротка нотатка.">${element.description || ""}</textarea>
`;
marker.setPopupContent(div);
});
Territory_card.getHomestead.markers[element.id] = marker; // сохраним ссылку на маркер
}
},
},
// Сортування
sort(mode, load) {
const idx = Math.max(1, Math.min(4, Number(mode) || 1));
['sort_1', 'sort_2', 'sort_3', 'sort_4'].forEach((id, i) => {
document.getElementById(id)?.setAttribute('data-state', i + 1 === idx ? 'active' : '');
});
localStorage.setItem('sort_mode', idx);
if (!load) this.getEntrances({ update: false });
},
// Редактор дати
dateEditor: {
open({ id, number, updated_at }) {
const block = document.getElementById('card-new-date');
const input = document.getElementById('card-new-date-input');
const button = document.getElementById('card-new-date-button');
// Приводимо дату до ISO без зсуву часового поясу
let date = new Date(updated_at == 0 ? Date.now() : updated_at);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
input.value = date.toISOString().slice(0, 16)
// Призначаємо обробники
input.setAttribute("onchange", `Territory_card.dateEditor.edit({ id: ${id}, number: ${number} })`)
button.setAttribute("onclick", `Territory_card.dateEditor.edit({ id: ${id}, number: ${number}, type: 'now'})`)
// Показуємо блок
block.style.display = "";
requestAnimationFrame(() => block.style.opacity = "1");
},
close() {
// Робимо плавне зникнення
const block = document.getElementById('card-new-date');
block.style.opacity = "0";
const onTransitionEnd = () => {
block.style.display = "none";
block.removeEventListener("transitionend", onTransitionEnd);
};
block.addEventListener("transitionend", onTransitionEnd);
},
edit({ id, number, type }) {
let input = document.getElementById('card-new-date-input').value;
if (type == "now") {
Territory_card.cloud.messApartment({ number: number, id: id, update: true });
} else {
if (input) {
const ts = new Date(input).getTime();
Territory_card.cloud.messApartment({ number, id, update: true, time: ts });
} else {
Territory_card.cloud.messApartment({ number: number, id: id });
}
}
this.close();
}
}
}

View File

@@ -0,0 +1,152 @@
.page-card {
width: calc(100% - 40px);
display: flex;
flex-direction: column;
margin: 20px 20px 0 20px;
}
.page-card>.list-controls {
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;
}
#page-card-sort {
display: flex;
}
#page-card-sort button {
display: none;
font-size: 18px;
background: 0;
cursor: pointer;
min-width: 30px;
height: 30px;
padding: 0;
margin-right: 10px;
align-items: center;
align-content: center;
justify-content: center;
color: var(--ColorThemes3);
border-radius: calc(var(--border-radius) - 5px - 2px);
border: 1px solid var(--ColorThemes0);
}
#page-card-sort button[data-state="active"] {
background: var(--PrimaryColor);
}
#page-card-sort button:hover {
background: var(--ColorThemes3);
}
#page-card-sort button>svg {
width: 22px;
fill: var(--PrimaryColorText);
}
#page-card-sort button:hover svg {
fill: var(--ColorThemes1);
}
#page-card-sort button:hover {
display: none;
opacity: 1;
}
#page-card-sort button[data-state="active"] {
display: flex;
}
#page-card-status {
display: flex;
}
#page-card-status>div {
display: none;
}
#page-card-status>div[data-state="active"] {
display: flex;
align-items: center;
margin-right: 2px;
}
#page-card-status>div>svg {
width: 25px;
height: 25px;
fill: var(--ColorThemes3);
margin-top: -2px;
}
#card-new-date {
display: flex;
width: 100%;
height: 100%;
left: 0;
top: 0;
position: fixed;
z-index: 999;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgb(31 31 33 / 41%);
transition: all .2s ease 0s;
}
#card-new-date>.mess {
display: flex;
width: 300px;
height: 100px;
border: 1px solid var(--ColorThemes2);
background: var(--ColorThemes0);
box-shadow: 0px 2px 3px rgb(0 0 0 / 5%), 0px 5px 15px rgb(0 0 0 / 5%), 0px 4px 8px rgb(0 0 0 / 5%), 0px 0px 1px rgb(0 0 0 / 5%);
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -160px;
flex-direction: column;
align-items: center;
z-index: 9999;
padding: 10px;
border-radius: 15px;
justify-content: space-between;
}
#card-new-date>.mess>button {
width: 100%;
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background-color: var(--PrimaryColor);
color: var(--PrimaryColorText);
cursor: pointer;
font-size: var(--FontSize4);
font-weight: 400;
}
#card-new-date>.mess>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background-color: var(--ColorThemes2);
color: var(--ColorThemes3);
cursor: pointer;
font-size: var(--FontSize4);
font-weight: 400;
-webkit-appearance: none;
-moz-appearance: none;
}

View File

@@ -0,0 +1,136 @@
<div class="page-constructor">
<div id="part-1" class="part_block">
<h1>
<span>Крок 1.</span> Інформація про будинок, територію, точки на карті
</h1>
<div id="info-type">
<div class="tabs">
<input
type="radio"
id="info-type-house"
value="house"
name="info-type"
checked
/>
<label class="tab" for="info-type-house">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 14.5 6 A 1.50015 1.50015 0 0 0 13 7.5 L 13 25 L 7.5 25 A 1.50015 1.50015 0 0 0 6 26.5 L 6 42.5 A 1.50015 1.50015 0 1 0 9 42.5 L 9 28 L 14.253906 28 A 1.50015 1.50015 0 0 0 14.740234 28 L 19 28 L 19 42.5 A 1.50015 1.50015 0 1 0 22 42.5 L 22 26.746094 A 1.50015 1.50015 0 0 0 22 26.259766 L 22 22 L 33.253906 22 A 1.50015 1.50015 0 0 0 33.740234 22 L 39 22 L 39 42.5 A 1.50015 1.50015 0 1 0 42 42.5 L 42 20.5 A 1.50015 1.50015 0 0 0 40.5 19 L 35 19 L 35 7.5 A 1.50015 1.50015 0 0 0 33.5 6 L 14.5 6 z M 16 9 L 32 9 L 32 19 L 20.5 19 A 1.50015 1.50015 0 0 0 19 20.5 L 19 25 L 16 25 L 16 9 z M 20 12 C 19.448 12 19 12.448 19 13 L 19 15 C 19 15.552 19.448 16 20 16 L 22 16 C 22.552 16 23 15.552 23 15 L 23 13 C 23 12.448 22.552 12 22 12 L 20 12 z M 26 12 C 25.448 12 25 12.448 25 13 L 25 15 C 25 15.552 25.448 16 26 16 L 28 16 C 28.552 16 29 15.552 29 15 L 29 13 C 29 12.448 28.552 12 28 12 L 26 12 z M 26 25 C 25.448 25 25 25.448 25 26 L 25 28 C 25 28.552 25.448 29 26 29 L 28 29 C 28.552 29 29 28.552 29 28 L 29 26 C 29 25.448 28.552 25 28 25 L 26 25 z M 33 25 C 32.448 25 32 25.448 32 26 L 32 28 C 32 28.552 32.448 29 33 29 L 35 29 C 35.552 29 36 28.552 36 28 L 36 26 C 36 25.448 35.552 25 35 25 L 33 25 z M 13 31 C 12.448 31 12 31.448 12 32 L 12 34 C 12 34.552 12.448 35 13 35 L 15 35 C 15.552 35 16 34.552 16 34 L 16 32 C 16 31.448 15.552 31 15 31 L 13 31 z M 26 31 C 25.448 31 25 31.448 25 32 L 25 34 C 25 34.552 25.448 35 26 35 L 28 35 C 28.552 35 29 34.552 29 34 L 29 32 C 29 31.448 28.552 31 28 31 L 26 31 z M 33 31 C 32.448 31 32 31.448 32 32 L 32 34 C 32 34.552 32.448 35 33 35 L 35 35 C 35.552 35 36 34.552 36 34 L 36 32 C 36 31.448 35.552 31 35 31 L 33 31 z M 13 37 C 12.448 37 12 37.448 12 38 L 12 40 C 12 40.552 12.448 41 13 41 L 15 41 C 15.552 41 16 40.552 16 40 L 16 38 C 16 37.448 15.552 37 15 37 L 13 37 z M 26 37 C 25.448 37 25 37.448 25 38 L 25 40 C 25 40.552 25.448 41 26 41 L 28 41 C 28.552 41 29 40.552 29 40 L 29 38 C 29 37.448 28.552 37 28 37 L 26 37 z M 33 37 C 32.448 37 32 37.448 32 38 L 32 40 C 32 40.552 32.448 41 33 41 L 35 41 C 35.552 41 36 40.552 36 40 L 36 38 C 36 37.448 35.552 37 35 37 L 33 37 z"
/>
</svg>
<span>Багатоквартирний будинок</span>
</label>
<input
type="radio"
id="info-type-homestead"
value="homestead"
name="info-type"
/>
<label class="tab" for="info-type-homestead">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 24 5.015625 C 22.851301 5.015625 21.70304 5.3892757 20.753906 6.1367188 A 1.50015 1.50015 0 0 0 20.751953 6.1367188 L 8.859375 15.509766 C 7.0558128 16.931133 6 19.102989 6 21.400391 L 6 39.488281 C 6 41.403236 7.5850452 42.988281 9.5 42.988281 L 38.5 42.988281 C 40.414955 42.988281 42 41.403236 42 39.488281 L 42 21.400391 C 42 19.102989 40.944187 16.931133 39.140625 15.509766 L 39 15.396484 L 39 7.5 A 1.50015 1.50015 0 0 0 37.5 6 L 32.5 6 A 1.50015 1.50015 0 0 0 31 7.5 L 31 9.09375 L 27.246094 6.1367188 C 26.29696 5.3892758 25.148699 5.015625 24 5.015625 z M 24 8.0078125 C 24.489801 8.0078125 24.979759 8.1705836 25.390625 8.4941406 L 31.572266 13.363281 A 1.50015 1.50015 0 0 0 34 12.185547 L 34 9 L 36 9 L 36 16.125 A 1.50015 1.50015 0 0 0 36.572266 17.302734 L 37.285156 17.865234 C 38.369594 18.719867 39 20.019792 39 21.400391 L 39 39.488281 C 39 39.783326 38.795045 39.988281 38.5 39.988281 L 9.5 39.988281 C 9.2049548 39.988281 9 39.783326 9 39.488281 L 9 21.400391 C 9 20.019792 9.6304058 18.719867 10.714844 17.865234 L 22.609375 8.4941406 C 23.020241 8.1705836 23.510199 8.0078125 24 8.0078125 z M 14.5 23.988281 A 1.50015 1.50015 0 0 0 13 25.488281 L 13 33.488281 A 1.50015 1.50015 0 0 0 14.5 34.988281 L 20.5 34.988281 A 1.50015 1.50015 0 0 0 22 33.488281 L 22 25.488281 A 1.50015 1.50015 0 0 0 20.5 23.988281 L 14.5 23.988281 z M 27.5 23.988281 A 1.50015 1.50015 0 0 0 26 25.488281 L 26 33.488281 A 1.50015 1.50015 0 0 0 27.5 34.988281 L 33.5 34.988281 A 1.50015 1.50015 0 0 0 35 33.488281 L 35 25.488281 A 1.50015 1.50015 0 0 0 33.5 23.988281 L 27.5 23.988281 z M 16 26.988281 L 19 26.988281 L 19 31.988281 L 16 31.988281 L 16 26.988281 z M 29 26.988281 L 32 26.988281 L 32 31.988281 L 29 31.988281 L 29 26.988281 z"
/>
</svg>
<span>Житловий район</span>
</label>
<input
type="radio"
id="info-type-points"
value="points"
name="info-type"
/>
<label class="tab" for="info-type-points">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 24 4 C 16.285455 4 10 10.285455 10 18 C 10 21.46372 11.272608 24.643548 13.359375 27.085938 L 13.359375 27.087891 L 13.361328 27.087891 C 13.361328 27.087891 19.149094 33.866566 21.298828 35.917969 C 22.798087 37.348278 25.199464 37.347492 26.699219 35.917969 C 29.129083 33.600994 34.636721 27.090553 34.640625 27.085938 L 34.642578 27.082031 C 36.728766 24.639939 38 21.462159 38 18 C 38 10.285455 31.714545 4 24 4 z M 24 7 C 30.093455 7 35 11.906545 35 18 C 35 20.73228 34.005417 23.211194 32.359375 25.136719 L 32.359375 25.138672 L 32.357422 25.138672 C 32.357422 25.138672 26.632181 31.83589 24.628906 33.746094 C 24.258577 34.099392 23.73947 34.099392 23.369141 33.746094 C 21.715477 32.16807 15.643092 25.141834 15.638672 25.136719 L 15.636719 25.132812 C 13.99327 23.20762 13 20.730712 13 18 C 13 11.906545 17.906545 7 24 7 z M 24 12 C 22.125 12 20.528815 12.757133 19.503906 13.910156 C 18.478997 15.063179 18 16.541667 18 18 C 18 19.458333 18.478997 20.936821 19.503906 22.089844 C 20.528815 23.242867 22.125 24 24 24 C 25.875 24 27.471185 23.242867 28.496094 22.089844 C 29.521003 20.936821 30 19.458333 30 18 C 30 16.541667 29.521003 15.063179 28.496094 13.910156 C 27.471185 12.757133 25.875 12 24 12 z M 24 15 C 25.124999 15 25.778816 15.367867 26.253906 15.902344 C 26.728997 16.436821 27 17.208333 27 18 C 27 18.791667 26.728997 19.563179 26.253906 20.097656 C 25.778816 20.632133 25.124999 21 24 21 C 22.875001 21 22.221184 20.632133 21.746094 20.097656 C 21.271003 19.563179 21 18.791667 21 18 C 21 17.208333 21.271003 16.436821 21.746094 15.902344 C 22.221184 15.367867 22.875001 15 24 15 z M 12.771484 29.441406 C 8.2264844 30.754406 5 32.953 5 36 C 5 41.252 14.558 44 24 44 C 33.442 44 43 41.252 43 36 C 43 32.954 39.775422 30.757359 35.232422 29.443359 C 34.654422 30.099359 33.863187 30.993844 32.992188 31.964844 C 37.418188 33.005844 40 34.691 40 36 C 40 38.039 33.767 41 24 41 C 14.233 41 8 38.039 8 36 C 8 34.69 10.586531 33.001938 15.019531 31.960938 C 14.152531 30.995938 13.355484 30.100406 12.771484 29.441406 z"
/>
</svg>
<span>Точки на карті</span>
</label>
<span class="glider"></span>
</div>
</div>
<form id="info-form">
<div>
<label for="info-title">Назва вулиці</label>
<input
type="text"
id="info-title"
name="address"
required
value=""
/>
</div>
<div>
<label for="info-number">Номер будинку / частини</label>
<input type="text" id="info-number" name="number" required value="" />
</div>
<div>
<label for="info-settlement">Місто</label>
<input
type="text"
id="info-settlement"
name="settlement"
required
value="Тернопіль"
/>
</div>
<button type="submit" id="part-1-button">Далі</button>
</form>
</div>
<div id="part-2" class="part_block" style="display: none">
<h1 id="part-2-title"><span>Крок 2.</span> Створення будинку / ділянки</h1>
<div class="osm-info">
<div>
<label for="info-settlement-title">OSM iD</label>
<div>
<input
type="text"
placeholder="123, 345, 678"
onchange="Territory_constructor.osm.autoPoligon(this.value)"
/>
<a
href="https://www.openstreetmap.org/#map=19/49.561725/25.604458"
target="_blank"
title="Де знайти OSM iD ?"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="100px"
height="100px"
>
<path
d="M 24 4 C 12.972066 4 4 12.972074 4 24 C 4 35.027926 12.972066 44 24 44 C 35.027934 44 44 35.027926 44 24 C 44 12.972074 35.027934 4 24 4 z M 24 7 C 33.406615 7 41 14.593391 41 24 C 41 33.406609 33.406615 41 24 41 C 14.593385 41 7 33.406609 7 24 C 7 14.593391 14.593385 7 24 7 z M 24 14 A 2 2 0 0 0 24 18 A 2 2 0 0 0 24 14 z M 23.976562 20.978516 A 1.50015 1.50015 0 0 0 22.5 22.5 L 22.5 33.5 A 1.50015 1.50015 0 1 0 25.5 33.5 L 25.5 22.5 A 1.50015 1.50015 0 0 0 23.976562 20.978516 z"
></path>
</svg>
</a>
</div>
</div>
<span>або</span>
<button onclick="Territory_constructor.osm.newPoligon()">Обрати на карті</button>
</div>
<div class="block-map">
<div id="map"></div>
</div>
<button type="button" id="part-2-button">Далі</button>
</div>
<div id="part-3" class="part_block" style="display: none">
<h1 id="part-3-title"><span>Крок 3.</span> Створення квартир</h1>
<button type="button" id="part-3-button">Зберегти</button>
</div>
</div>

View File

@@ -0,0 +1,909 @@
let map, houseGroup, homesteadGroup, buildingGroup, pointsGroup;
let numApartments = 1;
let mode = '';
const Territory_constructor = {
info: {},
async init() {
let html = await fetch('/lib/pages/territory/constructor/index.html').then((response) => response.text());
app.innerHTML = html;
map = "";
houseGroup = "";
homesteadGroup = "";
buildingGroup = "";
pointsGroup = "";
numApartments = 1;
this.info = {
type: 'house',
points: [],
points_number: [],
geo: {},
osm_id: [],
zoom: 17,
title: null,
number: null,
settlement: null
}
const infoTypeInputs = document.querySelectorAll('input[name="info-type"]');
const infoForm = document.getElementById('info-form');
const infoLabels = {
points: {
title: "Назва",
number: "Номер",
settlement: "Місто",
init: () => Territory_constructor.points.init(),
next: () => Territory_constructor.points.next(),
save: () => Territory_constructor.points.save()
},
homestead: {
title: "Назва району / села",
number: "Номер району",
settlement: "Місто",
init: () => Territory_constructor.homestead.init(),
next: () => Territory_constructor.homestead.next(),
save: () => Territory_constructor.homestead.save()
},
house: {
title: "Назва вулиці",
number: "Номер будинку",
settlement: "Місто",
init: () => Territory_constructor.house.init(),
next: () => Territory_constructor.house.next(),
save: () => Territory_constructor.house.save()
}
};
let currentInit = infoLabels['house'].init;
let currentNext = infoLabels['house'].next;
let currentSave = infoLabels['house'].save;
function renderInfoForm(type) {
const labels = infoLabels[type];
currentInit = labels.init;
currentNext = labels.next;
currentSave = labels.save;
infoForm.innerHTML = `
<div>
<label for="info-title">${labels.title}</label>
<input type="text" id="info-title" name="address" required value=""/>
</div>
<div>
<label for="info-number">${labels.number}</label>
<input type="text" id="info-number" name="number" required value=""/>
</div>
<div>
<label for="info-settlementv">${labels.settlement}</label>
<input type="text" id="info-settlement" name="settlement" required value="Тернопіль"/>
</div>
<button type="submit" id="part-1-button">Далі</button>
`;
}
// Обработчик submit формы
infoForm.addEventListener('submit', (event) => {
event.preventDefault();
document.getElementById('part-1-button').style.display = "none";
['title', 'number', 'settlement'].forEach(key => {
Territory_constructor.info[key] = document.getElementById(`info-${key}`).value;
});
if (currentInit) currentInit();
});
// Слушатели радиокнопок
infoTypeInputs.forEach(radio => {
radio.addEventListener('change', event => {
const value = event.target.value;
const part_2 = document.getElementById('part-2');
const part_2_button = document.getElementById('part-2-button');
part_2.style.display = "none";
part_2_button.style.display = "";
const part_3 = document.getElementById('part-3');
part_3.style.display = "none";
renderInfoForm(value);
console.log(`Вибрано: ${value}`);
this.info.type = value;
this.info.osm_id = null;
this.info.geo = {};
this.info.points_number = [];
this.info.points = [];
this.house.apartments.quantity = 0;
this.house.apartments.list = [];
});
});
document.getElementById('part-2-button').addEventListener('click', (event) => {
event.preventDefault();
document.getElementById('part-2-button').style.display = "none";
if (currentNext) currentNext();
});
document.getElementById('part-3-button').addEventListener('click', (event) => {
event.preventDefault();
// document.getElementById('part-3-button').style.display = "none";
if (currentSave) currentSave();
});
},
points: {
init() {
console.log('points');
// const part_2 = document.getElementById('part-2');
// const part_2_title = document.getElementById('part-2-title');
// part_2_title.innerHTML = `<span>Крок 2.</span> Створення точок на карті`;
// part_2.style.display = "";
},
next() {
console.log('points next');
},
save() {
console.log('points next save');
}
},
homestead: {
init() {
console.log('homestead');
const part_2 = document.getElementById('part-2');
const part_2_title = document.getElementById('part-2-title');
part_2_title.innerHTML = `<span>Крок 2.</span> Створення ділянки`;
part_2.style.display = "";
Territory_constructor.osm.init();
},
next() {
console.log('homestead next');
const part_3 = document.getElementById('part-3');
const title = part_3.querySelector('h1');
const button = part_3.querySelector('#part-3-button');
part_3.innerHTML = '';
title.innerHTML = `<span>Крок 3.</span> Створення будинків`;
part_3.appendChild(title);
part_3.innerHTML += `
<div class="info">
<p>*Натисніть кнопку нижче, а потім клацайте на карті, щоб додати будинки. Після цього натисніть "Зберегти".</p>
<p>*Щоб видалити будинок, клацніть на ньому та у спливаючому вікні оберіть "Видалити".</p>
<br />
<button onclick="Territory_constructor.homestead.building.newHouse(this)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z"></path></svg>
<span>Додати будинок</span>
</button>
</div>
`;
part_3.appendChild(button);
part_3.style.display = "";
this.building.init();
},
async save() {
Territory_constructor.info.buildings = Territory_constructor.homestead.building.list;
console.log(Territory_constructor.info);
Territory_constructor.save();
},
building: {
list: [], editing: false,
async init() {
this.editing = false;
setLeafletCursor('pointer');
// Обробник кліку на карту
homesteadGroup.on('click', e => {
console.log(this.editing);
if (e.layer instanceof L.Marker || !this.editing) return;
const { lat, lng } = e.latlng;
console.log(`Координати: ${lat.toFixed(5)}, ${lng.toFixed(5)}`);
setLeafletCursor('progress');
this.editing = false;
this.addBuilding({ geo: e.latlng, title: this.list.length + 1 });
});
},
async addBuilding({ geo, title }) {
this.list.push({
title: title,
geo: geo
});
// Додаємо маркер на карту
const redDot = L.divIcon({
className: "leaflet_drop",
html: `<div id="redDot_${this.list.length}"></div>`,
iconSize: [16, 16],
iconAnchor: [8, 8]
});
const marker = L.marker(geo, { icon: redDot }).addTo(buildingGroup);
marker.bindPopup(`
Будинок: ${this.list.length}<br>
Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}<br>
<button class="map_dell" onclick="Territory_constructor.homestead.building.delleteBuilding({id: ${this.list.length}})" type="button">Видалити</button>
`);
setLeafletCursor('crosshair');
this.editing = true;
},
async delleteBuilding({ id }) {
const el = document.getElementById(`redDot_${id}`);
if (el) el.remove();
this.list = this.list.filter(item => item.title !== id);
const block = document.getElementById(`Building_${id}`);
if (block) block.remove();
houseGroup.eachLayer(layer => {
if (layer instanceof L.Marker && layer.getPopup()?.getContent().includes(`Будинок: ${id}`)) {
houseGroup.removeLayer(layer);
}
});
},
newHouse(element) {
const btn = element;
this.editing = !this.editing;
setLeafletCursor(this.editing ? 'crosshair' : 'pointer');
btn.innerHTML = this.editing
? `<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><span>Завершити додавання</span>`
: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"> <path d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z" ></path> </svg><span>Додати будинок</span>`;
if (this.editing) alert("Натискаючи на карту будуть створюватись нові точки (будинки)");
}
}
},
house: {
init() {
console.log('house');
const part_2 = document.getElementById('part-2');
const part_2_title = document.getElementById('part-2-title');
part_2_title.innerHTML = `<span>Крок 2.</span> Конструктор будинків`;
part_2.style.display = "";
Territory_constructor.osm.init();
},
next() {
console.log('house next');
const part_3 = document.getElementById('part-3');
const title = part_3.querySelector('h1');
const button = part_3.querySelector('#part-3-button');
part_3.innerHTML = '';
title.innerHTML = `<span>Крок 3.</span> Конструктор квартир`;
part_3.appendChild(title);
part_3.innerHTML += `<input onchange="Territory_constructor.house.apartments.editQuantity(this.value)" type="number" value="1" id="next-apartment-title" title="Авто-номер наступної квартири">`
part_3.appendChild(button);
part_3.style.display = "";
this.apartments.init();
},
async save() {
console.log('house next save');
Territory_constructor.info.entrances = this.apartments.list.map((entrance, entranceIndex) => {
let apartments = [];
let apartmentCounter = 0;
entrance.list.forEach((floor, floorIndex) => {
floor.forEach(apartment => {
apartments.push({
title: apartment.title,
apartment_number: apartmentCounter++,
floors_number: floorIndex + 1
});
});
});
return {
title: entrance.title,
entrance_number: entranceIndex,
apartments
};
});
Territory_constructor.save();
},
apartments: {
quantity: 0,
list: [],
init() {
const part_3 = document.getElementById("part-3");
const part_3_Button = part_3.querySelector("#part-3-button");
this.quantity++;
const newEntrance = {
title: `Під'їзд ${this.list.length + 1}`,
list: [[{ title: this.quantity }]]
};
this.list.push(newEntrance);
const eIndex = this.list.length - 1;
const floorIndex = 0;
const apartmentIndex = 0;
const houseDiv = this.createHouse(eIndex);
const entranceDiv = this.createEntrance(eIndex);
const floorDiv = this.createFloor(eIndex, floorIndex);
const apartmentDiv = this.createApartment(eIndex, floorIndex, apartmentIndex, this.quantity);
floorDiv.insertBefore(apartmentDiv, floorDiv.querySelector(".floor-info"));
entranceDiv.appendChild(floorDiv);
houseDiv.insertBefore(entranceDiv, houseDiv.querySelector(".entrance-button"));
part_3.insertBefore(houseDiv, part_3_Button);
},
createApartment(entrance, floor, apartment, value) {
const div = document.createElement("div");
div.className = "apartment";
div.id = `apartment-${entrance}-${floor}-${apartment}`;
div.innerHTML = `
<input onchange="Territory_constructor.house.apartments.editApartment(${entrance}, ${floor}, ${apartment}, this.value)" type="text" value="${value}">
<button onclick="Territory_constructor.house.apartments.deleteApartment(${entrance}, ${floor}, ${apartment})" title="Видалити квартиру" type="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>
`;
return div;
},
createFloor(entrance, floor) {
const div = document.createElement("div");
div.className = "floor";
div.id = `floor-${entrance}-${floor}`;
div.innerHTML = `
<div class="floor-info">
<h2>Поверх ${floor + 1}</h2>
<button onclick="Territory_constructor.house.apartments.addApartment(${entrance}, ${floor})" title="Додати квартиру" type="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>
</div>`;
return div;
},
createEntrance(entrance) {
const div = document.createElement("div");
div.className = "entrance";
div.id = `entrance-${entrance}`;
div.innerHTML = `
<div class="entrance-info">
<input onchange="Territory_constructor.house.apartments.editEntrance(${entrance}, this.value)" type="text" value="Під'їзд ${entrance + 1}">
<button onclick="Territory_constructor.house.apartments.addFloors(${entrance})" title="Додати поверх" type="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>
</div>`;
return div;
},
createHouse() {
const div = document.createElement("div");
div.id = `house`;
div.innerHTML = `
<button class="entrance-button" onclick="Territory_constructor.house.apartments.addEntrance()" title="Додати під'їзд" type="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>`;
return div;
},
addEntrance() {
const blockHouse = document.getElementById("house");
const houseButton = blockHouse.querySelector(".entrance-button");
this.editQuantity(this.quantity + 1);
const newEntrance = {
title: `Під'їзд ${this.list.length + 1}`,
list: [[{ title: this.quantity }]]
};
this.list.push(newEntrance);
const eIndex = this.list.length - 1;
const floorIndex = 0;
const apartmentIndex = 0;
const entranceDiv = this.createEntrance(eIndex);
const floorDiv = this.createFloor(eIndex, floorIndex);
const apartmentDiv = this.createApartment(eIndex, floorIndex, apartmentIndex, this.quantity);
floorDiv.insertBefore(apartmentDiv, floorDiv.querySelector(".floor-info"));
entranceDiv.appendChild(floorDiv);
blockHouse.insertBefore(entranceDiv, houseButton);
},
editEntrance(entrance, value) {
this.list[entrance].title = value;
},
addFloors(entrance) {
const entranceBlock = document.getElementById(`entrance-${entrance}`);
const entranceInfo = entranceBlock.querySelector(".entrance-info");
this.editQuantity(this.quantity + 1);
this.list[entrance].list.push([{ title: this.quantity }]);
const fIndex = this.list[entrance].list.length - 1;
const floorDiv = this.createFloor(entrance, fIndex);
const aptDiv = this.createApartment(entrance, fIndex, 0, this.quantity);
floorDiv.insertBefore(aptDiv, floorDiv.querySelector(".floor-info"));
entranceInfo.after(floorDiv);
},
addApartment(entrance, floor) {
const blockFloor = document.getElementById(`floor-${entrance}-${floor}`);
const floorInfo = blockFloor.querySelector(".floor-info");
this.editQuantity(this.quantity + 1);
this.list[entrance].list[floor].push({ title: this.quantity });
const aIndex = this.list[entrance].list[floor].length - 1;
const aptDiv = this.createApartment(entrance, floor, aIndex, this.quantity);
blockFloor.insertBefore(aptDiv, floorInfo);
},
editApartment(entrance, floor, apartment, value) {
this.list[entrance].list[floor][apartment].title = value;
},
deleteApartment(entrance, floor, apartment) {
this.list[entrance].list[floor].splice(apartment, 1);
document.getElementById(`apartment-${entrance}-${floor}-${apartment}`)?.remove();
this.editQuantity(this.quantity - 1);
},
editQuantity(value) {
const next_apartment_title = document.getElementById('next-apartment-title');
next_apartment_title.style.display = "";
next_apartment_title.value = value;
this.quantity = Number(value);
}
}
},
osm: {
init() {
const center = { lat: 49.5629016, lng: 25.6145625 };
const zoom = 19;
const googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
});
const osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
const mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', {
maxZoom: 20, minZoom: 15, tms: true
});
if (!map) {
houseGroup = new L.FeatureGroup();
homesteadGroup = new L.FeatureGroup();
buildingGroup = new L.FeatureGroup();
pointsGroup = new L.FeatureGroup();
map = L.map('map', {
renderer: L.canvas(), center, zoom,
layers: [googleHybrid, osm, mytile, houseGroup, homesteadGroup, buildingGroup, pointsGroup],
zoomControl: false
});
L.control.layers(
{ "Google Hybrid": googleHybrid, "OpenStreetMap": osm, "Territory Map": mytile },
{
"Багатоповерхові будинки": houseGroup,
"Житлові райони": homesteadGroup,
"Приватні будинки": buildingGroup,
"Точки на карті": pointsGroup
},
{ position: 'bottomright' }
).addTo(map);
map.pm.addControls({
position: 'bottomright',
drawCircleMarker: false,
drawPolyline: false,
drawPolygon: false,
drawRectangle: false,
drawCircle: false,
drawText: false,
drawMarker: false,
cutPolygon: false,
tooltips: false,
editMode: true,
dragMode: true,
});
map.pm.toggleControls()
// Событие после завершения рисования
map.on('pm:create', e => {
const layer = e.layer;
let LatLngs = layer.getLatLngs();
LatLngs[0].push(LatLngs[0][0]);
Territory_constructor.info.points.push(LatLngs);
Territory_constructor.info.points_number.push(this.center(layer.getLatLngs()));
let geo = this.center(layer.getLatLngs());
const house = layer; // сохраняем именно слой
if (Territory_constructor.info.type === 'house') {
houseGroup.addLayer(house);
} else if (Territory_constructor.info.type === 'homestead') {
homesteadGroup.addLayer(house);
}
house.bindPopup(`
Координати: ${geo.lat.toFixed(5)}, ${geo.lng.toFixed(5)}<br>
<button class="map_dell" type="button">Видалити</button>
`);
// при открытии popup вешаем обработчик удаления
house.on('popupopen', (e) => {
if (Territory_constructor.homestead.building.editing) {
house.closePopup();
return;
}
const btn = e.popup.getElement().querySelector('.map_dell');
if (btn) {
btn.addEventListener('click', () => {
Territory_constructor.osm.delete(house);
});
}
});
Territory_constructor.osm.autoZoom(Territory_constructor.info.points);
});
map.pm.setLang("ua");
}
houseGroup.clearLayers();
homesteadGroup.clearLayers();
buildingGroup.clearLayers();
pointsGroup.clearLayers();
},
newPoligon() {
if (Territory_constructor.info.type === 'house') {
map.pm.enableDraw('Polygon', {
snappable: true,
snapDistance: 20,
layerGroup: houseGroup,
templineStyle: {
color: '#585858',
radius: 500,
fillOpacity: 0.4,
dashArray: '5, 10',
dashOffset: '20',
},
hintlineStyle: {
color: '#C14D4D',
dashArray: '5, 10'
},
pathOptions: {
color: "#585858",
fillColor: "#f2bd53",
fillOpacity: 0.8
}
});
} else if (Territory_constructor.info.type === 'homestead') {
map.pm.enableDraw('Polygon', {
snappable: true,
snapDistance: 20,
layerGroup: houseGroup,
templineStyle: {
color: '#585858',
radius: 500,
fillOpacity: 0.3,
dashArray: '5, 10',
dashOffset: '20',
},
hintlineStyle: {
color: '#C14D4D',
dashArray: '5, 10'
},
pathOptions: {
color: "#f2bd53",
fillColor: "#f2bd53",
radius: 500,
fillOpacity: 0.3,
dashArray: '5, 10'
}
});
}
},
async autoPoligon(IDs) {
if (!IDs) return;
const ids_list = IDs.replace(/\s+/g, "").split(',');
Territory_constructor.info.osm_id = ids_list;
houseGroup.clearLayers();
homesteadGroup.clearLayers();
Territory_constructor.info.points = [];
Territory_constructor.info.points_number = [];
Territory_constructor.info.geo = {}
// 1006306041, 1006306065
for (let i = 0; i < ids_list.length; i++) {
const element = await Territory_constructor.osm.getOSM(Territory_constructor.info.osm_id[i]);
// Преобразуем координаты в LatLng
const LatLngs = [[]];
element[0].forEach(feature => LatLngs[0].push({ lat: feature.lat, lng: feature.lng }));
// Замыкаем полигон
if (LatLngs[0][0] && LatLngs[0][0] !== LatLngs[0][LatLngs[0].length - 1]) {
LatLngs[0].push(LatLngs[0][0]);
}
// Считаем центр
const center = this.center(LatLngs);
// Сохраняем в points / points_number
Territory_constructor.info.points.push(LatLngs);
Territory_constructor.info.points_number.push(center);
// Создаем L.polygon
const polyOptions = Territory_constructor.info.type === 'homestead'
? { color: "#f2bd53", fillColor: "#f2bd53", fillOpacity: 0.4, dashArray: '5,10' }
: { color: "#585858", fillColor: "#f2bd53", fillOpacity: 0.8 };
const house = L.polygon(LatLngs, polyOptions);
// Добавляем в нужную группу
if (Territory_constructor.info.type === 'house') {
houseGroup.addLayer(house);
} else if (Territory_constructor.info.type === 'homestead') {
homesteadGroup.addLayer(house);
}
// Bind popup с кнопкой удаления
house.bindPopup(`
Координати: ${center.lat.toFixed(5)}, ${center.lng.toFixed(5)}<br>
<button class="map_dell" type="button">Видалити</button>
`);
house.on('popupopen', (e) => {
if (Territory_constructor.homestead.building.editing) {
house.closePopup();
return;
}
const btn = e.popup.getElement().querySelector('.map_dell');
if (btn) {
btn.addEventListener('click', () => {
Territory_constructor.osm.delete(house);
});
}
});
}
Territory_constructor.osm.autoZoom(Territory_constructor.info.points);
},
center(geo) {
// Получаем координаты полигона Leaflet
let latlngs = geo[0];
// Преобразуем в формат GeoJSON для Turf
const coordinates = latlngs.map(ll => [ll.lng, ll.lat]);
const polygonGeoJSON = {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [coordinates]
}
};
// Находим центроид
const centroid = turf.centroid(polygonGeoJSON);
latlngs = { lat: centroid.geometry.coordinates[1], lng: centroid.geometry.coordinates[0] }
return latlngs;
},
autoZoom(polygons) {
if (!polygons || !polygons.length) return;
const allBounds = [];
polygons.forEach(polygon => {
const ring = polygon[0];
if (!ring || ring.length < 3) return;
const coords = ring.map(p => [p.lng, p.lat]);
if (coords[0][0] !== coords[coords.length - 1][0] || coords[0][1] !== coords[coords.length - 1][1]) {
coords.push(coords[0]);
}
const polygonGeoJSON = turf.polygon([coords]);
const bbox = turf.bbox(polygonGeoJSON);
const bounds = L.latLngBounds(
[bbox[1], bbox[0]],
[bbox[3], bbox[2]]
);
allBounds.push(bounds);
});
if (!allBounds.length) return;
// Если один полигон, просто fitBounds на него
if (allBounds.length === 1) {
map.fitBounds(allBounds[0]);
} else {
// Несколько полигонов → объединяем bounds
let finalBounds = allBounds[0];
for (let i = 1; i < allBounds.length; i++) {
finalBounds = finalBounds.extend(allBounds[i]);
}
map.fitBounds(finalBounds);
}
if (map.getZoom() > 18) map.setZoom(18);
setTimeout(() => {
Territory_constructor.info.zoom = map.getZoom();
Territory_constructor.info.geo = map.getCenter();
}, 200)
},
delete(house) {
// убрать слой с карты
if (Editor.info.type === 'house') {
houseGroup.removeLayer(house);
} else if (Editor.info.type === 'homestead') {
homesteadGroup.removeLayer(house);
}
// найти индекс полигона в points
const target = house.getLatLngs()[0]; // вершины полигона
const index = Editor.info.points.findIndex((p) => {
const copy = p.slice(); // делаем копию
copy[0].pop(); // убираем последний элемент
if (isSamePolygon(p, target)) return true; // проверка как есть
if (isSamePolygon(copy, target)) return true; // проверка без последнего
return false;
});
function isSamePolygon(a, b) {
if (a.length !== b.length) return false;
return a.every((pt, i) => pt.lat === b[i].lat && pt.lng === b[i].lng);
}
if (index) {
// удалить из points и points_number по индексу
Editor.info.points.splice(index, 1);
Editor.info.points_number.splice(index, 1);
}
Editor.osm.autoZoom(Editor.info.points);
},
async getOSM(wayId) {
const overpassUrl = `https://overpass-api.de/api/interpreter?data=[out:json];way(${wayId});(._;>;);out;`;
return await fetch(overpassUrl)
.then(response => response.json())
.then(data => {
const nodes = new Map();
data.elements.forEach(el => {
if (el.type === "node") {
nodes.set(el.id, { lat: el.lat, lng: el.lon });
}
});
const way = data.elements.find(el => el.type === "way");
if (way) {
const coordinates = way.nodes.map(nodeId => nodes.get(nodeId));
return [coordinates];
} else {
console.error("Way не найден!");
}
})
.catch(error => console.error("Ошибка запроса:", error));
},
},
async save() {
const part_3_button = document.getElementById('part-3-button');
console.log(Territory_constructor.info);
setLeafletCursor('pointer');
Territory_constructor.homestead.building.editing = false;
const uuid = localStorage.getItem('uuid');
const URL = `${CONFIG.api}constructor`;
await fetch(URL, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify(Territory_constructor.info)
})
.then(response => {
if (response.status == 200) {
console.log({ 'setPack': 'ok' });
part_3_button.innerText = "Запис додано";
return response.json()
} else {
console.log('err');
part_3_button.innerText = "Помилка запису";
return
}
})
.then(data => {
console.log(data);
Territory.house.list = [];
Territory.homestead.list = [];
Router.navigate(`/territory/manager/${Territory_constructor.info.type}/${data.id}`);
setTimeout(() => {
part_3_button.innerText = "Зберегти";
}, 3000);
})
.catch(err => {
console.log(err);
part_3_button.innerText = "Помилка запису";
})
}
}

View File

@@ -0,0 +1,478 @@
.page-constructor {
width: calc(100% - 40px);
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 20px 0 20px;
}
.page-constructor>#part-1,
.page-constructor>#part-2,
.page-constructor>#part-3 {
border-radius: 10px;
width: calc(100% - 40px);
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: 20px;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
padding: 0 20px;
position: relative;
}
.page-constructor>#part-1>h1,
.page-constructor>#part-2>h1,
.page-constructor>#part-3>h1 {
width: calc(100% - 40px);
color: var(--ColorThemes3);
border-radius: var(--border-radius);
font-size: var(--FontSize5);
font-weight: 300;
padding: 20px 0;
position: relative;
}
.page-constructor>#part-1>h1>span,
.page-constructor>#part-2>h1>span,
.page-constructor>#part-3>h1>span {
font-weight: 500;
}
.page-constructor>#part-1>#info-type {
display: flex;
align-items: center;
justify-content: center;
}
.page-constructor>#part-1>#info-type>.tabs {
display: flex;
position: relative;
background-color: var(--ColorThemes0);
padding: 4px;
border-radius: 6px;
width: calc(100% - 8px);
z-index: 2;
}
.page-constructor>#part-1>#info-type>.tabs>input[type="radio"] {
display: none;
}
.page-constructor>#part-1>#info-type>.tabs>.tab {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
width: calc(100% / 3);
cursor: pointer;
padding: 0 15px;
transition: 0.15s ease-in;
color: var(--ColorThemes3);
fill: var(--ColorThemes3);
flex-direction: row;
z-index: 2;
}
.page-constructor>#part-1>#info-type>.tabs>.tab>svg {
width: 20px;
height: 20px;
}
.page-constructor>#part-1>#info-type>.tabs>.tab>span {
margin-left: 6px;
font-size: var(--FontSize1);
font-weight: 400;
}
.page-constructor>#part-1>#info-type>.tabs>input[type="radio"]:checked+label {
color: var(--PrimaryColorText);
fill: var(--PrimaryColorText);
}
.page-constructor>#part-1>#info-type>.tabs>.glider {
position: absolute;
display: flex;
height: 40px;
width: calc((100% - 8px) / 3);
background-color: var(--PrimaryColor);
z-index: 1;
border-radius: 4px;
transition: 0.25s ease-out;
}
@media (min-width: 601px) {
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider {
transform: translateX(0);
}
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider {
transform: translateX(100%);
}
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider {
transform: translateX(200%);
}
}
@media (max-width: 600px) {
.page-constructor>#part-1>#info-type>.tabs {
flex-direction: column;
}
.page-constructor>#part-1>#info-type>.tabs>.tab {
width: calc(100% - 8px);
padding: 0 4px;
}
.page-constructor>#part-1>#info-type>.tabs>.glider {
width: calc(100% - 8px);
}
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-house"]:checked~.glider {
transform: translateY(0);
}
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-homestead"]:checked~.glider {
transform: translateY(100%);
}
.page-constructor>#part-1>#info-type>.tabs>input[id="info-type-points"]:checked~.glider {
transform: translateY(200%);
}
}
.page-constructor>#part-1>form>div {
width: 100%;
display: flex;
margin: 20px 0;
align-items: flex-start;
flex-direction: column;
}
.page-constructor>#part-1>form>div>label {
display: flex;
justify-content: center;
flex-direction: column;
font-size: var(--FontSize1);
font-weight: 500;
margin-bottom: 5px;
}
.page-constructor>#part-1>form>div>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
}
.page-constructor>#part-1>form>button,
.page-constructor>#part-2>button,
.page-constructor>#part-3>button {
border-radius: 6px;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
width: 100%;
height: 40px;
font-size: var(--FontSize3);
font-weight: 400;
margin: 20px 0;
text-transform: uppercase;
}
.page-constructor>#part-2>.osm-info {
padding-bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.page-constructor>#part-2>.osm-info>div {
display: flex;
align-items: center;
width: 100%;
}
.page-constructor>#part-2>.osm-info>div {
width: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
}
.page-constructor>#part-2>.osm-info>div>label {
display: flex;
justify-content: center;
flex-direction: column;
font-size: var(--FontSize1);
font-weight: 500;
margin-bottom: 5px;
}
.page-constructor>#part-2>.osm-info>div>div {
display: flex;
align-items: center;
width: 100%;
}
.page-constructor>#part-2>.osm-info>div>div>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
}
.page-constructor>#part-2>.osm-info>div>div>a {
height: 26px;
width: 26px;
margin-left: 10px;
}
.page-constructor>#part-2>.osm-info>div>div>a>svg {
height: 26px;
width: 26px;
fill: var(--ColorThemes3);
}
.page-constructor>#part-2>.osm-info>span {
font-size: var(--FontSize5);
margin: 10px;
color: var(--ColorThemes3);
}
.page-constructor>#part-2>.osm-info>button {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 100%;
cursor: pointer;
transition: 0.15s ease-in;
color: var(--PrimaryColorText);
background: var(--PrimaryColor);
border-radius: 6px;
flex-direction: row;
z-index: 2;
font-size: var(--FontSize3);
}
.page-constructor>#part-2>.block-map {
width: 100%;
height: 500px;
border-radius: 6px;
overflow: hidden;
position: relative;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
}
.page-constructor>#part-2>.block-map>#map {
width: 100%;
height: 100%;
}
.page-constructor>#part-3>input {
font-weight: 500;
position: absolute;
right: 0;
top: 0;
padding: 10px;
margin: 13px;
font-size: var(--FontSize1);
background: var(--ColorThemes3);
color: var(--ColorThemes0);
border-radius: 6px;
width: 40px;
}
.page-constructor>#part-3>#house {
display: flex;
overflow: auto;
}
.page-constructor>#part-3>#house>button {
display: flex;
position: relative;
width: 34px;
min-width: 34px;
height: calc(100% - 10px);
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
margin: 0 10px;
border-radius: 6px;
align-items: center;
justify-content: center;
font-size: 30px;
cursor: pointer;
}
.page-constructor>#part-3>#house>button>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
transform: rotate(45deg);
}
.page-constructor>#part-3>#house>.entrance {
min-height: 200px;
border: 1px solid var(--ColorThemes3);
border-style: dashed;
border-radius: 6px;
margin: 0 10px 10px 0;
}
.page-constructor>#part-3>#house>.entrance>.entrance-info>input {
text-align: center;
font-size: var(--FontSize5);
font-weight: 400;
margin: 10px;
padding: 7px;
color: var(--ColorThemes0);
background: var(--ColorThemes3);
border-radius: 4px;
width: calc(100% - 14px - 20px);
}
.page-constructor>#part-3>#house>.entrance>.entrance-info>button,
.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>button {
display: flex;
position: relative;
width: 34px;
height: 34px;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
margin: 25px;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 30px;
cursor: pointer;
}
.page-constructor>#part-3>#house>.entrance>.entrance-info>button>svg,
.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>button>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
transform: rotate(45deg);
}
.page-constructor>#part-3>#house>.entrance>.floor {
position: relative;
display: flex;
width: calc(100% - 22px);
border: 1px solid var(--ColorThemes3);
margin: 10px;
border-radius: 4px;
}
.page-constructor>#part-3>#house>.entrance>.floor>.floor-info>h2 {
position: absolute;
width: 65px;
right: -1px;
top: -1px;
margin: 0;
background: var(--ColorThemes3);
color: var(--ColorThemes2);
border-radius: 0 4px 0 4px;
font-size: var(--FontSize1);
padding: 2px 4px;
text-align: center;
}
.page-constructor>#part-3>#house>.entrance>.floor>.apartment {
display: flex;
position: relative;
width: 60px;
height: 60px;
background: var(--ColorThemes1);
border: 2px solid var(--PrimaryColor);
margin: 10px;
border-radius: 4px;
align-items: center;
justify-content: center;
}
.page-constructor>#part-3>#house>.entrance>.floor>.apartment>input {
width: 50px;
height: 50px;
font-size: var(--FontSize5);
font-weight: 400;
text-align: center;
color: var(--ColorThemes3);
background: 0;
}
.page-constructor>#part-3>#house>.entrance>.floor>.apartment>button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
border-radius: 0 4px 0 4px;
background: var(--PrimaryColor);
font-size: var(--FontSize5);
margin: -2px;
border: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.page-constructor>#part-3>#house>.entrance>.floor>.apartment>button>svg {
width: 16px;
height: 16px;
fill: var(--PrimaryColorText);
}
.page-constructor>#part-3>.info {}
.page-constructor>#part-3>.info>p {
font-size: var(--FontSize2);
color: var(--ColorThemes3);
opacity: 0.8;
font-style: oblique;
}
.page-constructor>#part-3>.info>button {
border-radius: 6px;
background: var(--ColorThemes3);
width: fit-content;
height: 40px;
padding: 0 10px;
margin-bottom: 20px;
text-transform: uppercase;
display: flex
;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
}
.page-constructor>#part-3>.info>button>svg {
width: 20px;
height: 20px;
fill: var(--ColorThemes0);
margin-right: 10px;
}
.page-constructor>#part-3>.info>button>span {
font-size: var(--FontSize3);
font-weight: 400;
color: var(--ColorThemes0);
}

View File

@@ -0,0 +1,84 @@
<div class="page-editor">
<div id="part-1" class="part_block">
<h1>
<span>Крок 1.</span> Інформація про будинок, територію, точки на карті
</h1>
<form id="info-form">
<div>
<label for="info-title">Назва вулиці</label>
<input
type="text"
id="info-title"
name="address"
required
value=""
/>
</div>
<div>
<label for="info-number">Номер будинку / частини</label>
<input type="text" id="info-number" name="number" required value="" />
</div>
<div>
<label for="info-settlement">Місто</label>
<input
type="text"
id="info-settlement"
name="settlement"
required
value="Тернопіль"
/>
</div>
</form>
</div>
<div id="part-2" class="part_block">
<h1 id="part-2-title"><span>Крок 2.</span> Створення будинку / ділянки</h1>
<div class="osm-info">
<div>
<label for="info-osm">OSM iD</label>
<div>
<input
id="info-osm"
type="text"
placeholder="123, 345, 678"
onchange="Territory_editor.osm.autoPoligon(this.value)"
/>
<a
href="https://www.openstreetmap.org/#map=19/49.561725/25.604458"
target="_blank"
title="Де знайти OSM iD ?"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="100px"
height="100px"
>
<path
d="M 24 4 C 12.972066 4 4 12.972074 4 24 C 4 35.027926 12.972066 44 24 44 C 35.027934 44 44 35.027926 44 24 C 44 12.972074 35.027934 4 24 4 z M 24 7 C 33.406615 7 41 14.593391 41 24 C 41 33.406609 33.406615 41 24 41 C 14.593385 41 7 33.406609 7 24 C 7 14.593391 14.593385 7 24 7 z M 24 14 A 2 2 0 0 0 24 18 A 2 2 0 0 0 24 14 z M 23.976562 20.978516 A 1.50015 1.50015 0 0 0 22.5 22.5 L 22.5 33.5 A 1.50015 1.50015 0 1 0 25.5 33.5 L 25.5 22.5 A 1.50015 1.50015 0 0 0 23.976562 20.978516 z"
></path>
</svg>
</a>
</div>
</div>
<span>або</span>
<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>
</div>
<div id="part-3" class="part_block" style="display: none">
<h1 id="part-3-title"><span>Крок 3.</span> Створення квартир</h1>
<div id="house"></div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,398 @@
.page-editor {
width: calc(100% - 40px);
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 20px 0 20px;
}
.page-editor>#part-1,
.page-editor>#part-2,
.page-editor>#part-3 {
border-radius: 10px;
width: calc(100% - 40px);
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: 20px;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
padding: 0 20px;
position: relative;
}
.page-editor>#part-1>h1,
.page-editor>#part-2>h1,
.page-editor>#part-3>h1 {
width: calc(100% - 40px);
color: var(--ColorThemes3);
border-radius: var(--border-radius);
font-size: var(--FontSize5);
font-weight: 300;
padding: 20px 0;
position: relative;
}
.page-editor>#part-1>h1>span,
.page-editor>#part-2>h1>span,
.page-editor>#part-3>h1>span {
font-weight: 500;
}
.page-editor>#part-1>form>div {
width: 100%;
display: flex;
margin: 0 0 20px 0;
align-items: flex-start;
flex-direction: column;
}
.page-editor>#part-1>form>div>label {
display: flex;
justify-content: center;
flex-direction: column;
font-size: var(--FontSize1);
font-weight: 500;
margin-bottom: 5px;
}
.page-editor>#part-1>form>div>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
}
.page-editor>#part-1>form>button,
.page-editor>#part-2>button,
.page-editor>#part-3>button {
border-radius: 6px;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
width: 100%;
height: 40px;
font-size: var(--FontSize3);
font-weight: 400;
margin: 0 0 20px 0;
text-transform: uppercase;
}
.page-editor>#part-2>.osm-info {
padding-bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.page-editor>#part-2>.osm-info>div {
display: flex;
align-items: center;
width: 100%;
}
.page-editor>#part-2>.osm-info>div {
width: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
}
.page-editor>#part-2>.osm-info>div>label {
display: flex;
justify-content: center;
flex-direction: column;
font-size: var(--FontSize1);
font-weight: 500;
margin-bottom: 5px;
}
.page-editor>#part-2>.osm-info>div>div {
display: flex;
align-items: center;
width: 100%;
}
.page-editor>#part-2>.osm-info>div>div>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
}
.page-editor>#part-2>.osm-info>div>div>a {
height: 26px;
width: 26px;
margin-left: 10px;
}
.page-editor>#part-2>.osm-info>div>div>a>svg {
height: 26px;
width: 26px;
fill: var(--ColorThemes3);
}
.page-editor>#part-2>.osm-info>span {
font-size: var(--FontSize5);
margin: 10px;
color: var(--ColorThemes3);
}
.page-editor>#part-2>.osm-info>button {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 100%;
cursor: pointer;
transition: 0.15s ease-in;
color: var(--PrimaryColorText);
background: var(--PrimaryColor);
border-radius: 6px;
flex-direction: row;
z-index: 2;
font-size: var(--FontSize3);
}
.page-editor>#part-2>.block-map {
width: 100%;
height: 500px;
border-radius: 6px;
overflow: hidden;
position: relative;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
margin-bottom: 20px;
}
.page-editor>#part-2>.block-map>#map {
width: 100%;
height: 100%;
}
.page-editor>#part-3>input {
font-weight: 500;
position: absolute;
right: 0;
top: 0;
padding: 10px;
margin: 13px;
font-size: var(--FontSize1);
background: var(--ColorThemes3);
color: var(--ColorThemes0);
border-radius: 6px;
width: 40px;
}
.page-editor>#part-3>#house {
display: flex;
overflow: auto;
}
.page-editor>#part-3>#house>button {
display: flex;
position: relative;
width: 34px;
min-width: 34px;
height: calc(100% - 10px);
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
margin: 0 10px;
border-radius: 6px;
align-items: center;
justify-content: center;
font-size: 30px;
cursor: pointer;
}
.page-editor>#part-3>#house>button>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
transform: rotate(45deg);
}
.page-editor>#part-3>#house>.entrance {
min-height: 200px;
border: 1px solid var(--ColorThemes3);
border-style: dashed;
border-radius: 6px;
margin: 0 10px 10px 0;
}
.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header {
display: flex;
align-items: center;
min-width: 180px;
}
.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>input {
text-align: center;
font-size: var(--FontSize5);
font-weight: 400;
margin: 10px;
padding: 7px;
color: var(--ColorThemes0);
background: var(--ColorThemes3);
border-radius: 4px;
width: calc(100% - 14px - 20px);
}
.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>button {
display: flex;
margin: 10px 10px 10px 0px;
border-radius: 4px;
min-width: 33px;
min-height: 33px;
align-items: center;
justify-content: center;
cursor: pointer;
}
.page-editor>#part-3>#house>.entrance>.entrance-info>.entrance-header>button>svg {
width: 16px;
height: 16px;
fill: #C14D4D;
}
.page-editor>#part-3>#house>.entrance>.entrance-info>button,
.page-editor>#part-3>#house>.entrance>.floor>.floor-info>button {
display: flex;
position: relative;
width: 34px;
height: 34px;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
margin: 25px;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 30px;
cursor: pointer;
}
.page-editor>#part-3>#house>.entrance>.entrance-info>button>svg,
.page-editor>#part-3>#house>.entrance>.floor>.floor-info>button>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
transform: rotate(45deg);
}
.page-editor>#part-3>#house>.entrance>.floor {
position: relative;
display: flex;
width: calc(100% - 22px);
border: 1px solid var(--ColorThemes3);
margin: 10px;
border-radius: 4px;
}
.page-editor>#part-3>#house>.entrance>.floor>.floor-info>h2 {
position: absolute;
width: 65px;
right: -1px;
top: -1px;
margin: 0;
background: var(--ColorThemes3);
color: var(--ColorThemes2);
border-radius: 0 4px 0 4px;
font-size: var(--FontSize1);
padding: 2px 4px;
text-align: center;
}
.page-editor>#part-3>#house>.entrance>.floor>.apartment {
display: flex;
position: relative;
width: 60px;
height: 60px;
background: var(--ColorThemes1);
border: 2px solid var(--PrimaryColor);
margin: 10px;
border-radius: 4px;
align-items: center;
justify-content: center;
}
.page-editor>#part-3>#house>.entrance>.floor>.apartment>input {
width: 50px;
height: 50px;
font-size: var(--FontSize5);
font-weight: 400;
text-align: center;
color: var(--ColorThemes3);
background: 0;
}
.page-editor>#part-3>#house>.entrance>.floor>.apartment>button {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
border-radius: 0 4px 0 4px;
background: var(--PrimaryColor);
font-size: var(--FontSize5);
margin: -2px;
border: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.page-editor>#part-3>#house>.entrance>.floor>.apartment>button>svg {
width: 16px;
height: 16px;
fill: var(--PrimaryColorText);
}
.page-editor>#part-3>.info {}
.page-editor>#part-3>.info>p {
font-size: var(--FontSize2);
color: var(--ColorThemes3);
opacity: 0.8;
font-style: oblique;
}
.page-editor>#part-3>.info>button {
border-radius: 6px;
background: var(--ColorThemes3);
width: fit-content;
height: 40px;
padding: 0 10px;
margin-bottom: 20px;
text-transform: uppercase;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
}
.page-editor>#part-3>.info>button>svg {
width: 20px;
height: 20px;
fill: var(--ColorThemes0);
margin-right: 10px;
}
.page-editor>#part-3>.info>button>span {
font-size: var(--FontSize3);
font-weight: 400;
color: var(--ColorThemes0);
}

View File

@@ -0,0 +1,3 @@
<div class="page-territory_history">
<div id="list"></div>
</div>

View File

@@ -0,0 +1,80 @@
const Territory_History = {
list: [],
async init() {
let html = await fetch('/lib/pages/territory/history/index.html').then((response) => response.text());
app.innerHTML = html;
this.setHTML()
},
async loadAPI(url) {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
return await response.json();
},
async setHTML() {
const block_list = document.getElementById('list');
const url = `${CONFIG.api}history/apartments/list?limit=300`;
let list = this.list.length > 0 ? this.list : await this.loadAPI(url);
list.sort((a, b) => b.id - a.id);
let html = "";
for (const element of list) {
html += this.renderCard({ element });
}
block_list.innerHTML = html;
},
renderCard: ({ element }) => {
const labels = ["", "Відмова (Не цікавить)", "Не заходити (Груба відмова)", "Нема домофона", "Повторна відвідина", "Немає вдома", "Свідки Єгови"];
const color_status = [
["var(--ColorThemes2)", "var(--ColorThemes3)"],
["#fbf1e0", "#ff8300"],
["#fce3e2", "#ff0000"],
["#d7ddec", "#2919bd"],
["#d5e9dd", "#11a568"],
["#d7ebfa", "#3fb4fc"],
["#e8dbf5", "#b381eb"]
];
const [bg, color] = color_status[element.status];
let description = element.description
? `<div class="description"><p>${element.description}</p></div>`
: ``;
return `
<div class="card" style="background:${bg};color:${color};border:1px solid ${color}">
<div class="info" title="${element.id}">
<a href="/territory/card/house/${element.house_id}" class="address">
<p title="${element.address.entrance}">${element.address.house.title} ${element.address.house.number}</p>
</a>
<div class="apartment">
<p>кв. ${element.address.apartment}</p>
</div>
<div class="status">
<p>${labels[element.status]}</p>
</div>
<a href="/sheeps/${element.sheep.id}" class="name">
<p>${element.sheep.name ?? "--"}</p>
</a>
<div class="date">
<p>${formattedDateTime(element.created_at)}</p>
</div>
</div>
${description}
</div>
`;
},
}

View File

@@ -0,0 +1,75 @@
.page-territory_history {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
position: relative;
}
.page-territory_history>#list {
width: 100%;
overflow: auto;
padding: 20px;
}
.page-territory_history>#list>.card {
display: flex;
min-width: calc(100% - 2px);
width: 700px;
background: var(--ColorThemes1);
border: 1px solid var(--ColorThemes2);
border-radius: var(--border-radius);
margin-bottom: 10px;
flex-direction: column;
position: relative;
}
.page-territory_history>#list>.card>.info {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px;
}
.page-territory_history>#list>.card>.info>div {
text-align: center;
}
.page-territory_history>#list>.card>.info>div>a,
.page-territory_history>#list>.card>.info>div>p {
font-size: var(--FontSize3);
}
.page-territory_history>#list>.card>.info>.address {
width: 20%;
text-align: left;
}
.page-territory_history>#list>.card>.info>.apartment {
width: 60px;
}
.page-territory_history>#list>.card>.info>.status {
width: 180px;
}
.page-territory_history>#list>.card>.info>.name {
width: 150px;
text-align: center;
}
.page-territory_history>#list>.card>.info>.date {
width: 125px;
}
.page-territory_history>#list>.card>.description {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 10px 10px 10px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize2);
padding: 10px;
border-radius: 8px;
}

View File

@@ -1,7 +1,7 @@
<div class="page-territory">
<div class="buttons-list" id="buttons-list">
<a
href="/constructor"
href="/territory/constructor"
data-route
id="constructorButton"
style="display: none"
@@ -14,7 +14,7 @@
<span>Конструктор</span>
</a>
<button onclick="Territory.report()">
<button onclick="Territory_list.report()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M13.13,2H5.958c-1.1,0-2,0.9-2,2v16c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V8.828c0-0.53-0.211-1.039-0.586-1.414l-4.828-4.828 C14.169,2.211,13.66,2,13.13,2z M11.255,17.711l-1.297-1.297l-1.251,1.293c-0.39,0.39-1.024,0.39-1.414,0l0,0 c-0.39-0.39-0.39-1.024,0-1.414l1.958-1.989c0.39-0.396,1.027-0.398,1.42-0.006l1.287,1.287l2.335-2.293 c0.39-0.39,1.024-0.39,1.414,0l0,0c0.39,0.39,0.39,1.024,0,1.414l-3.042,3.008C12.274,18.102,11.644,18.1,11.255,17.711z M12.958,9 V3.5l5.5,5.5H12.958z"
@@ -40,21 +40,21 @@
<div class="list-controls">
<div id="page-territory-sort">
<button id="sort_1" onclick="Territory.sort('2')" data-state="active">
<button id="sort_1" onclick="Territory_list.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"
></path>
</svg>
</button>
<button id="sort_2" onclick="Territory.sort('3')" data-state="">
<button id="sort_2" onclick="Territory_list.sort('3')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M 31.478516 6 A 1.50015 1.50015 0 0 0 30.439453 6.4394531 L 23.439453 13.439453 A 1.50015 1.50015 0 1 0 25.560547 15.560547 L 30 11.121094 L 30 41.5 A 1.50015 1.50015 0 1 0 33 41.5 L 33 11.121094 L 37.439453 15.560547 A 1.50015 1.50015 0 1 0 39.560547 13.439453 L 32.560547 6.4394531 A 1.50015 1.50015 0 0 0 31.478516 6 z M 13.375 8.0058594 C 13.257547 8.01575 13.139641 8.0379219 13.025391 8.0761719 L 10.025391 9.0761719 C 9.2393906 9.3381719 8.8141719 10.188609 9.0761719 10.974609 C 9.3381719 11.760609 10.190609 12.188828 10.974609 11.923828 L 12 11.580078 L 12 20.5 C 12 21.329 12.671 22 13.5 22 C 14.329 22 15 21.329 15 20.5 L 15 9.5 C 15 9.018 14.767953 8.5652031 14.376953 8.2832031 C 14.082953 8.0717031 13.727359 7.9761875 13.375 8.0058594 z M 13 27 C 10.344 27 8.387625 28.682109 8.015625 31.287109 C 7.898625 32.107109 8.4671094 32.867375 9.2871094 32.984375 C 10.106109 33.102375 10.867375 32.533891 10.984375 31.712891 C 11.096375 30.931891 11.537 30 13 30 C 14.103 30 15 30.897 15 32 C 15 33.103 14.103 34 13 34 C 10.592 34 8 35.721 8 39.5 C 8 40.329 8.672 41 9.5 41 L 16.5 41 C 17.329 41 18 40.329 18 39.5 C 18 38.671 17.329 38 16.5 38 L 11.308594 38 C 11.781594 37.093 12.664 37 13 37 C 15.757 37 18 34.757 18 32 C 18 29.243 15.757 27 13 27 z"
></path>
</svg>
</button>
<button id="sort_3" onclick="Territory.sort('4')" data-state="">
<button id="sort_3" onclick="Territory_list.sort('4')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
transform="rotate(180 32.5 24.5106)"
@@ -66,7 +66,7 @@
></path>
</svg>
</button>
<button id="sort_4" onclick="Territory.sort('1')" data-state="">
<button id="sort_4" onclick="Territory_list.sort('1')" data-state="">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
id="svg_1"
@@ -82,7 +82,7 @@
<select
id="list-controls-filter-status"
name="status"
onchange="Territory.filter()"
onchange="Territory_list.filter()"
>
<option value="0" selected>Всі території</option>
<option value="1">Зайняті території</option>
@@ -96,7 +96,7 @@
<div>
<button
id="territory_entrances_true"
onclick="Territory.house.territoryType('true')"
onclick="Territory_list.house.territoryType('true')"
data-state=""
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
@@ -108,7 +108,7 @@
<button
id="territory_entrances_false"
onclick="Territory.house.territoryType('false')"
onclick="Territory_list.house.territoryType('false')"
data-state=""
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">

View File

@@ -1,6 +1,6 @@
const Territory = {
const Territory_list = {
init: async () => {
let html = await fetch('/lib/pages/territory/index.html').then((response) => response.text());
let html = await fetch('/lib/pages/territory/list/index.html').then((response) => response.text());
app.innerHTML = html;
let selectStatus = document.getElementById('list-controls-filter-status');
@@ -17,7 +17,7 @@ const Territory = {
}
// Застосовуємо режим сортування
Territory.sort(localStorage.getItem('territory_sort_mode'));
Territory_list.sort(localStorage.getItem('territory_sort_mode'));
if (localStorage.getItem('territory_entrances') == 'true') {
@@ -37,8 +37,8 @@ const Territory = {
});
localStorage.setItem('territory_sort_mode', idx);
Territory.house.setHTML();
Territory.homestead.setHTML();
Territory_list.house.setHTML();
Territory_list.homestead.setHTML();
},
house: {
@@ -52,8 +52,8 @@ const Territory = {
"Authorization": uuid
}
});
Territory.house.list = await response.json();
return Territory.house.list;
Territory_list.house.list = await response.json();
return Territory_list.house.list;
},
setHTML: async function () {
const block_house = document.getElementById('list-house');
@@ -164,8 +164,8 @@ const Territory = {
localStorage.setItem('territory_entrances', type);
document.getElementById('territory_entrances_true').setAttribute('data-state', type === 'false' ? 'active' : '');
document.getElementById('territory_entrances_false').setAttribute('data-state', type === 'true' ? 'active' : '');
Territory.house.list = [];
Territory.house.setHTML();
Territory_list.house.list = [];
Territory_list.house.setHTML();
}
},
homestead: {
@@ -247,8 +247,8 @@ const Territory = {
localStorage.setItem("filterStatus", selectStatus);
Territory.house.setHTML();
Territory.homestead.setHTML();
Territory_list.house.setHTML();
Territory_list.homestead.setHTML();
},
report: async () => {
const uuid = localStorage.getItem("uuid");

View File

@@ -0,0 +1,117 @@
<div class="page-territory_manager">
<div id="territory-info">
<div class="territory-info-image" id="CardPicture">
<img id="info-picture" src="" alt="" style="display: none" />
<div id="map_territory_manager"></div>
<div class="menu">
<a
id="menu-picture-error"
class="menu-picture-"
title="Зображення не знайдено!"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
style="fill: #c14d4d"
>
<path
d="M 3.7070312 2.2929688 L 2.2929688 3.7070312 L 26.292969 27.707031 L 27.707031 26.292969 L 26.375 24.960938 C 27.299776 24.784872 28 23.976233 28 23 L 28 7 C 28 5.895 27.105 5 26 5 L 6.4140625 5 L 3.7070312 2.2929688 z M 2.1367188 6.2851562 C 2.0517188 6.5071563 2 6.747 2 7 L 2 23 C 2 24.105 2.895 25 4 25 L 20.851562 25 L 17.851562 22 L 5 22 L 5 15 L 7.2890625 12.710938 C 7.5140625 12.485938 7.7747812 12.317219 8.0507812 12.199219 L 2.1367188 6.2851562 z M 23 8 C 24.105 8 25 8.895 25 10 C 25 11.105 24.105 12 23 12 C 21.895 12 21 11.105 21 10 C 21 8.895 21.895 8 23 8 z M 19 14.001953 C 19.61925 14.001953 20.238437 14.238437 20.710938 14.710938 L 25 19 L 25 22 L 23.414062 22 L 16.707031 15.292969 L 17.289062 14.710938 C 17.761563 14.238437 18.38075 14.001953 19 14.001953 z"
/>
</svg>
</a>
<a
id="menu-picture-ok"
class="menu-picture"
style="display: none"
title="Зображення знайдено"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
style="fill: #8bc34a"
>
<path
d="M 4 5 C 2.895 5 2 5.895 2 7 L 2 23 C 2 24.105 2.895 25 4 25 L 26 25 C 27.105 25 28 24.105 28 23 L 28 7 C 28 5.895 27.105 5 26 5 L 4 5 z M 23 8 C 24.105 8 25 8.895 25 10 C 25 11.105 24.105 12 23 12 C 21.895 12 21 11.105 21 10 C 21 8.895 21.895 8 23 8 z M 9 13.001953 C 9.61925 13.001953 10.238437 13.238437 10.710938 13.710938 L 13.972656 16.972656 L 15 18 L 16.15625 19.15625 C 16.57825 19.57825 17.259641 19.574344 17.681641 19.152344 C 18.104641 18.730344 18.104641 18.044094 17.681641 17.621094 L 16.529297 16.470703 L 17.289062 15.710938 C 18.234063 14.765937 19.765937 14.765937 20.710938 15.710938 L 25 20 L 25 22 L 5 22 L 5 16 L 7.2890625 13.710938 C 7.7615625 13.238437 8.38075 13.001953 9 13.001953 z"
/>
</svg>
</a>
<a
id="editor_button"
class="menu-edit"
style="display: none"
data-route
title="Змінити територію"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M 22.828125 3 C 22.316375 3 21.804562 3.1954375 21.414062 3.5859375 L 19 6 L 24 11 L 26.414062 8.5859375 C 27.195062 7.8049375 27.195062 6.5388125 26.414062 5.7578125 L 24.242188 3.5859375 C 23.851688 3.1954375 23.339875 3 22.828125 3 z M 17 8 L 5.2597656 19.740234 C 5.2597656 19.740234 6.1775313 19.658 6.5195312 20 C 6.8615312 20.342 6.58 22.58 7 23 C 7.42 23.42 9.6438906 23.124359 9.9628906 23.443359 C 10.281891 23.762359 10.259766 24.740234 10.259766 24.740234 L 22 13 L 17 8 z M 4 23 L 3.0566406 25.671875 A 1 1 0 0 0 3 26 A 1 1 0 0 0 4 27 A 1 1 0 0 0 4.328125 26.943359 A 1 1 0 0 0 4.3378906 26.939453 L 4.3632812 26.931641 A 1 1 0 0 0 4.3691406 26.927734 L 7 26 L 5.5 24.5 L 4 23 z"
/>
</svg>
</a>
</div>
</div>
<div class="territory-info-text">
<h1>Вулиця:</h1>
<h2 id="info-title">--</h2>
</div>
<div class="territory-info-text">
<h1>Номер:</h1>
<h2 id="info-number">--</h2>
</div>
<div class="territory-info-text">
<h1>Населений пункт:</h1>
<h2 id="info-settlement">--</h2>
</div>
<div class="territory-info-text">
<textarea
name="info-description"
id="info-description"
placeholder="Примітка"
onchange="Territory_Manager.info.update()"
></textarea>
</div>
</div>
<div id="territory-entrance"></div>
</div>
<div id="territory-new" style="display: none; opacity: 0">
<div class="mess">
<select id="new-worker-group" name="new-worker-name">
<option value="" selected="" disabled="">Оберіть...</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
<input
id="new-worker-name"
type="text"
name="new-worker-name"
list="list_sheeps"
/>
<div>
<button onclick="Territory_Manager.mess.close()">Закрити</button>
<button
id="new-worker-button"
onclick="Territory_Manager.newWorker()"
style="background: var(--PrimaryColor); color: var(--PrimaryColorText)"
>
Призначити
</button>
</div>
</div>
</div>
<datalist id="list_sheeps"></datalist>

View File

@@ -0,0 +1,489 @@
let map_territory, type_territory;
const Territory_Manager = {
init: async (type, id) => {
let html = await fetch('/lib/pages/territory/manager/index.html').then((response) => response.text());
app.innerHTML = html;
type_territory = type;
let sheeps_list = [];
if (Sheeps.sheeps_list.list.length == 0) {
sheeps_list = await Sheeps.sheeps_list.loadAPI();
} else {
sheeps_list = Sheeps.sheeps_list.list;
}
let editor_button = document.getElementById('editor_button');
if (USER.possibilities.can_add_territory) {
editor_button.style.display = "";
editor_button.setAttribute("href", `/territory/editor/${type}/${id}`)
}
await Territory_Manager.info.setHTML(type, id);
Territory_Manager.entrances.setHTML(type, id);
for (let i = 0; i < sheeps_list.length; i++) {
const element = sheeps_list[i];
document.getElementById('list_sheeps').innerHTML += `
<option value="${element.name}">${element.name}</option>
`;
}
const card = document.getElementById('territory-new');
const mess = card.querySelector('.mess');
if (!card.dataset.listenerAdded) {
card.addEventListener('click', function (event) {
if (!mess.contains(event.target)) {
Territory_Manager.mess.close();
}
});
card.dataset.listenerAdded = 'true';
}
},
info: {
list: {},
loadAPI: async (url) => {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
Territory_Manager.info.list = await response.json();
return Territory_Manager.info.list;
},
setHTML: async (type, id) => {
const url = `${CONFIG.api}${type}/${id}`;
const data = await Territory_Manager.info.loadAPI(url);
Territory_Manager.map(type, data);
const info_picture = document.getElementById('info-picture');
const info_title = document.getElementById('info-title');
const info_number = document.getElementById('info-number');
const info_settlement = document.getElementById('info-settlement');
const info_description = document.getElementById('info-description');
if (!info_picture.dataset.listener) {
info_picture.addEventListener("click", () => {
const state = info_picture.getAttribute('data-state');
info_picture.setAttribute('data-state', state === 'active' ? '' : 'active');
});
info_picture.dataset.listener = "true";
}
info_picture.setAttribute("src", "");
info_title.textContent = data.title;
info_number.textContent = data.number;
info_settlement.textContent = data.settlement;
info_description.value = data.description;
const urlImage = `https://sheep-service.com/cards/${type}/${type === "house" ? "T" : "H"}${id}.webp`;
try {
const checkImage = await fetch(urlImage, { method: 'GET' });
const showOk = checkImage.ok;
document.getElementById('menu-picture-ok').style.display = showOk ? '' : 'none';
document.getElementById('menu-picture-error').style.display = showOk ? 'none' : '';
if (showOk) {
document.getElementById('menu-picture-ok').setAttribute("href", urlImage);
}
} catch (err) {
document.getElementById('menu-picture-error').style.display = '';
document.getElementById('menu-picture-ok').style.display = 'none';
}
},
update: async () => {
const uuid = localStorage.getItem('uuid');
const data = Territory_Manager.info.list;
data.description = document.getElementById('info-description').value;
const url = `${CONFIG.api}${type_territory}/${data.id}`;
try {
const response = await fetch(url, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify(data)
});
if (response.ok) {
console.log('ok');
await response.json();
} else {
console.log('err');
}
} catch (e) {
console.log('update error', e);
}
}
},
entrances: {
list: [],
loadAPI: async (url) => {
const uuid = localStorage.getItem("uuid");
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
Territory_Manager.entrances.list = await response.json();
return Territory_Manager.entrances.list;
},
setHTML: async (type, id) => {
const container = document.getElementById('territory-entrance');
container.innerHTML = "";
const sheeps_list = Sheeps.sheeps_list.list;
const renderName = (history) => {
if (history.name === "Групова") return `<p>${history.name} ${history.group_id}</p>`;
const sheep = sheeps_list.find(item => item.name === history.name);
return sheep ? `<a href="/sheeps/${sheep.id}" data-route>${history.name}</a>` : `<a>${history.name}</a>`;
};
const renderWorking = (element, i = 0) => `
<div class="entrance" data-state="working">
<div id="title">
<h1>${element.title ?? ''}</h1>
<a href="/territory/card/${type}/${id}" title="Редактор квартир" data-route>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"/></svg>
</a>
</div>
<div><h1>Територію опрацьовує:</h1>${renderName(element.history)}</div>
<div><h1>Територія видана:</h1><h2>${formattedDate(element.history.date.start)}</h2></div>
<div><h1>Варто забрати:</h1><h2>${formattedDate(element.history.date.end) ?? formattedDate(element.history.date.start + (1000 * 2629743 * 4))}</h2></div>
<div class="edit_working">
<button onclick="Territory_Manager.endWorker('${type}', ${id}, ${element.history.id})" style="color: #121214;background: #c14d4d;">Забрати</button>
<button onclick="Territory_Manager.share('${type}'${type === "house" ? `, ${i}` : ""})">Відправити посилання</button>
</div>
</div>
`;
const renderFree = (element, i = 0) => `
<div class="entrance">
<div id="title">
<h1>${element.title ?? ''}</h1>
<a href="/territory/card/${type}/${id}" title="Редактор квартир" data-route>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M 12.5 6 C 8.9280619 6 6 8.9280619 6 12.5 L 6 35.5 C 6 39.071938 8.9280619 42 12.5 42 L 35.5 42 C 39.071938 42 42 39.071938 42 35.5 L 42 12.5 C 42 8.9280619 39.071938 6 35.5 6 L 12.5 6 z M 12.5 9 L 14 9 L 14 15 L 9 15 L 9 12.5 C 9 10.549938 10.549938 9 12.5 9 z M 17 9 L 35.5 9 C 37.450062 9 39 10.549938 39 12.5 L 39 15 L 17 15 L 17 9 z M 9 18 L 14 18 L 14 23 L 9 23 L 9 18 z M 17 18 L 39 18 L 39 23 L 17 23 L 17 18 z M 9 26 L 14 26 L 14 31 L 9 31 L 9 26 z M 17 26 L 39 26 L 39 31 L 17 31 L 17 26 z M 9 34 L 14 34 L 14 39 L 12.5 39 C 10.549938 39 9 37.450062 9 35.5 L 9 34 z M 17 34 L 39 34 L 39 35.5 C 39 37.450062 37.450062 39 35.5 39 L 17 39 L 17 34 z"/></svg>
</a>
</div>
<div><h1>Територія не опрацьовується</h1></div>
<div><h1>Останнє опрацювання:</h1><h2>${formattedDate(element.history.date.end) ?? "..."}</h2></div>
<div class="edit">
<button onclick="Territory_Manager.mess.open({type: '${type}', id: ${id}, number: ${i}, mode: false})" style="color: var(--ColorThemes0);background: var(--ColorThemes3);">Призначити груповою</button>
<button onclick="Territory_Manager.mess.open({type: '${type}', id: ${id}, number: ${i}, mode: true})">Призначити вісника</button>
</div>
</div>
`;
let html = "";
if (type === "house") {
const url = `${CONFIG.api}house/${id}/entrances`;
const list = await Territory_Manager.entrances.loadAPI(url);
for (let i = 0; i < list.length; i++) {
const el = list[i];
html += el.working ? renderWorking(el, i) : renderFree(el, i);
}
} else if (type === "homestead") {
const url = `${CONFIG.api}homestead/${id}`;
const el = await Territory_Manager.entrances.loadAPI(url);
html += el.working ? renderWorking(el) : renderFree(el);
}
container.innerHTML = html;
}
},
map: (type, 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;
if (map_territory && map_territory.remove) map_territory.remove();
const mapElement = document.getElementById('map_territory_manager');
if (!mapElement) return;
let googleHybrid = L.tileLayer('http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 20,
minZoom: 15,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3']
});
let osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
});
let mytile = L.tileLayer('https://sheep-service.com/map/{z}/{x}/{y}.webp', {
maxZoom: 20,
minZoom: 15,
tms: true
});
map_territory = L.map(mapElement, {
renderer: L.canvas(),
center: [lat, lng],
zoom: 0,
zoomControl: false,
layers: [
googleHybrid,
osm,
mytile
]
});
map_territory.setZoom((data.zoom - 1));
let baseMaps = {
"Google Hybrid": googleHybrid,
"OpenStreetMap": osm,
"Sheep Service Map": mytile,
};
let layerControl = L.control.layers(baseMaps, [], { position: 'bottomright' }).addTo(map_territory);
map_territory.pm.setLang("ua");
const polygonOptions = type === "homestead" ? {
color: "#f2bd53",
radius: 500,
fillOpacity: 0.3,
dashArray: '20,15',
dashOffset: '20',
} : {
color: "#585858",
fillColor: "#f2bd53",
fillOpacity: 0.8
};
L.polygon(data.points, polygonOptions).addTo(map_territory);
console.log(data.zoom);
// setTimeout(() => {
// map_territory.setZoom(data.zoom);
// }, 200)
},
mess: {
open: ({ type, id, number, mode }) => {
const block = document.getElementById('territory-new');
const groupInput = document.getElementById('new-worker-group');
const nameInput = document.getElementById('new-worker-name');
const button = document.getElementById('new-worker-button');
// Показуємо блок
block.style.display = "";
requestAnimationFrame(() => block.style.opacity = "1");
groupInput.style.display = mode ? 'none' : 'flex';
nameInput.style.display = mode ? 'flex' : 'none';
button.onclick = () => Territory_Manager.newWorker({ type, id, number, mode });
},
close: () => {
// Робимо плавне зникнення
const block = document.getElementById('territory-new');
block.style.opacity = "0";
const onTransitionEnd = () => {
block.style.display = "none";
block.removeEventListener("transitionend", onTransitionEnd);
};
block.addEventListener("transitionend", onTransitionEnd);
}
},
newWorker: async ({ type, id, number, mode }) => {
const uuid = localStorage.getItem('uuid');
const sheepName = document.getElementById('new-worker-name').value;
const groupId = Number(document.getElementById('new-worker-group').value);
const newButton = document.getElementById('new-worker-button');
const groupButton = document.getElementById('group-working-button');
let territory_id;
let URL;
const sheep = Sheeps.sheeps_list.list.find(e => e.name === sheepName);
if (type === "house") {
territory_id = Territory_Manager.entrances.list[number]?.id;
URL = `${CONFIG.api}history/entrance/${territory_id}`;
} else if (type === "homestead") {
territory_id = Territory_Manager.info.list.id;
URL = `${CONFIG.api}history/homestead/${territory_id}`;
}
if (!territory_id || !URL) {
console.warn("Невірні дані для призначення.");
return;
}
const data = {
name: mode ? sheepName : "Групова",
group_id: mode ? sheep?.group_id : groupId,
sheep_id: mode ? sheep?.id : null
};
try {
const response = await fetch(URL, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify(data)
});
if (!response.ok) throw new Error("Failed to assign");
Territory_Manager.mess.close();
Territory_Manager.entrances.list = [];
await Territory_Manager.entrances.setHTML(type, id);
} catch (err) {
console.error('❌ Error:', err);
const errorText = "Помилка";
if (newButton) newButton.innerText = errorText;
if (groupButton) groupButton.innerText = errorText;
}
},
endWorker: async (type, id, territory_id) => {
const button = document.getElementById('end-working-button');
const uuid = localStorage.getItem('uuid');
const URL = type === "house"
? `${CONFIG.api}history/entrance/${territory_id}`
: `${CONFIG.api}history/homestead/${territory_id}`;
try {
const response = await fetch(URL, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
},
body: JSON.stringify({})
});
if (!response.ok) throw new Error("Запит не успішний");
Territory_Manager.entrances.list = [];
await Territory_Manager.entrances.setHTML(type, id);
} catch (error) {
console.error("❌ Помилка зняття призначення:", error);
if (button) button.innerText = "Помилка";
}
},
share: async (type, number) => {
const isHouse = type === "house";
const territory = isHouse ? Territory_Manager.entrances.list[number] : Territory_Manager.info.list;
const id = territory.id;
const pos = Sheeps.sheeps_list.list.map(e => e.id).indexOf(territory.history.sheep_id);
let sheep = Sheeps.sheeps_list.list[pos];
console.log(pos);
const shareUrl = pos > 0
? `\n\nПосилання на програму:\n • https://sheep-service.com/?uuid=${sheep.uuid}`
: '';
const description = Territory_Manager.info.list.description?.length > 0
? `\n\nДодатково:\n${Territory_Manager.info.list.description}`
: '';
if (!navigator.share) {
console.log("Sorry! Your browser does not support Web Share API");
return;
}
try {
const baseUrl = "https://sheep-service.com/cards";
const url = isHouse
? `${baseUrl}/house/T${Territory_Manager.info.list.id}.webp`
: `${baseUrl}/homestead/H${Territory_Manager.info.list.id}.webp`;
const response = await fetch(url);
if (!response.ok) throw new Error('Image not found');
const blob = await response.blob();
const fileName = `${isHouse ? "E" + id : "H" + id}.webp`;
const file = new File([blob], fileName, { type: blob.type });
const shareText = `Територія:\n${isHouse ? "E" + id : "H" + id}\n\nНаселений пункт:\n${Territory_Manager.info.list.settlement}\n\nВулиця:\n${Territory_Manager.info.list.title} ${Territory_Manager.info.list.number} (${territory.title})${description}\n\nПризначення:\nЗ ${formattedDate(territory.history.date.start)}\n • До ${formattedDate(territory.history.date.end) ?? formattedDate(territory.history.date.start + (1000 * 2629743 * 4))}${shareUrl}`;
console.log(shareText);
await navigator.share({
text: shareText,
files: [file]
});
console.log('Успешно отправлено!');
} catch (error) {
console.error('Ошибка при отправке:', error);
}
},
getScreen: async () => {
const center = map_territory.getCenter();
const zoom = (map_territory.getZoom() + 2) || 17;
const info = Territory_Manager.info.list;
const params = new URLSearchParams({
lat: center.lat,
lng: center.lng,
type: type_territory,
wayId: info.osm_id,
zoom,
address: info.title,
number: info.number,
id: info.id
});
const url = `https://sheep-service.com/api/generator/cards?${params.toString()}`;
const uuid = localStorage.getItem("uuid");
try {
const response = await fetch(url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": uuid
}
});
const result = await response.json();
const urlImage = `https://sheep-service.com/cards/${type_territory}/${type_territory === "house" ? "T" : "H"}${info.id}.webp`;
const errorElem = document.getElementById('menu-picture-error');
const okElem = document.getElementById('menu-picture-ok');
if (result) {
errorElem.style.display = 'none';
okElem.style.display = '';
okElem.setAttribute("href", urlImage);
} else {
errorElem.style.display = '';
okElem.style.display = 'none';
}
} catch (err) {
console.error('Ошибка при получении скрина:', err);
}
}
}

View File

@@ -0,0 +1,428 @@
.page-territory_manager {
width: calc(100% - 18px);
display: flex;
flex-direction: row;
margin: 20px 9px 0 9px;
justify-content: space-between;
position: relative;
}
#territory-info,
#territory-entrance {
width: 100%;
min-height: calc(100vh - 40px - 50px);
height: fit-content;
margin: 0 10px 15px;
border-radius: 15px;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--ColorThemes1);
color: var(--ColorThemes3);
border: 1px solid var(--ColorThemes2);
box-shadow: var(--shadow-l1);
transition: all .2sease 0s;
}
#territory-info {
position: sticky;
overflow: auto;
top: 20px;
}
#territory-info>.territory-info-image {
width: calc(100% - 20px);
min-height: 200px;
margin: 10px;
border-radius: 10px;
background: var(--ColorThemes0);
position: relative;
display: flex;
overflow: hidden;
background-position: 50%;
background-size: cover;
justify-content: center;
aspect-ratio: 1147 / 751;
max-height: 300px;
}
#territory-info>.territory-info-image>img {
width: 100%;
object-fit: cover;
z-index: 3;
cursor: pointer;
}
#territory-info>.territory-info-image>#map_territory_manager {
width: 100%;
height: 100%;
}
#territory-info>.territory-info-image>img[data-state="active"] {
object-fit: contain;
}
#territory-info>.territory-info-image>.menu {
position: absolute;
right: 10px;
top: 10px;
z-index: 999;
background: var(--ColorThemes0);
border: 1px solid var(--ColorThemes2);
padding: 5px;
border-radius: 6px;
display: flex;
justify-content: space-between;
}
#territory-info>.territory-info-image>.menu>.menu-picture {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
#territory-info>.territory-info-image>.menu>.menu-picture>svg {
width: 22px;
height: 22px;
}
#territory-info>.territory-info-image>.menu>.menu-edit {
width: 30px;
height: 30px;
border-radius: 2px;
background: var(--PrimaryColor);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
}
#territory-info>.territory-info-image>.menu>.menu-edit>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
}
#territory-info>.territory-info-text {
width: calc(100% - 40px);
display: flex;
flex-direction: row;
margin: 10px;
border-radius: 10px;
position: relative;
align-items: center;
background: var(--ColorThemes0);
padding: 10px;
}
#territory-info>.territory-info-text>h1 {
font-size: var(--FontSize3);
font-weight: 400;
color: var(--ColorThemes3);
}
#territory-info>.territory-info-text>h2 {
font-size: var(--FontSize3);
font-weight: 300;
color: var(--ColorThemes3);
opacity: 0.8;
margin: 0 7px;
}
#territory-info>.territory-info-text>textarea {
width: 100%;
min-height: 100px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
font-size: var(--FontSize3);
resize: vertical;
border-radius: 0;
}
#territory-info>#editor_button {
width: calc(100% - 20px);
font-size: var(--FontSize2);
font-weight: 400;
min-height: 40px;
display: flex;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
cursor: pointer;
border: 0;
position: relative;
border-radius: 8px;
margin: 20px 10px 10px 10px;
align-items: center;
justify-content: center;
}
#territory-entrance>.entrance {
width: calc(100% - 40px);
color: var(--ColorThemes3);
background-color: var(--ColorThemes2);
border: 1px solid var(--ColorThemes0);
box-shadow: var(--shadow-l1);
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 10px;
padding: 10px;
min-height: 100px;
justify-content: center;
position: relative;
border-radius: 10px;
}
#territory-entrance>.entrance[data-state="working"] {
border: 1px solid var(--PrimaryColor);
}
#territory-entrance>.entrance>#title {
display: flex;
align-items: center;
height: 30px;
margin-bottom: 10px;
justify-content: space-between;
}
#territory-entrance>.entrance>#title>h1 {
width: 100%;
height: 30px;
background: var(--ColorThemes0);
color: var(--ColorThemes3);
border-radius: 6px;
font-size: var(--FontSize5);
font-weight: 400;
display: flex;
align-items: center;
justify-content: center;
}
#territory-entrance>.entrance>#title>a {
min-height: 30px;
min-width: 30px;
padding: 0;
margin: 0 0 0 10px;
background: var(--PrimaryColor);
display: flex;
align-items: center;
justify-content: center;
}
#territory-entrance>.entrance>#title>a>svg {
width: 25px;
height: 25px;
fill: var(--PrimaryColorText);
}
#territory-entrance>.entrance>div {
display: flex;
margin-bottom: 10px;
flex-direction: row;
align-items: center;
width: 100%;
}
#territory-entrance>.entrance>div>h1 {
font-size: var(--FontSize3);
font-weight: 400;
color: var(--ColorThemes3);
}
#territory-entrance>.entrance>div>h2,
#territory-entrance>.entrance>div>a,
#territory-entrance>.entrance>div>p {
font-size: var(--FontSize3);
font-weight: 300;
color: var(--ColorThemes3);
opacity: 0.8;
margin: 0 7px;
display: flex;
align-items: flex-start;
}
#territory-entrance>.entrance>div>a,
#territory-entrance>.entrance>div>p {
color: var(--ColorThemes0);
background: var(--ColorThemes3);
border-radius: 6px;
padding: 2px 5px;
font-weight: 400;
opacity: 1;
}
.date_old {
background: var(--ColorThemes1) !important;
color: var(--ColorThemes3) !important;
font-size: 18px !important;
padding: 5px 10px !important;
}
#territory-entrance>.entrance>.edit_working,
#territory-entrance>.entrance>.edit {
margin-bottom: 0;
}
#territory-entrance>.entrance>.edit_working>button,
#territory-entrance>.entrance>.edit>button {
width: 100%;
font-size: var(--FontSize2);
font-weight: 400;
min-height: 40px;
display: flex;
padding: 2px 10px;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
cursor: pointer;
border: 0;
position: relative;
border-radius: 8px;
align-items: center;
justify-content: center;
}
#territory-entrance>.entrance>.edit_working>button:nth-child(2n),
#territory-entrance>.entrance>.edit>button:nth-child(2n) {
margin-left: 5px;
}
#territory-entrance>.entrance>.edit_working>button:nth-child(2n-1),
#territory-entrance>.entrance>.edit>button:nth-child(2n-1) {
margin-right: 5px;
}
#territory-entrance>.entrance>button>span {
display: none;
}
#territory-entrance>.entrance>button>svg {
width: 20px;
height: 20px;
fill: var(--PrimaryColorText);
}
#territory-entrance>.entrance>.apartment_button {
width: 100%;
font-size: var(--FontSize2);
font-weight: 400;
min-height: 40px;
display: flex;
background: var(--PrimaryColor);
color: var(--PrimaryColorText);
cursor: pointer;
border: 0;
position: relative;
border-radius: 8px;
margin: 20px 0 2px 0;
align-items: center;
justify-content: center;
}
#territory-new {
display: flex;
width: 100%;
height: 100%;
left: 0;
top: 0;
position: fixed;
z-index: 999;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: rgb(31 31 33 / 41%);
transition: all .2s ease 0s;
}
#territory-new>.mess {
display: flex;
width: 300px;
height: 100px;
border: 1px solid var(--ColorThemes2);
background: var(--ColorThemes0);
box-shadow: 0px 2px 3px rgb(0 0 0 / 5%), 0px 5px 15px rgb(0 0 0 / 5%), 0px 4px 8px rgb(0 0 0 / 5%), 0px 0px 1px rgb(0 0 0 / 5%);
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -160px;
flex-direction: column;
align-items: center;
z-index: 9999;
padding: 10px;
border-radius: 15px;
justify-content: space-between;
}
#territory-new>.mess>span {
color: var(--ColorThemes3);
font-size: 18px;
text-align: center;
}
#territory-new>.mess>select {
width: 100%;
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background-color: var(--ColorThemes2);
color: var(--ColorThemes3);
}
#territory-new>.mess>input {
width: calc(100% - 10px);
min-width: 140px;
padding: 0 5px;
border-radius: 6px;
height: 30px;
background-color: var(--ColorThemes2);
color: var(--ColorThemes3);
}
#territory-new>.mess>div {
width: 100%;
display: flex;
justify-content: space-between;
flex-direction: row;
}
#territory-new>.mess>div>button {
text-decoration: none;
font-size: var(--FontSize4);
font-weight: 400;
height: 35px;
color: var(--ColorThemes0);
padding: 2px 10px;
background: var(--ColorThemes3);
cursor: pointer;
border: 0;
margin-bottom: 5px;
margin-left: 0;
width: calc(50% - 5px);
border-radius: 10px;
}
@media (max-width: 1000px),
(max-height: 540px) {
.page-territory_manager {
flex-direction: column;
justify-content: flex-start;
}
#territory-info,
#territory-entrance {
width: calc(100% - 20px);
height: fit-content;
min-height: 100px;
}
#territory-info {
position: relative;
top: auto;
}
}