This commit is contained in:
2025-03-31 00:22:21 +03:00
commit 38f2a05107
146 changed files with 66771 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

92
.gitignore vendored Normal file
View File

@@ -0,0 +1,92 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
firebase-debug.*.log*
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
node_modules
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# General
.DS_Store
.AppleDouble
.LSOverride
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.ftppass
*.sqlite

261
README.md Normal file
View File

@@ -0,0 +1,261 @@
# Start
```
sudo apt update
```
```
sudo apt upgrade -y
```
```
sudo apt install -y curl gnupg2 ca-certificates lsb-release
```
```
sudo apt install zip
```
<br>
<br>
# Docker Install
```
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
```
<br>
<br>
# Docker Compose Install
```
sudo apt install -y docker-compose
```
```
docker-compose --version
```
<br>
<br>
# Start Sheep-Service
```
cd /home/rozenrod/webapps/sheep-service.com
```
```
docker-compose pull && docker-compose -p Sheep-Service up --build -d
```
<br>
<br>
# NGINX Install
```
sudo apt install nginx
```
```
sudo systemctl status nginx
```
```
sudo systemctl enable nginx
```
```
sudo nano /etc/nginx/nginx.conf
```
```
proxy_cache_path /etc/nginx/cache levels=1:2 keys_zone=all:5m inactive=10m max_size=2g;
limit_req_zone $binary_remote_addr zone=one:5m rate=30r/s;
client_max_body_size 20M;
```
<br>
<br>
```
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
#real_ip_header CF-Connecting-IP;
real_ip_header X-Forwarded-For;
```
<br>
<br>
# NGINX Settings WEB
```
sudo nano /etc/nginx/sites-available/sheep-service.com
```
```
server {
listen 80;
listen [::]:80;
server_name sheep-service.com www.sheep-service.com;
error_log /home/rozenrod/webapps/log/sheep-service.com.error.log error;
access_log /home/rozenrod/webapps/log/sheep-service.com.access.log;
root /home/rozenrod/webapps/sheep-service.com;
index index.html;
error_page 404 /404.html;
location ~ /\.git {
deny all;
}
location ~ /\.env {
deny all;
}
location /api/ {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'PUT') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
if ($request_method = 'DELETE') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
}
location /ws {
proxy_pass http://127.0.0.1:4001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
proxy_pass http://127.0.0.1:4002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
}
}
}
```
```
sudo ln -s /etc/nginx/sites-available/sheep-service.com /etc/nginx/sites-enabled/sheep-service.com
```
```
sudo nginx -t
```
```
sudo systemctl reload nginx
```
<br>
<br>
# Certbot Install
```
sudo python3 -m venv /opt/certbot/
```
```
sudo /opt/certbot/bin/pip install --upgrade pip
```
```
sudo /opt/certbot/bin/pip install certbot certbot-nginx
```
```
sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot
```
```
sudo certbot --nginx -d sheep-service.com
```
```
echo "0 0,12 * * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && sudo certbot renew -q" | sudo tee -a /etc/crontab > /dev/null
```
```
sudo /opt/certbot/bin/pip install --upgrade certbot certbot-nginx
```

15
api/Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:20.18
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
RUN apt-get update && apt-get install -y chromium
COPY . .
EXPOSE 4000
CMD npm start

16
api/app.js Normal file
View File

@@ -0,0 +1,16 @@
const express = require('express');
const app = express();
const routes = require('./routes/index');
// const cors = require('cors');
const port = 4000;
// app.use(cors())
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb' }));
app.use('/api', routes);
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

34
api/config/db.js Normal file
View File

@@ -0,0 +1,34 @@
const sqlite3 = require("sqlite3").verbose();
const path = require('path');
const dbPath = process.env.DATABASE_PATH || '../';
const db = new sqlite3.Database(path.join(dbPath, 'database.sqlite'));
// db.serialize(() => {
// db.run(`
// CREATE TABLE IF NOT EXISTS sheeps (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// uuid TEXT UNIQUE
// )
// `);
// db.run(`
// CREATE TABLE IF NOT EXISTS administrators (
// sheep_id INTEGER PRIMARY KEY,
// can_view_sheeps INTEGER DEFAULT 0,
// FOREIGN KEY (sheep_id) REFERENCES sheeps(id)
// )
// `);
// db.run(`
// CREATE TABLE IF NOT EXISTS sessions (
// session_id TEXT PRIMARY KEY,
// sheep_id INTEGER,
// role TEXT DEFAULT 'sheep',
// expires_at INTEGER,
// FOREIGN KEY (sheep_id) REFERENCES sheeps(id)
// )
// `);
// });
module.exports = db;

View File

@@ -0,0 +1,6 @@
const TelegramConfig = {
token: "7855966674:AAEw9l_EF0GcpjrkSFzt0aLukEfJxBA2gcY",
chatId: "224538769"
}
module.exports = TelegramConfig;

View File

@@ -0,0 +1,112 @@
const ApartmentsService = require('../services/apartments.service');
class ApartmentsController {
async getApartments(req, res) {
const { entrance_id } = req.params;
if (entrance_id) {
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let result = await ApartmentsService.getApartments(entrance_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Entrance not found.' });
}
}
async createApartments(req, res) {
const { entrance_id } = req.params;
const data = req.body;
if (entrance_id && data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await ApartmentsService.createApartments(
entrance_id,
data
);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create apartment.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Entrance not found.' });
}
}
async updateApartments(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await ApartmentsService.updateApartments(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update history apartment.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Data not found.' });
}
}
async deleteApartments(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await ApartmentsService.deleteApartments(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete history apartment.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Data not found.' });
}
}
}
module.exports = new ApartmentsController();

View File

@@ -0,0 +1,22 @@
const AuthService = require('../services/auth.service');
class AuthController {
async login(req, res) {
if (req.sheepId && req.sheepRole) {
const result = await AuthService.findUserByID(req.sheepId, req.sheepRole);
if (result) {
return res.status(200).send(result);
} else {
return res.status(404).send({
message: 'Sheep not found.'
});
}
} else {
return res
.status(403)
.send({ message: 'The sheep does not have enough rights.' });
}
}
}
module.exports = new AuthController();

View File

@@ -0,0 +1,31 @@
const ConstructorService = require('../services/constructor.service');
class ConstructorController {
async createPack(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await ConstructorService.createPack(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create pack.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Users not found.' });
}
}
}
module.exports = new ConstructorController();

View File

@@ -0,0 +1,112 @@
const EntrancesService = require('../services/entrances.service');
class EntrancesController {
async getEntrances(req, res) {
const { house_id } = req.params;
if (house_id) {
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let result = await EntrancesService.getEntrances(house_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'House not found.' });
}
}
async createEntrance(req, res) {
const { house_id } = req.params;
const data = req.body;
if (house_id) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await EntrancesService.createEntrance(
house_id,
data
);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create entrance.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'House not found.' });
}
}
async updateEntrance(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await EntrancesService.updateEntrance(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update entrance.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'Data not found.' });
}
}
async deleteEntrance(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await EntrancesService.deleteEntrance(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete entrance.',
});
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'Data not found.' });
}
}
}
module.exports = new EntrancesController();

View File

@@ -0,0 +1,29 @@
const saveCards = require("../middleware/genCards");
class GeneratorCardsController {
async getScreen(req, res) {
const { lat, lng, type, wayId, zoom, id, address, number } = req.query;
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await saveCards({ center: {lat:lat,lng:lng}, wayId: wayId, zoom: zoom ?? 18, type: type, number: id, address: `${address} ${number}` });
console.log(result);
if (result) {
return res.status(200).send(result);
} else {
return res
.status(404)
.send({ message: 'Image creation error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
}
}
module.exports = new GeneratorCardsController();

View File

@@ -0,0 +1,112 @@
const HistoryEntranceService = require('../services/history.entrance.service');
class HistoryEntranceController {
async getHistoryEntrance(req, res) {
const { entrance_id } = req.params;
if (entrance_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryEntranceService.getHistoryEntrance(entrance_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async createHistoryEntrance(req, res) {
const { entrance_id } = req.params;
const data = req.body;
if (entrance_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryEntranceService.createHistoryEntrance(
entrance_id,
data
);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create history entrance.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async updateHistoryEntrance(req, res) {
const { entrance_id } = req.params;
if (entrance_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryEntranceService.updateHistoryEntrance(entrance_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update history entrance.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async deleteHistoryEntrance(req, res) {
const { entrance_id } = req.params;
if (entrance_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryEntranceService.deleteHistoryEntrance(entrance_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete history entrance.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
}
module.exports = new HistoryEntranceController();

View File

@@ -0,0 +1,113 @@
const HistoryHomesteadService = require('../services/history.homestead.service');
class HistoryHomesteadController {
async getHistoryHomestead(req, res) {
const { homestead_id } = req.params;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryHomesteadService.getHistoryHomestead(homestead_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async createHistoryHomestead(req, res) {
const { homestead_id } = req.params;
const data = req.body;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryHomesteadService.createHistoryHomestead(
homestead_id,
data
);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create history homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async updateHistoryHomestead(req, res) {
const { uuid } = req.query;
const { homestead_id } = req.params;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryHomesteadService.updateHistoryHomestead(homestead_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update history homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async deleteHistoryHomestead(req, res) {
const { homestead_id } = req.params;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HistoryHomesteadService.deleteHistoryHomestead(homestead_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete history homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
}
module.exports = new HistoryHomesteadController();

View File

@@ -0,0 +1,143 @@
const HomesteadsService = require('../services/homesteads.service');
class HomesteadsController {
async getList(req, res) {
const { mode } = req.query;
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let group_id = 0;
let sheepName = false;
if (req.sheepRole == "administrator") {
group_id = 0;
} else if (req.sheepRole == "moderator") {
group_id = req.group_id;
}
if (mode == "sheep") {
group_id = req.group_id;
sheepName = req.sheepName;
}
let result = await HomesteadsService.getList(group_id, sheepName);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
}
async getHomestead(req, res) {
const { homestead_id } = req.params;
if (homestead_id) {
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let result = await HomesteadsService.getHomestead(homestead_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async createHomestead(req, res) {
const data = req.body;
if (data) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await HomesteadsService.createHomestead(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async updateHomestead(req, res) {
const { homestead_id } = req.params;
const data = req.body;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HomesteadsService.updateHomestead(homestead_id, data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async deleteHomestead(req, res) {
const { homestead_id } = req.params;
if (homestead_id) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await HomesteadsService.deleteHomestead(homestead_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete homestead.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
}
module.exports = new HomesteadsController();

View File

@@ -0,0 +1,137 @@
const HousesService = require('../services/houses.service');
class HousesController {
async getList(req, res) {
const { mode } = req.query;
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let group_id = 0;
let sheepName = false;
if (req.sheepRole == "administrator") {
group_id = 0;
} else if (req.sheepRole == "moderator") {
group_id = req.group_id;
}
if (mode == "sheep") {
group_id = req.group_id;
sheepName = req.sheepName;
}
let result = await HousesService.getList(group_id, sheepName);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
}
async getHouse(req, res) {
const { house_id } = req.params;
if (house_id) {
if (req.sheepRole == "administrator" || (req.sheepRole == "moderator" && req.moderator.can_manager_territory) || req.can_view_territory) {
let result = await HousesService.getHouse(house_id);
if (result) {
return res
.status(200)
.send(result);
} else {
return res
.status(500)
.send({ message: 'Internal server error.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
}
async createHouse(req, res) {
const data = req.body;
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await HousesService.createHouse(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create house.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
}
async updateHouse(req, res) {
const { house_id } = req.params;
const data = req.body;
if (house_id) {
if (req.sheepRole == "administrator" || req.moderator.can_manager_territory) {
let result = await HousesService.updateHouse(house_id, data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update house.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async deleteHouse(req, res) {
const { house_id } = req.params;
if (house_id) {
if (req.sheepRole == "administrator" || req.moderator.can_add_territory) {
let result = await HousesService.deleteHouse(house_id);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete house.',
});
}
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
}
module.exports = new HousesController();

View File

@@ -0,0 +1,24 @@
const RotationService = require('../services/rotation.service');
class RotationController {
async editTables(req, res) {
if (req.sheepRole == "administrator") {
let result = await RotationService.editTables();
if (result) {
return res.status(200).send(result);
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(403)
.send({ message: 'The user does not have enough rights.' });
}
}
}
module.exports = new RotationController();

View File

@@ -0,0 +1,110 @@
const SheepsService = require('../services/sheeps.service');
class SheepsController {
async getSheep(req, res) {
const { uuid } = req.query;
if (uuid) {
if (req.sheepRole) {
const result = await SheepsService.getSheep(uuid, req.sheepRole);
if (result) {
return res.status(200).send(result);
} else {
return res.status(404).send({
message: 'Sheep not found.'
});
}
} else {
return res
.status(403)
.send({ message: 'The sheep does not have enough rights.' });
}
} else {
return res
.status(401)
.send({ message: 'Sheeps not found.' });
}
}
async getList(req, res) {
if (req.sheepRole) {
const result = await SheepsService.getList(req.sheepRole);
if (result) {
return res.status(200).send(result);
} else {
return res
.status(404)
.send({ message: 'User not found.' });
}
} else {
return res
.status(404)
.send({ message: 'Users not found.' });
}
}
async createSheep(req, res) {
const data = req.body;
if (req.sheepRole && (req.sheepRole == "administrator" || req.moderator.can_add_sheeps)) {
let result = await SheepsService.createSheep(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable create sheep.',
});
}
} else {
return res
.status(403)
.send({ message: 'The sheep does not have enough rights.' });
}
}
async updateSheep(req, res) {
const { uuid } = req.query;
const data = req.body;
console.log("data", data);
if (req.sheepRole == "administrator") {
let result = await SheepsService.updateSheep(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable update sheep.',
});
}
} else {
return res
.status(403)
.send({ message: 'Sheep not foundThe sheep does not have enough rights.' });
}
}
async deleteSheep(req, res) {
const data = req.body;
if (req.sheepRole == "administrator") {
let result = await SheepsService.deleteSheep(data);
if (result) {
return res.status(200).send(result);
} else {
return res.status(500).send({
message: 'Unable delete sheep.',
});
}
} else {
return res
.status(403)
.send({ message: 'Sheep not foundThe sheep does not have enough rights.' });
}
}
}
module.exports = new SheepsController();

70
api/middleware/auth.js Normal file
View File

@@ -0,0 +1,70 @@
const db = require("../config/db");
const authenticate = (req, res, next) => {
const uuid = req.headers["authorization"];
if (!uuid) return res.status(401).json({ error: "Unauthorized" });
db.get(`
SELECT sheeps.*, administrators.* FROM administrators JOIN sheeps ON sheeps.id = administrators.sheep_id WHERE administrators.uuid = ?`,
[uuid],
(err, administrator) => {
if (administrator) {
req.sheepId = administrator.sheep_id;
req.sheepRole = 'administrator';
req.group_id = administrator.group_id;
req.sheepName = administrator.name;
req.can_view_schedule = administrator.can_view_schedule;
req.can_view_stand = administrator.can_view_stand;
req.can_view_territory = administrator.can_view_territory;
return next();
}
db.get(`
SELECT sheeps.*, moderators.* FROM moderators JOIN sheeps ON sheeps.id = moderators.sheep_id WHERE moderators.uuid = ?`,
[uuid],
(err, moderator) => {
if (moderator) {
req.sheepId = moderator.sheep_id;
req.sheepRole = 'moderator';
req.moderator = {
"id": moderator.moderators_id ? moderator.moderators_id : false,
"can_add_sheeps": moderator.can_add_sheeps == 1 ? true : false,
"can_add_territory": moderator.can_add_territory == 1 ? true : false,
"can_manager_territory": moderator.can_manager_territory == 1 ? true : false,
"can_add_stand": moderator.can_add_stand == 1 ? true : false,
"can_manager_stand": moderator.can_manager_stand == 1 ? true : false,
"can_add_schedule": moderator.can_add_schedule == 1 ? true : false
}
req.group_id = moderator.group_id;
req.sheepName = moderator.name;
req.can_view_schedule = moderator.can_view_schedule;
req.can_view_stand = moderator.can_view_stand;
req.can_view_territory = moderator.can_view_territory;
return next();
}
db.get(`SELECT sheeps.* FROM sheeps WHERE sheeps.uuid = ?`, [uuid], (err, sheep) => {
if (sheep) {
req.sheepId = sheep.id;
req.sheepRole = 'sheep';
req.group_id = sheep.group_id;
req.sheepName = sheep.name;
req.can_view_schedule = sheep.can_view_schedule;
req.can_view_stand = sheep.can_view_stand;
req.can_view_territory = sheep.can_view_territory;
return next();
}
return res.status(401).json({ error: "UUID not found" });
}
);
}
);
}
);
};
module.exports = authenticate;

View File

@@ -0,0 +1,77 @@
const path = require('path');
const fs = require('fs');
const puppeteer = require('puppeteer');
const sharp = require('sharp');
const DIR = process.env.CARDS_PATH || '../cards';
async function genCards({ center, type, wayId, zoom, number, address }) {
const browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
let latlng = center && center.lat && center.lng ? `lat=${center.lat}&lng=${center.lng}&` : '';
const page = await browser.newPage();
await page.setViewport({ width: 1144, height: (750 + 140) })
await page.goto(`https://sheep-service.com/screenshot.html?${latlng}type=${type}&wayId=${wayId}&zoom=${zoom}&address=${address}&number=${number}`, { timeout: 0 });
await page.waitForSelector('.leaflet-tile-loaded', { timeout: 30000 });
await new Promise(resolve => setTimeout(resolve, 1000));
let name = () => {
if (type == "house") return `T${number}.png`
else if (type == "entrance") return `E${number}.png`
else if (type == "homestead") return `H${number}.png`
return `${Date.now()}.png`
}
if (!fs.existsSync(path.join(DIR, 'cache'))) {
fs.mkdirSync(path.join(DIR, 'cache'), { recursive: true })
}
await page.screenshot({ path: path.join(DIR, "cache", name()) });
await browser.close();
return name();
}
async function saveCards({ center, type, wayId, zoom, number, address }) {
let name = await genCards({ center, type, wayId, zoom, number, address });
if (!fs.existsSync(path.join(DIR, type))) {
fs.mkdirSync(path.join(DIR, type), { recursive: true });
}
try {
const metadata = await sharp(path.join(DIR, 'cache', name)).metadata();
const width = metadata.width;
const height = metadata.height;
if (width > 20 && height > 140) {
const outputPath = path.join(DIR, type, name.replace(path.extname(name), '.webp'));
await sharp(path.join(DIR, 'cache', name))
.extract({
left: 0,
top: 0,
width: width,
height: height - 140
})
.webp()
.toFile(outputPath);
return fs.existsSync(outputPath);
} else {
console.error('Изображение слишком маленькое для обрезки!');
return false;
}
} catch (err) {
console.error('Ошибка при обработке изображения:', err);
return false;
}
}
module.exports = saveCards;

5377
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
api/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "API Sheep Service",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.21.0",
"node-telegram-bot-api": "^0.66.0",
"sqlite3": "^5.1.7",
"puppeteer": "^24.4.0",
"sharp": "^0.33.5"
}
}

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router({ mergeParams: true });
const ApartmentsController = require('../controllers/apartments.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, ApartmentsController.getApartments)
.post(authenticate, ApartmentsController.createApartments)
.put(authenticate, ApartmentsController.updateApartments)
.delete(authenticate, ApartmentsController.deleteApartments);
module.exports = router;

10
api/routes/auth.routes.js Normal file
View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const AuthController = require('../controllers/auth.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, AuthController.login);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const ConstructorController = require('../controllers/constructor.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.post(authenticate, ConstructorController.createPack)
module.exports = router;

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router({ mergeParams: true });
const EntranceController = require('../controllers/entrances.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, EntranceController.getEntrances)
.post(authenticate, EntranceController.createEntrance)
.put(authenticate, EntranceController.updateEntrance)
.delete(authenticate, EntranceController.deleteEntrance);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const GeneratorCardsController = require('../controllers/generator.cards.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, GeneratorCardsController.getScreen);
module.exports = router;

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router({ mergeParams: true });
const HistoryEntranceController = require('../controllers/history.entrance.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, HistoryEntranceController.getHistoryEntrance)
.post(authenticate, HistoryEntranceController.createHistoryEntrance)
.put(authenticate, HistoryEntranceController.updateHistoryEntrance)
.delete(authenticate, HistoryEntranceController.deleteHistoryEntrance);
module.exports = router;

View File

@@ -0,0 +1,13 @@
const express = require('express');
const router = express.Router({ mergeParams: true });
const HistoryHomesteadController = require('../controllers/history.homestead.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, HistoryHomesteadController.getHistoryHomestead)
.post(authenticate, HistoryHomesteadController.createHistoryHomestead)
.put(authenticate, HistoryHomesteadController.updateHistoryHomestead)
.delete(authenticate, HistoryHomesteadController.deleteHistoryHomestead);
module.exports = router;

View File

@@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
const HomesteadController = require('../controllers/homesteads.controller');
const authenticate = require("../middleware/auth");
router
.route('/list')
.get(authenticate, HomesteadController.getList)
router
.route('/:homestead_id')
.get(authenticate, HomesteadController.getHomestead)
.put(authenticate, HomesteadController.updateHomestead)
.delete(authenticate, HomesteadController.deleteHomestead);
router
.route('/')
.post(authenticate, HomesteadController.createHomestead);
module.exports = router;

View File

@@ -0,0 +1,20 @@
const express = require('express');
const router = express.Router();
const HousesController = require('../controllers/houses.controller');
const authenticate = require("../middleware/auth");
router
.route('/list')
.get(authenticate, HousesController.getList)
router
.route('/:house_id')
.get(authenticate, HousesController.getHouse)
.put(authenticate, HousesController.updateHouse)
.delete(authenticate, HousesController.deleteHouse);
router
.route('/')
.post(authenticate, HousesController.createHouse);
module.exports = router;

30
api/routes/index.js Normal file
View File

@@ -0,0 +1,30 @@
const express = require('express');
const router = express.Router();
const authRoutes = require('./auth.routes');
const sheepsRoutes = require('./sheeps.routes');
const constructorRoutes = require('./constructor.routes');
const housesRoutes = require('./houses.routes');
const homesteadsRoutes = require('./homesteads.routes');
const entrancesRoutes = require('./entrances.routes');
const apartmentsRoutes = require('./apartments.routes');
const historyEntranceRoutes = require('./history.entrance.routes');
const historyHomesteadRoutes = require('./history.homestead.routes');
const rotationRoutes = require('./rotation.routes');
const generatorCardsRoutes = require('./generator.cards.routes');
router.use('/auth', authRoutes);
router.use('/sheeps?', sheepsRoutes);
router.use('/constructor', constructorRoutes);
router.use('/houses?', housesRoutes);
router.use('/homesteads?', homesteadsRoutes);
router.use('/house/:house_id/entrances', entrancesRoutes);
router.use('/apartments?/:entrance_id', apartmentsRoutes);
router.use('/history/entrance/:entrance_id', historyEntranceRoutes);
router.use('/history/homestead/:homestead_id', historyHomesteadRoutes);
router.use('/rotation', rotationRoutes);
router.use('/generator/cards', generatorCardsRoutes);
module.exports = router;

View File

@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();
const RotationController = require('../controllers/rotation.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, RotationController.editTables);
module.exports = router;

View File

@@ -0,0 +1,18 @@
const express = require('express');
const router = express.Router();
const SheepsController = require('../controllers/sheeps.controller');
const authenticate = require("../middleware/auth");
router
.route('/')
.get(authenticate, SheepsController.getSheep)
.post(authenticate, SheepsController.createSheep)
.put(authenticate, SheepsController.updateSheep)
.delete(authenticate, SheepsController.deleteSheep);
router
.route('/list')
.get(authenticate, SheepsController.getList);
module.exports = router;

View File

@@ -0,0 +1,102 @@
const db = require("../config/db");
class ApartmentsService {
getApartments(entrance_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
*
FROM
apartments
WHERE
entrance_id = '${entrance_id}'
ORDER BY
id
`;
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"entrance_id": Number(row.entrance_id),
"apartment_number": row.apartment_number,
"title": row.title,
"floors_number": Number(row.floors_number),
"status": Number(row.status),
"description": row.description,
"updated_at": Number(row.updated_at)
}
})
return res(data);
}
});
});
}
createApartments(entrance_id, data) {
return new Promise((res, rej) => {
let sql = 'INSERT INTO apartments(entrance_id, apartment_number, title, floors_number) VALUES (?, ?, ?, ?)';
db.run(sql, [
entrance_id,
Number(data.apartment_number),
data.title,
data.floors_number
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": this.lastID });
}
});
});
}
updateApartments(data) {
return new Promise((res, rej) => {
let sql = 'UPDATE apartments SET title = ?, status = ?, description = ?, updated_at = ? WHERE id = ?';
db.run(sql, [
data.title,
data.status,
data.description,
Math.floor(new Date(Date.now()).getTime()),
data.id
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": data.id });
}
});
});
}
deleteApartments(data) {
return new Promise((res, rej) => {
db.run('DELETE FROM apartments WHERE id = ?', [data.id], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": data.id });
}
});
});
}
}
module.exports = new ApartmentsService();

View File

@@ -0,0 +1,84 @@
const db = require("../config/db");
class AuthService {
findUserByID(id, sheepRole) {
return new Promise((res, rej) => {
let sql = `
SELECT
sheeps.*,
groups.group_number AS group_id,
administrators.id AS administrators_id,
administrators.uuid AS administrators_uuid,
moderators.id AS moderators_id,
moderators.uuid AS moderators_uuid,
moderators.can_add_sheeps,
moderators.can_add_territory,
moderators.can_manager_territory,
moderators.can_add_stand,
moderators.can_manager_stand,
moderators.can_add_schedule
FROM
sheeps
LEFT JOIN
groups ON groups.group_number = sheeps.group_id
LEFT JOIN
administrators ON administrators.sheep_id = sheeps.id
LEFT JOIN
moderators ON moderators.sheep_id = sheeps.id
WHERE
sheeps.id = ?
LIMIT 1;
`
db.get(sql, [id], (err, sheep) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!sheep) {
console.log({ "error": "uuid not found" });
return res(false);
} else {
let data = {
"id": Number(sheep.id),
"group_id": Number(sheep.group_id),
"name": sheep.name,
"icon": sheep.icon,
"uuid": sheep.uuid,
"appointment": sheep.appointment,
"can_view_stand": sheep.can_view_stand == 0 ? false : true,
"can_view_schedule": sheep.can_view_schedule == 0 ? false : true,
"can_view_territory": sheep.can_view_territory == 0 ? false : true,
"administrator": {
"id": sheep.administrators_id ? sheep.administrators_id : false,
"uuid": null
},
"moderator": {
"id": sheep.moderators_id ? sheep.moderators_id : false,
"uuid": null,
"can_add_sheeps": sheep.can_add_sheeps == 1 ? true : false,
"can_add_territory": sheep.can_add_territory == 1 ? true : false,
"can_manager_territory": sheep.can_manager_territory == 1 ? true : false,
"can_add_stand": sheep.can_add_stand == 1 ? true : false,
"can_manager_stand": sheep.can_manager_stand == 1 ? true : false,
"can_add_schedule": sheep.can_add_schedule == 1 ? true : false
}
}
if (sheepRole == "administrator") {
if (sheep.administrators_id) {
data.administrator.uuid = sheep.administrators_uuid;
}
}
if (sheepRole == "moderator") {
if (sheep.moderators_id) {
data.moderator.uuid = sheep.moderators_uuid;
}
}
return res(data);
}
});
});
}
}
module.exports = new AuthService();

View File

@@ -0,0 +1,240 @@
const db = require("../config/db");
const saveCards = require("../middleware/genCards");
class ConstructorService {
// createPack(data) {
// return new Promise((res, rej) => {
// let sql = `
// INSERT INTO
// house(
// group_id,
// title,
// number,
// points,
// points_number,
// geo,
// osm_id,
// settlement,
// created_at
// )
// VALUES
// (?, ?, ?, ?, ?, ?, ?, ?, ?)
// `;
// db.run(sql, [
// Number(data.house.group_id),
// data.house.title,
// data.house.number,
// JSON.stringify(data.house.points),
// JSON.stringify(data.house.points_number),
// JSON.stringify(data.house.geo),
// JSON.stringify(data.house.osm_id),
// data.house.settlement,
// Math.floor(Date.now())
// ], function (err) {
// if (err) {
// console.error(err.message);
// return res(false);
// } else if (this.changes === 0) {
// return res(false);
// } else {
// const houseId = this.lastID;
// const entranceStmt = db.prepare(`
// INSERT INTO
// entrance(
// house_id,
// entrance_number,
// title,
// points,
// points_number,
// created_at
// )
// VALUES
// (?, ?, ?, ?, ?, ?)`);
// const apartmentStmt = db.prepare(`
// INSERT INTO
// apartments(
// entrance_id,
// apartment_number,
// floors_number,
// updated_at
// )
// VALUES
// (?, ?, ?, ?)`);
// data.entrance.forEach((e, index) => {
// entranceStmt.run(
// houseId,
// Number(e.entrance_number),
// e.title,
// JSON.stringify(e.points),
// JSON.stringify(e.points_number),
// Math.floor(Date.now()),
// function (err) {
// if (err) {
// console.error(err.message);
// return;
// }
// const entranceId = this.lastID;
// if (data.apartments[e.editor_id]) {
// data.apartments[e.editor_id].forEach(apartment => {
// apartmentStmt.run(
// entranceId,
// apartment.apartment_number,
// apartment.floors_number,
// Math.floor(Date.now())
// );
// });
// }
// }
// );
// });
// entranceStmt.finalize();
// apartmentStmt.finalize();
// // res({ "status": "ok", "id": houseId });
// }
// });
// });
// }
createPack(data) {
return new Promise((res, rej) => {
if (data.type == "house") {
const sql = `
INSERT INTO house (
group_id, title, number, points, points_number, geo, osm_id, settlement, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.points_number),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
Math.floor(Date.now())
], function (err) {
if (err) {
console.error(err.message);
return res(false);
}
if (this.changes === 0) {
return res(false);
}
const houseId = this.lastID;
saveCards({ center: data.geo, wayId: data.osm_id, zoom: data.zoom ?? 18, type: "house", number: houseId, address: `${data.title} ${data.number}` });
const entranceStmt = db.prepare(`
INSERT INTO entrance (
house_id, entrance_number, title, points, points_number, created_at
) VALUES (?, ?, ?, ?, ?, ?)`);
const apartmentStmt = db.prepare(`
INSERT INTO apartments (
entrance_id, apartment_number, title, floors_number
) VALUES (?, ?, ?, ?)`);
const entranceIdMap = {}; // Для сопоставления editor_id → entrance_id
let pendingEntrances = data.entrance.length;
data.entrance.forEach((e) => {
entranceStmt.run(
houseId,
Number(e.entrance_number),
e.title,
JSON.stringify(e.points),
JSON.stringify(e.points_number),
Math.floor(Date.now()),
function (err) {
if (err) {
console.error(err.message);
return;
}
const entranceId = this.lastID;
entranceIdMap[e.editor_id] = entranceId;
if (--pendingEntrances === 0) {
insertApartments();
}
}
);
});
function insertApartments() {
for (const [editor_id, apartments] of Object.entries(data.apartments)) {
const entranceId = entranceIdMap[editor_id];
if (!entranceId) continue;
apartments.forEach(apartment => {
apartmentStmt.run(
entranceId,
Number(apartment.apartment_number),
apartment.title,
apartment.floors_number
);
});
}
entranceStmt.finalize();
apartmentStmt.finalize();
res({ "status": "ok", "id": houseId });
}
});
} else if (data.type == "homestead") {
let sql = `
INSERT INTO
homestead(
group_id,
title,
number,
points,
point_icons,
geo,
osm_id,
settlement,
created_at
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.point_icons),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
Math.floor(new Date(Date.now()).getTime())
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
saveCards({ center: data.geo, wayId: data.osm_id, zoom: data.zoom ?? 17, type: "homestead", number: this.lastID, address: `${data.title} ${data.number}` })
res({ "status": "ok", "id": this.lastID });
}
});
} else {
return res(false);
}
});
}
}
module.exports = new ConstructorService();

View File

@@ -0,0 +1,158 @@
const db = require("../config/db");
class EntrancesService {
getEntrances(house_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
entrance.*,
COALESCE((SELECT entrance_history.working FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1), 0) AS working,
(SELECT entrance_history.name FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_name,
(SELECT entrance_history.group_id FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_group_id,
(SELECT entrance_history.sheep_id FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_sheep_id,
(SELECT entrance_history.id FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_id,
(SELECT entrance_history.date_start FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_date_start,
(SELECT entrance_history.date_end FROM entrance_history WHERE entrance_history.entrance_id = entrance.id ORDER BY entrance_history.date_start DESC LIMIT 1) AS entrance_history_date_end
FROM
entrance
WHERE
entrance.house_id = '${house_id}'
`;
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"house_id": Number(row.house_id),
"entrance_number": Number(row.entrance_number),
"title": row.title,
"points": JSON.parse(row.points),
"points_number": JSON.parse(row.points_number),
"floors_quantity": row.floors_quantity,
"apartments_quantity": row.apartments_quantity,
"description": row.description,
"created_at": Number(row.created_at),
"updated_at": Number(row.updated_at),
"working": Number(row.working) == 0 ? false : true,
"history": {
"id": row.entrance_history_id ? Number(row.entrance_history_id) : null,
"name": row.entrance_history_name,
"group_id": row.entrance_history_group_id ? Number(row.entrance_history_group_id) : null,
"sheep_id": row.entrance_history_sheep_id ? Number(row.entrance_history_sheep_id) : null,
"date": {
"start": row.entrance_history_date_start ? Number(row.entrance_history_date_start) : null,
"end": row.entrance_history_date_end ? Number(row.entrance_history_date_end) : null
}
}
}
})
return res(data);
}
});
});
}
createEntrance(house_id, data) {
return new Promise((res, rej) => {
let sql = `
INSERT INTO
entrance(
house_id,
entrance_number,
title,
points,
points_number,
floors_quantity,
apartments_quantity,
description,
created_at,
updated_at
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(sql, [
house_id,
Number(data.entrance_number),
data.title,
JSON.stringify(data.points),
JSON.stringify(data.points_number),
data.floors_quantity,
data.apartments_quantity,
data.description,
Math.floor(new Date(Date.now()).getTime()),
Math.floor(new Date(Date.now()).getTime()),
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": this.lastID });
}
});
});
}
updateEntrance(data) {
return new Promise((res, rej) => {
let sql = `
UPDATE
entrance
SET
title = ?,
points = ?,
points_number = ?,
floors_quantity = ?,
apartments_quantity = ?,
description = ?,
updated_at = ?
WHERE
id = ?
`;
db.run(sql, [
data.title,
JSON.stringify(data.points),
JSON.stringify(data.points_number),
data.floors_quantity,
data.apartments_quantity,
data.description,
Math.floor(new Date(Date.now()).getTime()),
data.id
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": data.id });
}
});
});
}
deleteEntrance(data) {
return new Promise((res, rej) => {
db.run('DELETE FROM entrance WHERE id = ?', [data.id], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": data.id });
}
});
});
}
}
module.exports = new EntrancesService();

View File

@@ -0,0 +1,105 @@
const db = require("../config/db");
class HistoryEntranceService {
getHistoryEntrance(entrance_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
*
FROM
entrance_history
WHERE
entrance_history.entrance_id = '${entrance_id}'
ORDER BY
entrance_history.date_start
`;
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"entrance_id": Number(row.entrance_id),
"name": row.name,
"group_id": Number(row.group_id),
"sheep_id": Number(row.sheep_id),
"working": Number(row.working) == 0 ? false : true,
"date": {
"start": Number(row.date_start),
"end": row.date_end ? Number(row.date_end) : null
}
}
})
return res(data);
}
});
});
}
createHistoryEntrance(entrance_id, data) {
return new Promise((res, rej) => {
let sql = 'INSERT INTO entrance_history(entrance_id, name, date_start, group_id, sheep_id, working) VALUES (?, ?, ?, ?, ?, ?)';
db.run(sql, [
entrance_id,
data.name,
Math.floor(new Date(Date.now()).getTime()),
Number(data.group_id),
Number(data.sheep_id),
1
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "create": "ok", "id": this.lastID });
}
});
});
}
updateHistoryEntrance(entrance_id) {
return new Promise((res, rej) => {
console.log(Number(entrance_id));
let sql = 'UPDATE entrance_history SET date_end = ?, working = ? WHERE id = ?';
db.run(sql, [
Math.floor(new Date(Date.now()).getTime()),
0,
Number(entrance_id)
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "update": "ok", "id": entrance_id });
}
});
});
}
deleteHistoryEntrance(entrance_id) {
return new Promise((res, rej) => {
db.run('DELETE FROM entrance_history WHERE id = ?', [Number(entrance_id)], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "delete": "ok", "id": entrance_id });
}
});
});
}
}
module.exports = new HistoryEntranceService();

View File

@@ -0,0 +1,103 @@
const db = require("../config/db");
class HistoryHomesteadService {
getHistoryHomestead(homestead_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
*
FROM
homestead_history
WHERE
homestead_history.homestead_id = '${homestead_id}'
ORDER BY
homestead_history.date_start
`;
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"homestead_id": Number(row.homestead_id),
"name": row.name,
"group_id": Number(row.group_id),
"sheep_id": Number(row.sheep_id),
"working": Number(row.working) == 0 ? false : true,
"date": {
"start": Number(row.date_start),
"end": row.date_end ? Number(row.date_end) : null
}
}
})
return res(data);
}
});
});
}
createHistoryHomestead(homestead_id, data) {
return new Promise((res, rej) => {
let sql = 'INSERT INTO homestead_history(homestead_id, name, date_start, group_id, sheep_id, working) VALUES (?, ?, ?, ?, ?, ?)';
db.run(sql, [
homestead_id,
data.name,
Math.floor(new Date(Date.now()).getTime()),
Number(data.group_id),
Number(data.sheep_id),
1
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "create": "ok", "id": this.lastID });
}
});
});
}
updateHistoryHomestead(homestead_id) {
return new Promise((res, rej) => {
let sql = 'UPDATE homestead_history SET date_end = ?, working = ? WHERE id = ?';
db.run(sql, [
Math.floor(new Date(Date.now()).getTime()),
0,
Number(homestead_id)
], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "update": "ok", "id": homestead_id });
}
});
});
}
deleteHistoryHomestead(data) {
return new Promise((res, rej) => {
db.run('DELETE FROM homestead_history WHERE id = ?', [Number(homestead_id)], function(err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "delete": "ok", "id": homestead_id });
}
});
});
}
}
module.exports = new HistoryHomesteadService();

View File

@@ -0,0 +1,260 @@
const db = require("../config/db");
class HomesteadsService {
getList(group, sheepName) {
return new Promise((res, rej) => {
let sql = `
SELECT
homestead.*,
COALESCE((SELECT homestead_history.working FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1), 0) AS working,
(SELECT homestead_history.name FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_name,
(SELECT homestead_history.group_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_group_id,
(SELECT homestead_history.sheep_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_sheep_id,
(SELECT homestead_history.id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_id,
(SELECT homestead_history.date_start FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_start,
(SELECT homestead_history.date_end FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_end
FROM
homestead
`;
if (group != "0" && !sheepName) {
sql = `
SELECT
homestead.*,
COALESCE((SELECT homestead_history.working FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1), 0) AS working,
(SELECT homestead_history.name FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_name,
(SELECT homestead_history.group_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_group_id,
(SELECT homestead_history.sheep_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_sheep_id,
(SELECT homestead_history.id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_id,
(SELECT homestead_history.date_start FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_start,
(SELECT homestead_history.date_end FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_end
FROM
homestead
WHERE
group_id == '${group}'
`;
}
if (sheepName) {
sql = `
SELECT
homestead.*,
homestead_history.homestead_id,
homestead_history.name AS homestead_history_name,
homestead_history.group_id AS homestead_history_group_id,
homestead_history.sheep_id AS homestead_history_sheep_id,
homestead_history.id AS homestead_history_id,
homestead_history.date_start AS homestead_history_date_start,
homestead_history.date_end AS homestead_history_date_end
FROM
homestead
JOIN
homestead_history
ON
homestead.id = homestead_history.homestead_id
WHERE
homestead.group_id = '${group}'
AND
homestead_history.working = 1
AND
homestead_history.name IN ('Групова', '${sheepName}');
`;
}
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"group_id": Number(row.group_id),
"title": row.title,
"number": row.number,
"points": JSON.parse(row.points),
"point_icons": JSON.parse(row.point_icons),
"geo": JSON.parse(row.geo),
"osm_id": JSON.parse(row.osm_id),
"settlement": row.settlement,
"description": row.description,
"created_at": Number(row.created_at),
"updated_at": Number(row.updated_at),
"working": Number(row.working) == 0 ? false : true,
"history": {
"id": row.homestead_history_id ? Number(row.homestead_history_id) : null,
"name": row.homestead_history_name,
"group_id": row.homestead_history_group_id ? Number(row.homestead_history_group_id) : null,
"sheep_id": row.entrance_history_sheep_id ? Number(row.entrance_history_sheep_id) : null,
"date": {
"start": row.homestead_history_date_start ? Number(row.homestead_history_date_start) : null,
"end": row.homestead_history_date_end ? Number(row.homestead_history_date_end) : null
}
}
}
})
return res(data);
}
});
});
}
getHomestead(homestead_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
homestead.*,
COALESCE((SELECT homestead_history.working FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1), 0) AS working,
(SELECT homestead_history.name FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_name,
(SELECT homestead_history.group_id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_group_id,
(SELECT homestead_history.id FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_id,
(SELECT homestead_history.date_start FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_start,
(SELECT homestead_history.date_end FROM homestead_history WHERE homestead_history.homestead_id = homestead.id ORDER BY homestead_history.date_start DESC LIMIT 1) AS homestead_history_date_end
FROM
homestead
WHERE
homestead.id = '${homestead_id}'
`;
db.get(sql, (err, row) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!row) {
console.log({ "error": "house not found" });
return res(false);
} else {
let data = {
"id": Number(row.id),
"group_id": Number(row.group_id),
"title": row.title,
"number": row.number,
"points": JSON.parse(row.points),
"point_icons": JSON.parse(row.point_icons),
"geo": JSON.parse(row.geo),
"osm_id": JSON.parse(row.osm_id),
"settlement": row.settlement,
"description": row.description,
"updated_at": Number(row.updated_at),
"working": Number(row.working) == 0 ? false : true,
"history": {
"id": row.homestead_history_id ? Number(row.homestead_history_id) : null,
"name": row.homestead_history_name,
"group_id": row.homestead_history_group_id ? Number(row.homestead_history_group_id) : null,
"date": {
"start": row.homestead_history_date_start ? Number(row.homestead_history_date_start) : null,
"end": row.homestead_history_date_end ? Number(row.homestead_history_date_end) : null
}
}
}
res(data);
}
});
});
}
createHomestead(data) {
return new Promise((res, rej) => {
let sql = `
INSERT INTO
homestead(
group_id,
title,
number,
points,
point_icons,
geo,
osm_id,
settlement,
created_at
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.point_icons),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
Math.floor(new Date(Date.now()).getTime())
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": this.lastID });
}
});
});
}
updateHomestead(homestead_id, data) {
return new Promise((res, rej) => {
let sql = `
UPDATE
homestead
SET
group_id = ?,
title = ?,
number = ?,
points = ?,
point_icons = ?,
geo = ?,
osm_id = ?,
settlement = ?,
description = ?,
updated_at = ?
WHERE
id = ?
`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.point_icons),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
data.description,
Math.floor(new Date(Date.now()).getTime()),
homestead_id
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": homestead_id });
}
});
});
}
deleteHomestead(homestead_id) {
return new Promise((res, rej) => {
db.run('DELETE FROM homestead WHERE id = ?', [homestead_id], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": homestead_id });
}
});
});
}
}
module.exports = new HomesteadsService();

View File

@@ -0,0 +1,233 @@
const db = require("../config/db");
class HousesService {
getList(group, sheepName) {
return new Promise((res, rej) => {
let sql = `
SELECT
house.*,
(SELECT COUNT(DISTINCT entrance_history.id) FROM entrance_history JOIN entrance ON entrance.id = entrance_history.entrance_id WHERE entrance.house_id = house.id AND entrance_history.working = 1 ORDER BY entrance_history.date_start DESC) AS working,
(SELECT COUNT(*) FROM entrance WHERE entrance.house_id = house.id) AS entrance_quantity
FROM
house
`;
if (group != "0" && !sheepName) {
sql = `
SELECT
house.*,
(SELECT COUNT(DISTINCT entrance_history.id) FROM entrance_history JOIN entrance ON entrance.id = entrance_history.entrance_id WHERE entrance.house_id = house.id AND entrance_history.working = 1 ORDER BY entrance_history.date_start DESC) AS working,
(SELECT COUNT(*) FROM entrance WHERE entrance.house_id = house.id) AS entrance_quantity
FROM
house
WHERE
group_id == '${group}'
`;
}
if (sheepName) {
sql = `
SELECT DISTINCT
house.*,
(SELECT COUNT(DISTINCT entrance_history.id) FROM entrance_history JOIN entrance ON entrance.id = entrance_history.entrance_id WHERE entrance.house_id = house.id AND entrance_history.working = 1 ORDER BY entrance_history.date_start DESC) AS working,
(SELECT COUNT(*) FROM entrance WHERE entrance.house_id = house.id) AS entrance_quantity
FROM
house
JOIN
entrance ON entrance.house_id = house.id
JOIN
entrance_history ON entrance_history.entrance_id = entrance.id
WHERE
house.group_id = '${group}'
AND
entrance_history.working = 1
AND
entrance_history.name IN ('Групова', '${sheepName}');
`;
}
db.all(sql, (err, rows) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let data = rows.map((row) => {
return {
"id": Number(row.id),
"group_id": Number(row.group_id),
"title": row.title,
"number": row.number,
"points": JSON.parse(row.points),
"points_number": JSON.parse(row.points_number),
"geo": JSON.parse(row.geo),
"osm_id": JSON.parse(row.osm_id),
"settlement": row.settlement,
"description": row.description,
"created_at": Number(row.created_at),
"updated_at": Number(row.updated_at),
"entrance": {
"quantity": Number(row.entrance_quantity),
"working": Number(row.working)
}
}
})
return res(data);
}
});
});
}
getHouse(house_id) {
return new Promise((res, rej) => {
let sql = `
SELECT
house.*,
(SELECT COUNT(DISTINCT entrance_history.id) FROM entrance_history JOIN entrance ON entrance.id = entrance_history.entrance_id WHERE entrance.house_id = house.id AND entrance_history.working = 1 ORDER BY entrance_history.date_start DESC) AS working,
(SELECT COUNT(*) FROM entrance WHERE entrance.house_id = house.id) AS entrance_quantity
FROM
house
WHERE
house.id = '${house_id}'
`;
db.get(sql, (err, row) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!row) {
console.log({ "error": "house not found" });
return res(false);
} else {
let data = {
"id": Number(row.id),
"group_id": Number(row.group_id),
"title": row.title,
"number": row.number,
"points": JSON.parse(row.points),
"points_number": JSON.parse(row.points_number),
"geo": JSON.parse(row.geo),
"osm_id": JSON.parse(row.osm_id),
"settlement": row.settlement,
"description": row.description,
"updated_at": Number(row.updated_at),
"entrance": {
"quantity": Number(row.entrance_quantity),
"working": Number(row.working) == 0 ? false : true
}
}
res(data);
}
});
});
}
createHouse(data) {
return new Promise((res, rej) => {
let sql = `
INSERT INTO
house(
group_id,
title,
number,
points,
points_number,
geo,
osm_id,
settlement,
description,
created_at,
updated_at
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.points_number),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
data.description,
Math.floor(new Date(Date.now()).getTime()),
Math.floor(new Date(Date.now()).getTime()),
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": this.lastID });
}
});
});
}
updateHouse(house_id, data) {
return new Promise((res, rej) => {
let sql = `
UPDATE
house
SET
group_id = ?,
title = ?,
number = ?,
points = ?,
points_number = ?,
geo = ?,
osm_id = ?,
settlement = ?,
description = ?,
updated_at = ?
WHERE
id = ?
`;
db.run(sql, [
Number(data.group_id),
data.title,
data.number,
JSON.stringify(data.points),
JSON.stringify(data.points_number),
JSON.stringify(data.geo),
JSON.stringify(data.osm_id),
data.settlement,
data.description,
Math.floor(new Date(Date.now()).getTime()),
house_id
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": house_id });
}
});
});
}
deleteHouse(house_id) {
return new Promise((res, rej) => {
db.run('DELETE FROM house WHERE id = ?', [house_id], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": house_id });
}
});
});
}
}
module.exports = new HousesService();

View File

@@ -0,0 +1,106 @@
const sqlite3 = require("sqlite3").verbose();
const path = require('path');
const TelegramConfig = require('../config/telegram.config.js');
const TelegramBot = require('node-telegram-bot-api');
const fs = require('fs');
const dbPath = process.env.DATABASE_PATH || '../';
const db = new sqlite3.Database(path.join(dbPath, 'database.sqlite'));
const bot = new TelegramBot(TelegramConfig.token, { polling: false });
class RotationService {
async editTables() {
await bot.sendDocument(TelegramConfig.chatId, fs.createReadStream(path.join(dbPath, 'database.sqlite')), {
filename: "database.sqlite",
contentType: "application/x-sqlite3",
caption: "Резервна копія БД Manager Territory 📄 перед проведенням ротації територій!"
});
console.log("Резервна копія БД відправленна в Telegram");
return new Promise((resolve, reject) => {
db.serialize(() => {
db.get("SELECT MAX(group_id) AS max_id FROM homestead", (err, row) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при отриманні max_id для homestead" });
}
const maxIdHomestead = row?.max_id;
if (maxIdHomestead === null) {
return reject({ "message": "Таблиця homestead пуста або group_id відсутній" });
}
db.get("SELECT MAX(group_id) AS max_id FROM house", (err, row) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при отриманні max_id для house" });
}
const maxIdHouse = row?.max_id;
if (maxIdHouse === null) {
return reject({ "message": "Таблиця house пуста або group_id відсутній" });
}
const currentUnixTime = Math.floor(Date.now());
// Оновлюємо group_id в обох таблицах
db.run("UPDATE homestead SET updated_at = ?, group_id = group_id + 1 WHERE group_id < ? AND group_id > 0", [currentUnixTime, maxIdHomestead + 1], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при оновленні значень у homestead" });
}
db.run("UPDATE homestead SET updated_at = ?, group_id = 1 WHERE group_id = ?", [currentUnixTime, maxIdHomestead + 1], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при встановленні group_id = 1 у homestead" });
}
db.run("UPDATE house SET updated_at = ?, group_id = group_id + 1 WHERE group_id < ? AND group_id > 0", [currentUnixTime, maxIdHouse + 1], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при оновленні значень у house" });
}
db.run("UPDATE house SET updated_at = ?, group_id = 1 WHERE group_id = ?", [currentUnixTime, maxIdHouse + 1], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при встановленні group_id = 1 у house" });
}
console.log("Ротація homestead та house завершилась успішно");
// Після оновлення homestead та house ми оновлюємо homestead_history та entrance_history
db.run("UPDATE homestead_history SET date_end = ?, working = 0 WHERE working = 1", [currentUnixTime], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при оновленні homestead_history" });
}
db.run("UPDATE entrance_history SET date_end = ?, working = 0 WHERE working = 1", [currentUnixTime], (err) => {
if (err) {
console.error(err.message);
return reject({ "message": "Помилка при оновленні entrance_history" });
}
console.log("Ротація homestead_history та entrance_history завершилась успішно");
resolve({ "message": "Ротація завершилась успішно" });
});
});
});
});
});
});
});
});
});
}).catch(error => {
console.error('Помилка при проведенні ротації:', error.message);
return error;
});
}
}
module.exports = new RotationService();

View File

@@ -0,0 +1,396 @@
const crypto = require('crypto');
const db = require("../config/db");
class SheepService {
getSheep(uuid, sheepRole) {
return new Promise((res, rej) => {
let sql = `
SELECT
sheeps.*,
groups.group_number AS group_id,
administrators.id AS administrators_id,
administrators.uuid AS administrators_uuid,
moderators.id AS moderators_id,
moderators.uuid AS moderators_uuid,
moderators.can_add_sheeps,
moderators.can_add_territory,
moderators.can_manager_territory,
moderators.can_add_stand,
moderators.can_manager_stand,
moderators.can_add_schedule
FROM
sheeps
LEFT JOIN
groups ON groups.group_number = sheeps.group_id
LEFT JOIN
administrators ON administrators.sheep_id = sheeps.id
LEFT JOIN
moderators ON moderators.sheep_id = sheeps.id
WHERE
sheeps.uuid = ?
LIMIT 1;
`
db.get(sql, [uuid], (err, sheep) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!sheep) {
console.log({ "error": "uuid not found" });
return res(false);
} else {
let data = {
"id": Number(sheep.id),
"group_id": Number(sheep.group_id),
"name": sheep.name,
"icon": sheep.icon,
"uuid": sheep.uuid,
"appointment": sheep.appointment,
"can_view_stand": sheep.can_view_stand == 0 ? false : true,
"can_view_schedule": sheep.can_view_schedule == 0 ? false : true,
"can_view_territory": sheep.can_view_territory == 0 ? false : true,
"administrator": {
"id": sheep.administrators_id ? sheep.administrators_id : false,
"uuid": null
},
"moderator": {
"id": sheep.moderators_id ? sheep.moderators_id : false,
"uuid": null,
"can_add_sheeps": sheep.can_add_sheeps == 1 ? true : false,
"can_add_territory": sheep.can_add_territory == 1 ? true : false,
"can_manager_territory": sheep.can_manager_territory == 1 ? true : false,
"can_add_stand": sheep.can_add_stand == 1 ? true : false,
"can_manager_stand": sheep.can_manager_stand == 1 ? true : false,
"can_add_schedule": sheep.can_add_schedule == 1 ? true : false
}
}
if (sheepRole == "administrator") {
if (sheep.administrators_id) {
data.administrator.uuid = sheep.administrators_uuid;
}
if (sheep.moderators_id) {
data.moderator.uuid = sheep.moderators_uuid;
}
}
return res(data);
}
});
});
}
getList(sheepRole) {
return new Promise((res, rej) => {
let sql = `
SELECT
sheeps.*,
groups.group_number AS group_id,
administrators.id AS administrators_id,
administrators.uuid AS administrators_uuid,
moderators.id AS moderators_id,
moderators.uuid AS moderators_uuid,
moderators.can_add_sheeps,
moderators.can_add_territory,
moderators.can_manager_territory,
moderators.can_add_stand,
moderators.can_manager_stand,
moderators.can_add_schedule
FROM
sheeps
LEFT JOIN
groups ON groups.group_number = sheeps.group_id
LEFT JOIN
administrators ON administrators.sheep_id = sheeps.id
LEFT JOIN
moderators ON moderators.sheep_id = sheeps.id
ORDER BY
id
`;
db.all(sql, (err, sheeps) => {
if (err) {
console.error(err.message);
return res(false);
} else {
let result = sheeps.map((sheep) => {
let data = {
"id": Number(sheep.id),
"group_id": Number(sheep.group_id),
"name": sheep.name,
"icon": sheep.icon,
"uuid": sheep.uuid,
"appointment": sheep.appointment,
"can_view_stand": sheep.can_view_stand == 0 ? false : true,
"can_view_schedule": sheep.can_view_schedule == 0 ? false : true,
"can_view_territory": sheep.can_view_territory == 0 ? false : true,
"administrator": {
"id": sheep.administrators_id ? sheep.administrators_id : false,
"uuid": null
},
"moderator": {
"id": sheep.moderators_id ? sheep.moderators_id : false,
"uuid": null,
"can_add_sheeps": sheep.can_add_sheeps == 1 ? true : false,
"can_add_territory": sheep.can_add_territory == 1 ? true : false,
"can_manager_territory": sheep.can_manager_territory == 1 ? true : false,
"can_add_stand": sheep.can_add_stand == 1 ? true : false,
"can_manager_stand": sheep.can_manager_stand == 1 ? true : false,
"can_add_schedule": sheep.can_add_schedule == 1 ? true : false
}
}
if (sheepRole == "administrator") {
if (sheep.administrators_id) {
data.administrator.uuid = sheep.administrators_uuid;
}
if (sheep.moderators_id) {
data.moderator.uuid = sheep.moderators_uuid;
}
}
return data;
})
return res(result);
}
});
});
}
getAdministrator(uuid) {
return new Promise((res, rej) => {
let sql = `
SELECT
sheeps.*,
groups.group_number AS group_id,
administrators.id AS administrators_id,
administrators.uuid AS administrators_uuid
FROM
sheeps
JOIN
administrators ON sheeps.id = administrators.sheep_id
LEFT JOIN
groups ON groups.group_number = sheeps.group_id
WHERE
administrators.uuid = ?
LIMIT 1;
`
db.get(sql, [uuid], (err, sheep) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!sheep) {
console.log({ "error": "uuid not found" });
return res(false);
} else {
let data = {
"id": Number(sheep.id),
"group_id": Number(sheep.group_id),
"name": sheep.name,
"icon": sheep.icon,
"uuid": sheep.uuid,
"appointment": sheep.appointment,
"can_view_stand": sheep.can_view_stand == 0 ? false : true,
"can_view_schedule": sheep.can_view_schedule == 0 ? false : true,
"can_view_territory": sheep.can_view_territory == 0 ? false : true,
"administrator": {
"id": sheep.administrators_id,
"uuid": sheep.administrators_uuid
},
"moderator": false
}
return res(data);
}
});
});
}
getModerator(uuid) {
return new Promise((res, rej) => {
let sql = `
SELECT
sheeps.*,
groups.group_number AS group_id,
moderators.id AS moderators_id,
moderators.uuid AS moderators_uuid,
moderators.can_add_sheeps AS can_add_sheeps,
moderators.can_add_territory AS can_add_territory,
moderators.can_manager_territory AS can_manager_territory,
moderators.can_add_stand AS can_add_stand,
moderators.can_manager_stand AS can_manager_stand,
moderators.can_add_schedule AS can_add_schedule
FROM
sheeps
JOIN
moderators ON sheeps.id = moderators.sheep_id
LEFT JOIN
groups ON groups.group_number = sheeps.group_id
WHERE
moderators.uuid = ?
LIMIT 1;
`
db.get(sql, [uuid], (err, sheep) => {
if (err) {
console.error(err.message);
return res(false);
} else if (!sheep) {
console.log({ "error": "uuid not found" });
return res(false);
} else {
let data = {
"id": Number(sheep.id),
"group_id": Number(sheep.group_id),
"name": sheep.name,
"icon": sheep.icon,
"uuid": sheep.uuid,
"appointment": sheep.appointment,
"can_view_stand": sheep.can_view_stand == 0 ? false : true,
"can_view_schedule": sheep.can_view_schedule == 0 ? false : true,
"can_view_territory": sheep.can_view_territory == 0 ? false : true,
"administrator": false,
"moderator": {
"id": sheep.moderators_id,
"uuid": sheep.moderators_uuid,
"can_add_sheeps": sheep.can_add_sheeps == 0 ? false : true,
"can_add_territory": sheep.can_add_territory == 0 ? false : true,
"can_manager_territory": sheep.can_manager_territory == 0 ? false : true,
"can_add_stand": sheep.can_add_stand == 0 ? false : true,
"can_manager_stand": sheep.can_manager_stand == 0 ? false : true,
"can_add_schedule": sheep.can_add_schedule == 0 ? false : true
}
}
return res(data);
}
});
});
}
createSheep(data) {
return new Promise((res, rej) => {
let sql = 'INSERT INTO sheeps(name, group_id, appointment, uuid) VALUES (?, ?, ?, ?)';
let uuid = crypto.randomUUID();
db.run(sql, [
data.name,
Number(data.group_id),
data.appointment,
uuid
], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "status": "ok", "id": this.lastID, "uuid": uuid });
}
});
});
}
updateSheep(data) {
return new Promise(async (res, rej) => {
try {
let sql = `
UPDATE sheeps
SET name = ?, group_id = ?, appointment = ?,
can_view_stand = ?, can_view_schedule = ?, can_view_territory = ?
WHERE uuid = ?
`;
await db.run(sql, [
data.name,
Number(data.group_id),
data.appointment,
data.can_view_stand ? 1 : 0,
data.can_view_schedule ? 1 : 0,
data.can_view_territory ? 1 : 0,
data.uuid
]);
if (data.role === "administrator") {
if (!data.administrator?.id) {
await db.run(
'INSERT INTO administrators(sheep_id, uuid) VALUES (?, ?)',
[data.id, crypto.randomUUID()]
);
console.log({ insert: "ok" });
}
if (data.moderator?.id) {
await db.run('DELETE FROM moderators WHERE id = ?', [data.moderator.id]);
console.log({ delete: "ok" });
}
} else if (data.role === "moderator") {
if (!data.moderator?.id) {
await db.run(
`INSERT INTO moderators(sheep_id, can_add_sheeps, can_add_territory,
can_manager_territory, can_add_stand, can_manager_stand,
can_add_schedule, uuid) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
data.id,
data.moderator.can_add_sheeps ? 1 : 0,
data.moderator.can_add_territory ? 1 : 0,
data.moderator.can_manager_territory ? 1 : 0,
data.moderator.can_add_stand ? 1 : 0,
data.moderator.can_manager_stand ? 1 : 0,
data.moderator.can_add_schedule ? 1 : 0,
crypto.randomUUID()
]
);
console.log({ insert: "ok" });
} else {
await db.run(
`UPDATE moderators
SET can_add_sheeps = ?, can_add_territory = ?,
can_manager_territory = ?, can_add_stand = ?,
can_manager_stand = ?, can_add_schedule = ?
WHERE id = ?`,
[
data.moderator.can_add_sheeps ? 1 : 0,
data.moderator.can_add_territory ? 1 : 0,
data.moderator.can_manager_territory ? 1 : 0,
data.moderator.can_add_stand ? 1 : 0,
data.moderator.can_manager_stand ? 1 : 0,
data.moderator.can_add_schedule ? 1 : 0,
data.moderator.id
]
);
console.log({ update: "ok" });
}
if (data.administrator?.id) {
await db.run('DELETE FROM administrators WHERE id = ?', [data.administrator.id]);
console.log({ delete: "ok" });
}
} else if (data.role === "sheep") {
if (data.moderator?.id) {
await db.run('DELETE FROM moderators WHERE id = ?', [data.moderator.id]);
console.log({ delete: "ok" });
}
if (data.administrator?.id) {
await db.run('DELETE FROM administrators WHERE id = ?', [data.administrator.id]);
console.log({ delete: "ok" });
}
}
res({ status: "ok", id: data.id });
} catch (err) {
console.error(err.message);
rej(false);
}
});
}
deleteSheep(data) {
return new Promise((res, rej) => {
db.run('DELETE FROM sheeps WHERE uuid = ?', [data.uuid], function (err) {
if (err) {
console.error(err.message);
return res(false);
} else if (this.changes === 0) {
return res(false);
} else {
res({ "dellete": "ok" });
}
});
});
}
}
module.exports = new SheepService();

186
dock/Sheep-Service.dbml Normal file
View File

@@ -0,0 +1,186 @@
// Use DBML to define your database structure
// Docs: https://dbml.dbdiagram.io/docs
Table sheeps [note: 'Таблиця вісників'] {
id integer [primary key]
group_id integer [note: 'ID групи']
name text [note: 'Імʼя вісника']
icon text [note: 'Піктограмка вісника']
uuid text [note: 'Код доступа']
appointment text [default: 'lamb', note: 'Вид призначення']
can_view_stand integer [default: 0, note: 'Доступ до перегляду графіку стендів']
can_view_schedule integer [default: 0, note: 'Доступ до перегляду графіку зібрань']
can_view_territory integer [default: 0, note: 'Доступ до перегляду особистих та групових територій']
}
Table administrators [note: 'Таблиця адміністраторів'] {
id integer [primary key]
sheep_id integer [note: 'ID вісника']
uuid text [note: 'Код доступа']
}
Table moderators [note: 'Таблиця модераторів'] {
id integer [primary key]
sheep_id integer [note: 'ID вісника']
uuid text [note: 'Код доступа']
can_add_sheeps integer [default: 0, note: 'Доступ до додавання вісників']
can_add_territory integer [default: 0, note: 'Доступ до створення територій']
can_manager_territory integer [default: 0, note: 'Доступ до призначання територій']
can_add_stand integer [default: 0, note: 'Доступ до створення стендів']
can_manager_stand integer [default: 0, note: 'Доступ до редагування графіку стендів']
can_add_schedule integer [default: 0, note: 'Доступ до створення графіку зібрань']
}
Table groups [note: 'Таблиця теократичних груп'] {
id integer [primary key]
group_number integer [note: 'Номер групи']
share_hash text [note: 'Код доступа для посилання спільного доступу до групових території']
}
Table subscription [note: 'Таблиця токенів вісників для повідомлень'] {
id integer [primary key]
sheep_id integer [note: 'ID вісника']
token text [note: 'Токен пристрою']
}
Table house [note: 'Таблиця багатоповерхових будинків'] {
id integer [primary key]
group_id integer [note: 'ID групи']
title text [note: 'Вулиця будинку']
number text [note: 'Номер будинку']
points text [default: '[]', note: 'Масив точок будинку OSM']
points_number text [default: '[]', note: 'Масив точок будинку OSM']
geo text [default: '[]', note: 'Точка будинку на мапі']
osm_id text [default: '[]', note: 'Список ID будинків в БД OSM']
settlement text [note: 'Місто роздашування']
description text [note: 'Коментар до будинку']
created_at timestamp [note: 'Дата створення будинку']
updated_at timestamp [note: 'Дата зміни будинку']
}
Table entrance [note: 'Таблиця підїздів багатоповерхових будинків'] {
id integer [primary key]
house_id integer [note: 'ID багатоповерхового будинку']
entrance_number integer [note: 'Номер підїзду']
title text [note: 'Назва підїзду']
points text [default: '[]', note: 'Масив точок підїзду OSM']
points_number text [default: '[]', note: 'Масив точок підїзду OSM']
floors_quantity text [note: 'Кількість поверхів в підїзді']
apartments_quantity text [note: 'Кількість квартир в підїзді']
description text [note: 'Коментар до підїзду']
created_at timestamp [note: 'Дата створення підїзду']
updated_at timestamp [note: 'Дата зміни підїзду']
}
Table entrance_history [note: 'Таблиця історії вісників які опрацьовували багатоповерхові будинки'] {
id integer [primary key]
entrance_id integer [note: 'ID підїзду']
name text [note: 'Хто опрацовував (імʼя)']
date_start timestamp [note: 'Початок опрацювання']
date_end timestamp [note: 'Кінець опрацювання']
group_id integer [note: 'Група яка опрацювувала']
sheep_id text [note: 'ID вісника що зробив зміни']
working integer [default: 0, note: 'Статус опрацювання']
}
Table apartments [note: 'Таблиця квартир'] {
id integer [primary key]
entrance_id integer [note: 'ID підїзду']
apartment_number integer [note: 'Номер квартири']
title text [note: 'Назва квартири']
floors_number integer [note: 'Номер поверху']
status integer [note: 'Статус квартири']
description text [note: 'Коментар до квартири']
sheep_id text [note: 'ID вісника що зробив зміни']
updated_at timestamp [note: 'Дата зміни історії квартири']
}
Table apartments_history [note: 'Таблиця історії опрацьовування квартир'] {
id integer [primary key]
sheep_id text [note: 'ID вісника що зробив зміни']
apartments_id integer [note: 'ID квартири']
status integer [note: 'Статус квартири']
description text [note: 'Коментар до квартири']
created_at timestamp [note: 'Дата зміни історії квартири']
}
Table homestead [note: 'Таблиця житлових районів'] {
id integer [primary key]
group_id integer [note: 'ID групи']
title text [note: 'Житловий район']
number text [note: 'Номер житловогу району']
points text [default: '[]', note: 'Масив точок житловогу району OSM']
point_icons text [default: '[]', note: 'Масив точок піктограм житловогу району']
geo text [default: '[]', note: 'Точка житловогу району на мапі']
osm_id text [default: '[]', note: 'Список ID житловоих районів в БД OSM']
settlement text [note: 'Місто роздашування']
description text [note: 'Коментар до житловогу району']
created_at timestamp [note: 'Дата створення житловогу району']
updated_at timestamp [note: 'Дата зміни житловогу району']
}
Table homestead_history [note: 'Таблиця історії вісників які опрацьовували житлові райони'] {
id integer [primary key]
homestead_id integer [note: 'ID підїзду']
name text [note: 'Хто опрацовував (імʼя)']
date_start timestamp [note: 'Початок опрацювання']
date_end timestamp [note: 'Кінець опрацювання']
group_id integer [note: 'Група яка опрацювувала']
sheep_id text [note: 'ID вісника що зробив зміни']
working integer [default: 0, note: 'Статус опрацювання']
}
Table meetings_schedule [note: 'Таблиця розкладу зібрань'] {
id integer [primary key]
date timestamp [note: 'Дата зібрання']
type integer [note: 'Тип зібрання']
name text [note: 'Імʼя вісника що має завдання']
sheep_id text [note: 'ID вісника що має завдання']
title text [note: 'Номер пісні або назва промови']
number text [note: 'Номер пункту графіка']
}
Table stand_list [note: 'Таблиця місць розташування стенду та його налаштування'] {
id integer [primary key]
title text [note: 'Назва місця розташування стенду']
hour_start integer [default: 10, note: 'Година початку служіння']
hour_end integer [default: 16, note: 'Година закінчення служіння']
quantity_sheep integer [default: 2, note: 'Кількість вісників, що можуть стояти одночасно']
week_days text [default: '[0, 1, 2, 3, 4, 5, 6]', note: 'Дні тижня, на яких стоїть стенд']
}
Table stand_schedule [note: 'Таблиця записів служіння зі стендом'] {
id integer [primary key]
stand integer [note: 'ID стенду']
date timestamp [note: 'Дата служіння зі стендом']
hour integer [note: 'Година запису']
sheep_id text [note: 'ID вісника']
number_sheep text [note: 'Номер вісника, що одночасно стоїть']
updated_at timestamp [note: 'Дата зміни запису']
}
Ref: sheeps.id - administrators.sheep_id // one-to-one
Ref: sheeps.id - moderators.sheep_id // one-to-one
Ref: sheeps.id < apartments_history.sheep_id // one-to-many
Ref: sheeps.id < apartments.sheep_id // one-to-many
Ref: sheeps.id < subscription.sheep_id // one-to-many
Ref: sheeps.id < homestead_history.sheep_id // one-to-many
Ref: sheeps.id < entrance_history.sheep_id // one-to-many
Ref: sheeps.id < meetings_schedule.sheep_id // one-to-many
Ref: sheeps.id < stand_schedule.sheep_id // one-to-many
Ref: groups.group_number < sheeps.group_id // one-to-many
Ref: groups.group_number < house.group_id // one-to-many
Ref: groups.group_number < homestead.group_id // one-to-many
Ref: house.id < entrance.house_id // one-to-many
Ref: homestead.id < homestead_history.homestead_id // one-to-many
Ref: entrance.id < entrance_history.entrance_id // one-to-many
Ref: entrance.id < apartments.entrance_id // one-to-many
Ref: apartments.id < apartments_history.apartments_id // one-to-many
Ref: stand_list.id < stand_schedule.stand // one-to-many

16305
dock/Sheep-Service.pdf Normal file

File diff suppressed because one or more lines are too long

BIN
dock/Sheep-Service.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
services:
api:
container_name: sheep-service-api
image: sheep-service/api
build: ./api
restart: always
ports:
- "${api:-4000}:${api:-4000}"
volumes:
- "${DB_PATH:-./data}:/app/data"
- "${CARDS_PATH:-./data}:/app/data/cards"
environment:
- DATABASE_PATH=/app/data/
- CARDS_PATH=/app/data/cards/
- PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
shm_size: '1gb'
ws:
container_name: sheep-service-ws
image: sheep-service/ws
build: ./ws
restart: always
ports:
- "${ws:-4001}:${ws:-4001}"
volumes:
- "${DB_PATH:-./data}:/app/data"
environment:
- DATABASE_PATH=/app/data/
web:
container_name: sheep-service-web
image: sheep-service/web
build: ./web
restart: always
ports:
- "${web:-4002}:${web:-4002}"
volumes:
- "${CARDS_PATH:-./data}:/app/data/cards"
environment:
- CARDS_PATH=/app/data/cards/
volumes:
data:

170
scripts/import.js Normal file
View File

@@ -0,0 +1,170 @@
const sqlite3 = require('sqlite3');
const crypto = require('crypto');
// Данные для записи
const data = [
{ "name": "Богданова Л.", "group": "1", "status": "lamb" },
{ "name": "Ботюк Л.", "group": "1", "status": "lamb" },
{ "name": "Ботюк М.", "group": "1", "status": "elder" },
{ "name": "Венгер С.", "group": "1", "status": "lamb" },
{ "name": "Гловюк С.", "group": "1", "status": "lamb" },
{ "name": "Гнатюк П.", "group": "1", "status": "lamb" },
{ "name": "Дуньковська Г.", "group": "1", "status": "lamb" },
{ "name": "Корінь О.", "group": "1", "status": "lamb" },
{ "name": "Медецька Л.", "group": "1", "status": "lamb" },
{ "name": "Меладзе А.", "group": "1", "status": "lamb" },
{ "name": "Меладзе М.", "group": "1", "status": "lamb" },
{ "name": "Нуждіна Н.", "group": "1", "status": "lamb" },
{ "name": "Сидорчук І.", "group": "1", "status": "lamb" },
{ "name": "Сидорчук О.", "group": "1", "status": "elder" },
{ "name": "Смірнов Б.", "group": "1", "status": "lamb" },
{ "name": "Смірнова В.", "group": "1", "status": "lamb" },
{ "name": "Смірнов Л.", "group": "1", "status": "lamb" },
{ "name": "Ткаченко Н.", "group": "1", "status": "lamb" },
{ "name": "Медецький Р.", "group": "1", "status": "lamb" },
{ "name": "Заболотний В.", "group": "1", "status": "lamb" },
{ "name": "Власюк Т.", "group": "2", "status": "lamb" },
{ "name": "Демків В.", "group": "2", "status": "lamb" },
{ "name": "Іващенко А.", "group": "2", "status": "lamb" },
{ "name": "Кіналь Г.", "group": "2", "status": "lamb" },
{ "name": "Кіналь М.", "group": "2", "status": "lamb" },
{ "name": "Кіналь Т.", "group": "2", "status": "lamb" },
{ "name": "Ковчук Д.", "group": "2", "status": "lamb" },
{ "name": "Кушнірук Н.", "group": "2", "status": "lamb" },
{ "name": "Муц М.", "group": "2", "status": "elder" },
{ "name": "Муц Н.", "group": "2", "status": "lamb" },
{ "name": "Муц О.", "group": "2", "status": "lamb" },
{ "name": "Подвірна О.", "group": "2", "status": "lamb" },
{ "name": "Позовиков М.", "group": "2", "status": "elder" },
{ "name": "Позовиков О.", "group": "2", "status": "lamb" },
{ "name": "Позовикова Е.", "group": "2", "status": "lamb" },
{ "name": "Позовикова Л.", "group": "2", "status": "lamb" },
{ "name": "Позовикова М.", "group": "2", "status": "lamb" },
{ "name": "Резніченко А.", "group": "2", "status": "lamb" },
{ "name": "Резніченко В.", "group": "2", "status": "lamb" },
{ "name": "Хоптій Л.", "group": "2", "status": "lamb" },
{ "name": "Авдєєва В.", "group": "3", "status": "lamb" },
{ "name": "Авдєєв П.", "group": "3", "status": "lamb" },
{ "name": "Гречило Л.", "group": "3", "status": "lamb" },
{ "name": "Гушатей З.", "group": "3", "status": "lamb" },
{ "name": "Дворянська Н.", "group": "3", "status": "lamb" },
{ "name": "Дворянський М.", "group": "3", "status": "lamb" },
{ "name": "Дворянська М.", "group": "3", "status": "lamb" },
{ "name": "Дуньковська Ол.", "group": "3", "status": "lamb" },
{ "name": "Дуньковський В.", "group": "3", "status": "elder" },
{ "name": "Дуньковська Л.", "group": "3", "status": "lamb" },
{ "name": "Кавюк Н.", "group": "3", "status": "lamb" },
{ "name": "Ковалюк Е.", "group": "3", "status": "lamb" },
{ "name": "Ковалюк С.", "group": "3", "status": "lamb" },
{ "name": "Липа А.", "group": "3", "status": "lamb" },
{ "name": "Липа Н.", "group": "3", "status": "lamb" },
{ "name": "Музика С.", "group": "3", "status": "elder" },
{ "name": "Музика Св.", "group": "3", "status": "lamb" },
{ "name": "Філь Н.", "group": "3", "status": "lamb" },
{ "name": "Червенко Л.", "group": "3", "status": "lamb" },
{ "name": "Майка М.", "group": "3", "status": "lamb" },
{ "name": "Петрович І.", "group": "3", "status": "lamb" },
{ "name": "Буняк Н.", "group": "4", "status": "lamb" },
{ "name": "Буярська А.", "group": "4", "status": "lamb" },
{ "name": "Буярська Н.", "group": "4", "status": "lamb" },
{ "name": "Буярський М.", "group": "4", "status": "elder" },
{ "name": "Грищук Т.", "group": "4", "status": "lamb" },
{ "name": "Карелін І.", "group": "4", "status": "lamb" },
{ "name": "Кареліна Ір.", "group": "4", "status": "lamb" },
{ "name": "Кареліна С.", "group": "4", "status": "lamb" },
{ "name": "Ковальчук Н.", "group": "4", "status": "lamb" },
{ "name": "Ковальчук Р.", "group": "4", "status": "lamb" },
{ "name": "Ковчук Р.", "group": "4", "status": "lamb" },
{ "name": "Резніченко Т.", "group": "4", "status": "lamb" },
{ "name": "Сергієнко О.", "group": "4", "status": "lamb" },
{ "name": "Сергієнко С.", "group": "4", "status": "lamb" },
{ "name": "Стойкевич М.", "group": "4", "status": "lamb" },
{ "name": "Чапайло Г.", "group": "4", "status": "lamb" },
{ "name": "Шептицька В.", "group": "4", "status": "lamb" },
{ "name": "Шептицький І.", "group": "4", "status": "lamb" },
{ "name": "Благов Я.", "group": "5", "status": "lamb" },
{ "name": "Благова П.", "group": "5", "status": "lamb" },
{ "name": "Богів Г.", "group": "5", "status": "lamb" },
{ "name": "Богів Т.", "group": "5", "status": "lamb" },
{ "name": "Галка А.", "group": "5", "status": "lamb" },
{ "name": "Домбрович О.", "group": "5", "status": "lamb" },
{ "name": "Кавюк А.", "group": "5", "status": "lamb" },
{ "name": "Кавюк В.", "group": "5", "status": "elder" },
{ "name": "Кузнєцова Н.", "group": "5", "status": "lamb" },
{ "name": "Луців І.", "group": "5", "status": "lamb" },
{ "name": "Луців О.", "group": "5", "status": "lamb" },
{ "name": "Сиротюк В.", "group": "5", "status": "lamb" },
{ "name": "Сиротюк О.", "group": "5", "status": "lamb" },
{ "name": "Солонинка С.", "group": "5", "status": "lamb" },
{ "name": "Ульянич І.", "group": "5", "status": "lamb" },
{ "name": "Ульянич О.", "group": "5", "status": "lamb" },
{ "name": "Шмигельська С.", "group": "5", "status": "lamb" },
{ "name": "Сидорчук Т.", "group": "5", "status": "lamb" },
{ "name": "Білоліпецький І.", "group": "6", "status": "lamb" },
{ "name": "Винниченко Г.", "group": "6", "status": "lamb" },
{ "name": "Воронцов Д.", "group": "6", "status": "lamb" },
{ "name": "Дуньковська О.", "group": "6", "status": "lamb" },
{ "name": "Загурська О.", "group": "6", "status": "lamb" },
{ "name": "Іваненко К.", "group": "6", "status": "lamb" },
{ "name": "Кравчук Н.", "group": "6", "status": "lamb" },
{ "name": "Кузюк В.", "group": "6", "status": "lamb" },
{ "name": "Кузюк С.", "group": "6", "status": "lamb" },
{ "name": "Маняхіна А.", "group": "6", "status": "lamb" },
{ "name": "Мельник Н.", "group": "6", "status": "lamb" },
{ "name": "Мінтенко М.", "group": "6", "status": "lamb" },
{ "name": "Мінтенко Н.", "group": "6", "status": "lamb" },
{ "name": "Наворинська Н.", "group": "6", "status": "lamb" },
{ "name": "Носевич І.", "group": "6", "status": "lamb" },
{ "name": "Носевич Т.", "group": "6", "status": "lamb" },
{ "name": "Страшок М.", "group": "6", "status": "elder" },
{ "name": "Страшок О.", "group": "6", "status": "lamb" },
{ "name": "Мельник О.", "group": "6", "status": "lamb" },
{ "name": "Бугайов Д.", "group": "7", "status": "lamb" },
{ "name": "Гергель Л.", "group": "7", "status": "lamb" },
{ "name": "Гергель О.", "group": "7", "status": "lamb" },
{ "name": "Горун А.", "group": "7", "status": "lamb" },
{ "name": "Ковчук Л.", "group": "7", "status": "lamb" },
{ "name": "Ковчук Н.", "group": "7", "status": "lamb" },
{ "name": "Михайлів С.", "group": "7", "status": "lamb" },
{ "name": "Музика І.", "group": "7", "status": "lamb" },
{ "name": "Музика О.", "group": "7", "status": "lamb" },
{ "name": "Наворинський Р.", "group": "7", "status": "lamb" },
{ "name": "Парила І.", "group": "7", "status": "lamb" },
{ "name": "Резніченко А.", "group": "7", "status": "elder" },
{ "name": "Резніченко В.", "group": "7", "status": "lamb" },
{ "name": "Семчишин Ф.", "group": "7", "status": "elder" },
{ "name": "Сувалко В.", "group": "7", "status": "elder" },
{ "name": "Сувалко Н.", "group": "7", "status": "lamb" },
{ "name": "Якубович Л.", "group": "7", "status": "lamb" }
];
// Подключение к базе данных (или создание, если не существует)
const db = new sqlite3.Database('../database.sqlite', (err) => {
if (err) {
console.error('Ошибка при подключении к БД:', err.message);
} else {
console.log('Подключение к SQLite успешно');
}
});
// Вставка данных
const insertData = () => {
const stmt = db.prepare("INSERT INTO sheep (name, group_id, appointment, hash) VALUES (?, ?, ?, ?)");
data.forEach(user => {
stmt.run(user.name, user.group, user.status, crypto.randomUUID());
});
stmt.finalize();
console.log('Данные успешно записаны');
};
// Запуск вставки данных после создания таблицы
insertData();
// Закрытие соединения с БД
db.close((err) => {
if (err) {
console.error('Ошибка при закрытии БД:', err.message);
} else {
console.log('Соединение с БД закрыто');
}
});

161
scripts/migrator.js Normal file
View File

@@ -0,0 +1,161 @@
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('database.sqlite');
const db_old = new sqlite3.Database('old_db.sqlite');
// Створення підїздів
// const sql_1 = `SELECT * FROM areas`;
// db_old.all(sql_1, [], (err, areas) => {
// if (err) {
// throw err;
// }
// for (let i = 0; i < areas.length; i++) {
// const area = areas[i];
// if(area.type == "house"){
// // console.log(JSON.parse(area.entrance));
// db.get('SELECT * FROM house WHERE title = ? AND number = ?', [area.address_title, area.address_number], (err, house) => {
// if (err) {
// console.error(err.message);
// } else {
// let entrances = JSON.parse(area.entrance);
// let entrance_numbers = JSON.parse(area.entrance_number);
// for (let q = 0; q < entrances.length; q++) {
// const entrance = entrances[q];
// const number = entrance_numbers[q];
// console.log(entrance, number);
// db.run(`INSERT INTO entrance(house_id, entrance_number, title, points, points_number) VALUES(?, ?, ?, ?, ?)`,
// [
// house.id,
// q,
// `Під'їзд ${q+1}`,
// JSON.stringify(entrance),
// JSON.stringify(number)
// ],
// function (err) {
// if (err) {
// console.error(err.message);
// } else {
// console.log("New user entrance added with id " + this.lastID);
// }
// }
// );
// }
// // console.log(JSON.parse(area.entrance));
// }
// })
// }
// }
// });
// Міграція історії з старої БД в нову
// const sql_1 = `SELECT * FROM history ORDER BY date_start`;
// db_old.all(sql_1, [], (err, historys) => {
// if (err) {
// throw err;
// }
// for (let i = 0; i < historys.length; i++) {
// const history = historys[i];
// db_old.get('SELECT * FROM territory WHERE number = ?', [history.territory_number], (err, territory) => {
// if (err) {
// console.error(err.message);
// } else {
// // console.log(territory);
// let areas_id = JSON.parse(territory.areas_id)
// for (let index = 0; index < areas_id.length; index++) {
// const element = areas_id[index];
// db_old.get('SELECT * FROM areas WHERE id = ?', [element[0]], (err, area) => {
// if (err) {
// console.error(err.message);
// } else {
// if (area.type == "house") {
// console.log(area.address_title, area.address_number);
// db.get('SELECT * FROM house WHERE title = ? AND number = ?', [area.address_title, area.address_number], (err, house) => {
// if (err) {
// console.error(err.message);
// } else {
// console.log(area.address_title, area.address_number);
// console.log(house.id, element[1]);
// db.get('SELECT * FROM entrance WHERE house_id = ? AND entrance_number = ?', [house.id, element[1]], (err, entrance) => {
// if (err) {
// console.error(err.message);
// } else {
// console.log(house.id, element[1]);
// console.log(entrance.id, house.title, house.number, entrance.title);
// db.run(`INSERT INTO entrance_history(entrance_id, name, date_start, date_end, group_number, working) VALUES(?, ?, ?, ?, ?, ?)`,
// [
// entrance.id,
// history.name,
// history.date_start,
// history.date_end,
// history.group_number,
// history.working,
// ],
// function (err) {
// if (err) {
// console.error(err.message);
// } else {
// console.log("New user history added with id " + this.lastID);
// }
// }
// );
// }
// })
// }
// })
// } else {
// db.get('SELECT * FROM homestead WHERE title = ? AND number = ?', [area.address_title, area.address_number], (err, homestead) => {
// if (err) {
// console.error(err.message);
// } else {
// // console.log(house.id, house.title, house.number);
// db.run(`INSERT INTO homestead_history(homestead_id, name, date_start, date_end, group_number, working) VALUES(?, ?, ?, ?, ?, ?)`,
// [
// homestead.id,
// history.name,
// history.date_start,
// history.date_end,
// history.group_number,
// history.working,
// ],
// function (err) {
// if (err) {
// console.error(err.message);
// } else {
// console.log("New user history added with id " + this.lastID);
// }
// }
// );
// }
// })
// }
// }
// })
// }
// }
// })
// }
// });

14
scripts/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "import",
"version": "1.0.0",
"main": "import.js",
"scripts": {
"start": "node import.js"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"sqlite3": "^5.1.7"
}
}

181
scripts/updateDB.py Normal file
View File

@@ -0,0 +1,181 @@
import sqlite3
def table_exists(cursor, table_name):
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?;", (table_name,))
return cursor.fetchone() is not None
def create_tables():
conn = sqlite3.connect("database.sqlite")
cursor = conn.cursor()
tables = {
"sheep": """
CREATE TABLE IF NOT EXISTS sheep (
id INTEGER PRIMARY KEY,
group_id INTEGER,
name TEXT,
icon TEXT,
hash TEXT,
appointment TEXT DEFAULT 'lamb',
mode INTEGER DEFAULT 0,
cu_access INTEGER DEFAULT 0,
mt_access INTEGER DEFAULT 0,
ct_access INTEGER DEFAULT 0,
ms_access INTEGER DEFAULT 0,
sm_access INTEGER DEFAULT 0,
FOREIGN KEY (group_id) REFERENCES groups(id)
);""",
"groups": """
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY,
group_number INTEGER,
share_hash TEXT
);""",
"subscription": """
CREATE TABLE IF NOT EXISTS subscription (
id INTEGER PRIMARY KEY,
sheep_id INTEGER,
token TEXT,
FOREIGN KEY (sheep_id) REFERENCES sheep(id)
);""",
"house": """
CREATE TABLE IF NOT EXISTS house (
id INTEGER PRIMARY KEY,
group_id INTEGER,
title TEXT,
number TEXT,
points TEXT DEFAULT '[]',
points_number TEXT DEFAULT '[]',
geo TEXT DEFAULT '[]',
osm_id TEXT DEFAULT '[]',
settlement TEXT,
description TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES groups(id)
);""",
"entrance": """
CREATE TABLE IF NOT EXISTS entrance (
id INTEGER PRIMARY KEY,
house_id INTEGER,
entrance_number INTEGER,
title TEXT,
points TEXT DEFAULT '[]',
points_number TEXT DEFAULT '[]',
floors_quantity TEXT,
apartments_quantity TEXT,
description TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (house_id) REFERENCES house(id)
);""",
"entrance_history": """
CREATE TABLE IF NOT EXISTS entrance_history (
id INTEGER PRIMARY KEY,
entrance_id INTEGER,
name TEXT,
date_start TIMESTAMP,
date_end TIMESTAMP,
group_number INTEGER,
sheep_id TEXT,
working INTEGER DEFAULT 0,
FOREIGN KEY (entrance_id) REFERENCES entrance(id),
FOREIGN KEY (sheep_id) REFERENCES sheep(id)
);""",
"apartments": """
CREATE TABLE IF NOT EXISTS apartments (
id INTEGER PRIMARY KEY,
entrance_id INTEGER,
apartment_number INTEGER,
title TEXT,
floors_number INTEGER,
status INTEGER,
description TEXT,
sheep_id TEXT,
updated_at TIMESTAMP,
FOREIGN KEY (entrance_id) REFERENCES entrance(id),
FOREIGN KEY (sheep_id) REFERENCES sheep(id)
);""",
"apartments_history": """
CREATE TABLE IF NOT EXISTS apartments_history (
id INTEGER PRIMARY KEY,
apartments_id INTEGER,
status INTEGER,
description TEXT,
sheep_id TEXT,
created_at TIMESTAMP,
FOREIGN KEY (apartments_id) REFERENCES apartments(id)
);""",
"homestead": """
CREATE TABLE IF NOT EXISTS homestead (
id INTEGER PRIMARY KEY,
group_id INTEGER,
title TEXT,
number TEXT,
points TEXT DEFAULT '[]',
point_icons TEXT DEFAULT '[]',
geo TEXT DEFAULT '[]',
osm_id TEXT DEFAULT '[]',
settlement TEXT,
description TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES groups(id)
);""",
"homestead_history": """
CREATE TABLE IF NOT EXISTS homestead_history (
id INTEGER PRIMARY KEY,
homestead_id INTEGER,
name TEXT,
date_start TIMESTAMP,
date_end TIMESTAMP,
group_number INTEGER,
sheep_id TEXT,
working INTEGER DEFAULT 0,
FOREIGN KEY (homestead_id) REFERENCES homestead(id),
FOREIGN KEY (sheep_id) REFERENCES sheep(id)
);""",
"meetings_schedule": """
CREATE TABLE IF NOT EXISTS meetings_schedule (
id INTEGER PRIMARY KEY,
date TIMESTAMP,
type INTEGER,
name TEXT,
sheep_id TEXT,
title TEXT,
number TEXT,
FOREIGN KEY (sheep_id) REFERENCES sheep(id)
);""",
"stand_list": """
CREATE TABLE IF NOT EXISTS stand_list (
id INTEGER PRIMARY KEY,
title TEXT,
hour_start INTEGER DEFAULT 10,
hour_end INTEGER DEFAULT 16,
quantity_sheep INTEGER DEFAULT 2,
week_days TEXT DEFAULT '[0, 1, 2, 3, 4, 5, 6]'
);""",
"stand_schedule": """
CREATE TABLE IF NOT EXISTS stand_schedule (
id INTEGER PRIMARY KEY,
stand INTEGER,
date TIMESTAMP,
hour INTEGER,
sheep_id TEXT,
number_sheep TEXT,
updated_at TIMESTAMP,
FOREIGN KEY (sheep_id) REFERENCES sheep(id),
FOREIGN KEY (stand) REFERENCES stand_list(id)
);"""
}
for name, sql in tables.items():
if not table_exists(cursor, name):
cursor.execute(sql)
conn.commit()
conn.close()
if __name__ == "__main__":
create_tables()
print("Database setup complete.")

13
web/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM node:20.18
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
EXPOSE 4002
CMD npm start

4
web/config.js Normal file
View File

@@ -0,0 +1,4 @@
const CONFIG = {
"api": "https://sheep-service.com/api/",
"wss": "wss://sheep-service.com/ws"
}

551
web/css/main.css Normal file
View File

@@ -0,0 +1,551 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
@media (prefers-color-scheme: light) {
:root {
/* PrimaryColor */
--PrimaryColor: #28a55a;
--PrimaryColor: #f2bd53;
--PrimaryColorText: #2e2e2e;
/* BGColor */
--ColorThemes0: #fbfbfb;
--ColorThemes1: #f3f3f3;
--ColorThemes2: #e5e5df;
/* TextColor */
--ColorThemes3: #313131;
--ColorAnimation: linear-gradient(90deg, #f3f3f3, #efefef, #f3f3f3);
--shadow-l1: 0px 2px 4px rgba(0, 0, 0, 0.02), 0px 0px 2px rgba(0, 0, 0, 0.04), 0px 0px 1px rgba(0, 0, 0, 0.04);
--border-radius: 15px;
--CardAnimation: linear-gradient(to right, #fbfbfb 0%, #fbfbfb 30%, #d8d8d8 45%, #d8d8d8 50%, #fbfbfb 60%, #fbfbfb 100%);
}
}
@media (prefers-color-scheme: dark) {
:root {
/* PrimaryColor */
--PrimaryColor: #28a55a;
--PrimaryColor: #cb9e44;
--PrimaryColorText: #2e2e2e;
/* BGColor */
--ColorThemes0: #1c1c19;
--ColorThemes1: #21221d;
--ColorThemes2: #3a3a39;
/* TextColor */
--ColorThemes3: #f3f3f3;
--ColorAnimation: linear-gradient(90deg, #21221d, #242520, #21221d);
--shadow-l1: 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 0px 2px rgba(0, 0, 0, 0.06), 0px 0px 1px rgba(0, 0, 0, 0.04);
--border-radius: 15px;
--CardAnimation: linear-gradient(to right, #1c1c19 0%, #1c1c19 30%, #252525 45%, #252525 50%, #1c1c19 60%, #1c1c19 100%);
}
}
* {
border: 0;
padding: 0;
font-family: 'Roboto', sans-serif;
margin: 0;
font-weight: 300;
outline: none;
}
*:disabled {
opacity: 0.6 !important;
cursor: no-drop !important;
}
@media (min-width: 800px) {
* {
scroll-snap-type: none !important;
}
}
@media (pointer:fine) {
::-webkit-scrollbar {
width: 7px;
height: 7px;
background-color: #f9f9fd;
border-radius: var(--border-radius);
background: 0;
}
::-webkit-scrollbar-thumb {
background: var(--PrimaryColor);
border-radius: var(--border-radius);
}
::-webkit-scrollbar-track {
background: 0;
border: 0;
margin: 0;
border-radius: var(--border-radius);
}
}
a {
text-decoration: none;
font-size: 15px;
font-weight: 700;
cursor: pointer;
}
body {
background-color: var(--ColorThemes2);
color: var(--ColorThemes3);
/* transition: .3s ease; */
}
/* Стили для анимации обновления страници свайпом */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
select {
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
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%23F2BD53%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.7rem top 50%;
background-size: 0.65rem auto;
}
.hold-button{
user-select: none;
}
.custom-checkbox {
position: absolute;
z-index: -1;
opacity: 0;
}
.custom-checkbox+label {
display: inline-flex;
align-items: center;
user-select: none;
}
.custom-checkbox+label::before {
content: '';
display: inline-block;
width: 25px;
height: 25px;
flex-shrink: 0;
flex-grow: 0;
border: 1px solid #adb5bd;
border-radius: 10px;
margin-right: 0.5em;
background-repeat: no-repeat;
background-position: center center;
background-size: 50% 50%;
}
.custom-checkbox:checked+label::before {
border-color: var(--PrimaryColor);
background-color: var(--PrimaryColor);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
}
.custom-checkbox:not(:disabled):not(:checked)+label:hover::before {
border-color: var(--PrimaryColor);
}
.custom-checkbox:not(:disabled):active+label::before {
background-color: var(--PrimaryColor);
border-color: var(--PrimaryColor);
}
.custom-checkbox:focus:not(:checked)+label::before {
border-color: var(--PrimaryColor);
}
.custom-checkbox:disabled+label::before {
background-color: #e9ecef;
}
#swipe_updater {
position: absolute;
top: 0px;
width: 100%;
}
#swipe_block {
width: calc(100% - 252px);
margin-left: 252px;
height: 50px;
display: flex;
justify-content: center;
align-items: flex-end;
position: relative;
}
#swipe_icon {
width: 20px;
fill: var(--ColorThemes3);
transform: rotate(0deg);
position: absolute;
margin-top: -45px;
top: -45px;
background: var(--ColorThemes2);
border: 2px solid var(--ColorThemes3);
border-radius: 50%;
padding: 10px;
display: flex;
overflow: hidden;
height: 0;
opacity: 0;
transition: height 0ms 400ms, opacity 400ms 0ms;
}
#swipe_icon[data-state="active"] {
height: 20px;
opacity: 1;
transition: height 0ms 0ms, opacity 400ms 0ms;
}
@media (max-width: 1100px) {
#swipe_block {
width: calc(100% - 122px);
margin-left: 122px;
}
}
@media (max-width: 700px) {
#swipe_block {
width: 100%;
margin-left: 0;
}
}
/* Уведомление и кнопка обновления приложения */
#update_banner {
height: 55px;
transition: .3s ease;
}
#update_banner .content {
margin: 0 auto;
max-width: 300px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#update_banner .headline {
font-weight: 800;
font-size: 15px;
color: var(--PrimaryColorText);
font-family: 'Roboto', sans-serif;
}
#update_banner .subhead {
font-size: 13px;
text-align: center;
color: var(--PrimaryColorText);
font-family: 'Roboto', sans-serif;
}
#update_banner[data-state="noupdate"] {
display: none;
}
#update_banner[data-state="updateavailable"] {
display: block;
cursor: pointer;
background-color: var(--PrimaryColor);
color: var(--PrimaryColorText);
transition: .3s ease;
opacity: 0.95;
z-index: 9999;
position: fixed;
width: 300px;
margin: 10px;
padding: 5px;
border-radius: 25px;
right: 0;
}
#update_banner_icon {
display: none;
}
#update_banner_icon svg {
padding: 0px 50px;
width: 25px;
margin: -4px;
fill: var(--PrimaryColorText);
}
@media (max-width: 700px) {
#update_banner[data-state="updateavailable"] {
width: calc(100% - 30px);
}
}
/* Стили для меню */
#navigation {
position: fixed;
width: 230px;
height: calc(100vh - 60px);
min-height: 510px;
background: var(--ColorThemes2);
padding: 36px 10px;
-webkit-transition: width .2s ease 0s;
-o-transition: width .2s ease 0s;
transition: width .2s ease 0s;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#navigation>nav {
display: flex;
flex-direction: column;
height: 290px;
align-items: center;
justify-content: flex-start;
}
#navigation>nav>li {
width: 180px;
height: 50px;
list-style-type: none;
position: relative;
-webkit-transition: all .2s ease 0s;
-o-transition: all .2s ease 0s;
transition: all .2s ease 0s;
z-index: 1;
margin: 5px;
}
#navigation>nav>li>div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-start;
height: 50px;
padding: 0 10px;
border-radius: var(--border-radius);
-webkit-transition: all .2s ease 0s;
-o-transition: all .2s ease 0s;
transition: all .2s ease 0s;
opacity: 0.8;
cursor: pointer;
border: 2px;
border: 2px solid var(--ColorThemes2);
overflow: hidden;
}
#navigation>nav>li>div[data-state="active"] {
background: var(--ColorThemes3);
border: 2px solid var(--ColorThemes3);
box-shadow: var(--shadow-l1);
}
#navigation>nav>li:has(div[data-state="active"]) {
z-index: 10;
}
#navigation>nav>li>div>svg {
width: 25px;
height: 25px;
min-width: 25px;
min-height: 25px;
fill: var(--ColorThemes3);
}
#navigation>nav>li>div[data-state="active"] svg {
fill: var(--ColorThemes2);
}
#navigation>nav>li>div>b {
margin-left: 15px;
font-size: 14px;
font-weight: 300;
color: var(--ColorThemes3);
white-space: nowrap;
}
#navigation>nav>li>div[data-state="active"] b {
color: var(--ColorThemes2);
}
#navigation>nav>li>a {
position: absolute;
width: 100%;
height: 100%;
top: 2px;
}
@media (hover: hover) {
#navigation>nav>li:hover {
transform: scale(1.01);
}
#navigation>nav>li:hover>div {
border: 2px solid var(--ColorThemes3);
}
}
@media (max-width: 1100px) {
#navigation {
width: 100px;
}
#navigation>nav>li {
width: 50px;
}
#navigation>nav>li>div {
width: 30px;
justify-content: center;
}
#navigation>nav>li>div>b {
display: none;
}
}
#app {
background: var(--ColorThemes0);
position: absolute;
margin-top: 40px;
margin-left: 250px;
padding: 5px;
width: calc(100% - 250px - 10px);
min-height: calc(100% - 50px);
border-radius: 20px 0 0 0;
-webkit-transition: all .2sease 0s;
-o-transition: all .2s ease 0s;
transition: all .2sease 0s;
box-shadow: var(--shadow-l1);
display: flex;
}
@media (max-width: 1100px) {
#app {
margin-left: 120px;
width: calc(100% - 130px);
}
}
@media (max-width: 700px),
(max-height: 540px) {
body {
background: var(--ColorThemes0);
}
#navigation {
width: 100%;
height: 60px;
min-height: 60px;
padding: 0;
z-index: 99999990;
bottom: -1px;
border: 0;
margin: 0;
box-shadow: 0px 24px 32px rgba(0, 0, 0, .09), 0px 16px 24px rgba(0, 0, 0, .09), 0px 4px 8px rgba(0, 0, 0, .09), 0px 0px 1px rgba(0, 0, 0, .09);
}
#navigation>nav {
display: flex;
flex-direction: row;
height: 100%;
justify-content: space-around;
align-items: center;
}
#navigation>nav>li {
width: 40px;
height: 40px;
}
#navigation>nav>li:hover {
transform: scale(1.0);
}
#navigation>nav>li:hover>div {
border: 0;
box-shadow: none;
}
#navigation>nav>li>div {
width: 100%;
height: 40px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: 0;
box-shadow: none;
}
#navigation>nav>li>div>span {
display: none;
}
#navigation>nav>li>div[data-state="active"] {
background: 0;
border: 0;
box-shadow: none;
}
#navigation>nav>li>div[data-state="active"]>svg {
fill: var(--PrimaryColor);
}
#navigation>nav>li>button {
display: none;
}
#navigation[data-state="ios"] {
height: 70px;
}
#navigation[data-state="ios"]>nav {
align-items: flex-start;
}
#app {
margin-left: 0px;
width: 100%;
border-radius: 0;
padding: 0;
margin-top: 0;
padding-bottom: 80px;
height: auto;
}
}

94
web/fonts/PT_Mono/OFL.txt Normal file
View File

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

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

41
web/img/0.svg Normal file
View File

@@ -0,0 +1,41 @@
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" inkscape:export-ydpi="96" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" inkscape:export-xdpi="96" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" height="48" xmlns:svg="http://www.w3.org/2000/svg" style="fill:none;stroke:none;" viewBox="0 0 48 48" version="1.1">
<defs/>
<sodipodi:namedview pagecolor="#ffffff" inkscape:pagecheckerboard="true" borderlayer="true" bordercolor="#666666" inkscape:document-units="px"/>
<title>synchronize-cloud</title>
<g inkscape:groupmode="layer" id="MainComposition_a4ec5bcbf1d44168962622fcea8276bf" inkscape:label="synchronize-cloud">
<g inkscape:groupmode="layer" transform="matrix(1, 0, 0, 1, 0, 0)" opacity="1" id="Layer_9907dd6ff6004617bd3a525195c313f2" inkscape:label="arrows">
<g transform="translate(24 29)">
<g transform="rotate(0)">
<g transform="scale(1 1)">
<g transform="translate(-24 -29)">
<g opacity="1" id="Group_2cb26585a6274aff82b52d6629f47ff3" inkscape:label="Group 1">
<g transform="matrix(1, 0, 0, 1, 18.772, 33.991)" opacity="1" id="Stroke_eac34415491147c99ffc4335fed73f62" stroke="#000000" stroke-width="3" stroke-opacity="1" inkscape:label="Group 4">
<path sodipodi:nodetypes="ccc" 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"/>
</path>
</g>
<g transform="matrix(1, 0, 0, 1, 24.037, 34)" opacity="1" id="Stroke_808239d21b7e4a859d5a99d0baf98e17" stroke="#000000" stroke-width="3" stroke-opacity="1" inkscape:label="Group 3">
<path sodipodi:nodetypes="ccc" 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;"/>
</g>
<g transform="matrix(1, 0, 0, 1, 29.227, 24.009)" opacity="1" id="Stroke_c1f02aae6dea4564b6abe25c038848fe" stroke="#000000" stroke-width="3" stroke-opacity="1" inkscape:label="Group 2">
<path sodipodi:nodetypes="ccc" 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"/>
</path>
</g>
<g transform="matrix(1, 0, 0, 1, 23.963, 24)" opacity="1" id="Stroke_b8202dc468474015b6784aeccf30c7fe" stroke="#000000" stroke-width="3" stroke-opacity="1" inkscape:label="Group 1">
<path sodipodi:nodetypes="ccc" 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;"/>
</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"/>
</g>
</g>
</g>
<g inkscape:groupmode="layer" transform="matrix(1, 0, 0, 1, 0, 0)" opacity="1" id="Layer_27939f0fe5ec41fa8de333c2fda4c3f8" inkscape:label="cloud">
<g fill-opacity="1" transform="matrix(1, 0, 0, 1, 24, 22.5)" opacity="1" id="Fill_2eda0ab38e6f42adb64be815c093c02c" fill="#000000" inkscape:label="Group 1">
<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=""/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
web/img/1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

1
web/img/1.json Normal file

File diff suppressed because one or more lines are too long

6
web/img/1.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

BIN
web/img/2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
web/img/IconKitchen.zip Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
web/img/badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
web/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
web/img/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
web/img/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
web/img/icons/info.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

1
web/img/icons/info.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="#1A1A1A" 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"/></svg>

After

Width:  |  Height:  |  Size: 606 B

BIN
web/img/map/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

3
web/img/sheep.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path fill="#f2bd53" d="M 13 5 C 11.828237 5 10.793477 5.4713326 9.9394531 6.1503906 C 9.6361713 6.0843037 9.3462112 6 9 6 C 6.2504839 6 4 8.2504839 4 11 C 4 11.08059 4.0124881 11.1121 4.0175781 11.181641 C 2.9210459 12.039325 2.1098117 13.257229 2.0175781 14.726562 L 1.1054688 16.552734 A 1.0001165 1.0001165 0 0 0 2.7675781 17.640625 C 3.2526418 18.411838 3.9424236 19.03683 4.7597656 19.449219 L 5 19.675781 L 5 25 A 1.0001 1.0001 0 1 0 7 25 L 7 19.246094 A 1.0001 1.0001 0 0 0 6.6875 18.519531 L 6.046875 17.914062 A 1.0001 1.0001 0 0 0 5.7714844 17.728516 C 4.7470246 17.266071 4.0294077 16.259842 4.0039062 15.064453 A 1.0001 1.0001 0 0 0 4.0039062 14.939453 C 4.0261116 13.839378 4.6387768 12.906535 5.5429688 12.398438 A 1.0001 1.0001 0 0 0 6.0449219 11.394531 C 6.0183372 11.193412 6 11.068889 6 11 C 6 9.3315161 7.3315161 8 9 8 C 9.2766049 8 9.5584058 8.0531018 9.8652344 8.1464844 A 1.0001 1.0001 0 0 0 10.867188 7.8925781 C 11.414334 7.3394013 12.160136 7 13 7 C 13.943745 7 14.765257 7.4389147 15.322266 8.1289062 A 1.0001 1.0001 0 0 0 16.578125 8.3789062 C 17.019891 8.1381558 17.492971 8 18 8 C 19.125679 8 20.081212 8.6201548 20.599609 9.5390625 A 1.0001 1.0001 0 0 0 21.353516 10.039062 C 22.531272 10.179159 23.485346 10.980923 23.841797 12.068359 C 22.824708 11.859395 21.724766 12.146328 20.935547 12.935547 C 19.687547 14.183547 19.687547 16.207078 20.935547 17.455078 C 21.055851 17.575382 21.216262 17.686732 21.376953 17.796875 C 20.836332 18.524365 19.985264 19 19 19 C 18.492971 19 18.019891 18.861844 17.578125 18.621094 A 1.0001 1.0001 0 0 0 16.322266 18.871094 C 15.765471 19.56158 14.944275 20 14 20 C 13.291552 20 12.65587 19.746025 12.128906 19.320312 A 1.0001 1.0001 0 0 0 10.871094 19.320312 C 10.34413 19.746025 9.7084483 20 9 20 A 1.0001 1.0001 0 1 0 9 22 L 9 25 A 1.0001 1.0001 0 1 0 11 25 L 11 21.505859 C 11.170848 21.422563 11.337991 21.33349 11.5 21.236328 C 12.238737 21.67937 13.068046 22 14 22 C 14.730164 22 15.395058 21.803876 16 21.498047 L 16 25 A 1.0001 1.0001 0 1 0 18 25 L 18 20.806641 C 18.316031 20.913616 18.640829 21 19 21 C 19.343981 21 19.67546 20.953638 20 20.886719 L 20 25 A 1.0001 1.0001 0 1 0 22 25 L 22 20 A 1.0001 1.0001 0 0 0 22 19.974609 C 22.486448 19.603854 22.905789 19.152819 23.228516 18.630859 C 24.642494 19.048643 26.176371 19.140035 26.658203 18.658203 C 27.339744 17.976663 26.884402 15.198742 25.958984 13.625 A 1.0001 1.0001 0 0 0 25.966797 13.570312 C 25.987917 13.383949 26 13.19371 26 13 C 26 10.589178 24.248068 8.6335533 21.970703 8.171875 C 21.067822 6.9086944 19.672766 6 18 6 C 17.386577 6 16.866243 6.2341923 16.335938 6.4355469 C 15.448182 5.6006824 14.319642 5 13 5 z M 17.421875 12.279297 C 17.32925 12.296094 17.237609 12.342875 17.162109 12.421875 C 16.614109 12.997875 16 13.891 16 15 C 16 15.567 16.471922 16.023047 17.044922 15.998047 C 17.590922 15.974047 18 15.481547 18 14.935547 L 18 12.763672 C 18 12.435922 17.69975 12.228906 17.421875 12.279297 z M 23 14 C 23.552 14 24 14.448 24 15 C 24 15.552 23.552 16 23 16 C 22.448 16 22 15.552 22 15 C 22 14.448 22.448 14 23 14 z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

223
web/index.html Normal file
View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0" /> -->
<meta
name="viewport"
content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0"
/>
<!-- Start Single Page Apps for GitHub Pages -->
<script>
(function(){
var redirect = sessionStorage.redirect;
delete sessionStorage.redirect;
if (redirect && redirect != location.href) {
history.replaceState(null, null, redirect);
}
})();
</script>
<!-- End Single Page Apps for GitHub Pages -->
<title>Sheep Service</title>
<link rel="icon" href="/img/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<meta
name="theme-color"
content="#e5e5df"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="#252523"
media="(prefers-color-scheme: dark)"
/>
<!-- Конфигурация SW -->
<script src="/sw.js"></script>
<link rel="stylesheet" href="/css/main.css" />
<script src="/config.js" defer></script>
<script src="/lib/router/router.js" defer></script>
<script src="/lib/router/routes.js" defer></script>
<!-- Подключение стилей Leaflet.js -->
<link rel="stylesheet" href="/lib/components/leaflet/leaflet.css" />
<script src="/lib/components/leaflet/leaflet.js"></script>
<link rel="stylesheet" href="/lib/components/geoman/leaflet-geoman.css" />
<script src="/lib/components/geoman/leaflet-geoman.min.js"></script>
<script src="/lib/components/turf.min.js" defer></script>
<script src="/lib/components/clipboard.js" defer></script>
<script src="/lib/components/colorGroup.js" defer></script>
<script src="/lib/components/makeid.js" defer></script>
<script src="/lib/components/swipeUpdater.js" defer></script>
<script src="/lib/components/detectBrowser.js" defer></script>
<script src="/lib/components/detectOS.js" defer></script>
<script src="/lib/components/formattedDate.js" defer></script>
<script src="/lib/pages/home/script.js" defer></script>
<link href="/lib/pages/home/style.css" rel="stylesheet" />
<script src="/lib/pages/territory/script.js" defer></script>
<link href="/lib/pages/territory/style.css" rel="stylesheet" />
<script src="/lib/pages/territory_manager/script.js" defer></script>
<link href="/lib/pages/territory_manager/style.css" rel="stylesheet" />
<script src="/lib/pages/card/script.js" defer></script>
<link href="/lib/pages/card/style.css" rel="stylesheet" />
<script src="/lib/pages/options/script.js" defer></script>
<link href="/lib/pages/options/style.css" rel="stylesheet" />
<script src="/lib/pages/sheeps/script.js" defer></script>
<link href="/lib/pages/sheeps/style.css" rel="stylesheet" />
<script src="/lib/pages/constructor/script.js" defer></script>
<link href="/lib/pages/constructor/style.css" rel="stylesheet" />
<script src="/lib/pages/editor/script.js" defer></script>
<link href="/lib/pages/editor/style.css" rel="stylesheet" />
<script src="/lib/pages/stand/script.js" defer></script>
<link href="/lib/pages/stand/style.css" rel="stylesheet" />
<script src="/lib/app.js" defer></script>
</head>
<body>
<!-- Кнопка сповіщення та оновлення додатків -->
<div id="update_banner" data-state="noupdate">
<div class="content">
<div class="headline"></div>
<div class="subhead"></div>
<div id="update_banner_icon">
<svg
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.0"
width="64px"
height="64px"
viewBox="0 0 128 128"
xml:space="preserve"
>
<g>
<path
d="M64 9.75A54.25 54.25 0 0 0 9.75 64H0a64 64 0 0 1 128 0h-9.75A54.25 54.25 0 0 0 64 9.75z"
></path>
<animateTransform
attributeName="transform"
type="rotate"
from="0 64 64"
to="360 64 64"
dur="1400ms"
repeatCount="indefinite"
></animateTransform>
</g>
</svg>
</div>
</div>
</div>
<!-- Анімація оновлення сторінки свайпом -->
<div id="swipe_updater">
<div id="swipe_block">
<svg
id="swipe_icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
data-state="active"
>
<path
d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z"
></path>
</svg>
</div>
</div>
<!-- Меню застосунку -->
<div id="navigation">
<nav>
<li>
<div id="nav-home">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
d="M9 2H4C2.897 2 2 2.897 2 4v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C11 2.897 10.103 2 9 2zM20 2h-5c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V4C22 2.897 21.103 2 20 2zM9 15H4c-1.103 0-2 .897-2 2v3c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-3C11 15.897 10.103 15 9 15zM20 11h-5c-1.103 0-2 .897-2 2v7c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2v-7C22 11.897 21.103 11 20 11z"
/>
</svg>
<b>Головна</b>
</div>
<a href="/" data-route></a>
</li>
<li id="li-territory" style="display: none;">
<div id="nav-territory">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M24 2H14c-.55 0-1 .45-1 1v4l3.6 2.7c.25.19.4.49.4.8V14h8V3C25 2.45 24.55 2 24 2zM15.5 7C15.22 7 15 6.78 15 6.5v-2C15 4.22 15.22 4 15.5 4h2C17.78 4 18 4.22 18 4.5v2C18 6.78 17.78 7 17.5 7h-1.17H15.5zM23 4.5v2C23 6.78 22.78 7 22.5 7h-2C20.22 7 20 6.78 20 6.5v-2C20 4.22 20.22 4 20.5 4h2C22.78 4 23 4.22 23 4.5zM22.5 12h-2c-.28 0-.5-.22-.5-.5v-2C20 9.22 20.22 9 20.5 9h2C22.78 9 23 9.22 23 9.5v2C23 11.78 22.78 12 22.5 12zM1 11.51V27c0 .55.45 1 1 1h12c.55 0 1-.45 1-1V11.51c0-.32-.16-.62-.42-.81l-6-4.28C8.41 6.29 8.2 6.23 8 6.23S7.59 6.29 7.42 6.42l-6 4.28C1.16 10.89 1 11.19 1 11.51zM6.5 20h-2C4.22 20 4 19.78 4 19.5v-2C4 17.22 4.22 17 4.5 17h2C6.78 17 7 17.22 7 17.5v2C7 19.78 6.78 20 6.5 20zM7 22.5v2C7 24.78 6.78 25 6.5 25h-2C4.22 25 4 24.78 4 24.5v-2C4 22.22 4.22 22 4.5 22h2C6.78 22 7 22.22 7 22.5zM6.5 15h-2C4.22 15 4 14.78 4 14.5v-2C4 12.22 4.22 12 4.5 12h2C6.78 12 7 12.22 7 12.5v2C7 14.78 6.78 15 6.5 15zM9.5 17h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 20 9 19.78 9 19.5v-2C9 17.22 9.22 17 9.5 17zM9 14.5v-2C9 12.22 9.22 12 9.5 12h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 15 9 14.78 9 14.5zM9.5 22h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2C9.22 25 9 24.78 9 24.5v-2C9 22.22 9.22 22 9.5 22zM17 17v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V17c0-.55-.45-1-1-1H18C17.45 16 17 16.45 17 17zM19.5 18h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 18.22 19.22 18 19.5 18zM27 18.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2C26.78 18 27 18.22 27 18.5zM26.5 26h-2c-.28 0-.5-.22-.5-.5v-2c0-.28.22-.5.5-.5h2c.28 0 .5.22.5.5v2C27 25.78 26.78 26 26.5 26zM19.5 23h2c.28 0 .5.22.5.5v2c0 .28-.22.5-.5.5h-2c-.28 0-.5-.22-.5-.5v-2C19 23.22 19.22 23 19.5 23z"
/>
</svg>
<b>Території</b>
</div>
<a href="/territory" data-route></a>
</li>
<li id="li-sheeps" style="display: none;">
<div id="nav-sheeps">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M 42.5 14 C 37.813 14 34 18.038 34 23 C 34 27.962 37.813 32 42.5 32 C 47.187 32 51 27.962 51 23 C 51 18.038 47.187 14 42.5 14 z M 21.5 17 C 16.813 17 13 21.038 13 26 C 13 30.962 16.813 35 21.5 35 C 26.187 35 30 30.962 30 26 C 30 21.038 26.187 17 21.5 17 z M 42.5 18 C 44.981 18 47 20.243 47 23 C 47 25.757 44.981 28 42.5 28 C 40.019 28 38 25.757 38 23 C 38 20.243 40.019 18 42.5 18 z M 42.498047 34.136719 C 37.579021 34.136719 33.07724 35.947963 30.054688 38.962891 C 27.67058 37.796576 24.915421 37.136719 22 37.136719 C 14.956 37.136719 8.8129375 40.942422 6.7109375 46.607422 C 5.7409375 49.220422 7.7121406 52 10.494141 52 L 33.505859 52 C 35.43112 52 36.95694 50.674804 37.404297 49 L 53.431641 49 C 56.437641 49 59.121453 45.844281 57.564453 42.613281 C 55.084453 37.463281 49.169047 34.136719 42.498047 34.136719 z M 42.5 38.136719 C 47.565 38.136719 52.171937 40.633609 53.960938 44.349609 C 54.119938 44.687609 53.741687 45 53.429688 45 L 36.544922 45 C 35.777257 43.585465 34.746773 42.317451 33.503906 41.234375 C 35.78496 39.306575 39.034912 38.136719 42.5 38.136719 z"
/>
</svg>
<b>Вісники</b>
</div>
<a href="/sheeps" data-route></a>
</li>
<li id="li-schedule" style="display: none;">
<div id="nav-schedule">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<path
d="M47 23c3.314 0 6 2.686 6 6v17c0 3.309-2.691 6-6 6H17c-3.309 0-6-2.691-6-6V29c0-3.314 2.686-6 6-6H47zM22 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 46 22 45.552 22 45zM22 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C21.552 39 22 38.552 22 38zM30 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 46 30 45.552 30 45zM30 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 39 30 38.552 30 38zM30 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C29.552 32 30 31.552 30 31zM38 45v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 46 38 45.552 38 45zM38 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 39 38 38.552 38 38zM38 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C37.552 32 38 31.552 38 31zM46 38v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 39 46 38.552 46 38zM46 31v-2c0-.552-.448-1-1-1h-2c-.552 0-1 .448-1 1v2c0 .552.448 1 1 1h2C45.552 32 46 31.552 46 31zM17 20c-2.308 0-4.407.876-6 2.305V18c0-3.309 2.691-6 6-6h30c3.309 0 6 2.691 6 6v4.305C51.407 20.876 49.308 20 47 20H17z"
/>
</svg>
<b>Графік зібрань</b>
</div>
<a href="/schedule" data-route></a>
</li>
<li id="li-stand" style="display: none;">
<div id="nav-stand">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path
d="M 6.9707031 4 C 6.8307031 4 6.6807813 4.039375 6.5507812 4.109375 L 2.5507812 6.109375 C 2.0607813 6.349375 1.859375 6.9492188 2.109375 7.4492188 C 2.349375 7.9392188 2.9492187 8.140625 3.4492188 7.890625 L 6.4902344 6.3691406 L 12.5 20.650391 C 12.73 21.180391 13.040156 21.650547 13.410156 22.060547 C 12.040156 22.340547 11 23.56 11 25 C 11 26.65 12.35 28 14 28 C 15.65 28 17 26.65 17 25 C 17 24.52 16.869922 24.070156 16.669922 23.660156 C 17.479922 23.740156 18.319141 23.639062 19.119141 23.289062 L 26.400391 20.099609 C 26.910391 19.889609 27.159219 19.310781 26.949219 18.800781 C 26.749219 18.290781 26.160391 18.040234 25.650391 18.240234 C 25.630391 18.250234 25.619609 18.259531 25.599609 18.269531 L 18.320312 21.460938 C 16.770312 22.130938 14.999609 21.429141 14.349609 19.869141 L 7.9199219 4.609375 C 7.7599219 4.229375 7.3807031 3.99 6.9707031 4 z M 21.359375 8.0605469 C 21.229375 8.0605469 21.100703 8.090625 20.970703 8.140625 L 13.609375 11.269531 C 13.099375 11.479531 12.860078 12.070078 13.080078 12.580078 L 16.029297 19.179688 C 16.249297 19.689688 16.829844 19.930937 17.339844 19.710938 L 24.710938 16.589844 C 25.210938 16.369844 25.450234 15.789297 25.240234 15.279297 L 22.279297 8.6699219 C 22.119297 8.2899219 21.749375 8.0605469 21.359375 8.0605469 z M 14 24 C 14.56 24 15 24.44 15 25 C 15 25.56 14.56 26 14 26 C 13.44 26 13 25.56 13 25 C 13 24.44 13.44 24 14 24 z"
/>
</svg>
<b>Графік стенду</b>
</div>
<a href="/stand" data-route></a>
</li>
<li>
<div id="nav-options">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 172 172">
<path
d="M75.18001,14.33333c-3.43283,0 -6.36736,2.42659 -7.02669,5.79492l-2.39355,12.28971c-5.8821,2.22427 -11.32102,5.33176 -16.097,9.25228l-11.78581,-4.05924c-3.2465,-1.118 -6.81841,0.22441 -8.53841,3.19141l-10.80599,18.72852c-1.71283,2.97417 -1.08945,6.74999 1.49772,9.00033l9.44824,8.21647c-0.49137,3.0197 -0.81185,6.09382 -0.81185,9.25228c0,3.15846 0.32048,6.23258 0.81185,9.25228l-9.44824,8.21647c-2.58717,2.25033 -3.21055,6.02616 -1.49772,9.00032l10.80599,18.72852c1.71283,2.97417 5.29191,4.31623 8.53841,3.2054l11.78581,-4.05924c4.77441,3.91806 10.21756,7.01501 16.097,9.23828l2.39355,12.28972c0.65933,3.36833 3.59386,5.79492 7.02669,5.79492h21.63998c3.43283,0 6.36735,-2.42659 7.02669,-5.79492l2.39356,-12.28972c5.88211,-2.22427 11.32102,-5.33176 16.097,-9.25227l11.78581,4.05924c3.2465,1.118 6.81841,-0.21724 8.53841,-3.1914l10.80599,-18.74252c1.71284,-2.97417 1.08945,-6.73599 -1.49772,-8.98633l-9.44824,-8.21647c0.49137,-3.0197 0.81185,-6.09382 0.81185,-9.25228c0,-3.15846 -0.32048,-6.23258 -0.81185,-9.25228l9.44824,-8.21647c2.58717,-2.25033 3.21056,-6.02616 1.49772,-9.00033l-10.80599,-18.72852c-1.71283,-2.97417 -5.29191,-4.31624 -8.53841,-3.2054l-11.78581,4.05924c-4.7744,-3.91806 -10.21755,-7.01501 -16.097,-9.23828l-2.39356,-12.28971c-0.65933,-3.36833 -3.59385,-5.79492 -7.02669,-5.79492zM86,57.33333c15.83117,0 28.66667,12.8355 28.66667,28.66667c0,15.83117 -12.8355,28.66667 -28.66667,28.66667c-15.83117,0 -28.66667,-12.8355 -28.66667,-28.66667c0,-15.83117 12.8355,-28.66667 28.66667,-28.66667z"
></path>
</svg>
<b>Опції</b>
</div>
<a href="/options" data-route></a>
</li>
</nav>
</div>
<!-- Блок контенту застосунка -->
<div id="app"></div>
</body>
</html>

303
web/js/app.js Normal file
View File

@@ -0,0 +1,303 @@
let socket, username;
let listEntrances = []
let listApartment = []
let statusD = document.getElementById("status");
let hashD = document.getElementById("hash");
const urlParams = new URLSearchParams(window.location.search);
const share_hash = urlParams.get('share_hash');
let house = urlParams.get('id') ?? 81;
let entrance = 1;
let group_number = 1;
const api = "https://sheep-service.com";
const wss = "wss://sheep-service.com/ws";
// let color_status = [
// "#000000",
// "#C16917",
// "#b10202",
// "#3d3d3d",
// "#11734b",
// "#6cc5fc",
// "#5a3286"
// ];
// let color_status = [
// ["#ffffff", "#000000"],
// ["#e7af32", "#ffffff"],
// ["#fc2a2a", "#ffffff"],
// ["#3d3d3d", "#ffffff"],
// ["#11a568", "#ffffff"],
// ["#6cc5fc", "#ffffff"],
// ["#b381eb", "#ffffff"]
// ];
let color_status = [
["#ffffff", "#eaebef"],
["#fbf1e0", "#ff8300"],
["#fce3e2", "#ff0000"],
["#e2e0e0", "#3d3d3d"],
["#d5e9dd", "#11a568"],
["#d7ebfa", "#3fb4fc"],
["#e8dbf5", "#b381eb"]
];
function makeid(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
function start(name) {
if (!hash) return;
hashD.innerText = `HASH: ${name}`
username = name;
socket = new WebSocket(`${wss}?share_hash=${share_hash}`);
let user_hash = localStorage.getItem('hash') ?? share_hash;
socket.onopen = function (e) {
console.log("[WebSocket | open] Соединение установлено");
statusD.innerText = "WebSocket | open";
const message = {
event: 'connection',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
hash: user_hash,
username: name,
data: {
id: 1,
entrance_id: 1,
apartment_number: "1",
group_number: 1,
status: 1,
description: "",
created_at: 1727541827,
updated_at: 1727541827
}
}
socket.send(JSON.stringify(message))
};
socket.onmessage = function (event) {
let data = JSON.parse(event.data)
if (data.event == 'connection') {
if (data.username == username) return
console.log(`Добавлен новый пользователь по имени ${data.username}`);
} else if (data.event == 'message') {
update(data);
if (data.username == username) return
console.log(`${data.username} пишет: `, data.data);
}
};
socket.onclose = function (event) {
if (event.wasClean) {
statusD.innerText = "WebSocket | close"
console.log(`[WebSocket | close] Соединение закрыто чисто, код=${event.code} причина=${event.reason}`);
} else {
statusD.innerText = "WebSocket | close"
console.log('[WebSocket | close] Соединение прервано');
// setTimeout(function() {
// start(username);
// }, 1000);
const result = confirm(`З'єднання розірвано! Перепідключитись?`);
if (result) {
getEntrances();
start(username);
}
}
};
socket.onerror = function (error) {
console.log(`[WebSocket | error]`);
statusD.innerText = "WebSocket | error"
};
}
function mess(entrance_number, id, date_type) {
console.log(id, listApartment[entrance_number]);
const pos = listApartment[entrance_number].map(e => e.id).indexOf(id);
let apartment = listApartment[entrance_number][pos];
console.log(pos, apartment);
let status = document.getElementById(`status_${id}`);
let description = document.getElementById(`description_${id}`);
let date = new Date(document.getElementById(`date_${id}`).value);
const timestamp = date.getTime();
apartment.description = description.value;
apartment.status = Number(status.value);
apartment.updated_at = date_type ? getTimeInSeconds(timestamp) : getTimeInSeconds(),
status.style.backgroundColor = color_status[status.value][0];
status.style.color = color_status[status.value][1];
status.style.border = `1px solid ${color_status[status.value][1]}`;
let user_hash = localStorage.getItem('hash') ?? share_hash;
let message = {
event: 'message',
id: getTimeInSeconds(),
date: getTimeInSeconds(),
hash: user_hash,
username: username,
data: {
id: apartment.id,
entrance_id: apartment.entrance_id,
apartment_number: apartment.apartment_number,
group_number: apartment.group_number,
status: apartment.status,
description: apartment.description,
updated_at: apartment.updated_at,
}
}
socket.send(JSON.stringify(message));
sort(apartment.id, apartment.entrance_id);
}
function update(message) {
if (!document.getElementById(`status_${message.data.id}`)) return;
let now = new Date(message.data.updated_at);
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
document.getElementById(`status_${message.data.id}`).style.backgroundColor = color_status[message.data.status][0];
document.getElementById(`status_${message.data.id}`).style.color = color_status[message.data.status][1];
document.getElementById(`status_${message.data.id}`).style.border = `1px solid ${color_status[message.data.status][1]}`;
document.getElementById(`status_${message.data.id}`).value = message.data.status;
document.getElementById(`description_${message.data.id}`).value = message.data.description;
document.getElementById(`date_${message.data.id}`).value = now.toISOString().slice(0, 16);
}
function getEntrances(house_id = house) {
let url = `${api}/api/house/${house_id}/entrances?share_hash=${share_hash}`;
fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
listEntrances = data;
document.getElementById('list').innerHTML = "";
for (let i = 0; i < listEntrances.length; i++) {
const element = listEntrances[i];
let status = () => {
if (element.history.name == "Групова") return '<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>'
else return '<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>'
}
document.getElementById('list').innerHTML += `
<details ${element.history.name == "Групова" ? "open" : "disabled"}>
<summary>
<p>${element.title}</p>
${status()}
</summary>
<div id="apartments_${element.id}" class="apartments_list">
</div>
</details>
`;
getApartment(element.id, element.entrance_number);
console.log(element);
}
})
}
function getApartment(entrance_id, entrance_number) {
let url = `${api}/api/apartment/${entrance_id}?share_hash=${share_hash}`;
fetch(url)
.then(function (response) {
return response.json();
})
.then(function (data) {
listApartment[entrance_number] = data;
data.sort((a, b) => a.apartment_number - b.apartment_number);
data.sort((a, b) => a.updated_at - b.updated_at);
for (let i = 0; i < data.length; i++) {
const element = data[i];
let now = new Date(element.updated_at);
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
now = now.toISOString().slice(0, 16)
document.getElementById(`apartments_${entrance_id}`).innerHTML += `
<div id="card_${element.id}">
<span>кв.${element.title}</span>
<select id="status_${element.id}" onchange="mess(${entrance_number}, ${element.id})" style="background-color: ${color_status[element.status][0]}; color: ${color_status[element.status][1]}; border: 1px solid ${color_status[element.status][1]};" ${element.status == 2 ? "disabled" : ""}>
<option value="0" ${element.status == 0 ? "selected" : ""}></option>
<option value="1" ${element.status == 1 ? "selected" : ""}>Відмова</option>
<option value="2" ${element.status == 2 ? "selected" : ""}>Не заходити (Груба відмова)</option>
<option value="3" ${element.status == 3 ? "selected" : ""}>Нема домофона</option>
<option value="4" ${element.status == 4 ? "selected" : ""}>Повторна відвідина</option>
<option value="5" ${element.status == 5 ? "selected" : ""}>Немає вдома</option>
<option value="6" ${element.status == 6 ? "selected" : ""}>Свідки Єгови</option>
</select>
<input onchange="mess(${entrance_number}, ${element.id})" type="text" name="description" id="description_${element.id}" placeholder="Нотатки..." value="${element.description ?? ""}" ${element.status == 2 ? "disabled" : ""}>
<input onchange="mess(${entrance_number}, ${element.id}, true)" type="datetime-local" name="date" id="date_${element.id}" placeholder="Дата" value="${element.updated_at ? now : ""}" ${element.status == 2 ? "disabled" : ""} style="max-width: 170px;">
</div>
`;
}
})
}
function sort(id, entrance_id) {
let child = document.getElementById(`card_${id}`);
document.getElementById(`apartments_${entrance_id}`).removeChild(child);
document.getElementById(`apartments_${entrance_id}`).append(child);
child.style.border = "1px solid var(--PrimaryColor)";
}
function getTimeInSeconds(time = Date.now()) {
// Если время больше 10 знаков (это значит, что время в миллисекундах)
if (time.toString().length < 10) {
// Округляем до секунд, убирая последние 3 цифры (миллисекунды)
time = Math.floor(time * 1000);
}
return time;
}
getEntrances();
start(makeid(6));

116
web/lib/app.js Normal file
View File

@@ -0,0 +1,116 @@
let USER = {};
// Определение ID главного блока
let app = document.getElementById('app');
// Конфигурация роутера
Router.config({ mode: 'history' });
async function appReload() {
location.reload();
// Router.navigate(window.location.pathname, false).check();
// // Закрытие старого соединения WebSocket
// if (socket) socket.close(1000, "Перезапуск соединения");
// listEntrances = []
// listApartment = []
}
const Rotation = async () => {
const result = confirm(`Ви бажаєте провести ротацію територій між групами?`);
if (result) {
let rotationButton = document.getElementById('rotationButton-title');
rotationButton.innerText = "Зачекайте";
let uuid = localStorage.getItem("uuid");
const URL = `${CONFIG.api}rotation`;
await fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json, text/plain, */*",
"Authorization": uuid
}
})
.then(response => response.json())
.then(data => {
console.log(data);
rotationButton.innerText = 'Ротація завершилась успішно!';
Territory.house.setHTML();
Territory.homestead.setHTML();
setTimeout(() => {
rotationButton.innerText = "Провести ротацію";
}, 3000);
})
.catch(err => {
console.log(err);
rotationButton.innerText = "Помилка ротації!";
})
}
}
// Функция загрузки приложения
window.addEventListener('load', async function () {
console.log('[OS] ', detectOS());
if (window.matchMedia('(display-mode: standalone)').matches) {
if (detectOS() == 'Android') {
document.getElementById('navigation').dataset.state = '';
} else if (detectOS() == 'iOS') {
document.getElementById('navigation').dataset.state = 'ios';
localStorage.setItem('backToTop', 'false');
} else if (detectOS() == 'MacOS') {
document.getElementById('navigation').dataset.state = 'ios';
localStorage.setItem('backToTop', 'false');
} else {
document.getElementById('navigation').dataset.state = '';
}
}
let userInput = () => {
let h = prompt("Введіть ваше посилання з UUID:");
if (h) {
h = h.replace("https://sheep-service.com/?uuid=", "");
h = h.replace("https://sheep-service.com?uuid=", "");
h = h.replace("https://sheep-service.com?/hash=", "");
h = h.replace("https://sheep-service.com?hash=", "");
localStorage.setItem("uuid", h)
return h;
}
};
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('uuid')) {
localStorage.setItem("uuid", urlParams.get('uuid'))
}
if (urlParams.get('hash')) {
localStorage.setItem("uuid", urlParams.get('hash'))
}
let uuid = localStorage.getItem("uuid") ? localStorage.getItem("uuid") : userInput();
if (uuid) {
const URL = `${CONFIG.api}auth`;
USER = await fetch(URL, {
method: 'GET',
headers: {
"Content-Type": "application/json, text/plain, */*",
"Authorization": uuid
}
}).then((response) => response.json());
console.log("USER Info: ", USER);
if (USER.administrator.uuid || USER.moderator.uuid) document.getElementById("li-sheeps").style.display = "";
if (USER.administrator.uuid || (USER.moderator.uuid && USER.moderator.can_add_schedule)) document.getElementById("li-schedule").style.display = "";
if (USER.administrator.uuid || (USER.moderator.uuid && USER.moderator.can_manager_territory)) document.getElementById("li-territory").style.display = "";
if (USER.administrator.uuid || USER.can_view_stand) document.getElementById("li-stand").style.display = "";
}
Router.check().listen();
});

View File

@@ -0,0 +1,5 @@
clipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => console.log("Текст скопійовано!"))
.catch(err => console.error(err))
}

View File

@@ -0,0 +1,20 @@
let colorGroup = (number) => {
switch (number) {
case 1:
return "#c1ae4d"
case 2:
return "#93c14d"
case 3:
return "#4dc1a7"
case 4:
return "#4d90c1"
case 5:
return "#654dc1"
case 6:
return "#c14db7"
case 7:
return "#c1734d"
default:
return "#C14D4D"
}
}

View File

@@ -0,0 +1,35 @@
detectBrowser = () => {
const userAgent = navigator.userAgent;
let browser = "unkown";
// Detect browser name
browser = (/ucbrowser/i).test(userAgent) ? 'UCBrowser' : browser;
browser = (/edg/i).test(userAgent) ? 'Edge' : browser;
browser = (/googlebot/i).test(userAgent) ? 'GoogleBot' : browser;
browser = (/chromium/i).test(userAgent) ? 'Chromium' : browser;
browser = (/firefox|fxios/i).test(userAgent) && !(/seamonkey/i).test(userAgent) ? 'Firefox' : browser;
browser = (/; msie|trident/i).test(userAgent) && !(/ucbrowser/i).test(userAgent) ? 'IE' : browser;
browser = (/chrome|crios/i).test(userAgent) && !(/opr|opera|chromium|edg|ucbrowser|googlebot/i).test(userAgent) ? 'Chrome' : browser;;
browser = (/safari/i).test(userAgent) && !(/chromium|edg|ucbrowser|chrome|crios|opr|opera|fxios|firefox/i).test(userAgent) ? 'Safari' : browser;
browser = (/opr|opera/i).test(userAgent) ? 'Opera' : browser;
// detect browser version
switch (browser) {
case 'UCBrowser': return `${browser}/${browserVersion(userAgent,/(ucbrowser)\/([\d\.]+)/i)}`;
case 'Edge': return `${browser}/${browserVersion(userAgent,/(edge|edga|edgios|edg)\/([\d\.]+)/i)}`;
case 'GoogleBot': return `${browser}/${browserVersion(userAgent,/(googlebot)\/([\d\.]+)/i)}`;
case 'Chromium': return `${browser}/${browserVersion(userAgent,/(chromium)\/([\d\.]+)/i)}`;
case 'Firefox': return `${browser}/${browserVersion(userAgent,/(firefox|fxios)\/([\d\.]+)/i)}`;
case 'Chrome': return `${browser}/${browserVersion(userAgent,/(chrome|crios)\/([\d\.]+)/i)}`;
case 'Safari': return `${browser}/${browserVersion(userAgent,/(safari)\/([\d\.]+)/i)}`;
case 'Opera': return `${browser}/${browserVersion(userAgent,/(opera|opr)\/([\d\.]+)/i)}`;
case 'IE': const version = browserVersion(userAgent,/(trident)\/([\d\.]+)/i);
// IE version is mapped using trident version
// IE/8.0 = Trident/4.0, IE/9.0 = Trident/5.0
return version ? `${browser}/${parseFloat(version) + 4.0}` : `${browser}/7.0`;
default: return `unknown/0.0.0.0`;
}
}
browserVersion = (userAgent,regex) => {
return userAgent.match(regex) ? userAgent.match(regex)[2] : null;
}

View File

@@ -0,0 +1,12 @@
function detectOS() {
const platform = navigator.platform.toLowerCase(),
iosPlatforms = ['iphone', 'ipad', 'ipod', 'ipod touch'];
if (platform.includes('mac')) return 'MacOS';
if (iosPlatforms.includes(platform)) return 'iOS';
if (platform.includes('win')) return 'Windows';
if (/android/.test(navigator.userAgent.toLowerCase())) return 'Android';
if (/linux/.test(platform)) return 'Linux';
return 'unknown';
}

View File

@@ -0,0 +1,9 @@
let formattedDate = (unix_timestamp) => {
if(!unix_timestamp) return
let date = new Date(unix_timestamp);
let year = date.getFullYear() >= 10 ? date.getFullYear() : "0" + date.getFullYear();
let month = (date.getMonth() + 1) >= 10 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1);
let weekday = date.getDate() >= 10 ? date.getDate() : "0" + date.getDate();
return weekday + '.' + month + '.' + year;
}

View File

@@ -0,0 +1,283 @@
.marker-icon,
.marker-icon:focus {
background-color: #ffffff;
border: 1px solid #3388ff;
border-radius: 50%;
margin: -4px 0 0 -4px !important;
width: 6px !important;
height: 6px !important;
outline: 0;
transition: opacity ease 0.3s;
}
.marker-icon-middle,
.marker-icon-middle:focus {
opacity: 0.7;
margin: -6px 0 0 -6px !important;
width: 10px !important;
height: 10px !important;
}
.leaflet-pm-draggable {
cursor: move !important;
}
.cursor-marker {
cursor: crosshair;
pointer-events: none;
opacity: 0;
}
.cursor-marker.visible {
opacity: 1 !important;
}
.leaflet-pm-invalid {
stroke: red;
transition: fill ease 0s, stroke ease 0s;
}
.rect-style-marker,
.rect-start-marker {
opacity: 0;
}
.rect-style-marker.visible,
.rect-start-marker.visible {
opacity: 1 !important;
}
.vertexmarker-disabled {
opacity: 0.7;
}
.pm-text-marker {
width: 0;
height: 0;
}
.pm-textarea {
background-color: #fff;
color: #000;
resize: none;
border: none;
outline: 0;
cursor: pointer;
border-radius: 3px;
padding-left: 7px;
padding-bottom: 0;
padding-top: 4px;
}
.leaflet-pm-draggable .pm-textarea {
cursor: move;
}
.pm-textarea:focus,
.pm-textarea:focus-within,
.pm-textarea:focus-visible,
.pm-textarea:active {
border: 2px solid #000;
outline: 0;
}
.pm-textarea.pm-disabled {
border: none;
user-select: none;
}
.pm-textarea.pm-hasfocus {
cursor: auto;
}
.leaflet-pm-toolbar {
}
.leaflet-pm-toolbar .leaflet-buttons-control-button {
padding: 5px;
box-sizing: border-box;
position: relative;
z-index: 3;
}
.leaflet-pm-toolbar
.leaflet-pm-actions-container
a.leaflet-pm-action:first-child:not(.pos-right),
.leaflet-pm-toolbar
.leaflet-pm-actions-container
a.leaflet-pm-action:last-child.pos-right {
border-radius: 0;
}
.leaflet-pm-toolbar .button-container a.leaflet-buttons-control-button {
border-radius: 0;
}
.leaflet-pm-toolbar
.button-container:last-child
a.leaflet-buttons-control-button {
border-radius: 0 0 2px 2px;
}
.leaflet-pm-toolbar
.button-container:first-child
a.leaflet-buttons-control-button {
border-radius: 2px 2px 0 0;
}
.leaflet-pm-toolbar
.button-container:last-child
a.leaflet-buttons-control-button {
border-bottom: none;
}
.leaflet-pm-toolbar .control-fa-icon {
font-size: 19px;
line-height: 24px;
}
.leaflet-pm-toolbar .control-icon {
width: 100%;
height: 100%;
box-sizing: border-box;
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
}
.leaflet-pm-toolbar .leaflet-pm-icon-marker {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjUgKDY3NDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5BdG9tcy9JY29ucy9Ub29scy9NYXJrZXI8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNMTUuNSwyNC44NzgyOTU5IEMxNS4yOTA5MjAxLDI0Ljg3NzIyMTkgMTUuMTc0NDg1NywyNC44NDY3ODE3IDE0LjY1OTA4NjYsMjQuMjM1NDE2MyBDMTAuMjE5Njk1NSwxOS40MTE4MDU0IDgsMTUuNTAxNDM5MiA4LDEyLjUwNDMxNzcgQzgsOC4zNTk3OTc0NiAxMS4zNTc4NjQ0LDUgMTUuNSw1IEMxOS42NDIxMzU2LDUgMjMsOC4zNTk3OTc0NiAyMywxMi41MDQzMTc3IEMyMywxNyAxOC4yODc4MjE3LDIxLjkyNjgzNzggMTYuMzMzNjYwMSwyNC4yNDQwMTg2IEMxNS44MjI0NjIyLDI0Ljg1MDE4MDIgMTUuNzA5MDc5OSwyNC44NzkzNjk5IDE1LjUsMjQuODc4Mjk1OSBaIE0xNS41LDE1LjUzMjY5NDggQzE3LjI3NTIwMSwxNS41MzI2OTQ4IDE4LjcxNDI4NTcsMTQuMTE4MDAwNCAxOC43MTQyODU3LDEyLjM3Mjg4NjQgQzE4LjcxNDI4NTcsMTAuNjI3NzcyMyAxNy4yNzUyMDEsOS4yMTMwNzc5MiAxNS41LDkuMjEzMDc3OTIgQzEzLjcyNDc5OSw5LjIxMzA3NzkyIDEyLjI4NTcxNDMsMTAuNjI3NzcyMyAxMi4yODU3MTQzLDEyLjM3Mjg4NjQgQzEyLjI4NTcxNDMsMTQuMTE4MDAwNCAxMy43MjQ3OTksMTUuNTMyNjk0OCAxNS41LDE1LjUzMjY5NDggWiIgaWQ9InBhdGgtMSI+PC9wYXRoPgogICAgPC9kZWZzPgogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJBdG9tcy9JY29ucy9Ub29scy9NYXJrZXIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zLjAwMDAwMCwgLTMuMDAwMDAwKSI+CiAgICAgICAgICAgIDxtYXNrIGlkPSJtYXNrLTIiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgIDx1c2UgeGxpbms6aHJlZj0iI3BhdGgtMSI+PC91c2U+CiAgICAgICAgICAgIDwvbWFzaz4KICAgICAgICAgICAgPHVzZSBpZD0iTWFzayIgZmlsbD0iIzVCNUI1QiIgZmlsbC1ydWxlPSJub256ZXJvIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==);
}
.leaflet-pm-toolbar .leaflet-pm-icon-polygon {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPGRlZnM+CiAgICA8cGF0aCBpZD0icG9seWdvbi1hIiBkPSJNMTkuNDIwNjg5Miw5LjE2NTA5NzI1IEMxOS4xNTIzNjgxLDguNjY5OTI5MTQgMTksOC4xMDI3NTgzMSAxOSw3LjUgQzE5LDUuNTY3MDAzMzggMjAuNTY3MDAzNCw0IDIyLjUsNCBDMjQuNDMyOTk2Niw0IDI2LDUuNTY3MDAzMzggMjYsNy41IEMyNiw5LjI2MzIzNTk1IDI0LjY5NjE0NzEsMTAuNzIxOTQwNyAyMywxMC45NjQ1NTU2IEwyMywxOS4wMzU0NDQ0IEMyNC42OTYxNDcxLDE5LjI3ODA1OTMgMjYsMjAuNzM2NzY0IDI2LDIyLjUgQzI2LDI0LjQzMjk5NjYgMjQuNDMyOTk2NiwyNiAyMi41LDI2IEMyMC43MzY3NjQsMjYgMTkuMjc4MDU5MywyNC42OTYxNDcxIDE5LjAzNTQ0NDQsMjMgTDEwLjk2NDU1NTYsMjMgQzEwLjcyMTk0MDcsMjQuNjk2MTQ3MSA5LjI2MzIzNTk1LDI2IDcuNSwyNiBDNS41NjcwMDMzOCwyNiA0LDI0LjQzMjk5NjYgNCwyMi41IEM0LDIwLjU2NzAwMzQgNS41NjcwMDMzOCwxOSA3LjUsMTkgQzguMTAyNzU4MzEsMTkgOC42Njk5MjkxNCwxOS4xNTIzNjgxIDkuMTY1MDk3MjUsMTkuNDIwNjg5MiBMMTkuNDIwNjg5Miw5LjE2NTA5NzI1IFogTTIwLjgzNDkwNzMsMTAuNTc5MzA2MyBMMTAuNTc5MzEwOCwyMC44MzQ5MDI3IEMxMC42MDg2NzMxLDIwLjg4OTA4ODggMTAuNjM2NjQ2OSwyMC45NDQxMzcyIDEwLjY2MzE4NDQsMjEgTDE5LjMzNjgxNTYsMjEgQzE5LjY4MjU3NzUsMjAuMjcyMTU0IDIwLjI3MjE1NCwxOS42ODI1Nzc1IDIxLDE5LjMzNjgxNTYgTDIxLDEwLjY2MzE4NDQgQzIwLjk0NDEzNzIsMTAuNjM2NjQ2OSAyMC44ODkwODg4LDEwLjYwODY3MzEgMjAuODM0OTAyNywxMC41NzkzMTA4IFogTTIyLjUsOSBDMjMuMzI4NDI3MSw5IDI0LDguMzI4NDI3MTIgMjQsNy41IEMyNCw2LjY3MTU3Mjg4IDIzLjMyODQyNzEsNiAyMi41LDYgQzIxLjY3MTU3MjksNiAyMSw2LjY3MTU3Mjg4IDIxLDcuNSBDMjEsOC4zMjg0MjcxMiAyMS42NzE1NzI5LDkgMjIuNSw5IFogTTIyLjUsMjQgQzIzLjMyODQyNzEsMjQgMjQsMjMuMzI4NDI3MSAyNCwyMi41IEMyNCwyMS42NzE1NzI5IDIzLjMyODQyNzEsMjEgMjIuNSwyMSBDMjEuNjcxNTcyOSwyMSAyMSwyMS42NzE1NzI5IDIxLDIyLjUgQzIxLDIzLjMyODQyNzEgMjEuNjcxNTcyOSwyNCAyMi41LDI0IFogTTcuNSwyNCBDOC4zMjg0MjcxMiwyNCA5LDIzLjMyODQyNzEgOSwyMi41IEM5LDIxLjY3MTU3MjkgOC4zMjg0MjcxMiwyMSA3LjUsMjEgQzYuNjcxNTcyODgsMjEgNiwyMS42NzE1NzI5IDYsMjIuNSBDNiwyMy4zMjg0MjcxIDYuNjcxNTcyODgsMjQgNy41LDI0IFoiLz4KICA8L2RlZnM+CiAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMyAtMykiPgogICAgPG1hc2sgaWQ9InBvbHlnb24tYiIgZmlsbD0iI2ZmZiI+CiAgICAgIDx1c2UgeGxpbms6aHJlZj0iI3BvbHlnb24tYSIvPgogICAgPC9tYXNrPgogICAgPHVzZSBmaWxsPSIjNUI1QjVCIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHhsaW5rOmhyZWY9IiNwb2x5Z29uLWEiLz4KICAgIDxnIGZpbGw9IiM1QjVCNUIiIG1hc2s9InVybCgjcG9seWdvbi1iKSI+CiAgICAgIDxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==);
}
.leaflet-pm-toolbar .leaflet-pm-icon-polyline {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPGRlZnM+CiAgICA8cGF0aCBpZD0ibGluZS1hIiBkPSJNOS4xNjUwOTcyNSwxOS40MjA2ODkyIEwxOC40MjA2ODkyLDEwLjE2NTA5NzMgQzE4LjE1MjM2ODEsOS42Njk5MjkxNCAxOCw5LjEwMjc1ODMxIDE4LDguNSBDMTgsNi41NjcwMDMzOCAxOS41NjcwMDM0LDUgMjEuNSw1IEMyMy40MzI5OTY2LDUgMjUsNi41NjcwMDMzOCAyNSw4LjUgQzI1LDEwLjQzMjk5NjYgMjMuNDMyOTk2NiwxMiAyMS41LDEyIEMyMC44OTcyNDE3LDEyIDIwLjMzMDA3MDksMTEuODQ3NjMxOSAxOS44MzQ5MDI3LDExLjU3OTMxMDggTDEwLjU3OTMxMDgsMjAuODM0OTAyNyBDMTAuODQ3NjMxOSwyMS4zMzAwNzA5IDExLDIxLjg5NzI0MTcgMTEsMjIuNSBDMTEsMjQuNDMyOTk2NiA5LjQzMjk5NjYyLDI2IDcuNSwyNiBDNS41NjcwMDMzOCwyNiA0LDI0LjQzMjk5NjYgNCwyMi41IEM0LDIwLjU2NzAwMzQgNS41NjcwMDMzOCwxOSA3LjUsMTkgQzguMTAyNzU4MzEsMTkgOC42Njk5MjkxNCwxOS4xNTIzNjgxIDkuMTY1MDk3MjUsMTkuNDIwNjg5MiBaIE0yMS41LDEwIEMyMi4zMjg0MjcxLDEwIDIzLDkuMzI4NDI3MTIgMjMsOC41IEMyMyw3LjY3MTU3Mjg4IDIyLjMyODQyNzEsNyAyMS41LDcgQzIwLjY3MTU3MjksNyAyMCw3LjY3MTU3Mjg4IDIwLDguNSBDMjAsOS4zMjg0MjcxMiAyMC42NzE1NzI5LDEwIDIxLjUsMTAgWiBNNy41LDI0IEM4LjMyODQyNzEyLDI0IDksMjMuMzI4NDI3MSA5LDIyLjUgQzksMjEuNjcxNTcyOSA4LjMyODQyNzEyLDIxIDcuNSwyMSBDNi42NzE1NzI4OCwyMSA2LDIxLjY3MTU3MjkgNiwyMi41IEM2LDIzLjMyODQyNzEgNi42NzE1NzI4OCwyNCA3LjUsMjQgWiIvPgogIDwvZGVmcz4KICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zIC0zKSI+CiAgICA8bWFzayBpZD0ibGluZS1iIiBmaWxsPSIjZmZmIj4KICAgICAgPHVzZSB4bGluazpocmVmPSIjbGluZS1hIi8+CiAgICA8L21hc2s+CiAgICA8dXNlIGZpbGw9IiM1QjVCNUIiIGZpbGwtcnVsZT0ibm9uemVybyIgeGxpbms6aHJlZj0iI2xpbmUtYSIvPgogICAgPGcgZmlsbD0iIzVCNUI1QiIgbWFzaz0idXJsKCNsaW5lLWIpIj4KICAgICAgPHJlY3Qgd2lkdGg9IjMwIiBoZWlnaHQ9IjMwIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K);
}
.leaflet-pm-toolbar .leaflet-pm-icon-circle {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjUgKDY3NDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5BdG9tcy9JY29ucy9Ub29scy9DaXJjbGU8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNMTguMjg5Nzc1MSw2Ljc4NjAyMjc1IEMxOC44OTI0MTMxLDYuMjk0NjQ5ODEgMTkuNjYxNzk3LDYgMjAuNSw2IEMyMi40MzI5OTY2LDYgMjQsNy41NjcwMDMzOCAyNCw5LjUgQzI0LDEwLjMzODIwMyAyMy43MDUzNTAyLDExLjEwNzU4NjkgMjMuMjEzOTc3MiwxMS43MTAyMjQ5IEMyMy43MTk1OTksMTIuODcxMjA1MyAyNCwxNC4xNTI4NTcxIDI0LDE1LjUgQzI0LDIwLjc0NjcwNTEgMTkuNzQ2NzA1MSwyNSAxNC41LDI1IEM5LjI1MzI5NDg4LDI1IDUsMjAuNzQ2NzA1MSA1LDE1LjUgQzUsMTAuMjUzMjk0OSA5LjI1MzI5NDg4LDYgMTQuNSw2IEMxNS44NDcxNDI5LDYgMTcuMTI4Nzk0Nyw2LjI4MDQwMDk4IDE4LjI4OTc3NTEsNi43ODYwMjI3NSBaIE0xNy4xNTA0MjI4LDguNDgxNzU4NiBDMTYuMzI2MzU4MSw4LjE3MDM5MjM2IDE1LjQzMzA3NzcsOCAxNC41LDggQzEwLjM1Nzg2NDQsOCA3LDExLjM1Nzg2NDQgNywxNS41IEM3LDE5LjY0MjEzNTYgMTAuMzU3ODY0NCwyMyAxNC41LDIzIEMxOC42NDIxMzU2LDIzIDIyLDE5LjY0MjEzNTYgMjIsMTUuNSBDMjIsMTQuNTY2OTIyMyAyMS44Mjk2MDc2LDEzLjY3MzY0MTkgMjEuNTE4MjQxNCwxMi44NDk1NzcyIEMyMS4xOTYwMzgzLDEyLjk0NzM5NjggMjAuODU0MTYyMiwxMyAyMC41LDEzIEMxOC41NjcwMDM0LDEzIDE3LDExLjQzMjk5NjYgMTcsOS41IEMxNyw5LjE0NTgzNzc4IDE3LjA1MjYwMzIsOC44MDM5NjE2OSAxNy4xNTA0MjI4LDguNDgxNzU4NiBaIE0xNC41LDE3IEMxMy42NzE1NzI5LDE3IDEzLDE2LjMyODQyNzEgMTMsMTUuNSBDMTMsMTQuNjcxNTcyOSAxMy42NzE1NzI5LDE0IDE0LjUsMTQgQzE1LjMyODQyNzEsMTQgMTYsMTQuNjcxNTcyOSAxNiwxNS41IEMxNiwxNi4zMjg0MjcxIDE1LjMyODQyNzEsMTcgMTQuNSwxNyBaIE0yMC41LDExIEMyMS4zMjg0MjcxLDExIDIyLDEwLjMyODQyNzEgMjIsOS41IEMyMiw4LjY3MTU3Mjg4IDIxLjMyODQyNzEsOCAyMC41LDggQzE5LjY3MTU3MjksOCAxOSw4LjY3MTU3Mjg4IDE5LDkuNSBDMTksMTAuMzI4NDI3MSAxOS42NzE1NzI5LDExIDIwLjUsMTEgWiIgaWQ9InBhdGgtMSI+PC9wYXRoPgogICAgPC9kZWZzPgogICAgPGcgaWQ9IlN5bWJvbHMiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJBdG9tcy9JY29ucy9Ub29scy9DaXJjbGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0zLjAwMDAwMCwgLTMuMDAwMDAwKSI+CiAgICAgICAgICAgIDxtYXNrIGlkPSJtYXNrLTIiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgIDx1c2UgeGxpbms6aHJlZj0iI3BhdGgtMSI+PC91c2U+CiAgICAgICAgICAgIDwvbWFzaz4KICAgICAgICAgICAgPHVzZSBpZD0iTWFzayIgZmlsbD0iIzVCNUI1QiIgZmlsbC1ydWxlPSJub256ZXJvIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgPGcgaWQ9IkF0b21zL0NvbG9yL0dyZXkiIG1hc2s9InVybCgjbWFzay0yKSIgZmlsbD0iIzVCNUI1QiI+CiAgICAgICAgICAgICAgICA8cmVjdCBpZD0iUmVjdGFuZ2xlIiB4PSIwIiB5PSIwIiB3aWR0aD0iMzAiIGhlaWdodD0iMzAiPjwvcmVjdD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+);
}
.leaflet-pm-toolbar .leaflet-pm-icon-circle-marker {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KCjxzdmcgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjNUI1QjVCIiBzdHJva2Utd2lkdGg9IjgiCiAgICAgZmlsbD0ibm9uZSI+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjM1Ii8+CiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iMyIgZmlsbD0iIzVCNUI1QiIvPgo8L3N2Zz4=);
}
.leaflet-pm-toolbar .leaflet-pm-icon-rectangle {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPGRlZnM+CiAgICA8cGF0aCBpZD0icmVjdGFuZ2xlLWEiIGQ9Ik0yMywxMC45NjQ1NTU2IEwyMywxOS4wMzU0NDQ0IEMyNC42OTYxNDcxLDE5LjI3ODA1OTMgMjYsMjAuNzM2NzY0IDI2LDIyLjUgQzI2LDI0LjQzMjk5NjYgMjQuNDMyOTk2NiwyNiAyMi41LDI2IEMyMC43MzY3NjQsMjYgMTkuMjc4MDU5MywyNC42OTYxNDcxIDE5LjAzNTQ0NDQsMjMgTDEwLjk2NDU1NTYsMjMgQzEwLjcyMTk0MDcsMjQuNjk2MTQ3MSA5LjI2MzIzNTk1LDI2IDcuNSwyNiBDNS41NjcwMDMzOCwyNiA0LDI0LjQzMjk5NjYgNCwyMi41IEM0LDIwLjczNjc2NCA1LjMwMzg1MjkzLDE5LjI3ODA1OTMgNywxOS4wMzU0NDQ0IEw3LDEwLjk2NDU1NTYgQzUuMzAzODUyOTMsMTAuNzIxOTQwNyA0LDkuMjYzMjM1OTUgNCw3LjUgQzQsNS41NjcwMDMzOCA1LjU2NzAwMzM4LDQgNy41LDQgQzkuMjYzMjM1OTUsNCAxMC43MjE5NDA3LDUuMzAzODUyOTMgMTAuOTY0NTU1Niw3IEwxOS4wMzU0NDQ0LDcgQzE5LjI3ODA1OTMsNS4zMDM4NTI5MyAyMC43MzY3NjQsNCAyMi41LDQgQzI0LjQzMjk5NjYsNCAyNiw1LjU2NzAwMzM4IDI2LDcuNSBDMjYsOS4yNjMyMzU5NSAyNC42OTYxNDcxLDEwLjcyMTk0MDcgMjMsMTAuOTY0NTU1NiBaIE0yMSwxMC42NjMxODQ0IEMyMC4yNzIxNTQsMTAuMzE3NDIyNSAxOS42ODI1Nzc1LDkuNzI3ODQ1OTggMTkuMzM2ODE1Niw5IEwxMC42NjMxODQ0LDkgQzEwLjMxNzQyMjUsOS43Mjc4NDU5OCA5LjcyNzg0NTk4LDEwLjMxNzQyMjUgOSwxMC42NjMxODQ0IEw5LDE5LjMzNjgxNTYgQzkuNzI3ODQ1OTgsMTkuNjgyNTc3NSAxMC4zMTc0MjI1LDIwLjI3MjE1NCAxMC42NjMxODQ0LDIxIEwxOS4zMzY4MTU2LDIxIEMxOS42ODI1Nzc1LDIwLjI3MjE1NCAyMC4yNzIxNTQsMTkuNjgyNTc3NSAyMSwxOS4zMzY4MTU2IEwyMSwxMC42NjMxODQ0IFogTTcuNSw5IEM4LjMyODQyNzEyLDkgOSw4LjMyODQyNzEyIDksNy41IEM5LDYuNjcxNTcyODggOC4zMjg0MjcxMiw2IDcuNSw2IEM2LjY3MTU3Mjg4LDYgNiw2LjY3MTU3Mjg4IDYsNy41IEM2LDguMzI4NDI3MTIgNi42NzE1NzI4OCw5IDcuNSw5IFogTTIyLjUsOSBDMjMuMzI4NDI3MSw5IDI0LDguMzI4NDI3MTIgMjQsNy41IEMyNCw2LjY3MTU3Mjg4IDIzLjMyODQyNzEsNiAyMi41LDYgQzIxLjY3MTU3MjksNiAyMSw2LjY3MTU3Mjg4IDIxLDcuNSBDMjEsOC4zMjg0MjcxMiAyMS42NzE1NzI5LDkgMjIuNSw5IFogTTIyLjUsMjQgQzIzLjMyODQyNzEsMjQgMjQsMjMuMzI4NDI3MSAyNCwyMi41IEMyNCwyMS42NzE1NzI5IDIzLjMyODQyNzEsMjEgMjIuNSwyMSBDMjEuNjcxNTcyOSwyMSAyMSwyMS42NzE1NzI5IDIxLDIyLjUgQzIxLDIzLjMyODQyNzEgMjEuNjcxNTcyOSwyNCAyMi41LDI0IFogTTcuNSwyNCBDOC4zMjg0MjcxMiwyNCA5LDIzLjMyODQyNzEgOSwyMi41IEM5LDIxLjY3MTU3MjkgOC4zMjg0MjcxMiwyMSA3LjUsMjEgQzYuNjcxNTcyODgsMjEgNiwyMS42NzE1NzI5IDYsMjIuNSBDNiwyMy4zMjg0MjcxIDYuNjcxNTcyODgsMjQgNy41LDI0IFoiLz4KICA8L2RlZnM+CiAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMyAtMykiPgogICAgPG1hc2sgaWQ9InJlY3RhbmdsZS1iIiBmaWxsPSIjZmZmIj4KICAgICAgPHVzZSB4bGluazpocmVmPSIjcmVjdGFuZ2xlLWEiLz4KICAgIDwvbWFzaz4KICAgIDx1c2UgZmlsbD0iIzVCNUI1QiIgZmlsbC1ydWxlPSJub256ZXJvIiB4bGluazpocmVmPSIjcmVjdGFuZ2xlLWEiLz4KICAgIDxnIGZpbGw9IiM1QjVCNUIiIG1hc2s9InVybCgjcmVjdGFuZ2xlLWIpIj4KICAgICAgPHJlY3Qgd2lkdGg9IjMwIiBoZWlnaHQ9IjMwIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K);
}
.leaflet-pm-toolbar .leaflet-pm-icon-delete {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjUgKDY3NDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5BdG9tcy9JY29ucy9Ub29scy9FcmFzZXI8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNMTcuNzg3NDIxOSwxOC40ODEyNTUyIEwxMS42NDgwMDc5LDEzLjM0OTgxODQgTDYuNDA0NjYwMDksMTkuMzgxNjAwMSBMMTAuNTUzOTE1NiwyMi45ODg0OTI5IEwxMy44NjkzNCwyMi45ODg0OTI5IEwxNy43ODc0MjE5LDE4LjQ4MTI1NTIgWiBNMTYuNTA3NDI1MiwyMi45ODg0OTI5IEwyNi4wMDAwMDAyLDIyLjk4ODQ5MjkgTDI2LjAwMDAwMDIsMjQuOTg4NDkyOSBMMTAuMDAwMDAwMiwyNC45ODg0OTI5IEw5LjgwNzA4MzEzLDI0Ljk4ODQ5MjkgTDUuMDkyNTQyMDQsMjAuODkxMDE5MiBDNC4yNTg5MTI4NSwyMC4xNjYzNTY0IDQuMTcwNTc4MTQsMTguOTAzMTExMiA0Ljg5NTI0MDkzLDE4LjA2OTQ4MiBMMTYuMDQ4MjQ0NCw1LjIzOTQxOTE2IEMxNi43NzI5MDcyLDQuNDA1Nzg5OTggMTguMDM2MTUyNSw0LjMxNzQ1NTI2IDE4Ljg2OTc4MTYsNS4wNDIxMTgwNiBMMjQuOTA3NDU4MywxMC4yOTA1OTAzIEMyNS43NDEwODc1LDExLjAxNTI1MzEgMjUuODI5NDIyMiwxMi4yNzg0OTgzIDI1LjEwNDc1OTQsMTMuMTEyMTI3NSBMMTYuNTA3NDI1MiwyMi45ODg0OTI5IFoiIGlkPSJwYXRoLTEiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJTeW1ib2xzIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8ZyBpZD0iQXRvbXMvSWNvbnMvVG9vbHMvRXJhc2VyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMy4wMDAwMDAsIC0zLjAwMDAwMCkiPgogICAgICAgICAgICA8bWFzayBpZD0ibWFzay0yIiBmaWxsPSJ3aGl0ZSI+CiAgICAgICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNwYXRoLTEiPjwvdXNlPgogICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgIDx1c2UgaWQ9IkNvbWJpbmVkLVNoYXBlIiBmaWxsPSIjNUI1QjVCIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHhsaW5rOmhyZWY9IiNwYXRoLTEiPjwvdXNlPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+);
}
.leaflet-pm-toolbar .leaflet-pm-icon-edit {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPGRlZnM+CiAgICA8cGF0aCBpZD0iZWRpdF9hbmNob3ItYSIgZD0iTTEzLjUsMTEgQzExLjU2NzAwMzQsMTEgMTAsOS40MzI5OTY2MiAxMCw3LjUgQzEwLDUuNTY3MDAzMzggMTEuNTY3MDAzNCw0IDEzLjUsNCBDMTUuNDMyOTk2Niw0IDE3LDUuNTY3MDAzMzggMTcsNy41IEMxNyw5LjQzMjk5NjYyIDE1LjQzMjk5NjYsMTEgMTMuNSwxMSBaIE0xMy41LDkgQzE0LjMyODQyNzEsOSAxNSw4LjMyODQyNzEyIDE1LDcuNSBDMTUsNi42NzE1NzI4OCAxNC4zMjg0MjcxLDYgMTMuNSw2IEMxMi42NzE1NzI5LDYgMTIsNi42NzE1NzI4OCAxMiw3LjUgQzEyLDguMzI4NDI3MTIgMTIuNjcxNTcyOSw5IDEzLjUsOSBaIE0xMi4wMDAyODg5LDcuNTI5NzM4OTMgQzEyLjAxMjU5ODMsOC4xNjI3MzY3MiAxMi40MTcwMTk3LDguNjk5NjY0MyAxMi45ODA3MTExLDguOTA3Njc5NjYgTDMsMTUgTDMsMTMgTDEyLjAwMDI4ODksNy41Mjk3Mzg5MyBaIE0xNC4yMTcyNzIyLDYuMTgyMjg0NzIgTDE5LjQ1MzEyNSwzIEwyMi42NTg5MzU1LDMgTDE0Ljk4OTEwMiw3LjY4MTczODg1IEMxNC45OTYyOTcxLDcuNjIyMTY0NTkgMTUsNy41NjE1MTQ3MiAxNSw3LjUgQzE1LDYuOTMxMzgzODEgMTQuNjgzNjA5OCw2LjQzNjY2NDUgMTQuMjE3MjcyMiw2LjE4MjI4NDcyIFogTTIzLjQ0MzQwNDIsMTkuMjg1MTczNiBMMjAuMTI4Mjc5OSwxOS4yODUxNzM2IEwyMS44NzI5OTgzLDIzLjUzNDk1MjUgQzIxLjk5NDUyOTYsMjMuODI5NTc3MyAyMS44NTU2NTQ2LDI0LjE1OTkyMDkgMjEuNTc3ODczNCwyNC4yODQ5MjA4IEwyMC4wNDE0Njc1LDI0Ljk1NDUxNDIgQzE5Ljc1NTA2MTMsMjUuMDc5NTE0MSAxOS40MzM4NzM4LDI0LjkzNjY3MDQgMTkuMzEyMzQyNiwyNC42NTA5NTE4IEwxNy42NTQ0MzY3LDIwLjYxNTQ1NDEgTDE0Ljk0NjE4NzMsMjMuNDAxMDE1MSBDMTQuNTg1MjgxMSwyMy43NzIxNzExIDE0LDIzLjQ4NjA0NjMgMTQsMjIuOTk5MjY1MyBMMTQsOS41NzE4MzUzMyBDMTQsOS4wNTkzMzU2MSAxNC42MjI1MzExLDguODA5NDkyIDE0Ljk0NjE1Niw5LjE3MDA4NTU1IEwyMy44MzQwMjkyLDE4LjMxMjAxNzkgQzI0LjE5MjUyOTEsMTguNjYxMzYxNSAyMy45Mjc5OTc5LDE5LjI4NTE3MzYgMjMuNDQzNDA0MiwxOS4yODUxNzM2IFoiLz4KICA8L2RlZnM+CiAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMyAtMykiPgogICAgPG1hc2sgaWQ9ImVkaXRfYW5jaG9yLWIiIGZpbGw9IiNmZmYiPgogICAgICA8dXNlIHhsaW5rOmhyZWY9IiNlZGl0X2FuY2hvci1hIi8+CiAgICA8L21hc2s+CiAgICA8dXNlIGZpbGw9IiM1QjVCNUIiIGZpbGwtcnVsZT0ibm9uemVybyIgeGxpbms6aHJlZj0iI2VkaXRfYW5jaG9yLWEiLz4KICAgIDxnIGZpbGw9IiM1QjVCNUIiIG1hc2s9InVybCgjZWRpdF9hbmNob3ItYikiPgogICAgICA8cmVjdCB3aWR0aD0iMzAiIGhlaWdodD0iMzAiLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=);
}
.leaflet-pm-toolbar .leaflet-pm-icon-drag {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgPGRlZnM+CiAgICA8cGF0aCBpZD0ibW92ZS1hIiBkPSJNMjEsMTQgTDIxLDEwIEwyNywxNSBMMjEsMjAgTDIxLDE2IEwxNiwxNiBMMTYsMjEgTDIwLDIxIEwxNSwyNyBMMTAsMjEgTDE0LDIxIEwxNCwxNiBMOSwxNiBMOSwyMCBMMywxNSBMOSwxMCBMOSwxNCBMMTQsMTQgTDE0LDkgTDEwLDkgTDE1LDMgTDIwLDkgTDE2LDkgTDE2LDE0IEwyMSwxNCBaIi8+CiAgPC9kZWZzPgogIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMgLTMpIj4KICAgIDxtYXNrIGlkPSJtb3ZlLWIiIGZpbGw9IiNmZmYiPgogICAgICA8dXNlIHhsaW5rOmhyZWY9IiNtb3ZlLWEiLz4KICAgIDwvbWFzaz4KICAgIDx1c2UgZmlsbD0iI0Q4RDhEOCIgeGxpbms6aHJlZj0iI21vdmUtYSIvPgogICAgPGcgZmlsbD0iIzVCNUI1QiIgbWFzaz0idXJsKCNtb3ZlLWIpIj4KICAgICAgPHJlY3Qgd2lkdGg9IjMwIiBoZWlnaHQ9IjMwIi8+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K);
}
.leaflet-pm-toolbar .leaflet-pm-icon-cut {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDUyLjUgKDY3NDY5KSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5BdG9tcy9JY29ucy9Ub29scy9TY2lzc29yczwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPgogICAgICAgIDxwYXRoIGQ9Ik0xMi45NjkxNTc0LDEzLjQ5Mzk0MzUgTDIxLjAzMTcwMzIsNS41NDE2NzAxMyBMMjMuNDY0OTQ5OSw1LjY3NzIyOTU3IEwxNy4wNDcwNzEzLDE0LjUxMDY4MTYgTDI3LjU2NjAzMzYsMTcuMTMzMzUzNSBMMjUuNzg5MTk0NCwxOC44MDEyNTg4IEwxNC41ODU0OTUxLDE3Ljg5ODc1MDYgTDEzLjY0ODc5NTUsMTkuMTg4MDA3IEMxMy43OTQ2MzksMTkuMjY1MDk1OCAxMy45MzY3OTg1LDE5LjM1MzQ0MTcgMTQuMDc0MTM3NywxOS40NTMyMjQ1IEMxNS42Mzc5NjQ4LDIwLjU4OTQxMTQgMTUuOTg0NjM1NywyMi43NzgyMDUyIDE0Ljg0ODQ0ODgsMjQuMzQyMDMyNCBDMTMuNzEyMjYxOSwyNS45MDU4NTk1IDExLjUyMzQ2ODEsMjYuMjUyNTMwNCA5Ljk1OTY0MDk2LDI1LjExNjM0MzUgQzguMzk1ODEzODQsMjMuOTgwMTU2NSA4LjA0OTE0Mjk2LDIxLjc5MTM2MjcgOS4xODUzMjk4NiwyMC4yMjc1MzU2IEM5Ljc0NTg3Mjc2LDE5LjQ1NjAxNDUgMTAuNTYyNjE4OCwxOC45ODA3NDc1IDExLjQzNDEyMTgsMTguODMzNjQwNyBMMTIuNjgwNTY1NiwxNy4xMTgwNTc5IEwxMi41MjM5NzI0LDE2LjM3NDcyMTYgTDExLjk1MDY5MzIsMTUuMzAxMjM5MSBMOS44OTMxMDY0NiwxNC43ODgyMjUxIEM5LjEzMDkzNzk2LDE1LjIzNTcyNjEgOC4xOTk3Nzg1NCwxNS4zOTY2NDQ3IDcuMjc0NDUzNTUsMTUuMTY1OTM1MiBDNS4zOTg4NzUxOSwxNC42OTgzMDEgNC4yNTc1MTA5NCwxMi43OTg3NTE5IDQuNzI1MTQ1MTUsMTAuOTIzMTczNiBDNS4xOTI3NzkzNSw5LjA0NzU5NTE5IDcuMDkyMzI4NDYsNy45MDYyMzA5NCA4Ljk2NzkwNjgyLDguMzczODY1MTUgQzEwLjg0MzQ4NTIsOC44NDE0OTkzNSAxMS45ODQ4NDk0LDEwLjc0MTA0ODUgMTEuNTE3MjE1MiwxMi42MTY2MjY4IEMxMS40NzYxNDY0LDEyLjc4MTM0NDkgMTEuNDI0MDMzNSwxMi45NDA0MDAxIDExLjM2MTg2MjcsMTMuMDkzMTk5OSBMMTIuOTY5MTU3NCwxMy40OTM5NDM1IFogTTcuNzU4Mjk3MzUsMTMuMjI1MzQzOCBDOC41NjIxMTY2NCwxMy40MjU3NTg0IDkuMzc2MjA5MTIsMTIuOTM2NjAyMyA5LjU3NjYyMzc4LDEyLjEzMjc4MyBDOS43NzcwMzg0NCwxMS4zMjg5NjM3IDkuMjg3ODgyMzMsMTAuNTE0ODcxMyA4LjQ4NDA2MzAzLDEwLjMxNDQ1NjYgQzcuNjgwMjQzNzMsMTAuMTE0MDQxOSA2Ljg2NjE1MTI2LDEwLjYwMzE5OCA2LjY2NTczNjYsMTEuNDA3MDE3MyBDNi40NjUzMjE5NCwxMi4yMTA4MzY2IDYuOTU0NDc4MDUsMTMuMDI0OTI5MSA3Ljc1ODI5NzM1LDEzLjIyNTM0MzggWiBNMTAuODAzMzYzOSwyMS40MDMxMDYxIEMxMC4zMTY0MjY2LDIyLjA3MzMxNzcgMTAuNDY0OTk5OCwyMy4wMTEzNzIyIDExLjEzNTIxMTUsMjMuNDk4MzA5NSBDMTEuODA1NDIzMSwyMy45ODUyNDY3IDEyLjc0MzQ3NzYsMjMuODM2NjczNSAxMy4yMzA0MTQ4LDIzLjE2NjQ2MTkgQzEzLjcxNzM1MjEsMjIuNDk2MjUwMiAxMy41Njg3Nzg4LDIxLjU1ODE5NTcgMTIuODk4NTY3MiwyMS4wNzEyNTg1IEMxMi4yMjgzNTU2LDIwLjU4NDMyMTIgMTEuMjkwMzAxMSwyMC43MzI4OTQ1IDEwLjgwMzM2MzksMjEuNDAzMTA2MSBaIiBpZD0icGF0aC0xIj48L3BhdGg+CiAgICA8L2RlZnM+CiAgICA8ZyBpZD0iU3ltYm9scyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IkF0b21zL0ljb25zL1Rvb2xzL1NjaXNzb3JzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMy4wMDAwMDAsIC0zLjAwMDAwMCkiPgogICAgICAgICAgICA8bWFzayBpZD0ibWFzay0yIiBmaWxsPSJ3aGl0ZSI+CiAgICAgICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNwYXRoLTEiPjwvdXNlPgogICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgIDx1c2UgaWQ9Ik1hc2siIGZpbGw9IiM1QjVCNUIiIGZpbGwtcnVsZT0ibm9uemVybyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTYuMDkzMTk0LCAxNS42NjMzNTEpIHJvdGF0ZSgtMzIuMDAwMDAwKSB0cmFuc2xhdGUoLTE2LjA5MzE5NCwgLTE1LjY2MzM1MSkgIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==);
}
.leaflet-pm-toolbar .leaflet-pm-icon-snapping {
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDU3LjEgKDgzMDg4KSAtIGh0dHBzOi8vc2tldGNoLmNvbSAtLT4KICAgIDx0aXRsZT5BdG9tcy9JY29ucy9Ub29scy9NYWduZXQ8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBkPSJNMjEuOTk5NDc1OSwxMC45NDI4MTgzIEwyMS45OTk5OTg1LDE2LjM3MTA0MTcgQzIyLDE2LjY4NzIwMDcgMjIsMTcuMDA1ODI3OCAyMiwxNy4zMjY5NDExIEMyMiwyMS41NjQ2NTQ1IDE4LjY0MjEzNTYsMjUgMTQuNSwyNSBDMTAuMzU3ODY0NCwyNSA3LDIxLjU2NDY1NDUgNywxNy4zMjY5NDExIEw3LjAwMDg3NTA4LDEwLjk5MDc1MDcgTDExLjAwMjI4MDgsMTAuOTk4NDEyNSBDMTEuMDAxNzAzMywxMS42OTgwMTE0IDExLjAwMTI0NywxMi40MTY4MjQ4IDExLjAwMDg5OTIsMTMuMTU1NDg4NyBMMTEsMTcuMzI2OTQxMSBDMTEsMTkuMzc1NjgwOSAxMi41ODc2ODQxLDIxIDE0LjUsMjEgQzE2LjQxMjMxNTksMjEgMTgsMTkuMzc1NjgwOSAxOCwxNy4zMjY5NDExIEMxOCwxNS4wNzAyMDMyIDE3Ljk5OTU2OTYsMTIuOTYxOTY2OCAxNy45OTg1MzksMTAuOTkxMDAzMiBMMjEuOTk5NDc1OSwxMC45NDI4MTgzIFogTTEwLDcgQzEwLjU1MjI4NDcsNyAxMSw3LjQ0NzcxNTI1IDExLDggTDExLDEwIEw3LDEwIEw3LDggQzcsNy40NDc3MTUyNSA3LjQ0NzcxNTI1LDcgOCw3IEwxMCw3IFogTTIxLDcgQzIxLjU1MjI4NDcsNyAyMiw3LjQ0NzcxNTI1IDIyLDggTDIyLDEwIEwxOCwxMCBMMTgsOCBDMTgsNy40NDc3MTUyNSAxOC40NDc3MTUzLDcgMTksNyBMMjEsNyBaIiBpZD0icGF0aC0xIj48L3BhdGg+CiAgICA8L2RlZnM+CiAgICA8ZyBpZD0iU3ltYm9scyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPGcgaWQ9IkF0b21zL0ljb25zL1Rvb2xzL01hZ25ldCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMuMDAwMDAwLCAtMy4wMDAwMDApIj4KICAgICAgICAgICAgPG1hc2sgaWQ9Im1hc2stMiIgZmlsbD0id2hpdGUiPgogICAgICAgICAgICAgICAgPHVzZSB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgPC9tYXNrPgogICAgICAgICAgICA8dXNlIGlkPSJNYXNrIiBmaWxsPSIjNUI1QjVCIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0LjUwMDAwMCwgMTYuMDAwMDAwKSByb3RhdGUoNDUuMDAwMDAwKSB0cmFuc2xhdGUoLTE0LjUwMDAwMCwgLTE2LjAwMDAwMCkgIiB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==);
}
.leaflet-pm-toolbar .leaflet-pm-icon-rotate {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgICA8ZGVmcz4KICAgICAgICA8cGF0aCBpZD0icm90YXRlIiBkPSJNMjEuMiw1LjhjLTAuMS0wLjItMC4yLTAuMy0wLjMtMC41bC0wLjEtMC4yYy0wLjEtMC4yLTAuMi0wLjMtMC4zLTAuNWwtMC4xLTAuMmMtMC4xLTAuMi0wLjItMC4zLTAuNC0wLjVsLTAuMi0wLjNsMi44LTMuMUwxOCwwLjZsLTQuNiwwLjFsMC41LDQuNWwwLjUsNC41bDMuMi0zLjZ2MC4xbDAuMSwwLjJjMC4xLDAuMSwwLjEsMC4yLDAuMiwwLjJsMC4xLDAuMkMxOCw3LDE4LDcuMSwxOC4xLDcuMmMwLjMsMC43LDAuNiwxLjQsMC43LDIuMWMwLjIsMS40LDAsMi45LTAuNiw0LjJMMTgsMTMuOUwxNy45LDE0bC0wLjMsMC41bC0wLjEsMC4yYy0wLjIsMC4yLTAuNCwwLjUtMC42LDAuN2MtMC41LDAuNS0xLjEsMS0xLjcsMS4zYy0wLjYsMC40LTEuMywwLjYtMi4xLDAuOGMtMC43LDAuMS0xLjUsMC4yLTIuMiwwLjFjLTAuOC0wLjEtMS41LTAuMy0yLjItMC41Yy0wLjctMC4zLTEuMy0wLjctMS45LTEuMmwtMC40LTAuNGwtMC4yLTAuM0w2LDE1Yy0wLjEtMC4xLTAuMi0wLjItMC4yLTAuM2wtMC4zLTAuNGwtMC4xLTAuMWwtMC4yLTAuNGMwLTAuMS0wLjEtMC4xLTAuMS0wLjJsLTAuMy0wLjVsLTAuMS0wLjJjLTAuMS0wLjMtMC4yLTAuNi0wLjMtMC45Yy0wLjItMC44LTAuMy0xLjYtMC4zLTIuNGMwLTAuMiwwLTAuMywwLTAuNVY4LjljMC0wLjIsMC0wLjMsMC4xLTAuNGwwLjEtMC42bDAuMi0wLjZjMC4zLTAuOCwwLjctMS41LDEuMi0yLjJjMC41LTAuNywxLjEtMS4zLDEuOC0xLjhjMC4yLTAuMSwwLjMtMC40LDAuMS0wLjZDNy41LDIuNiw3LjQsMi41LDcuMywyLjVINy4xTDcsMi42QzYuMSwzLDUuNCwzLjYsNC43LDQuMkM0LDQuOSwzLjUsNS43LDMsNi42Yy0wLjksMS44LTEuMiwzLjgtMC44LDUuOGMwLjEsMC41LDAuMiwwLjksMC4zLDEuNGwwLjMsMC44QzIuOSwxNC43LDMsMTQuOCwzLDE1bDAuMiwwLjRjMCwwLjEsMC4xLDAuMiwwLjEsMC4ybDAuMywwLjVjMC4xLDAuMiwwLjIsMC4zLDAuMywwLjVsMC4xLDAuMmMwLjEsMC4xLDAuMiwwLjMsMC4zLDAuNEw1LDE3LjhjMC43LDAuNywxLjYsMS4zLDIuNSwxLjhjMC45LDAuNSwxLjksMC44LDMsMC45YzAuNSwwLjEsMSwwLjEsMS41LDAuMWMwLjYsMCwxLjEsMCwxLjYtMC4xYzEtMC4yLDIuMS0wLjUsMy0xbDAuMi0wLjFjMC4yLTAuMSwwLjMtMC4yLDAuNS0wLjNsMC43LTAuNGMwLjItMC4xLDAuMy0wLjIsMC40LTAuM2wwLjItMC4yYzAuMi0wLjEsMC40LTAuMywwLjUtMC41bDAuMS0wLjFjMC4zLTAuMywwLjctMC43LDAuOS0xbDAuNi0wLjlsMC40LTAuNmMxLTEuOSwxLjQtNC4xLDEuMS02LjJDMjIsNy44LDIxLjcsNi43LDIxLjIsNS44eiIvPgogICAgPC9kZWZzPgogICAgPGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDIpIj4KICAgICAgICA8bWFzayBpZD0icm90YXRlLWIiIGZpbGw9IiNmZmYiPgogICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNyb3RhdGUiLz4KICAgICAgICA8L21hc2s+CiAgICAgICAgPHVzZSBmaWxsPSIjNUI1QjVCIiBmaWxsLXJ1bGU9Im5vbnplcm8iIHhsaW5rOmhyZWY9IiNyb3RhdGUiLz4KICAgICAgICA8ZyBmaWxsPSIjNUI1QjVCIiBtYXNrPSJ1cmwoI3JvdGF0ZS1iKSI+CiAgICAgICAgICAgIDxyZWN0IHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCIvPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==);
}
.leaflet-pm-toolbar .leaflet-pm-icon-text {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOm5vbmU7c3Ryb2tlOiM1YjViNWI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS13aWR0aDoyLjVweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlRleHQ8L3RpdGxlPjxnIGlkPSJFYmVuZV8yIiBkYXRhLW5hbWU9IkViZW5lIDIiPjxwb2x5bGluZSBjbGFzcz0iY2xzLTEiIHBvaW50cz0iMTkuNjQgNy4yNyAxOS42NCA0IDEyIDQgMTIgMjAgMTUuOTEgMjAgOC4wOSAyMCAxMiAyMCAxMiA0IDQuMzYgNCA0LjM2IDcuMjciLz48L2c+PC9zdmc+);
}
.leaflet-buttons-control-button:hover,
.leaflet-buttons-control-button:focus {
cursor: pointer;
background-color: #f4f4f4;
}
.active > .leaflet-buttons-control-button {
box-shadow: inset 0 -1px 5px 2px rgba(81, 77, 77, 0.31);
}
.leaflet-buttons-control-text-hide {
display: none;
}
.button-container {
position: relative;
}
.button-container .leaflet-pm-actions-container {
z-index: 2;
position: absolute;
top: 0;
left: 100%;
display: none;
white-space: nowrap;
direction: ltr;
}
.leaflet-right
.leaflet-pm-toolbar
.button-container
.leaflet-pm-actions-container {
right: 100%;
left: auto;
}
.button-container.active .leaflet-pm-actions-container {
display: block;
}
.button-container
.leaflet-pm-actions-container:not(.pos-right)
a.leaflet-pm-action:last-child {
border-radius: 0 3px 3px 0;
border-right: 0;
}
.button-container
.leaflet-pm-actions-container.pos-right
a.leaflet-pm-action:first-child {
border-radius: 3px 0 0 3px;
}
.button-container
.leaflet-pm-actions-container.pos-right
a.leaflet-pm-action:last-child {
border-right: 0;
}
.button-container .leaflet-pm-actions-container .leaflet-pm-action {
padding: 0 10px;
background-color: #666;
color: #fff;
display: inline-block;
width: auto;
border-right: 1px solid #eee;
user-select: none;
border-bottom: none;
height: 29px;
line-height: 29px;
}
.leaflet-pm-toolbar
.button-container:first-child.pos-right.active
a.leaflet-buttons-control-button {
border-top-left-radius: 0;
}
.leaflet-pm-toolbar
.button-container:first-child.active:not(.pos-right)
a.leaflet-buttons-control-button {
border-top-right-radius: 0;
}
.button-container .leaflet-pm-actions-container .leaflet-pm-action:hover,
.button-container .leaflet-pm-actions-container .leaflet-pm-action:focus {
cursor: pointer;
background-color: #777;
}
/* That the active control is always over the other controls */
.leaflet-pm-toolbar.activeChild {
z-index: 801;
}
.leaflet-buttons-control-button.pm-disabled {
background-color: #f4f4f4;
}
.leaflet-buttons-control-button.pm-disabled > .control-icon {
filter: opacity(0.6);
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,666 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #4CAF50;
border: 1px solid #666;
width: 7px !important;
height: 7px !important;
margin-left: -5.5px !important;
margin-top: -5.5px !important;
border-radius: 10px;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More