diff --git a/backend/apps/admin.js b/backend/apps/admin.js index 854a091..0cb3013 100644 --- a/backend/apps/admin.js +++ b/backend/apps/admin.js @@ -1,25 +1,46 @@ const crypto = require('crypto') const express = require('express') -const multer = require('multer') const db = require('../include/db') const bot = require('./bot') const fs = require('fs') -const cookieParser = require('cookie-parser') const app = express.Router() -const upload = multer({ - storage: multer.memoryStorage(), - limits: { - fileSize: 1_000_000 // 1mb - } -}) const sessions = {} +const cache = { + // email -> code + register: {}, + upgrade: {}, + recovery: {}, + 'change-password': {}, + 'change-email': {}, + 'change-email2': {} +} + +function checkEmail(email){ + return String(email) + .toLowerCase() + .match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) +} + +function sendEmail(email, subject, message) { + console.log(`${email} --> ${subject}: ${message}`) +} + +function checkPassword(password) { + return password.length >= 8 +} app.use((req, res, next) => { - if (req.path == '/customer/login' || - req.path == '/customer/register' || - req.path == '/customer/activate') + const public = [ + '/auth/email', + '/auth/telegram', + '/auth/email/register', + '/auth/email/recovery', + '/auth/logout' + ] + + if (public.includes(req.path)) return next() const asid = req.query.asid || req.cookies.asid @@ -31,105 +52,241 @@ app.use((req, res, next) => { next() }) -// CUSTOMER -app.post('/customer/login', (req, res, next) => { +// AUTH +function createSession(req, res, customer_id) { + if (!customer_id) + throw Error('AUTH_ERROR::500') + + res.locals.customer_id = customer_id + const asid = crypto.randomBytes(64).toString('hex') + req.session = sessions[asid] = {asid, customer_id } + res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`]) +} + +app.post('/auth/email', (req, res, next) => { res.locals.email = req.body?.email res.locals.password = req.body?.password - let customer_id = db - .prepare(` - select id - from customers - where is_active = 1 and ( - email is not null and email = :email and password is not null and password = :password or - email is null and password is null and telegram_user_id = :telegram_id - ) - `) + const customer_id = db + .prepare(`select id from customers where email = :email and password is not null and password = :password `) .pluck(true) .get(res.locals) - if (!customer_id && !res.locals.email && !res.locals.password) { - customer_id = db - .prepare(`insert into customers (telegram_user_id, is_active) values (:telegram_id, 1) returning id`) - .safeIntegers(true) - .pluck(true) - .get(res.locals) - } - if (!customer_id) throw Error('AUTH_ERROR::401') - res.locals.customer_id = customer_id - db - .prepare(`update customers set telegram_user_id = :telegram_id where id = :customer_id and email is not null`) - .run(res.locals) - - const asid = crypto.randomBytes(64).toString('hex') - req.session = sessions[asid] = {asid, customer_id } - res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`]) + createSession(req, res, customer_id) + res.status(200).json({success: true}) +}) + +app.post('/auth/telegram', (req, res, next) => { + let customer_id = db + .prepare(`select id from customers where telegram_id = :telegram_id`) + .pluck(true) + .get(res.locals) || db + .prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`) + .pluck(true) + .get(res.locals) + + createSession(req, res, customer_id) + res.status(200).json({success: true}) +}) + +/* + Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова + 1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код. + 2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК. + 3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК. +*/ +app.post('/auth/email/:action(register|upgrade)', (req, res, next) => { + const email = String(req.body.email ?? '').trim() + const code = String(req.body.code ?? '').trim() + const password = String(req.body.password ?? '').trim() + const action = req.params.action + + const stepNo = email && !code ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1 + if (stepNo == -1) + throw Error('BAD_STEP::400') + + if (stepNo == 1) { + if (!checkEmail(email)) + throw Error('INCORRECT_EMAIL::400') + + const customer_id = db + .prepare('select id from customers where email = :email') + .pluck(true) + .get({email}) + + if (customer_id) + throw Error('USED_EMAIL::400') + + const code = Math.random().toString().substr(2, 4) + cache[action][email] = code + sendEmail(email, action.toUpperCase(), `${email} => ${code}`) + } + + if (stepNo == 2) { + if (cache[action][email] != code) + throw Error('INCORRECT_CODE::400') + } + + if (stepNo == 3) { + if (!checkPassword(password)) + throw Error('INCORRECT_PASSWORD::400') + + const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' : + 'update customers set email = :email, password = :password where id = :id' + db.prepare(query).run({email, password, id: res.locals.customer_id}) + + delete cache[action][email] + } + + res.status(200).json({success: true}) +}) + + +/* + Смена email выполняется за ЧЕТЫРЕ последовательных вызовов + 1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код. + 2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК. + 3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email. + 4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет email пользователя на новый и возвращает ОК. +*/ +app.post('/auth/email/change-email', (req, res, next) => { + const email2 = String(req.body.email ?? '').trim() + const code = String(req.body.code ?? '').trim() + const code2 = String(req.body.code2 ?? '').trim() + + const email = db + .prepare('select email from customers where id = :customer_id') + .pluck(true) + .get(res.locals) + + const stepNo = !code ? 1 : code && !email ? 2 : code && email && !code2 ? 3 : code && email && code2 ? 4 : -1 + if (stepNo == -1) + throw Error('BAD_STEP::400') + + if (stepNo == 1) { + const code = Math.random().toString().substr(2, 4) + cache['change-email'][email] = code + sendEmail(email, 'CHANGE-EMAIL', `${email} => ${code}`) + } + + if (stepNo == 2) { + if (cache['change-email'][email] != code) + throw Error('INCORRECT_CODE::400') + } + + if (stepNo == 3) { + if (!checkEmail(email2)) + throw Error('INCORRECT_EMAIL::400') + + const code2 = Math.random().toString().substr(2, 4) + cache['change-email2'][email2] = code2 + sendEmail(email2, 'CHANGE-EMAIL2', `${email2} => ${code2}`) + } + + if (stepNo == 4) { + if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2) + throw Error('INCORRECT_CODE::400') + + const info = db + .prepare('update customers set email = :email where id = :customer_id') + .run(res.locals) + + if (info.changes == 0) + throw Error('BAD_REQUEST::400') + + delete cache['change-email'][email] + delete cache['change-email2'][email2] + } res.status(200).json({success: true}) }) -app.get('/customer/logout', (req, res, next) => { - delete sessions[req.session.asid] - res.setHeader('Set-Cookie', [`asid=; expired; httpOnly`]) +/* + Смена пароля/восстановление доступа выполняется за ТРИ последовательных вызова + 1. Отравляется пустой закпрос для смены запоса и email, в случае восстановления доступа. Сервер на email отправляет код. + 2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК. + 3. Отправляется email + код из письма + новый пароль. Сервер изменяет пароль и возвращает ОК. +*/ +app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => { + const code = String(req.body.code ?? '').trim() + const password = String(req.body.password) + const action = req.params.action + + const email = action == 'change-password' ? db + .prepare('select email from customers where id = :customer_id') + .pluck(true) + .get(res.locals) : + String(req.body.email ?? '').trim() + + const stepNo = action == 'change-password' ? + (!code && !password ? 1 : code && !password ? 2 : code && password ? 3 : -1) : + (!email && !code && !password ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1) + if (stepNo == -1) + throw Error('BAD_STEP::400') + + if (stepNo == 1) { + if (!checkEmail(email)) + throw Error('INCORRECT_EMAIL::400') + + const code = Math.random().toString().substr(2, 4) + cache[action][email] = code + sendEmail(email, action.toUpperCase(), `${email} => ${code}`) + } + + if (stepNo == 2) { + if (cache[action][email] != code) + throw Error('INCORRECT_CODE::400') + } + + if (stepNo == 3) { + if (cache[action][email] != code) + throw Error('INCORRECT_CODE::400') + + if (!checkPassword(password)) + throw Error('INCORRECT_PASSWORD::400') + + const info = db + .prepare('update customers set password = :password where email = :email') + .run({ email, password }) + + if (info.changes == 0) + throw Error('BAD_REQUEST::400') + + delete cache[action][email] + } + res.status(200).json({success: true}) }) -app.post('/customer/register', (req, res, next) => { - const email = String(req.body.email).trim() - const password = String(req.body.password).trim() +app.get('/auth/logout', (req, res, next) => { + if (req.session?.asid) + delete sessions[req.session.asid] - const validateEmail = email => String(email).toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) - if (!validateEmail(email)) - throw Error('INCORRECT_EMAIL::400') - - if (!password) - throw Error('EMPTY_PASSWORD::400') - - const row = db - .prepare('select id from customers where email = :email') - .run({email}) - if (row) - throw Error('DUPLICATE_EMAIL::400') - - const key = crypto.randomBytes(32).toString('hex') - const info = db - .prepare('insert into customers (email, password, activation_key) values (:email, :password, :key)') - .run({email, password, key}) - - // To-Do: SEND MAIL - console.log(`http://127.0.0.1:3000/api/customer/activate?key=${key}`) - res.status(200).json({success: true, data: key}) -}) - -app.get('/customer/activate', (req, res, next) => { - const row = db - .prepare('update customers set is_active = 1 where activation_key = :key returning id') - .get({key: req.query.key}) - - if (!row || !row.id) - throw Error('BAD_ACTIVATION_KEY::400') - - res.status(200).json({success: true}) + res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`]) + res.status(200).json({success: true}) }) +// CUSTOMER app.get('/customer/profile', (req, res, next) => { const row = db .prepare(` - select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_group_id + select id, name, email, plan, + coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, + upload_chat_id, generate_key(-id, :time) upload_token from customers - where id = :customer_id and is_active = 1 + where id = :customer_id `) .get(res.locals) - if (row?.upload_group_id) { - row.upload_group = db - .prepare(`select id, name, telegram_id from groups where id = :group_id and project_id is null`) + if (row?.upload_chat_id) { + row.upload_chat = db + .prepare(`select id, name, telegram_id from chats where id = :chat_id and project_id is null`) .safeIntegers(true) - .get({ group_id: row.upload_group_id}) - delete row.upload_group_id + .get({ chat_id: row.upload_chat_id}) + delete row.upload_chat_id } for (const key in row) { @@ -145,6 +302,8 @@ app.get('/customer/profile', (req, res, next) => { app.put('/customer/profile', (req, res, next) => { if (req.body.company instanceof Object) req.body.json_company = JSON.stringify(req.body.company) + else + delete req.body?.json_company const info = db .prepareUpdate( @@ -160,96 +319,91 @@ app.put('/customer/profile', (req, res, next) => { res.status(200).json({success: true}) }) -// PROJECT -app.get('/project', (req, res, next) => { - const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' +app.get('/customer/settings', (req, res, next) => { + const row = db + .prepare(`select coalesce(json_settings, '{}') from customers where id = :customer_id`) + .pluck(true) + .get(res.locals) + + res.status(200).json({success: true, data: JSON.parse(row)}) +}) - const rows = db +app.put('/customer/settings', (req, res, next) => { + res.locals.json_settings = JSON.stringify(req.body || {}) + + db + .prepare(`update customers set json_settings = :json_settings where id = :customer_id`) + .run(res.locals) + + res.status(200).json({success: true}) +}) + +// PROJECT +function getProject(id, customer_id) { + const row = db .prepare(` - select id, name, description, logo - from projects - where customer_id = :customer_id ${where} and is_deleted <> 1 + select id, name, description, logo, is_logo_bg, is_archived, + (select count(*) from chats where project_id = p.id) chat_count, + (select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count + from projects p + where customer_id = :customer_id and p.id = :id + order by name + `) + .get({id, customer_id}) + + if (!row) + throw Error('NOT_FOUND::404') + + row.is_archived = Boolean(row.is_archived) + row.is_logo_bg = Boolean(row.is_logo_bg) + + return row +} + +app.get('/project', (req, res, next) => { + const data = db + .prepare(` + select id, name, description, logo, is_logo_bg, is_archived, + (select count(*) from chats where project_id = p.id) chat_count, + (select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count + from projects p + where customer_id = :customer_id order by name `) .all(res.locals) - if (where && rows.length == 0) - throw Error('NOT_FOUND::404') + data.forEach(row => { + row.is_archived = Boolean(row.is_archived) + row.is_logo_bg = Boolean(row.is_logo_bg) + }) - res.status(200).json({success: true, data: where ? rows[0] : rows}) -}) - -app.get('/project/:pid(\\d+)', (req, res, next) => { - res.redirect(req.baseUrl + `/project?id=${req.params.pid}`) + res.status(200).json({success: true, data}) }) app.post('/project', (req, res, next) => { res.locals.name = req.body?.name res.locals.description = req.body?.description res.locals.logo = req.body?.logo + res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined const id = db .prepare(` - insert into projects (customer_id, name, description, logo) - values (:customer_id, :name, :description, :logo) + insert into projects (customer_id, name, description, logo, is_logo_bg) + values (:customer_id, :name, :description, :logo, :is_logo_bg) returning id `) .pluck(true) .get(res.locals) - res.status(200).json({success: true, data: id}) + const data = getProject(id, res.locals.customer_id) + res.status(200).json({success: true, data}) }) -app.put('/project/:pid(\\d+)', (req, res, next) => { - res.locals.id = req.params.pid - res.locals.name = req.body?.name - res.locals.description = req.body?.description - res.locals.logo = req.body?.logo - - const info = db - .prepareUpdate( - 'projects', - ['name', 'description', 'logo'], - res.locals, - ['id', 'customer_id']) - .run(res.locals) - - if (info.changes == 0) - throw Error('NOT_FOUND::404') - - res.status(200).json({success: true}) -}) - -app.delete('/project/:pid(\\d+)', async (req, res, next) => { - res.locals.id = req.params.pid - - const info = db - .prepare('update projects set id_deleted = 1 where id = :id and customer_id = :customer_id') - .run(res.locals) - - if (info.changes == 0) - throw Error('NOT_FOUND::404') - - const groupIds = db - .prepare(`select id from groups where project_id = :id`) - .pluck(true) - .all(res.locals) - - for (const groupId of groupIds) { - await bot.sendMessage(groupId, 'Проект удален') - await bot.leaveGroup(groupId) - } - - db.prepare(`updates groups set project_id = null where id in (${ groupIds.join(', ')})`).run() - - res.status(200).json({success: true}) -}) - -app.use ('/project/:pid(\\d+)/*', (req, res, next) => { +app.use ('(/project/:pid(\\d+)/*|/project/:pid(\\d+))', (req, res, next) => { res.locals.project_id = parseInt(req.params.pid) const row = db - .prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_deleted <> 1') + .prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_archived <> 1') .get(res.locals) if (!row) @@ -258,55 +412,134 @@ app.use ('/project/:pid(\\d+)/*', (req, res, next) => { next() }) -// USER -app.get('/project/:pid(\\d+)/user', (req, res, next) => { - const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' +app.get('/project/:pid(\\d+)', (req, res, next) => { + const data = getProject(req.params.pid, res.locals.customer_id) + res.status(200).json({success: true, data}) +}) - const rows = db +app.put('/project/:pid(\\d+)', (req, res, next) => { + res.locals.id = req.params.pid + res.locals.name = req.body?.name + res.locals.description = req.body?.description + res.locals.logo = req.body?.logo + res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined + + const info = db + .prepareUpdate( + 'projects', + ['name', 'description', 'logo', 'is_logo_bg'], + res.locals, + ['id', 'customer_id']) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + const data = getProject(req.params.pid, res.locals.customer_id) + res.status(200).json({success: true, data}) +}) + +app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => { + res.locals.id = req.params.pid + res.locals.is_archived = +(req.params.action == 'archive') + + const info = db + .prepare(` + update projects + set is_archived = :is_archived + where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived + `) + .run(res.locals) + + if (info.changes == 0) + throw Error('BAD_REQUEST::400') + + const chatIds = db + .prepare(`select id from chats where project_id = :id`) + .pluck(true) + .all(res.locals) + + for (const chatId of chatIds) { + await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.') + } + + const data = getProject(req.params.pid, res.locals.customer_id) + res.status(200).json({success: true, data}) +}) + +// USER +function getUser(id, project_id) { + const row = db .prepare(` select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo, - ud.fullname, ud.role, ud.department, ud.is_blocked + ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked, + cu.company_id from users u left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id + left join company_users cu on u.id = cu.user_id + where id = :id + `) + .safeIntegers(true) + .all({id, project_id}) + + if (!row) + throw Error('NOT_FOUND::404') + + row.is_blocked = Boolean(row.is_blocked) + + return row +} + +app.get('/project/:pid(\\d+)/user', (req, res, next) => { + const data = db + .prepare(` + select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo, + ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked, + cu.company_id + from users u + left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id + left join company_users cu on u.id = cu.user_id where id in ( select user_id - from group_users - where group_id in (select id from groups where project_id = :project_id) - ) ${where} + from chat_users + where chat_id in (select id from chats where project_id = :project_id) + ) `) .safeIntegers(true) .all(res.locals) - if (where && rows.length == 0) - throw Error('NOT_FOUND::404') + data.forEach(row => { + row.is_blocked = Boolean(row.is_blocked) + }) - res.status(200).json({success: true, data: where ? rows[0] : rows}) + res.status(200).json({success: true, data}) }) app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => { - res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`) + const data = getUser(req.params.uid, req.params.pid) + res.status(200).json({success: true, data}) }) app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => { res.locals.user_id = parseInt(req.params.uid) res.locals.fullname = req.body?.fullname + res.locals.email = req.body?.email + res.locals.phone = req.body?.phone res.locals.role = req.body?.role res.locals.department = req.body?.department - res.locals.is_blocked = req.body?.is_blocked + res.locals.is_blocked = 'is_blocked' in req.body ? +req.body.is_blocked : undefined const info = db - .prepareUpdate('user_details', - ['fullname', 'role', 'department', 'is_blocked'], + .prepareUpsert('user_details', + ['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'], res.locals, ['user_id', 'project_id'] ) - .all(res.locals) - - if (info.changes == 0) - throw Error('NOT_FOUND::404') + .run(res.locals) - res.status(200).json({success: true}) + const data = getUser(req.params.uid, req.params.pid) + res.status(200).json({success: true, data}) }) app.get('/project/:pid(\\d+)/token', (req, res, next) => { @@ -324,33 +557,47 @@ app.get('/project/:pid(\\d+)/token', (req, res, next) => { }) // COMPANY -app.get('/project/:pid(\\d+)/company', (req, res, next) => { - const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' - - const rows = db +function getCompany(id, project_id) { + const row = db .prepare(` - select id, name, email, phone, description, logo, + select id, name, address, email, phone, site, description, logo, (select json_group_array(user_id) from company_users where company_id = c.id) users from companies c - where project_id = :project_id ${where} + where c.id = :id and project_id = :project_id + order by name + `) + .get({id, project_id}) + + if (!row) + throw Error('NOT_FOUND::404') + + return row +} + +app.get('/project/:pid(\\d+)/company', (req, res, next) => { + const data = db + .prepare(` + select id, name, address, email, phone, site, description, logo, + (select json_group_array(user_id) from company_users where company_id = c.id) users + from companies c + where project_id = :project_id order by name `) .all(res.locals) - rows.forEach(row => row.users = JSON.parse(row.users || '[]')) + data.forEach(row => row.users = JSON.parse(row.users || '[]')) - if (where && rows.length == 0) - throw Error('NOT_FOUND::404') - - res.status(200).json({success: true, data: where ? rows[0] : rows}) + res.status(200).json({success: true, data}) }) app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { - res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`) + const data = getCompany(req.params.cid, req.params.pid) + res.status(200).json({success: true, data}) }) app.post('/project/:pid(\\d+)/company', (req, res, next) => { res.locals.name = req.body?.name + res.locals.address = req.body?.address res.locals.email = req.body?.email res.locals.phone = req.body?.phone res.locals.site = req.body?.site @@ -359,28 +606,31 @@ app.post('/project/:pid(\\d+)/company', (req, res, next) => { const id = db .prepare(` - insert into companies (project_id, name, email, phone, site, description, logo) - values (:project_id, :name, :email, :phone, :site, :description, :logo) + insert into companies (project_id, name, address, email, phone, site, description, logo) + values (:project_id, :name, :address, :email, :phone, :site, :description, :logo) returning id `) - .pluck(res.locals) + .pluck(true) .get(res.locals) - res.status(200).json({success: true, data: id}) + const data = getCompany(id, req.params.pid) + res.status(200).json({success: true, data}) }) app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { res.locals.id = parseInt(req.params.cid) res.locals.name = req.body?.name + res.locals.address = req.body?.address res.locals.email = req.body?.email res.locals.phone = req.body?.phone res.locals.site = req.body?.site res.locals.description = req.body?.description + res.locals.logo = req.body?.logo const info = db .prepareUpdate( 'companies', - ['name', 'email', 'phone', 'site', 'description'], + ['name', 'address', 'email', 'phone', 'site', 'description', 'logo'], res.locals, ['id', 'project_id']) .run(res.locals) @@ -388,11 +638,12 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { if (info.changes == 0) throw Error('NOT_FOUND::404') - res.status(200).json({success: true}) + const data = getCompany(req.params.cid, req.params.pid) + res.status(200).json({success: true, data}) }) app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { - res.locals.company_id = parseInt(req.params.cid) + res.locals.company_id = req.params.cid const info = db .prepare(`delete from companies where id = :company_id and project_id = :project_id`) @@ -401,42 +652,7 @@ app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => { if (info.changes == 0) throw Error('NOT_FOUND::404') - res.status(200).json({success: true}) -}) - -app.get('/project/:pid(\\d+)/group', (req, res, next) => { - const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : '' - - const rows = db - .prepare(` - select id, name, telegram_id, is_channel, user_count, bot_can_ban - from groups - where project_id = :project_id ${where} - `) - .all(res.locals) - - if (where && rows.length == 0) - throw Error('NOT_FOUND::404') - - res.status(200).json({success: true, data: where ? rows[0] : rows}) -}) - -app.get('/project/:pid(\\d+)/group/:gid(\\d+)', (req, res, next) => { - res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.uid}`) -}) - -app.delete('/project/:pid(\\d+)/group/:gid(\\d+)', async (req, res, next) => { - res.locals.group_id = parseInt(req.params.gid) - const info = db - .prepare(`update groups set project_id = null where id = :group_id and project_id = :project_id`) - .run(res.locals) - - if (info.changes == 0) - throw Error('NOT_FOUND::404') - - await bot.sendMessage(res.locals.group_id, 'Группа удалена из проекта') - - res.status(200).json({success: true}) + res.status(200).json({success: true, data: {id: req.params.cid}}) }) app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { @@ -456,8 +672,8 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { let rows = db .prepare(` select user_id - from group_users - where group_id in (select id from groups where project_id = :project_id) + from chat_users + where chat_id in (select id from chats where project_id = :project_id) `) .pluck(true) // .raw? .get(res.locals) @@ -478,7 +694,6 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { if (user_ids.some(user_id => !rows.contains(user_id))) throw Error('USED_MEMBER::400') - db .prepare(`delete from company_users where company_id = :company_id`) .run(res.locals) @@ -493,4 +708,59 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => { res.status(200).json({success: true}) }) +// CHATS +function getChat(id, project_id) { + const row = db + .prepare(` + select id, name, telegram_id, is_channel, description, logo, user_count, bot_can_ban + from chats c + where c.id = :id and project_id = :project_id + `) + .all({id, project_id}) + + if (!row) + throw Error('NOT_FOUND::404') + + row.is_channel = Boolean(row.is_channel) + row.bot_can_ban = Boolean(row.bot_can_ban) + + return row +} + +app.get('/project/:pid(\\d+)/chat', (req, res, next) => { + const data = db + .prepare(` + select id, name, telegram_id, is_channel, description, logo, user_count, bot_can_ban + from chats + where project_id = :project_id + `) + .all(res.locals) + + data.forEach(row => { + row.is_channel = Boolean(row.is_channel) + row.bot_can_ban = Boolean(row.bot_can_ban) + }) + + res.status(200).json({success: true, data}) +}) + +app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => { + const data = getChat(req.params.gid, req.params.pid) + res.status(200).json({success: true, data}) +}) + +app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => { + res.locals.chat_id = req.params.gid + const info = db + .prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`) + .run(res.locals) + + if (info.changes == 0) + throw Error('NOT_FOUND::404') + + await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта') + + res.status(200).json({success: true, data: {id: req.params.gid}}) +}) + module.exports = app \ No newline at end of file diff --git a/backend/apps/bot.js b/backend/apps/bot.js index 963680c..0f84d29 100644 --- a/backend/apps/bot.js +++ b/backend/apps/bot.js @@ -1,19 +1,14 @@ -const util = require('util') -const crypto = require('crypto') -const EventEmitter = require('events') -const fs = require('fs') const db = require('../include/db') const { Api, TelegramClient } = require('telegram') const { StringSession } = require('telegram/sessions') -const { NewMessage } = require('telegram/events') const { Button } = require('telegram/tl/custom/button') const { CustomFile } = require('telegram/client/uploads') -// const session = new StringSession('1AgAOMTQ5LjE1NC4xNjcuNTABuxdIxmjimA0hmWpdrlZ4Fo7uoIGU4Bu9+G5QprS6zdtyeMfcssWEZp0doLRX/20MomQyF4Opsos0El0Ifj5aiNgg01z8khMLMeT98jS+1U/sh32p3GxZfxyXSxX1bD0NLRaXnqVyNNswYqRZPhboT28NMjDqwlz0nrW9rge+QMJDL7jIkXgSs+cmJBINiqsEI8jWjXmc8TU/17gngtjUHRf5kRM4y5gsNC4O8cF5lcHRx0G/U5ZVihTID8ItQ6EdEHjz6e4XErbVOJ81PfYkqEoPXVvkEmRM0/VbvCzFfixfas4Vzczfn98OHLd8P2MXcgokZ2rppvIV3fQXOHxJbA0=') -const session = new StringSession('') - +let session let client +let BOT_ID +const BOT_NAME = 'ready_or_not_2025_bot' function registerUser (telegramId) { db @@ -78,121 +73,125 @@ async function updateUserPhoto (userId, data) { db .prepare(`update users set photo_id = :photo_id, photo = :photo where id = :user_id`) .safeIntegers(true) - .run({ user_id: userId, photo_id: photoId, photo: file.toString('base64') }) + .run({ user_id: userId, photo_id: photoId, photo: 'data:image/jpg;base64,' + file.toString('base64') }) } -async function registerGroup (telegramId, isChannel) { - db - .prepare(`insert or ignore into groups (telegram_id, is_channel) values (:telegram_id, :is_channel)`) +async function registerChat (telegramId, isChannel) { + const chat = db + .prepare(`select id, name, is_channel, access_hash from chats where telegram_id = :telegram_id`) .safeIntegers(true) - .run({ telegram_id: telegramId, is_channel: +isChannel }) + .get({telegram_id: telegramId}) - const row = db - .prepare(`select id, name from groups where telegram_id = :telegram_id`) - .safeIntegers(true) - .get({telegram_id: telegramId}) - - if (!row?.name) { - const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId } - const group = await client.getEntity(isChannel ? new Api.PeerChannel(entity) : new Api.PeerChat(entity)) - - db - .prepare(`update groups set name = :name where id = :group_id`) - .run({ group_id: row.id, name: group.title }) - } - - return row.id -} - -async function attachGroup(groupId, isChannel, projectId) { - const info = db - .prepare(`update groups set project_id = :project_id where id = :group_id and coalesce(project_id, 1) = 1`) - .run({ group_id: groupId, project_id: projectId }) - - if (info.changes == 1) { - const inputPeer = isChannel ? - new Api.InputPeerChannel({ channelId: tgGroupId }) : - new Api.InputPeerChat({ chatlId: tgGroupId }) - - const query = `select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id` - const message = db - .prepare(query) - .pluck(true) - .get({project_id: projectId}) - if (message) - await client.sendMessage(inputPeer, {message}) - } - - return info.changes == 1 -} - -async function onGroupAttach (tgGroupId, isChannel) { - const projectId = db - .prepare(`select project_id from groups where telegram_id = :telegram_id`) - .safeIntegers(true) - .pluck(true) - .get({ telegram_id: tgGroupId }) - - const entity = isChannel ? { channelId: tgGroupId } : { chatId: tgGroupId } - const inputPeer = await client.getEntity( isChannel ? + if (chat && chat.access_hash && chat.is_channel == isChannel && chat.name) + return chat.id + + const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId } + const tgChat = await client.getEntity( isChannel ? new Api.InputPeerChannel(entity) : new Api.InputPeerChat(entity) ) - - const resultBtn = await client.sendMessage(inputPeer, { - message: 'ReadyOrNot', - buttons: client.buildReplyMarkup([[Button.url('Открыть проект', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user_' + projectId)]]) + + const chatId = db + .prepare(`replace into chats (telegram_id, is_channel, access_hash, name) values (:telegram_id, :is_channel, :access_hash, :name) returning id`) + .safeIntegers(true) + .pluck(true) + .get({ + telegram_id: telegramId, + is_channel: +isChannel, + access_hash: tgChat.accessHash.value, + name: tgChat.title + }) + + await updateChat(chatId) + + return chatId +} + +async function updateChat (chatId) { + const chat = db + .prepare(`select id, telegram_id, access_hash, is_channel from chats where id = :id`) + .safeIntegers(true) + .get({id: chatId}) + + const peer = chat.is_channel ? + new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) : + new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }) + + const data = chat.is_channel ? + await client.invoke(new Api.channels.GetFullChannel({ channel: peer })) : + await client.invoke(new Api.messages.GetFullChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })) + + const file = data?.fullChat?.chatPhoto ? await client.downloadFile(new Api.InputPeerPhotoFileLocation({ peer, photoId: data.fullChat.chatPhoto?.id }, {})) : null + logo = file ? 'data:image/jpg;base64,' + file.toString('base64') : null + + db + .prepare(`update chats set description = :description, logo = :logo, user_count = :user_count, last_update_time = :last_update_time where id = :id`) + .safeIntegers(true) + .run({ + id: chatId, + description: data.fullChat.about, + logo, + user_count: data.fullChat.participantsCount - (data.users || []).filter(user => user.bot).length, + last_update_time: Math.floor(Date.now() / 1000) + }) +} + +async function attachChat(chatId, projectId) { + const chat = db + .prepare(`update chats set project_id = :project_id where id = :chat_id returning telegram_id, access_hash, is_channel`) + .safeIntegers(true) + .get({ chat_id: chatId, project_id: projectId }) + + if (!chat.telegram_id) + return console.error('Can\'t attach chat: ' + chatId + ' to project: ' + projectId) + + const peer = chat.is_channel ? + new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) : + new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }) + + const message = db + .prepare(`select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id`) + .pluck(true) + .get({project_id: projectId}) + + const resultBtn = await client.sendMessage(peer, { + message, + buttons: client.buildReplyMarkup([[Button.url('Открыть проект', `https://t.me/${BOT_NAME}/userapp?startapp=` + projectId)]]) }) await client.invoke(new Api.messages.UpdatePinnedMessage({ - peer: inputPeer, + peer, id: resultBtn.id, unpin: false })) - -//fs.appendFileSync('./1.log', '\n>' + tgGroupId + ':' + isChannel + '<\n') } -async function reloadGroupUsers(groupId, onlyReset) { +async function reloadChatUsers(chatId, onlyReset) { db - .prepare(`delete from group_users where group_id = :group_id`) - .run({ group_id: groupId }) + .prepare(`delete from chat_users where chat_id = :chat_id`) + .run({ chat_id: chatId }) if (onlyReset) return - const group = db - .prepare(`select telegram_id, is_channel, access_hash from groups where id = :group_id`) - .get({ group_id: groupId}) + const chat = db + .prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`) + .get({ chat_id: chatId}) - console.log (123, group) - - if (!group) + if (!chat) return - const tgGroupId = group.telegram_id - const isChannel = group.is_channel - let accessHash = group.access_hash - - console.log ('HERE') - - db - .prepare(`update groups set access_hash = :access_hash where id = :group_id`) - .safeIntegers(true) - .run({ - group_id: groupId, - access_hash: accessHash, - }) + const tgChatId = chat.telegram_id + const isChannel = chat.is_channel + const accessHash = chat.access_hash const result = isChannel ? await client.invoke(new Api.channels.GetParticipants({ - channel: new Api.PeerChannel({ channelId: tgGroupId }), + channel: new Api.PeerChannel({ channelId: tgChatId, accessHash }), filter: new Api.ChannelParticipantsRecent(), limit: 999999, offset: 0 - })) : await client.invoke(new Api.messages.GetFullChat({ - chatId: tgGroupId, - })) + })) : await client.invoke(new Api.messages.GetFullChat({ chatId: tgChatId, accessHash })) const users = result.users.filter(user => !user.bot) for (const user of users) { @@ -201,37 +200,42 @@ async function reloadGroupUsers(groupId, onlyReset) { if (updateUser(userId, user)) { await updateUserPhoto (userId, user) - const query = `insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)` - db.prepare(query).run({ group_id: groupId, user_id: userId }) + db + .prepare(`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`) + .run({ chat_id: chatId, user_id: userId }) } } + + db + .prepare(`update chats set user_count = (select count(1) from chat_users where chat_id = :chat_id) where id = :chat_id`) + .run({ chat_id: chatId}) } async function registerUpload(data) { if (!data.projectId || !data.media) - return false + return console.error ('registerUpload: ' + (data.projectId ? 'media' : 'project id') + ' is missing') - const uploadGroup = db - .prepare(` - select id, telegram_id, project_id, is_channel, access_hash - from groups - where id = (select upload_group_id - from customers - where id = (select customer_id from projects where id = :project_id limit 1) - limit 1) - limit 1 - `) - .safeIntegers(true) + const customer_id = db + .prepare(`select customer_id from projects where project_id = :project_id`) + .pluck(true) .get({project_id: data.projectId}) - if (!uploadGroup || !uploadGroup.telegram_id || uploadGroup.id == data.originGroupId) - return false + if (!customer_id) + return console.error ('registerUpload: The customer is not found for project: ' + data.projectId) - const tgUploadGroupId = uploadGroup.telegram_id + const chat = db + .prepare( + `select id, telegram_id, project_id, is_channel, access_hash from chats + where id = (select upload_chat_id from customers where id = :customer_id`) + .safeIntegers(true) + .get({ customer_id }) - const peer = uploadGroup.is_channel ? - new Api.PeerChannel({ channelId: tgUploadGroupId }) : - new Api.PeerChat({ chatlId: tgUploadGroupId }) + if (!chat || !chat.telegram_id || chat.id == data.originchatId) + return console.error ('registerUpload: The upload chat is not set for customer: ' + customer_id) + + const peer = chat.is_channel ? + new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) : + new Api.PeerChat({ chatlId: chat.telegram_id, accessHash: chat.access_hash }) let resultId = 0 @@ -247,26 +251,26 @@ async function registerUpload(data) { const update = result.updates.find(u => (u.className == 'UpdateNewMessage' || u.className == 'UpdateNewChannelMessage') && u.message.className == 'Message' && - (u.message.peerId.channelId?.value == tgUploadGroupId || u.message.peerId.chatId?.value == tgUploadGroupId) && + (u.message.peerId.channelId?.value == chat.telegram_id || u.message.peerId.chatId?.value == chat.telegram_id) && u.message.media) const udoc = update?.message?.media?.document if (udoc) { resultId = db .prepare(` - insert into documents (project_id, origin_group_id, origin_message_id, group_id, message_id, - file_id, access_hash, filename, mime, caption, size, published_by, parent_type, parent_id) - values (:project_id, :origin_group_id, :origin_message_id, :group_id, :message_id, - :file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :parent_type, :parent_id) + insert into files (project_id, origin_chat_id, origin_message_id, chat_id, message_id, + file_id, access_hash, filename, mime, caption, size, published_by, published, parent_type, parent_id) + values (:project_id, :origin_chat_id, :origin_message_id, :chat_id, :message_id, + :file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :published, :parent_type, :parent_id) returning id `) .safeIntegers(true) .pluck(true) .get({ project_id: data.projectId, - origin_group_id: data.originGroupId, + origin_chat_id: data.originchatId, origin_message_id: data.originMessageId, - group_id: uploadGroup.id, + chat_id: chat.id, message_id: update.message.id, file_id: udoc.id.value, filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName, @@ -275,14 +279,15 @@ async function registerUpload(data) { caption: data.caption, size: udoc.size.value, published_by: data.publishedBy, + published: data.published, parent_type: data.parentType, parent_id: data.parentId }) } } catch (err) { - fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n') - console.error('Message.registerUpload: ' + err.message) + //fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n') + console.error('registerUpload: ' + err.message) } return resultId @@ -290,14 +295,14 @@ async function registerUpload(data) { async function onNewServiceMessage (msg, isChannel) { const action = msg.action || {} - const tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value - const groupId = await registerGroup(tgGroupId, isChannel) + const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value + const chatId = await registerChat(tgChatId, isChannel) - // Group/Channel rename + // Сhat rename if (action.className == 'MessageActionChatEditTitle') { const info = db .prepare(` - update groups + update chats set name = :name, is_channel = :is_channel, last_update_time = :last_update_time where telegram_id = :telegram_id `) @@ -306,15 +311,18 @@ async function onNewServiceMessage (msg, isChannel) { name: action.title, is_channel: +isChannel, last_update_time: Math.floor (Date.now() / 1000), - telegram_id: tgGroupId + telegram_id: tgChatId }) + + if (info.changes == 0) + console.error('onNewServiceMessage: Can\'t update a chat title: ' + tgChatId) } // Chat to Channel if (action.className == 'MessageActionChatMigrateTo') { const info = db .prepare(` - update groups + update chats set telegram_id = :new_telegram_id, name = :name, is_channel = 1, last_update_time = :last_update_time where telegram_id = :old_telegram_id `) @@ -322,9 +330,12 @@ async function onNewServiceMessage (msg, isChannel) { .run({ name: action.title, last_update_time: Date.now() / 1000, - old_telegram_id: tgGroupId, + old_telegram_id: tgChatId, new_telegram_id: action.channelId.value }) + + if (info.changes == 0) + console.error('onNewServiceMessage: Can\'t apply a chat migration to channel: ' + tgChatId) } // User/s un/register @@ -335,7 +346,7 @@ async function onNewServiceMessage (msg, isChannel) { const tgUserIds = [action.user, action.users, action.userId].flat().filter(Boolean).map(e => BigInt(e.value)) const isAdd = action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChannelAddUser' - if (tgUserIds.indexOf(bot.id) == -1) { + if (tgUserIds.indexOf(BOT_ID) == -1) { // Add/remove non-bot users for (const tgUserId of tgUserIds) { const userId = registerUser(tgUserId) @@ -351,27 +362,29 @@ async function onNewServiceMessage (msg, isChannel) { } const query = isAdd ? - `insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)` : - `delete from group_users where group_id = :group_id and user_id = :user_id` - db.prepare(query).run({ group_id: groupId, user_id: userId }) + `insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)` : + `delete from chat_users where chat_id = :chat_id and user_id = :user_id` + db + .prepare(query) + .run({ chat_id: chatId, user_id: userId }) } } } } async function onNewMessage (msg, isChannel) { - const tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value - const groupId = await registerGroup(tgGroupId, isChannel) + const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value + const chatId = await registerChat(tgChatId, isChannel) // Document is detected if (msg.media?.document) { const doc = msg.media.document const projectId = db - .prepare(`select project_id from groups where telegram_id = :telegram_id`) + .prepare(`select project_id from chats where telegram_id = :telegram_id`) .safeIntegers(true) .pluck(true) - .get({telegram_id: tgGroupId}) + .get({telegram_id: tgChatId}) const media = new Api.InputMediaDocument({ id: new Api.InputDocument({ @@ -385,101 +398,55 @@ async function onNewMessage (msg, isChannel) { projectId, media, caption: msg.message, - originGroupId: groupId, + originchatId: chatId, originMessageId: msg.id, parentType: 0, - publishedBy: registerUser (msg.fromId?.userId?.value) + publishedBy: registerUser (msg.fromId?.userId?.value), + published: msg.date }) } - if (msg.message?.startsWith('KEY-')) { - let projectName = db + if (msg.message?.startsWith(`/start@${BOT_NAME} KEY-`) || msg.message?.startsWith('KEY-')) { + const rows = db .prepare(` - select name - from projects - where id in ( - select project_id from groups where id = :group_id - union - select id from projects where upload_group_id = :group_id) + select 1 from chats where id = :chat_id and project_id is not null + union all + select 1 from customers where upload_chat_id = :chat_id `) - .pluck(true) - .get({ group_id: groupId }) + .all({ chat_id: chatId }) - if (projectName) - return await bot.sendMessage(groupId, 'Группа уже используется на проекте ' + projectName) + if (rows.length) + return await sendMessage(chatId, 'Чат уже используется') - const [_, time64, key] = msg.message.substr(3).split('-') + const rawkey = msg.message.substr(msg.message?.indexOf('KEY-')) + const [_, time64, key] = rawkey.split('-') const now = Math.floor(Date.now() / 1000) const time = Buffer.from(time64, 'base64') if (now - 3600 >= time && time >= now) - return await bot.sendMessage(groupId, 'Время действия ключа для привязки истекло') + return await sendMessage(chatId, 'Время действия ключа для привязки истекло') - const projectId = db - .prepare(`select id from projects where generate_key(id, :time) = :key`) - .pluck(true) - .get({ key: msg.message.trim(), time }) - - if (projectId) { - await attachGroup(groupId, isChannel, projectId) - await onGroupAttach(tgGroupId, isChannel) + const row = db + .prepare(` + select (select id from projects where generate_key(id, :time) = :rawkey) project_id, + (select id from customers where generate_key(-id, :time) = :rawkey) customer_id + `) + .get({ rawkey, time }) + + if (row.project_id) { + await attachChat(chatId, row.project_id) + await reloadChatUsers(chatId) } - } - - if (msg.message?.startsWith('/start')) { - // Called by https://t.me/ready_or_not_2025_bot?startgroup= - if (/start@ready_or_not_2025_bot (-|)([\d]+)$/g.test(msg.message)) { - const tgUserId = msg.fromId?.userId?.value - const param = +msg.message.split(' ')[1] - // Set upload group for customer - if (param < 0) { - const customerId = -param + if (row.customer_id) { + const info = db + .prepare(`update customers set upload_chat_id = :chat_id where id = :customer_id`) + .safeIntegers(true) + .run({ customer_id: row.customer_id, chat_id: chatId }) - db - .prepare(` - update customers - set upload_group_id = :group_id - where id = :customer_id and telegram_user_id = :telegram_user_id - `) - .safeIntegers(true) - .run({ - group_id: groupId, - customer_id: customerId, - telegram_user_id: tgUserId - }) - } - - // Add group to project - if (param > 0) { - const projectId = param - - const customerId = db - .prepare(`select customer_id from projects where id = :project_id`) - .pluck(true) - .get({project_id: projectId}) - - db - .prepare(` - update groups - set project_id = :project_id - where id = :group_id and exists( - select 1 - from customers - where id = :customer_id and telegram_user_id = :telegram_user_id) - `) - .safeIntegers(true) - .run({ - project_id: projectId, - group_id: groupId, - customer_id: customerId, - telegram_user_id: tgUserId - }) - - await reloadGroupUsers(groupId, false) - await onGroupAttach(tgGroupId, isChannel) - } - } + if (info.changes == 0) + console.error('Can\'t set upload chat: ' + chatId + ' to customer: ' + row.customer_id) + } } } @@ -493,12 +460,19 @@ async function onNewUserMessage (msg) { updateUser(userId, user) await updateUserPhoto (userId, user) + const appButton = new Api.KeyboardButtonWebView({ + text: "Open Mini-App", // Текст на кнопке + url: "https://h5sj0gpz-3000.euw.devtunnels.ms/", // URL вашего Mini-App (HTTPS!) + }); + + const inputPeer = new Api.InputPeerUser({userId: tgUserId, accessHash: user.accessHash.value}) - const resultBtn = await client.sendMessage(inputPeer, { + await client.sendMessage(inputPeer, { message: 'Сообщение от бота', buttons: client.buildReplyMarkup([ - [Button.url('Админка', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=admin')], - [Button.url('Пользователь', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user')] + [Button.url('Админка', `https://t.me/${BOT_NAME}/userapp?startapp=admin`)], + [Button.url('Пользователь', `https://t.me/${BOT_NAME}/userapp?startapp=user`)], + [appButton] ]) }) } catch (err) { @@ -508,41 +482,125 @@ async function onNewUserMessage (msg) { } async function onUpdatePaticipant (update, isChannel) { - const tgGroupId = isChannel ? update.channelId?.value : update.chatlId?.value - if (!tgGroupId || update.userId?.value != bot.id) + const tgChatId = isChannel ? update.channelId?.value : update.chatlId?.value + if (!tgChatId || update.userId?.value != BOT_ID) return - const groupId = await registerGroup (tgGroupId, isChannel) + const chatId = await registerChat (tgChatId, isChannel) const isBan = update.prevParticipant && !update.newParticipant const isAdd = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant if (isBan || isAdd) - await reloadGroupUsers(groupId, isBan) + await reloadChatUsers(chatId, isBan) if (isBan) { db - .prepare(`update groups set project_id = null where id = :group_id`) - .run({group_id: groupId}) + .prepare(`update chats set project_id = null where id = :chat_id`) + .run({chat_id: chatId}) } const botCanBan = update.newParticipant?.adminRights?.banUsers || 0 db - .prepare(`update groups set bot_can_ban = :bot_can_ban where id = :group_id`) - .run({group_id: groupId, bot_can_ban: +botCanBan}) + .prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`) + .run({chat_id: chatId, bot_can_ban: +botCanBan}) } -class Bot extends EventEmitter { - - async start (apiId, apiHash, botAuthToken) { - this.id = 7236504417n +async function uploadFile(projectId, fileName, mime, data, parentType, parentId, publishedBy) { + const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 }) - client = new TelegramClient(session, apiId, apiHash, {}) + const media = new Api.InputMediaUploadedDocument({ + file, + mimeType: mime, + attributes: [new Api.DocumentAttributeFilename({ fileName })] + }) - client.addEventHandler(async (update) => { - if (update.className == 'UpdateConnectionState') - return + return await registerUpload({ + projectId, + media, + parentType, + parentId, + publishedBy, + published: Math.floor(Date.now() / 1000) + }) +} +async function downloadFile(projectId, fileId) { + const file = db + .prepare(` + select file_id, access_hash, '' thumbSize, filename, mime + from files where id = :id and project_id = :project_id + `) + .safeIntegers(true) + .get({project_id: projectId, id: fileId}) + + if (!file) + return false + + const result = await client.downloadFile(new Api.InputDocumentFileLocation({ + id: file.file_id, + accessHash: file.access_hash, + fileReference: Buffer.from(file.filename), + thumbSize: '' + }, {})) + + return { + filename: file.filename, + mime: file.mime, + size: result.length, + data: result + } +} + +async function sendMessage (chatId, message) { + const chat = db + .prepare(`select telegram_id, is_channel from chats where id = :chat_id`) + .get({ chat_id: chatId}) + + if (!chat) + return + + const entity = chat.is_channel ? { channelId: chat.telegram_id } : { chatId: chat.telegram_id } + const inputPeer = await client.getEntity( chat.is_channel ? + new Api.InputPeerChannel(entity) : + new Api.InputPeerChat(entity) + ) + + await client.sendMessage(inputPeer, {message}) + + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) + await delay(1000) +} + +async function leaveChat (chatId) { + const chat = db + .prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`) + .get({ chat_id: chatId}) + + if (!chat) + return + + if (chat.is_channel) { + const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash })) + await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer })) + } else { + await client.invoke(new Api.messages.DeleteChatUser({ chatId: chat.telegram_id, userId: this.id, accessHash: chat.access_hash })) + } +} + +async function start (apiId, apiHash, botAuthToken, sid) { + BOT_ID = BigInt(botAuthToken.split(':')[0]) + + session= new StringSession(sid || '') + client = new TelegramClient(session, apiId, apiHash, {}) + + client.addEventHandler(async (update) => { + if (update.className == 'UpdateConnectionState') + return + + try { + // console.log(update) + if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') { const msg = update?.message const isChannel = update.className == 'UpdateNewChannelMessage' @@ -557,98 +615,13 @@ class Bot extends EventEmitter { if (update.className == 'UpdateChatParticipant' || update.className == 'UpdateChannelParticipant') await onUpdatePaticipant(update, update.className == 'UpdateChannelParticipant') - }) - - await client.start({botAuthToken}) - } - - async uploadDocument(projectId, fileName, mime, data, parentType, parentId, publishedBy) { - const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 }) - - const media = new Api.InputMediaUploadedDocument({ - file, - mimeType: mime, - attributes: [new Api.DocumentAttributeFilename({ fileName })] - }) - - return await registerUpload({ - projectId, - media, - parentType, - parentId, - publishedBy - }) - } - - async downloadDocument(projectId, documentId) { - const document = db - .prepare(` - select file_id, access_hash, '' thumbSize, filename, mime - from documents where id = :document_id and project_id = :project_id - `) - .safeIntegers(true) - .get({project_id: projectId, document_id: documentId}) - - if (!document) - return false - - const result = await client.downloadFile(new Api.InputDocumentFileLocation({ - id: document.file_id, - accessHash: document.access_hash, - fileReference: Buffer.from(document.filename), - thumbSize: '' - }, {})) - - return { - filename: document.filename, - mime: document.mime, - size: result.length, - data: result + } catch (err) { + console.error(err) } - } + }) - async reloadGroupUsers(groupId, onlyReset) { - return reloadGroupUsers(groupId, onlyReset) - } + await client.start({botAuthToken}) +} - async sendMessage (groupId, message) { - const group = db - .prepare(`select telegram_id, is_channel from groups where id = :group_id`) - .get({ group_id: groupId}) - - if (!group) - return - - const entity = group.is_channel ? { channelId: group.telegram_id } : { chatId: group.telegram_id } - const inputPeer = await client.getEntity( group.is_channel ? - new Api.InputPeerChannel(entity) : - new Api.InputPeerChat(entity) - ) - - await client.sendMessage(inputPeer, {message}) - - const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) - await delay(1000) - } - - async leaveGroup (groupId) { - const group = db - .prepare(`select telegram_id, is_channel from groups where id = :group_id`) - .get({ group_id: groupId}) - - if (!group) - return - - if (group.is_channel) { - const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: group.telegram_id })) - await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer })) - } else { - await client.invoke(new Api.messages.DeleteChatUser({ chatId: group.telegram_id, userId: this.id })) - } - } -} - -const bot = new Bot() - -module.exports = bot +module.exports = { start, uploadFile, downloadFile, reloadChatUsers, sendMessage } diff --git a/backend/data/init.sql b/backend/data/init.sql index 675cc28..2fa537c 100644 --- a/backend/data/init.sql +++ b/backend/data/init.sql @@ -5,15 +5,15 @@ create table if not exists customers ( name text check(name is null or trim(name) <> '' and length(name) < 256), email text check(email is null or trim(email) <> '' and length(email) < 128), password text check(password is null or length(password) > 7 and length(password) < 64), - telegram_user_id integer, + telegram_id integer, plan integer, json_balance text default '{}', - activation_key text, - is_active integer default 0, + is_blocked integer default 0, json_company text default '{}', - upload_group_id integer, + upload_chat_id integer, json_backup_server text default '{}', - json_backup_params text default '{}' + json_backup_params text default '{}', + json_settings text default '{}' ) strict; create table if not exists projects ( @@ -22,34 +22,36 @@ create table if not exists projects ( name text not null check(trim(name) <> '' and length(name) < 256), description text check(description is null or length(description) < 4096), logo text, - is_deleted integer default 0 + is_logo_bg integer default 0, + is_archived integer default 0 ) strict; -create table if not exists groups ( +create table if not exists chats ( id integer primary key autoincrement, project_id integer references projects(id) on delete cascade, name text, telegram_id integer, + description text, + logo text, access_hash integer, is_channel integer check(is_channel in (0, 1)) default 0, bot_can_ban integer default 0, + owner_id integer references users(id) on delete set null, user_count integer, last_update_time integer ); -create unique index if not exists idx_groups_telegram_id on groups (telegram_id); +create unique index if not exists idx_chats_telegram_id on chats (telegram_id); create table if not exists users ( id integer primary key autoincrement, telegram_id integer, access_hash integer, - firstname text, - lastname text, - username text, + firstname text check(firstname is null or length(firstname) < 256), + lastname text check(lastname is null or length(lastname) < 256), + username text check(username is null or length(username) < 256), photo_id integer, photo text, language_code text, - phone text, - json_phone_projects text default '[]', json_settings text default '{}' ) strict; create unique index if not exists idx_users_telegram_id on users (telegram_id); @@ -57,9 +59,11 @@ create unique index if not exists idx_users_telegram_id on users (telegram_id); create table if not exists user_details ( user_id integer references users(id) on delete cascade, project_id integer references projects(id) on delete cascade, - fullname text, - role text, - department text, + fullname text check(fullname is null or length(fullname) < 256), + email text check(email is null or length(email) < 256), + phone text check(phone is null or length(phone) < 256), + role text check(role is null or length(role) < 256), + department text check(department is null or length(department) < 256), is_blocked integer check(is_blocked in (0, 1)) default 0, primary key (user_id, project_id) ) strict; @@ -88,12 +92,12 @@ create table if not exists meetings ( meet_date integer ) strict; -create table if not exists documents ( +create table if not exists files ( id integer primary key autoincrement, project_id integer references projects(id) on delete cascade, - origin_group_id integer references groups(id) on delete set null, + origin_chat_id integer references chats(id) on delete set null, origin_message_id integer, - group_id integer references groups(id) on delete set null, + chat_id integer references chats(id) on delete set null, message_id integer, file_id integer, access_hash integer, @@ -102,6 +106,7 @@ create table if not exists documents ( caption text check(caption is null or length(caption) < 4096), size integer, published_by integer references users(id) on delete set null, + published integer, parent_type integer check(parent_type in (0, 1, 2)) default 0, parent_id integer, backup_state integer default 0 @@ -111,6 +116,7 @@ create table if not exists companies ( id integer primary key autoincrement, project_id integer references projects(id) on delete cascade, name text not null check(length(name) < 4096), + address text check(address is null or length(address) < 512), email text check(email is null or length(email) < 128), phone text check(phone is null or length(phone) < 128), site text check(site is null or length(site) < 128), @@ -143,20 +149,19 @@ create table if not exists company_users ( primary key (company_id, user_id) ) without rowid; -create table if not exists group_users ( - group_id integer references groups(id) on delete cascade, +create table if not exists chat_users ( + chat_id integer references chats(id) on delete cascade, user_id integer references users(id) on delete cascade, - primary key (group_id, user_id) + primary key (chat_id, user_id) ) without rowid; pragma foreign_keys = on; -create trigger if not exists trg_groups_update after update -on groups +create trigger if not exists trg_chats_update after update on chats when NEW.project_id is null begin - delete from group_users where group_id = NEW.id; + delete from chat_users where chat_id = NEW.id; end; diff --git a/i18n-2.xlsm b/i18n-2.xlsm index 246ef95..2f47286 100644 Binary files a/i18n-2.xlsm and b/i18n-2.xlsm differ diff --git a/icons-svg/_old/icon-3-points.svg b/icons-svg/_old/icon-3-points.svg new file mode 100644 index 0000000..45061b0 --- /dev/null +++ b/icons-svg/_old/icon-3-points.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-bell-solid.svg b/icons-svg/_old/icon-bell-solid.svg new file mode 100644 index 0000000..b035fcb --- /dev/null +++ b/icons-svg/_old/icon-bell-solid.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-bell.svg b/icons-svg/_old/icon-bell.svg new file mode 100644 index 0000000..2c16e72 --- /dev/null +++ b/icons-svg/_old/icon-bell.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-bookmark.svg b/icons-svg/_old/icon-bookmark.svg new file mode 100644 index 0000000..75a4988 --- /dev/null +++ b/icons-svg/_old/icon-bookmark.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-calendar.svg b/icons-svg/_old/icon-calendar.svg new file mode 100644 index 0000000..8d9689c --- /dev/null +++ b/icons-svg/_old/icon-calendar.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-change.svg b/icons-svg/_old/icon-change.svg new file mode 100644 index 0000000..a360c36 --- /dev/null +++ b/icons-svg/_old/icon-change.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-clear.svg b/icons-svg/_old/icon-clear.svg new file mode 100644 index 0000000..bffa7da --- /dev/null +++ b/icons-svg/_old/icon-clear.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-clipboard.svg b/icons-svg/_old/icon-clipboard.svg new file mode 100644 index 0000000..f9284d3 --- /dev/null +++ b/icons-svg/_old/icon-clipboard.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-download.svg b/icons-svg/_old/icon-download.svg new file mode 100644 index 0000000..923d2ca --- /dev/null +++ b/icons-svg/_old/icon-download.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-dropdown.svg b/icons-svg/_old/icon-dropdown.svg new file mode 100644 index 0000000..ebe51d1 --- /dev/null +++ b/icons-svg/_old/icon-dropdown.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-exit.svg b/icons-svg/_old/icon-exit.svg new file mode 100644 index 0000000..0a258e6 --- /dev/null +++ b/icons-svg/_old/icon-exit.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-eye.svg b/icons-svg/_old/icon-eye.svg new file mode 100644 index 0000000..aab114d --- /dev/null +++ b/icons-svg/_old/icon-eye.svg @@ -0,0 +1,66 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-add.svg b/icons-svg/_old/icon-file-add.svg new file mode 100644 index 0000000..2ed67fb --- /dev/null +++ b/icons-svg/_old/icon-file-add.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-archive.svg b/icons-svg/_old/icon-file-archive.svg new file mode 100644 index 0000000..7ad2930 --- /dev/null +++ b/icons-svg/_old/icon-file-archive.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-audio.svg b/icons-svg/_old/icon-file-audio.svg new file mode 100644 index 0000000..d0a2c91 --- /dev/null +++ b/icons-svg/_old/icon-file-audio.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-delete.svg b/icons-svg/_old/icon-file-delete.svg new file mode 100644 index 0000000..90da6c1 --- /dev/null +++ b/icons-svg/_old/icon-file-delete.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-doc.svg b/icons-svg/_old/icon-file-doc.svg new file mode 100644 index 0000000..110f42d --- /dev/null +++ b/icons-svg/_old/icon-file-doc.svg @@ -0,0 +1,71 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-dwg.svg b/icons-svg/_old/icon-file-dwg.svg new file mode 100644 index 0000000..5ff1137 --- /dev/null +++ b/icons-svg/_old/icon-file-dwg.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-img.svg b/icons-svg/_old/icon-file-img.svg new file mode 100644 index 0000000..fbc4d12 --- /dev/null +++ b/icons-svg/_old/icon-file-img.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-lock.svg b/icons-svg/_old/icon-file-lock.svg new file mode 100644 index 0000000..8f54bf1 --- /dev/null +++ b/icons-svg/_old/icon-file-lock.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-merge.svg b/icons-svg/_old/icon-file-merge.svg new file mode 100644 index 0000000..3d1094f --- /dev/null +++ b/icons-svg/_old/icon-file-merge.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-move.svg b/icons-svg/_old/icon-file-move.svg new file mode 100644 index 0000000..a93ea32 --- /dev/null +++ b/icons-svg/_old/icon-file-move.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-move.svg.2019_10_02_17_03_29.0.svg b/icons-svg/_old/icon-file-move.svg.2019_10_02_17_03_29.0.svg new file mode 100644 index 0000000..1ba8485 --- /dev/null +++ b/icons-svg/_old/icon-file-move.svg.2019_10_02_17_03_29.0.svg @@ -0,0 +1,64 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons-svg/_old/icon-file-pdf.svg b/icons-svg/_old/icon-file-pdf.svg new file mode 100644 index 0000000..e916a14 --- /dev/null +++ b/icons-svg/_old/icon-file-pdf.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-ppt.svg b/icons-svg/_old/icon-file-ppt.svg new file mode 100644 index 0000000..bded736 --- /dev/null +++ b/icons-svg/_old/icon-file-ppt.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-rename.svg b/icons-svg/_old/icon-file-rename.svg new file mode 100644 index 0000000..c92510d --- /dev/null +++ b/icons-svg/_old/icon-file-rename.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-skp.svg b/icons-svg/_old/icon-file-skp.svg new file mode 100644 index 0000000..0125894 --- /dev/null +++ b/icons-svg/_old/icon-file-skp.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-ttf.svg b/icons-svg/_old/icon-file-ttf.svg new file mode 100644 index 0000000..413d6fa --- /dev/null +++ b/icons-svg/_old/icon-file-ttf.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-txt.svg b/icons-svg/_old/icon-file-txt.svg new file mode 100644 index 0000000..3e79ffe --- /dev/null +++ b/icons-svg/_old/icon-file-txt.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-types.svg b/icons-svg/_old/icon-file-types.svg new file mode 100644 index 0000000..f4ed68f --- /dev/null +++ b/icons-svg/_old/icon-file-types.svg @@ -0,0 +1,202 @@ + + + + + + + + + + image/svg+xml + + + + + + + + X + + + P + + W + + + V + + + + + + + + + diff --git a/icons-svg/_old/icon-file-update.svg b/icons-svg/_old/icon-file-update.svg new file mode 100644 index 0000000..071a909 --- /dev/null +++ b/icons-svg/_old/icon-file-update.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-video.svg b/icons-svg/_old/icon-file-video.svg new file mode 100644 index 0000000..021cb24 --- /dev/null +++ b/icons-svg/_old/icon-file-video.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-vsd.svg b/icons-svg/_old/icon-file-vsd.svg new file mode 100644 index 0000000..4a4b8e9 --- /dev/null +++ b/icons-svg/_old/icon-file-vsd.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file-xls.svg b/icons-svg/_old/icon-file-xls.svg new file mode 100644 index 0000000..9a2f4b2 --- /dev/null +++ b/icons-svg/_old/icon-file-xls.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-file.svg b/icons-svg/_old/icon-file.svg new file mode 100644 index 0000000..d080be2 --- /dev/null +++ b/icons-svg/_old/icon-file.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-filter.svg b/icons-svg/_old/icon-filter.svg new file mode 100644 index 0000000..e9beebd --- /dev/null +++ b/icons-svg/_old/icon-filter.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-flag-solid.svg b/icons-svg/_old/icon-flag-solid.svg new file mode 100644 index 0000000..0355b8b --- /dev/null +++ b/icons-svg/_old/icon-flag-solid.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-flag.svg b/icons-svg/_old/icon-flag.svg new file mode 100644 index 0000000..ca49463 --- /dev/null +++ b/icons-svg/_old/icon-flag.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-folder-delete.svg b/icons-svg/_old/icon-folder-delete.svg new file mode 100644 index 0000000..ef734a3 --- /dev/null +++ b/icons-svg/_old/icon-folder-delete.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-folder-move.svg b/icons-svg/_old/icon-folder-move.svg new file mode 100644 index 0000000..0cfc391 --- /dev/null +++ b/icons-svg/_old/icon-folder-move.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-folder-over.svg b/icons-svg/_old/icon-folder-over.svg new file mode 100644 index 0000000..036d0a7 --- /dev/null +++ b/icons-svg/_old/icon-folder-over.svg @@ -0,0 +1,73 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons-svg/_old/icon-folder-plus.svg b/icons-svg/_old/icon-folder-plus.svg new file mode 100644 index 0000000..f962982 --- /dev/null +++ b/icons-svg/_old/icon-folder-plus.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-folder-rename.svg b/icons-svg/_old/icon-folder-rename.svg new file mode 100644 index 0000000..4ea6c71 --- /dev/null +++ b/icons-svg/_old/icon-folder-rename.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-folder-update.svg b/icons-svg/_old/icon-folder-update.svg new file mode 100644 index 0000000..366918f --- /dev/null +++ b/icons-svg/_old/icon-folder-update.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-folder.svg b/icons-svg/_old/icon-folder.svg new file mode 100644 index 0000000..a04805a --- /dev/null +++ b/icons-svg/_old/icon-folder.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-full-view.svg b/icons-svg/_old/icon-full-view.svg new file mode 100644 index 0000000..57c2062 --- /dev/null +++ b/icons-svg/_old/icon-full-view.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-gear.svg b/icons-svg/_old/icon-gear.svg new file mode 100644 index 0000000..1f3607d --- /dev/null +++ b/icons-svg/_old/icon-gear.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-info.svg b/icons-svg/_old/icon-info.svg new file mode 100644 index 0000000..19e5427 --- /dev/null +++ b/icons-svg/_old/icon-info.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-left-arrow.svg b/icons-svg/_old/icon-left-arrow.svg new file mode 100644 index 0000000..d525c28 --- /dev/null +++ b/icons-svg/_old/icon-left-arrow.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-lock-close.svg b/icons-svg/_old/icon-lock-close.svg new file mode 100644 index 0000000..8de196a --- /dev/null +++ b/icons-svg/_old/icon-lock-close.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-lock-open.svg b/icons-svg/_old/icon-lock-open.svg new file mode 100644 index 0000000..a5b1535 --- /dev/null +++ b/icons-svg/_old/icon-lock-open.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-logo-2.svg.png b/icons-svg/_old/icon-logo-2.svg.png new file mode 100644 index 0000000..8d67e75 Binary files /dev/null and b/icons-svg/_old/icon-logo-2.svg.png differ diff --git a/icons-svg/_old/icon-logo.png b/icons-svg/_old/icon-logo.png new file mode 100644 index 0000000..c0349ee Binary files /dev/null and b/icons-svg/_old/icon-logo.png differ diff --git a/icons-svg/_old/icon-logo.svg b/icons-svg/_old/icon-logo.svg new file mode 100644 index 0000000..63a3358 --- /dev/null +++ b/icons-svg/_old/icon-logo.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-mail.svg b/icons-svg/_old/icon-mail.svg new file mode 100644 index 0000000..3523091 --- /dev/null +++ b/icons-svg/_old/icon-mail.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-messege-2.svg b/icons-svg/_old/icon-messege-2.svg new file mode 100644 index 0000000..90a63fa --- /dev/null +++ b/icons-svg/_old/icon-messege-2.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons-svg/_old/icon-messege-2.svg.png b/icons-svg/_old/icon-messege-2.svg.png new file mode 100644 index 0000000..6c92ada Binary files /dev/null and b/icons-svg/_old/icon-messege-2.svg.png differ diff --git a/icons-svg/_old/icon-messege-3.svg.png b/icons-svg/_old/icon-messege-3.svg.png new file mode 100644 index 0000000..da0edb6 Binary files /dev/null and b/icons-svg/_old/icon-messege-3.svg.png differ diff --git a/icons-svg/_old/icon-messege-done.svg b/icons-svg/_old/icon-messege-done.svg new file mode 100644 index 0000000..c11672a --- /dev/null +++ b/icons-svg/_old/icon-messege-done.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-messege-second.svg b/icons-svg/_old/icon-messege-second.svg new file mode 100644 index 0000000..76e7b72 --- /dev/null +++ b/icons-svg/_old/icon-messege-second.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-messege.svg b/icons-svg/_old/icon-messege.svg new file mode 100644 index 0000000..debcca4 --- /dev/null +++ b/icons-svg/_old/icon-messege.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-open-folder.svg b/icons-svg/_old/icon-open-folder.svg new file mode 100644 index 0000000..9cb65a9 --- /dev/null +++ b/icons-svg/_old/icon-open-folder.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-paperclip.svg b/icons-svg/_old/icon-paperclip.svg new file mode 100644 index 0000000..3ee20ef --- /dev/null +++ b/icons-svg/_old/icon-paperclip.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-pencil.svg b/icons-svg/_old/icon-pencil.svg new file mode 100644 index 0000000..aad5bfc --- /dev/null +++ b/icons-svg/_old/icon-pencil.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-plus.svg b/icons-svg/_old/icon-plus.svg new file mode 100644 index 0000000..2660c08 --- /dev/null +++ b/icons-svg/_old/icon-plus.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-question.svg b/icons-svg/_old/icon-question.svg new file mode 100644 index 0000000..683e884 --- /dev/null +++ b/icons-svg/_old/icon-question.svg @@ -0,0 +1,40 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons-svg/_old/icon-question2.svg b/icons-svg/_old/icon-question2.svg new file mode 100644 index 0000000..475beb3 --- /dev/null +++ b/icons-svg/_old/icon-question2.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons-svg/_old/icon-right-arrow.svg b/icons-svg/_old/icon-right-arrow.svg new file mode 100644 index 0000000..bb147f9 --- /dev/null +++ b/icons-svg/_old/icon-right-arrow.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-save (2).svg b/icons-svg/_old/icon-save (2).svg new file mode 100644 index 0000000..6a755b6 Binary files /dev/null and b/icons-svg/_old/icon-save (2).svg differ diff --git a/icons-svg/_old/icon-save.svg b/icons-svg/_old/icon-save.svg new file mode 100644 index 0000000..1b1b9d9 --- /dev/null +++ b/icons-svg/_old/icon-save.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-search-1.svg b/icons-svg/_old/icon-search-1.svg new file mode 100644 index 0000000..e8a82ac --- /dev/null +++ b/icons-svg/_old/icon-search-1.svg @@ -0,0 +1,146 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/icons-svg/_old/icon-search-file.svg b/icons-svg/_old/icon-search-file.svg new file mode 100644 index 0000000..c7c9ea7 --- /dev/null +++ b/icons-svg/_old/icon-search-file.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-search-task.svg b/icons-svg/_old/icon-search-task.svg new file mode 100644 index 0000000..6ad3fb1 --- /dev/null +++ b/icons-svg/_old/icon-search-task.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-send.svg b/icons-svg/_old/icon-send.svg new file mode 100644 index 0000000..10fa45a --- /dev/null +++ b/icons-svg/_old/icon-send.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-simple-view.svg b/icons-svg/_old/icon-simple-view.svg new file mode 100644 index 0000000..5d316fb --- /dev/null +++ b/icons-svg/_old/icon-simple-view.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-sort.svg b/icons-svg/_old/icon-sort.svg new file mode 100644 index 0000000..0da7f36 --- /dev/null +++ b/icons-svg/_old/icon-sort.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-support.svg b/icons-svg/_old/icon-support.svg new file mode 100644 index 0000000..0bf3ed4 --- /dev/null +++ b/icons-svg/_old/icon-support.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-cancel.svg b/icons-svg/_old/icon-task-cancel.svg new file mode 100644 index 0000000..aba5ccc --- /dev/null +++ b/icons-svg/_old/icon-task-cancel.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-comment.svg b/icons-svg/_old/icon-task-comment.svg new file mode 100644 index 0000000..a275843 --- /dev/null +++ b/icons-svg/_old/icon-task-comment.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-delete.svg b/icons-svg/_old/icon-task-delete.svg new file mode 100644 index 0000000..4ccd728 --- /dev/null +++ b/icons-svg/_old/icon-task-delete.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-done.svg b/icons-svg/_old/icon-task-done.svg new file mode 100644 index 0000000..807d6f9 --- /dev/null +++ b/icons-svg/_old/icon-task-done.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-plus.svg b/icons-svg/_old/icon-task-plus.svg new file mode 100644 index 0000000..7e8b714 --- /dev/null +++ b/icons-svg/_old/icon-task-plus.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-remark.svg b/icons-svg/_old/icon-task-remark.svg new file mode 100644 index 0000000..1a41d26 --- /dev/null +++ b/icons-svg/_old/icon-task-remark.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-task-request.svg b/icons-svg/_old/icon-task-request.svg new file mode 100644 index 0000000..7773212 --- /dev/null +++ b/icons-svg/_old/icon-task-request.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-telephone.svg b/icons-svg/_old/icon-telephone.svg new file mode 100644 index 0000000..ad144de --- /dev/null +++ b/icons-svg/_old/icon-telephone.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-time-load.svg b/icons-svg/_old/icon-time-load.svg new file mode 100644 index 0000000..3f62b9d --- /dev/null +++ b/icons-svg/_old/icon-time-load.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-timer.svg b/icons-svg/_old/icon-timer.svg new file mode 100644 index 0000000..9c92d51 --- /dev/null +++ b/icons-svg/_old/icon-timer.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/_old/icon-upload.svg b/icons-svg/_old/icon-upload.svg new file mode 100644 index 0000000..b6c7d77 --- /dev/null +++ b/icons-svg/_old/icon-upload.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons-svg/_old/icon-user-solid.svg b/icons-svg/_old/icon-user-solid.svg new file mode 100644 index 0000000..5949225 --- /dev/null +++ b/icons-svg/_old/icon-user-solid.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-user.svg b/icons-svg/_old/icon-user.svg new file mode 100644 index 0000000..f09a3ed --- /dev/null +++ b/icons-svg/_old/icon-user.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/_old/icon-users-solid.svg b/icons-svg/_old/icon-users-solid.svg new file mode 100644 index 0000000..4581f6e --- /dev/null +++ b/icons-svg/_old/icon-users-solid.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-archive.svg b/icons-svg/icon-file-archive.svg new file mode 100644 index 0000000..7ad2930 --- /dev/null +++ b/icons-svg/icon-file-archive.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-audio.svg b/icons-svg/icon-file-audio.svg new file mode 100644 index 0000000..d0a2c91 --- /dev/null +++ b/icons-svg/icon-file-audio.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-code.svg b/icons-svg/icon-file-code.svg new file mode 100644 index 0000000..caec668 --- /dev/null +++ b/icons-svg/icon-file-code.svg @@ -0,0 +1,34 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons-svg/icon-file-default.svg b/icons-svg/icon-file-default.svg new file mode 100644 index 0000000..d080be2 --- /dev/null +++ b/icons-svg/icon-file-default.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-doc.svg b/icons-svg/icon-file-doc.svg new file mode 100644 index 0000000..110f42d --- /dev/null +++ b/icons-svg/icon-file-doc.svg @@ -0,0 +1,71 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-dwg.svg b/icons-svg/icon-file-dwg.svg new file mode 100644 index 0000000..5ff1137 --- /dev/null +++ b/icons-svg/icon-file-dwg.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-img.svg b/icons-svg/icon-file-img.svg new file mode 100644 index 0000000..fbc4d12 --- /dev/null +++ b/icons-svg/icon-file-img.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-pdf.svg b/icons-svg/icon-file-pdf.svg new file mode 100644 index 0000000..e916a14 --- /dev/null +++ b/icons-svg/icon-file-pdf.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-ppt.svg b/icons-svg/icon-file-ppt.svg new file mode 100644 index 0000000..bded736 --- /dev/null +++ b/icons-svg/icon-file-ppt.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-skp.svg b/icons-svg/icon-file-skp.svg new file mode 100644 index 0000000..0125894 --- /dev/null +++ b/icons-svg/icon-file-skp.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-ttf.svg b/icons-svg/icon-file-ttf.svg new file mode 100644 index 0000000..413d6fa --- /dev/null +++ b/icons-svg/icon-file-ttf.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-txt.svg b/icons-svg/icon-file-txt.svg new file mode 100644 index 0000000..3e79ffe --- /dev/null +++ b/icons-svg/icon-file-txt.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-video.svg b/icons-svg/icon-file-video.svg new file mode 100644 index 0000000..021cb24 --- /dev/null +++ b/icons-svg/icon-file-video.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-vsd.svg b/icons-svg/icon-file-vsd.svg new file mode 100644 index 0000000..4a4b8e9 --- /dev/null +++ b/icons-svg/icon-file-vsd.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/icons-svg/icon-file-xls.svg b/icons-svg/icon-file-xls.svg new file mode 100644 index 0000000..9a2f4b2 --- /dev/null +++ b/icons-svg/icon-file-xls.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + diff --git a/package-lock.json b/package-lock.json index cbf24a2..0545aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@quasar/cli": "^2.5.0", "@quasar/extras": "^1.16.4", "axios": "^1.2.1", + "dayjs": "^1.11.13", "pinia": "^2.0.11", "quasar": "^2.16.0", "vue": "^3.4.18", @@ -1661,9 +1662,9 @@ } }, "node_modules/@quasar/extras": { - "version": "1.16.16", - "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.16.tgz", - "integrity": "sha512-aswGUbEyLvt45KB1u6hBD3s82KnOdkqTn6YVu3xX5aGgwQkCWPyqb3FMTEHG+4+gGTMp4pIcnng96RlqswQctQ==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.17.0.tgz", + "integrity": "sha512-KqAHdSJfIDauiR1nJ8rqHWT0diqD0QradZKoVIZJAilHAvgwyPIY7MbyR2z4RIMkUIMUSqBZcbshMpEw+9A30w==", "license": "MIT", "funding": { "type": "github", @@ -4593,6 +4594,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -9049,9 +9056,9 @@ } }, "node_modules/quasar": { - "version": "2.17.7", - "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.17.7.tgz", - "integrity": "sha512-nPJdHoONlcW7WEU2Ody907Wx945Zfyuea/KP4LBaEn5AcL95PUWp8Gz/0zDYNnFw0aCWRtye3SUAdQl5tmrn5w==", + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.18.1.tgz", + "integrity": "sha512-db/P64Mzpt1uXJ0MapaG+IYJQ9hHDb5KtTCoszwC78DR7sA+Uoj7nBW2EytwYykIExEmqavOvKrdasTvqhkgEg==", "license": "MIT", "engines": { "node": ">= 10.18.1", diff --git a/package.json b/package.json index c3c4c86..48a5004 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@quasar/cli": "^2.5.0", "@quasar/extras": "^1.16.4", "axios": "^1.2.1", + "dayjs": "^1.11.13", "pinia": "^2.0.11", "quasar": "^2.16.0", "vue": "^3.4.18", diff --git a/quasar.config.ts b/quasar.config.ts index cfae256..902d21b 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from '#q-app/wrappers' import { fileURLToPath } from 'node:url' +import path from 'path' export default defineConfig((ctx) => { return { @@ -14,10 +15,9 @@ export default defineConfig((ctx) => { // https://v2.quasar.dev/quasar-cli-vite/boot-files boot: [ 'i18n', + 'telegram-boot', 'axios', - 'auth-init', - 'global-components', - 'telegram-boot' + 'global-components' ], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css @@ -41,11 +41,18 @@ export default defineConfig((ctx) => { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build build: { + publicPath: '/', + vueRouterBase: '/', target: { browser: [ 'es2022', 'firefox115', 'chrome115', 'safari14' ], node: 'node20' }, + alias: { + 'composables': path.resolve(__dirname, './src/composables'), + 'types': path.resolve(__dirname, './src/types') + }, + typescript: { strict: true, vueShim: true @@ -53,8 +60,8 @@ export default defineConfig((ctx) => { }, vueRouterMode: 'history', // available values: 'hash', 'history' - vueDevtools: true, // Должно быть true - devtool: 'source-map', // Для лучшей отладки + // vueDevtools: true, // Должно быть true + // devtool: 'source-map', // Для лучшей отладки // vueRouterBase, // vueDevtools, // vueOptionsAPI: false, @@ -103,8 +110,8 @@ export default defineConfig((ctx) => { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver devServer: { - vueDevtools: true, - port: 9000, + // vueDevtools: true, + port: 9005, proxy: { '/api': { target: 'http://localhost:3000', @@ -132,7 +139,7 @@ export default defineConfig((ctx) => { // directives: [], // Quasar plugins - plugins: [ 'Notify' ] + plugins: [ 'Notify', 'BottomSheet' ] }, // animations: 'all', // --- includes all animations diff --git a/src/App.vue b/src/App.vue index 2df1ee8..d2eeb01 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,10 +3,64 @@ + + + + \ No newline at end of file diff --git a/src/boot/auth-init.ts b/src/boot/auth-init.ts deleted file mode 100644 index 8ec1dd9..0000000 --- a/src/boot/auth-init.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useAuthStore } from 'stores/auth' - -export default async () => { - const authStore = useAuthStore() - await authStore.initialize() -} \ No newline at end of file diff --git a/src/boot/axios.ts b/src/boot/axios.ts index 2272de7..4c44fdc 100644 --- a/src/boot/axios.ts +++ b/src/boot/axios.ts @@ -16,23 +16,10 @@ declare module 'vue' { // "export default () => {}" function below (which runs individually // for each client) const api = axios.create({ - baseURL: '/', + baseURL: '/api/miniapp', withCredentials: true // Важно для работы с cookies }) -api.interceptors.response.use( - response => response, - async error => { - if (error.response?.status === 401) { - const authStore = useAuthStore() - await authStore.logout() - } - console.error(error) - return Promise.reject(new Error()) - } - -) - export default defineBoot(({ app }) => { // for use inside Vue files (Options API) through this.$axios and this.$api @@ -43,6 +30,6 @@ export default defineBoot(({ app }) => { app.config.globalProperties.$api = api // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form) // so you can easily perform requests against your app's API -}); +}) -export { api }; +export { api } diff --git a/src/boot/global-components.ts b/src/boot/global-components.ts index d9cba81..4488a41 100644 --- a/src/boot/global-components.ts +++ b/src/boot/global-components.ts @@ -1,14 +1,24 @@ import { boot } from 'quasar/wrappers' -import pnPageCard from '../components/admin/pnPageCard.vue' -import pnScrollList from '../components/admin/pnScrollList.vue' -import pnAutoAvatar from '../components/admin/pnAutoAvatar.vue' -import pnOverlay from '../components/admin/pnOverlay.vue' -import pnImageSelector from '../components/admin/pnImageSelector.vue' +import pnPageCard from 'components/pnPageCard.vue' +import pnScrollList from 'components/pnScrollList.vue' +import pnAutoAvatar from 'components/pnAutoAvatar.vue' +import pnOverlay from 'components/pnOverlay.vue' +import pnDialogBody from 'components/pnDialogBody.vue' +import pnImageSelector from 'components/pnImageSelector.vue' +import pnTaskPriorityIcon from 'components/pnTaskPriorityIcon.vue' -export default boot(async ({ app }) => { // eslint-disable-line - app.component('pnPageCard', pnPageCard) - app.component('pnScrollList', pnScrollList) - app.component('pnAutoAvatar', pnAutoAvatar) - app.component('pnOverlay', pnOverlay) - app.component('pnImageSelector', pnImageSelector) +const components = { + pnPageCard, + pnScrollList, + pnAutoAvatar, + pnOverlay, + pnImageSelector, + pnDialogBody, + pnTaskPriorityIcon +} + +export default boot(({ app }) => { + Object.entries(components).forEach(([name, component]) => { + app.component(name, component) + }) }) diff --git a/src/boot/helpers.ts b/src/boot/helpers.ts index 286e65a..6b1a2ea 100644 --- a/src/boot/helpers.ts +++ b/src/boot/helpers.ts @@ -1,38 +1,73 @@ -export function isObjEqual(a: object, b: object): boolean { - // Сравнение примитивов и null/undefined - if (a === b) return true - if (!a || !b) return false - if (Object.keys(a).length !== Object.keys(b).length) return false +function isDirty ( + obj1: Record | null | undefined, + obj2: Record | null | undefined +): boolean { + const actualObj1 = obj1 ?? {} + const actualObj2 = obj2 ?? {} - // Получаем все уникальные ключи из обоих объектов - const allKeys = new Set([ - ...Object.keys(a), - ...Object.keys(b) - ]) + const filteredObj1 = filterIgnored(actualObj1) + const filteredObj2 = filterIgnored(actualObj2) + + const allKeys = new Set([...Object.keys(filteredObj1), ...Object.keys(filteredObj2)]) - // Проверяем каждое свойство for (const key of allKeys) { - const valA = a[key as keyof typeof a] - const valB = b[key as keyof typeof b] - - // Если одно из значений undefined - объекты разные - if (valA === undefined || valB === undefined) return false - - // Рекурсивное сравнение для вложенных объектов - if (typeof valA === 'object' && typeof valB === 'object') { - if (!isObjEqual(valA, valB)) return false - } - // Сравнение примитивов - else if (!Object.is(valA, valB)) { - return false - } + const hasKey1 = Object.hasOwn(filteredObj1, key) + const hasKey2 = Object.hasOwn(filteredObj2, key) + + if (hasKey1 !== hasKey2) return false + + if (hasKey1 && hasKey2) { + const val1 = filteredObj1[key] + const val2 = filteredObj2[key] + + if (typeof val1 === 'string' && typeof val2 === 'string') { + if (val1.trim() !== val2.trim()) return false + } else if (val1 !== val2) { + return false + } + } } return true } -export function parseIntString (s: string | string[] | undefined) :number | null { +function filterIgnored(obj: Record): Record { + const filtered: Record = {} + + for (const key in obj) { + const originalValue = obj[key] + + // Пропускаем значения, которые не string, number или boolean + if ( + typeof originalValue !== 'string' && + typeof originalValue !== 'number' && + typeof originalValue !== 'boolean' + ) { + continue + } + + let value = originalValue + + if (typeof value === 'string') { + value = value.trim() + if (value === '') continue + } + + if (value === 0 || value === false) continue + + filtered[key] = value + } + + return filtered +} + +function parseIntString (s: string | string[] | undefined) :number | null { if (typeof s !== 'string') return null const regex = /^[+-]?\d+$/ return regex.test(s) ? Number(s) : null +} + +export { + isDirty, + parseIntString } \ No newline at end of file diff --git a/src/boot/i18n.ts b/src/boot/i18n.ts index 5b89b30..8514993 100644 --- a/src/boot/i18n.ts +++ b/src/boot/i18n.ts @@ -1,11 +1,11 @@ -import { defineBoot } from '#q-app/wrappers'; -import { createI18n } from 'vue-i18n'; +import { defineBoot } from '#q-app/wrappers' +import { createI18n } from 'vue-i18n' -import messages from 'src/i18n'; +import messages from 'src/i18n' -export type MessageLanguages = keyof typeof messages; +export type MessageLanguages = keyof typeof messages // Type-define 'en-US' as the master schema for the resource -export type MessageSchema = typeof messages['en-US']; +export type MessageSchema = typeof messages['en-US'] // See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition /* eslint-disable @typescript-eslint/no-empty-object-type */ @@ -25,9 +25,9 @@ export default defineBoot(({ app }) => { const i18n = createI18n<{ message: MessageSchema }, MessageLanguages>({ locale: 'en-US', legacy: false, - messages, - }); + messages + }) // Set i18n instance on app - app.use(i18n); -}); + app.use(i18n) +}) diff --git a/src/boot/telegram-boot.ts b/src/boot/telegram-boot.ts index 960a4ea..aa8c7df 100644 --- a/src/boot/telegram-boot.ts +++ b/src/boot/telegram-boot.ts @@ -1,18 +1,25 @@ -import type { BootParams } from '@quasar/app' +import { defineBoot } from '#q-app/wrappers' +import type { WebApp } from "@twa-dev/types" +import { useProjectsStore } from 'stores/projects' -export default ({ app }: BootParams) => { - - // Инициализация Telegram WebApp +declare global { + interface Window { + Telegram: { + WebApp: WebApp + } + } +} + +export default defineBoot(({ app }) => { if (window.Telegram?.WebApp) { const webApp = window.Telegram.WebApp - // Помечаем приложение как готовое webApp.ready() - // window.Telegram.WebApp.requestFullscreen() - // Опционально: сохраняем объект в Vue-приложение для глобального доступа webApp.SettingsButton.isVisible = true - // webApp.BackButton.isVisible = true app.config.globalProperties.$tg = webApp - // Для TypeScript: объявляем тип для инжекции + const projectStore = useProjectsStore() + if (Number(webApp.initDataUnsafe.start_param)) { + projectStore.setStartProjectId(Number(webApp.initDataUnsafe.start_param)) + } app.provide('tg', webApp) } -} \ No newline at end of file +}) diff --git a/src/components/admin/account-page/optionPayment.vue b/src/components/admin/account-page/optionPayment.vue deleted file mode 100644 index b909710..0000000 --- a/src/components/admin/account-page/optionPayment.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/src/components/admin/account-page/qtyChatCard.vue b/src/components/admin/account-page/qtyChatCard.vue deleted file mode 100644 index 9ff7891..0000000 --- a/src/components/admin/account-page/qtyChatCard.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/src/components/admin/accountHelper.vue b/src/components/admin/accountHelper.vue deleted file mode 100644 index 5bde9b2..0000000 --- a/src/components/admin/accountHelper.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/admin/companyInfoBlock.vue b/src/components/admin/companyInfoBlock.vue deleted file mode 100644 index ebb4812..0000000 --- a/src/components/admin/companyInfoBlock.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/src/components/admin/login-page/loginLogo.vue b/src/components/admin/login-page/loginLogo.vue deleted file mode 100644 index 8403946..0000000 --- a/src/components/admin/login-page/loginLogo.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - - diff --git a/src/components/admin/pnPageCard.vue b/src/components/admin/pnPageCard.vue deleted file mode 100644 index b9c056e..0000000 --- a/src/components/admin/pnPageCard.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/src/components/admin/pnScrollList.vue b/src/components/admin/pnScrollList.vue deleted file mode 100644 index 03470a0..0000000 --- a/src/components/admin/pnScrollList.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/components/admin/project-page/ProjectPageChats.vue b/src/components/admin/project-page/ProjectPageChats.vue deleted file mode 100644 index 181e3d2..0000000 --- a/src/components/admin/project-page/ProjectPageChats.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - - - diff --git a/src/components/admin/project-page/ProjectPageCompanies.vue b/src/components/admin/project-page/ProjectPageCompanies.vue deleted file mode 100644 index e254b3c..0000000 --- a/src/components/admin/project-page/ProjectPageCompanies.vue +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - diff --git a/src/components/admin/project-page/ProjectPageHeader.vue b/src/components/admin/project-page/ProjectPageHeader.vue deleted file mode 100644 index 8422be1..0000000 --- a/src/components/admin/project-page/ProjectPageHeader.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - - - diff --git a/src/components/admin/project-page/ProjectPagePersons.vue b/src/components/admin/project-page/ProjectPagePersons.vue deleted file mode 100644 index f29da04..0000000 --- a/src/components/admin/project-page/ProjectPagePersons.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/src/components/admin/companyInfoPersons.vue b/src/components/companyInfoPersons.vue similarity index 100% rename from src/components/admin/companyInfoPersons.vue rename to src/components/companyInfoPersons.vue diff --git a/src/components/meetingBlock.vue b/src/components/meetingBlock.vue new file mode 100644 index 0000000..4a2cdbd --- /dev/null +++ b/src/components/meetingBlock.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/src/components/admin/meshBackground.vue b/src/components/meshBackground.vue similarity index 100% rename from src/components/admin/meshBackground.vue rename to src/components/meshBackground.vue diff --git a/src/components/admin/pnAutoAvatar.vue b/src/components/pnAutoAvatar.vue similarity index 88% rename from src/components/admin/pnAutoAvatar.vue rename to src/components/pnAutoAvatar.vue index d68a2cb..fb6c8fa 100644 --- a/src/components/admin/pnAutoAvatar.vue +++ b/src/components/pnAutoAvatar.vue @@ -3,7 +3,7 @@ :style="{ backgroundColor: stringToColour(props.name) } " class="fit flex items-center justify-center text-white" > - {{ props.name.substring(0, 1) }} + {{ props.name ? props.name.substring(0, 1) : '' }} @@ -14,6 +14,7 @@ }>() const stringToColour = (str: string) => { + if (!str) return '#eee' let hash = 0 str.split('').forEach(char => { hash = char.charCodeAt(0) + ((hash << 5) - hash) diff --git a/src/components/pnDialogBody.vue b/src/components/pnDialogBody.vue new file mode 100644 index 0000000..da26e2c --- /dev/null +++ b/src/components/pnDialogBody.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/components/admin/pnImageSelector.vue b/src/components/pnImageSelector.vue similarity index 100% rename from src/components/admin/pnImageSelector.vue rename to src/components/pnImageSelector.vue diff --git a/src/components/admin/pnOverlay.vue b/src/components/pnOverlay.vue similarity index 100% rename from src/components/admin/pnOverlay.vue rename to src/components/pnOverlay.vue diff --git a/src/components/pnPageCard.vue b/src/components/pnPageCard.vue new file mode 100644 index 0000000..db8cf53 --- /dev/null +++ b/src/components/pnPageCard.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/components/pnScrollList.vue b/src/components/pnScrollList.vue new file mode 100644 index 0000000..0d88983 --- /dev/null +++ b/src/components/pnScrollList.vue @@ -0,0 +1,180 @@ + + + + + \ No newline at end of file diff --git a/src/components/pnTaskPriorityIcon.vue b/src/components/pnTaskPriorityIcon.vue new file mode 100644 index 0000000..298b338 --- /dev/null +++ b/src/components/pnTaskPriorityIcon.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/src/components/admin/projectInfoBlock.vue b/src/components/projectInfoBlock.vue similarity index 100% rename from src/components/admin/projectInfoBlock.vue rename to src/components/projectInfoBlock.vue diff --git a/src/components/taskItem.vue b/src/components/taskItem.vue new file mode 100644 index 0000000..4ff1afc --- /dev/null +++ b/src/components/taskItem.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/css/app.scss b/src/css/app.scss index 77bcac9..c07df57 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -34,13 +34,90 @@ $base-height: 100; body { overflow: hidden !important; } + +.main-content { + max-width: 600px; + margin: 0 auto; +} + +.fix-fab-offset { + margin-right: calc(max((100vw - var(--body-width))/2, 0px) + 18px) !important; +} .projects-header { background-color: #eee; } .top-rounded-card { - border-top-left-radius: var(--top-raduis); - border-top-right-radius: var(--top-raduis); + border-top-left-radius: var(--top-raduis) !important; + border-top-right-radius: var(--top-raduis) !important; +} + +.orline { + display: flex; + flex-direction: row; +} + +.orline:before, +.orline:after { + content: ""; + flex: 1 1; + border-bottom: 1px solid; + margin: auto; +} + +.pn-icon { + font-family: 'pn-icon'; + font-style: normal; +} + +@font-face { + font-family: 'pn-icon'; + src: url("./fonts/pn.woff") format("woff") +} + +.icon-file-default:before { + content: "\e900"; +} +.icon-file-doc:before { + content: "\e901"; +} +.icon-file-xls:before { + content: "\e902"; +} +.icon-file-ppt:before { + content: "\e903"; +} +.icon-file-vsd:before { + content: "\e904"; +} +.icon-file-pdf:before { + content: "\e905"; +} +.icon-file-archive:before { + content: "\e906"; +} +.icon-file-img:before { + content: "\e907"; +} +.icon-file-dwg:before { + content: "\e908"; +} +.icon-file-skp:before { + content: "\e909"; +} +.icon-file-ttf:before { + content: "\e90a"; +} +.icon-file-txt:before { + content: "\e90b"; +} +.icon-file-audio:before { + content: "\e90c"; +} +.icon-file-video:before { + content: "\e90d"; +} +.icon-file-code:before { + content: "\e90e"; } - \ No newline at end of file diff --git a/src/css/fonts/pn.eot b/src/css/fonts/pn.eot new file mode 100644 index 0000000..5ac9bdd Binary files /dev/null and b/src/css/fonts/pn.eot differ diff --git a/src/css/fonts/pn.svg b/src/css/fonts/pn.svg new file mode 100644 index 0000000..c139986 --- /dev/null +++ b/src/css/fonts/pn.svg @@ -0,0 +1,25 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/css/fonts/pn.ttf b/src/css/fonts/pn.ttf new file mode 100644 index 0000000..1078b5d Binary files /dev/null and b/src/css/fonts/pn.ttf differ diff --git a/src/css/fonts/pn.woff b/src/css/fonts/pn.woff new file mode 100644 index 0000000..432f8d6 Binary files /dev/null and b/src/css/fonts/pn.woff differ diff --git a/src/i18n/en-US/index.ts b/src/i18n/en-US/index.ts index 40f68b3..ada2b0e 100644 --- a/src/i18n/en-US/index.ts +++ b/src/i18n/en-US/index.ts @@ -1 +1 @@ -export default { EN: 'EN', RU: 'RU', continue: 'Continue', back: 'Back', month: 'month', months: 'months', slogan: 'Work together - it\'s magic!', under_construction: 'Under construction.', login__email: 'E-mail', login__password: 'Password', login__forgot_password: 'Forgot Password?', login__sign_in: 'Log in', login__incorrect_login_data: 'User data not found. Edit your auth details before continuing', login__or_continue_as: 'or continue as', login__terms_of_use: 'Terms of use', login__accept_terms_of_use: 'I accept the', login__register: 'Create account', login__registration_message_error: 'Error', login__licensing_agreement: 'Licensing agreement', login__have_account: 'Already have an accont?', user__logout: 'Logout', projects__projects: 'Projects', projects__show_archive: 'Show archive', projects__hide_archive: 'Hide archive', projects__restore_archive_warning: 'Attention!', projects__restore_archive_warning_message: 'To restore a project from an archive, you must manually attach chats to it.', project__chats: 'Chats', project__persons: 'Persons', project__companies: 'Companies', project__edit: 'Edit', project__backup: 'Backup', project__archive: 'Archive', project__archive_warning: 'Are you sure?', project__archive_warning_message: 'Chat tracking in the project will be disabled after moving to the archive.', project__delete: 'Delete', project__delete_warning: 'Warning!', project__delete_warning_message: 'All project data will be removed. This action cannot be undone.', project_chats__search: 'Search', project_chats__send_chat: 'Request for attach chat', project_chats__send_chat_description: 'Provide instructions to the chat admin', project_chats__attach_chat: 'Attach chat', project_chats__attach_chat_description: 'Requires chat administrator privileges', project_chat__delete_warning: 'Warning!', project_chat__delete_warning_message: 'Chat tracking will be discontinued. If necessary, the cat can be attached again.', project_card__project_card: 'Project card', project_card__add_project: 'Add project', project_card__project_name: 'Name', project_card__project_description: 'Description', project_card__btn_accept: 'Accept', project_card__btn_back: 'Back', project_card__image_use_as_background_chats: 'logo as background for chats', project_card__error_name: 'Field is required', forgot_password__password_recovery: 'Password recovery', account_helper__enter_email: 'Enter account e-mail', account_helper__email: 'E-mail', account_helper__confirm_email: 'Confirm e-mail', account_helper__confirm_email_message: 'Enter the Code from e-mail to continue recover your password. If you haven\'t received an e-mail with the Code, check the Spam folder.', account_helper__code: 'Code', account_helper__code_error: 'Incorrect code. Ensure your e-mail is correct and try again.', account_helper__set_password: 'Set password', account_helper__password: 'Password', account_helper__finish: 'Finish', account_helper__finish_after_message: 'Done!', account__user_settings: 'User settings', account__your_company: 'Your company', account__change_auth_message_2: 'After creating a user, all data from the Telegram account will be transferred to the new account.', account__change_auth_btn: 'Create system account', account__change_auth_warning: 'WARNING!', account__change_auth_warning_message: 'Reverse data transfer is not possible.', account__chats: 'Chats', account__chats_active: 'Active', account__chats_unbound: 'Unbound', account__chats_free: 'Free', account__chats_total: 'Total', account__subscribe: 'Subscribe', account__subscribe_description: 'With a subscription, you can attach more active chats.', account__auth_change_method: 'Change authorization method', account__auth_change_method_description: 'In case of corporate use, it is recommended to log in with a username and password.', account__auth_change_password: 'Change account password', account__auth_change_password_description: 'Access to the email address used for system login is required.', account__auth_change_account: 'Change account e-mail', account__auth_change_account_description: 'Access to both the current and new email addresses used for system authentication is required.', account__company_data: 'Your company data', account__company_data_description: 'Projects will automatically include this data.', account__manual: 'Manual', account__manual_description: 'Go to our Telegram channel with video tutorials.', account__support: 'Support', account__support_description: 'Need help? Contact us!', account__terms_of_use: 'Terms of use', account__privacy: 'Privacy and Cookie Policy', company__mask: 'Company cloacking', mask__title_table: 'Excluded', mask__help_title: 'Cloacking', mask__help_message: 'It is possible to cloacking a company by representing its personnel as your own to companies other than those on the exclusion list.', company_info__title_card: 'Company card', company_info__name: 'Name', company_info__description: 'Description', company_info__persons: 'Persons', company_create__title_card: 'Add company', project_persons__search: 'Search', person_card__title: 'Person card', person_card__name: 'Name', person_card__company: 'Company name', person_card__department: 'Department', person_card__role: 'Role', settings__title: 'Settings', settings__language: 'Language', settings__font_size: 'Font size', terms__title: 'Terms of use', subscribe__title: 'Subscribe', subscribe__current_balance: 'Current balance', subscribe__token_formula: '1 = 1 day of access to 1 chat', subscribe__token_formula_description: 'unbound and free chats are not counted', subscribe__info: 'With a subscription, you can attach more chats. Archived chats are not counted.', subscribe__about: 'about', subscribe__select_payment_1: 'You can pay for your subscription using ', subscribe__select_payment_2: 'Telegram stars', subscribe__select_option_1: 'Telegram stars', subscribe__select_option_2: 'Telegram stars', subscribe__select_option_3: 'Telegram stars', subscribe__select_option_user: 'Telegram stars' } \ No newline at end of file +export default { EN: 'EN', RU: 'RU', continue: 'Continue', back: 'Back', close: 'Close', month: 'month', months: 'months', slogan: 'Work together - it\'s magic!', under_construction: 'Under construction.', B: 'B', kB: 'kB', MB: 'MB', GB: 'GB', TB: 'TB', main__chats: 'Chats', main__tasks: 'Tasks', main__meetings: 'Meetings', main__files: 'Files', main__users: 'Contacts', chats__search: 'Search', tasks__search: 'Search', tasks__filters: 'Filters', tasks__filters_types: 'Task types', tasks__filters_in: 'Assigned to me', tasks__filters_out: 'Assigned by me', tasks__filters_watch: 'Following', tasks__filters_priority: 'Task priority', tasks__filters_priority_normal: 'Normal', tasks__filters_priority_important: 'Important', tasks__filters_priority_critical: 'Critical', task_add__title: 'Create task', task_add__name: 'Title', task_add__description: 'Description', task_add__plan_date: 'Planed date', task_add__priority: 'Priority', task_add__priority_normal: 'Normal', task_add__priority_important: 'Important', task_add__priority_critical: 'Critical', task_add__attached_chat: 'Attached chat', task_add__attach_files: 'Attached files', task_add__assigned_to: 'Assignee', task_add__watch: 'Followers', meetings__search: 'Search', meetings__previous: 'Previous', meetings__previous_hide: 'Hide', meeting_create__title_card: 'Create meeting', meeting_edit__title_card: 'Edit meeting', meeting_view__title_card: 'Meeting card', meeting_info__name: 'Title', meeting_info__description: 'Description', meeting_info__date: 'Date', meeting_info__time: 'Time', meeting_info__attach_chat: 'Attach to chat', meeting_info__participants: 'Participants', meeting_info__attach_files: 'Files', meeting_info__canceled: 'Canceled', meeting_info__dialog_cancel_title: 'Cancel the meeting?', meeting_info__dialog_cancel_ok: 'Confirm', meeting_info__dialog_cancel_delete: 'Delete', meeting_info__dialog_restore_title: 'Restore the meeting?', meeting_info__dialog_restore_ok: 'Confirm', files__search: 'Search', files__filters: 'Filters', files__filters_extension: 'Extensions (types)', files__filters_source: 'Source', files__filters_source_chats: 'chat', files__filters_source_tasks: 'task', files__filters_source_meetings: 'meeting', files__filters_by: 'Author', files__filters_size: 'Size', files__filters_size_small: 'small (less than 5MB)', files__filters_size_middle: 'middle (5-25MB)', files__filters_size_big: 'big (25-100MB)', files__filters_size_very_big: 'very big (more 100MB)', files_filters_reset: 'Reset filters', header__my_projects: 'My projects', header__all_projects: 'All projects', users__search: 'Search', user_card__title: 'User card', user_card__name: 'Name', user_card__phone: 'Phone', user_card__email: 'Email', user_card__position: 'Position', settings__title: 'Settings', settings__language: 'Language', settings__font_size: 'Font size' } \ No newline at end of file diff --git a/src/i18n/ru-RU/index.ts b/src/i18n/ru-RU/index.ts index e3dd6da..72da05d 100644 --- a/src/i18n/ru-RU/index.ts +++ b/src/i18n/ru-RU/index.ts @@ -1 +1 @@ -export default { EN: 'EN', RU: 'RU', continue: 'Продолжить', back: 'Назад', month: 'мес.', months: 'мес.', slogan: 'Работайте вместе - это волшебство!', under_construction: 'В разработке.', login__email: 'Электронная почта', login__password: 'Пароль', login__forgot_password: 'Забыли пароль?', login__sign_in: 'Войти', login__incorrect_login_data: 'Пользователь с такими данными не найден. Отредактируйте введенные данные', login__or_continue_as: 'или продолжить', login__terms_of_use: 'Пользовательское соглашение', login__accept_terms_of_use: 'Я принимаю', login__register: 'Зарегестрироваться', login__registration_message_error: 'Ошибка', login__licensing_agreement: 'Договор о лицензировании', login__have_account: 'Есть учетная запись', user__logout: 'Выход', projects__projects: 'Проекты', projects__show_archive: 'Показать архив', projects__hide_archive: 'Скрыть архив', projects__restore_archive_warning: 'Внимание!', projects__restore_archive_warning_message: 'При восстановлении проекта из архива - присоединение чатов к проекту требуется осуществлять вручную.', project__chats: 'Чаты', project__persons: 'Люди', project__companies: 'Компании', project__edit: 'Редактировать', project__backup: 'Резервная копия', project__archive: 'В архив', project__archive_warning: 'Вы уверены?', project__archive_warning_message: 'После перемещения проекта в архив отслеживание чатов будет отключено.', project__delete: 'Удалить', project__delete_warning: 'Внимание!', project__delete_warning_message: 'Все данные проекта будут безвозвратно удалены.', project_chats__search: 'Поиск', project_chats__send_chat: 'Запрос на добавление чата', project_chats__send_chat_description: 'Отправить инструкцию администратору чата', project_chats__attach_chat: 'Добавить чат', project_chats__attach_chat_description: 'Необходимы права администратора чата', project_chat__delete_warning: 'Внимание!', project_chat__delete_warning_message: 'Отслеживание чата будет прекращено. При необходимости чат можно будет подключить снова.', project_card__project_card: 'Карточка компании', project_card__add_project: 'Новый проект', project_card__project_name: 'Название', project_card__project_description: 'Описание', project_card__btn_accept: 'Подтвердить', project_card__btn_back: 'Назад', project_card__image_use_as_background_chats: 'логотип в качестве фона для чатов', project_card__error_name: 'Поле обязательно к заполнению', forgot_password__password_recovery: 'Восстановление пароля', account_helper__enter_email: 'Введите электронную почту', account_helper__email: 'Электронная почта', account_helper__confirm_email: 'Подтверждение электронной почты', account_helper__confirm_email_message: 'Введите код из письма для продолжения восстановления пароля. Если не получили письмо с кодом - проверьте папку Спам', account_helper__code: 'Код', account_helper__code_error: 'Был введен неверный код. Проверьте адрес электронной почты и повторите попытку.', account_helper__set_password: 'Установка пароля', account_helper__password: 'Пароль', account_helper__finish: 'Отправить', account_helper__finish_after_message: 'Готово!', account__user_settings: 'Пользовательские настройки', account__your_company: 'Ваша компания', account__change_auth_message_2: 'После создания пользователя все данные с учетной записи Telegram будут перенесены на новую учетную запись.', account__change_auth_btn: 'Создать пользователя', account__change_auth_warning: 'ВНИМАНИЕ!', account__change_auth_warning_message: 'Обратный перенос данных не возможен.', account__chats: 'Чаты', account__chats_active: 'Активные', account__chats_unbound: 'Открепленные', account__chats_free: 'Бесплатные', account__chats_total: 'Всего', account__subscribe: 'Подписка', account__subscribe_description: 'С помощью подписки можно подключить дополнительные чаты.', account__auth_change_method: 'Сменить способ авторизации', account__auth_change_method_description: 'В случае корпоративного использования рекомендуется входить в систему, указав логин и пароль.', account__auth_change_password: 'Изменить пользовательский пароль', account__auth_change_password_description: 'Необходим доступ к электронной почте, используемой для входа в систему.', account__auth_change_account: 'Сменить электронную почту учетной записи', account__auth_change_account_description: 'Необходим доступ к текущей и новой электронной почте, используемым для входа в систему.', account__company_data: 'Данные вашей компании', account__company_data_description: 'Эти данные будут автоматически подгружаться в проекты. ', account__manual: 'Инструкции', account__manual_description: 'Перейдите в наш Telegram-канал с обучающими видеороликами.', account__support: 'Поддержка', account__support_description: 'Есть вопросы - напишите нам!', account__terms_of_use: 'Пользовательское соглашение', account__privacy: 'Политика конфидециальности', company__mask: 'Маскировка компаний', mask__title_table: 'Исключения', mask__help_title: 'Маскировка', mask__help_message: 'Возможно замаскировать компанию, представляя ее персонал как собственный для других компаний, кроме тех, что есть в перечне исключений. ', company_info__title_card: 'Карточка компании', company_info__name: 'Название', company_info__description: 'Описание', company_info__persons: 'Сотрудники', company_create__title_card: 'Добавление компании', project_persons__search: 'Поиск', person_card__title: 'Карточка сотрудника', person_card__name: 'ФИО', person_card__company: 'Название компании', person_card__department: 'Подразделение', person_card__role: 'Функционал (должность)', settings__title: 'Настройки', settings__language: 'Язык', settings__font_size: 'Размер шрифта', terms__title: 'Пользовательское соглашение', subscribe__title: 'Подписка', subscribe__current_balance: 'Текущий баланс', subscribe__token_formula: '1 = 1 день подключения к 1 чату', subscribe__token_formula_description: 'отвязанные и бесплатные чаты не учитываются', subscribe__info: 'С помощью подписки можно подключить к бесплатным групповым чатам дополнительные. Архивные чаты не учитываются. ', subscribe__about: 'около', subscribe__select_payment_1: 'Вы можете оплатить подписку с помощью', subscribe__select_payment_2: 'Telegram stars', subscribe__select_option_1: 'Telegram stars', subscribe__select_option_2: 'Telegram stars', subscribe__select_option_3: 'Telegram stars', subscribe__select_option_user: 'Telegram stars' } \ No newline at end of file +export default { EN: 'EN', RU: 'RU', continue: 'Продолжить', back: 'Назад', close: 'Закрыть', month: 'мес.', months: 'мес.', slogan: 'Работайте вместе - это волшебство!', under_construction: 'В разработке.', B: 'Б', kB: 'КБ', MB: 'МБ', GB: 'ГБ', TB: 'ТБ', main__chats: 'Чаты', main__tasks: 'Задачи', main__meetings: 'Совещания', main__files: 'Файлы', main__users: 'Контакты', chats__search: 'Поиск', tasks__search: 'Поиск', tasks__filters: 'Фильтры', tasks__filters_types: 'Типы задач', tasks__filters_in: 'Входящие', tasks__filters_out: 'Порученные', tasks__filters_watch: 'Отслеживаемые', tasks__filters_priority: 'Приоритет задач', tasks__filters_priority_normal: 'Нормальный', tasks__filters_priority_important: 'Важный', tasks__filters_priority_critical: 'Критичный', task_add__title: 'Создание задачи', task_add__name: 'Заголовок', task_add__description: 'Описание', task_add__plan_date: 'Дата выполнения', task_add__priority: 'Приоритет', task_add__priority_normal: 'Нормальный', task_add__priority_important: 'Важный', task_add__priority_critical: 'Критичный', task_add__attached_chat: 'Связанный чат', task_add__attach_files: 'Прикрепленные файлы', task_add__assigned_to: 'Ответственный', task_add__watch: 'Наблюдатели', meetings__search: 'Поиск', meetings__previous: 'Прошедшие', meetings__previous_hide: 'Сбросить', meeting_create__title_card: 'Создать совещание', meeting_edit__title_card: 'Редактировать совещание', meeting_view__title_card: 'Карточка совещания', meeting_info__name: 'Тема', meeting_info__description: 'Описание', meeting_info__date: 'Дата', meeting_info__time: 'Время', meeting_info__attach_chat: 'Прикрепить к чату', meeting_info__participants: 'Участники', meeting_info__attach_files: 'Файлы', meeting_info__canceled: 'Отменено', meeting_info__dialog_cancel_title: 'Отменить совещание?', meeting_info__dialog_cancel_ok: 'Подтвердить', meeting_info__dialog_cancel_delete: 'Удалить', meeting_info__dialog_restore_title: 'Возобновить совещание?', meeting_info__dialog_restore_ok: 'Подтвердить', files__search: 'Поиск', files__filters: 'Фильтры', files__filters_extension: 'Расширения (типы)', files__filters_source: 'Источник ', files__filters_source_chats: 'чат', files__filters_source_tasks: 'задача', files__filters_source_meetings: 'совещание', files__filters_by: 'Автор', files__filters_size: 'Размер', files__filters_size_small: 'небольшой (менее 5 МБ)', files__filters_size_middle: 'средний (5-25МБ)', files__filters_size_big: 'большой (25-100МБ)', files__filters_size_very_big: 'очень большой (более 100МБ)', files_filters_reset: 'Сбросить фильтры', header__my_projects: 'Мои проекты', header__all_projects: 'Все проекты', users__search: 'Поиск', user_card__title: 'Карточка пользователя', user_card__name: 'Имя', user_card__phone: 'Телефон', user_card__email: 'Электронная почта', user_card__position: 'Позиция', settings__title: 'Настройки', settings__language: 'Язык', settings__font_size: 'Размер шрифта' } \ No newline at end of file diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 40a112d..934b0b0 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -1,60 +1,24 @@ diff --git a/src/pages/AccountChangeEmailPage.vue b/src/pages/AccountChangeEmailPage.vue deleted file mode 100644 index 73d4a21..0000000 --- a/src/pages/AccountChangeEmailPage.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/pages/AccountChangePasswordPage.vue b/src/pages/AccountChangePasswordPage.vue deleted file mode 100644 index 73d4a21..0000000 --- a/src/pages/AccountChangePasswordPage.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/pages/AccountCreatePage.vue b/src/pages/AccountCreatePage.vue deleted file mode 100644 index 38ee95c..0000000 --- a/src/pages/AccountCreatePage.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/src/pages/AccountForgotPasswordPage.vue b/src/pages/AccountForgotPasswordPage.vue deleted file mode 100644 index 8a71e0b..0000000 --- a/src/pages/AccountForgotPasswordPage.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/pages/AccountPage.vue b/src/pages/AccountPage.vue deleted file mode 100644 index 2b0f59c..0000000 --- a/src/pages/AccountPage.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - - - diff --git a/src/pages/CompanyCreatePage.vue b/src/pages/CompanyCreatePage.vue deleted file mode 100644 index 1b816ac..0000000 --- a/src/pages/CompanyCreatePage.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/src/pages/CompanyInfoPage.vue b/src/pages/CompanyInfoPage.vue deleted file mode 100644 index 8d1d56f..0000000 --- a/src/pages/CompanyInfoPage.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - - diff --git a/src/pages/CompanyMaskPage.vue b/src/pages/CompanyMaskPage.vue deleted file mode 100644 index 9a51a34..0000000 --- a/src/pages/CompanyMaskPage.vue +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - diff --git a/src/pages/CompanyYourPage.vue b/src/pages/CompanyYourPage.vue deleted file mode 100644 index 2678853..0000000 --- a/src/pages/CompanyYourPage.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue deleted file mode 100644 index ca3cd5d..0000000 --- a/src/pages/LoginPage.vue +++ /dev/null @@ -1,215 +0,0 @@ - - - - - diff --git a/src/pages/ProjectPage.vue b/src/pages/MainPage.vue similarity index 53% rename from src/pages/ProjectPage.vue rename to src/pages/MainPage.vue index 57658b2..3db9800 100644 --- a/src/pages/ProjectPage.vue +++ b/src/pages/MainPage.vue @@ -6,10 +6,7 @@ - + - @@ -66,35 +72,31 @@ diff --git a/src/pages/MeetingEditPage.vue b/src/pages/MeetingEditPage.vue new file mode 100644 index 0000000..6e637f9 --- /dev/null +++ b/src/pages/MeetingEditPage.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/pages/MeetingInfoPage.vue b/src/pages/MeetingInfoPage.vue new file mode 100644 index 0000000..6e637f9 --- /dev/null +++ b/src/pages/MeetingInfoPage.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/pages/PersonInfoPage.vue b/src/pages/PersonInfoPage.vue deleted file mode 100644 index 75e5cda..0000000 --- a/src/pages/PersonInfoPage.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - diff --git a/src/pages/PrivacyPage.vue b/src/pages/PrivacyPage.vue deleted file mode 100644 index bc1de39..0000000 --- a/src/pages/PrivacyPage.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/src/pages/ProjectCreatePage.vue b/src/pages/ProjectCreatePage.vue deleted file mode 100644 index 3540940..0000000 --- a/src/pages/ProjectCreatePage.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/src/pages/ProjectInfoPage.vue b/src/pages/ProjectInfoPage.vue deleted file mode 100644 index 0481631..0000000 --- a/src/pages/ProjectInfoPage.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/src/pages/ProjectsPage.vue b/src/pages/ProjectsPage.vue deleted file mode 100644 index 028a88c..0000000 --- a/src/pages/ProjectsPage.vue +++ /dev/null @@ -1,233 +0,0 @@ - - - - - diff --git a/src/pages/SettingsPage.vue b/src/pages/SettingsPage.vue index 84eebc2..cf7b5d2 100644 --- a/src/pages/SettingsPage.vue +++ b/src/pages/SettingsPage.vue @@ -1,11 +1,7 @@ + \ No newline at end of file diff --git a/src/pages/SubscribePage.vue b/src/pages/SubscribePage.vue deleted file mode 100644 index 6db5ca0..0000000 --- a/src/pages/SubscribePage.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - diff --git a/src/pages/TaskAddPage.vue b/src/pages/TaskAddPage.vue new file mode 100644 index 0000000..c308cbf --- /dev/null +++ b/src/pages/TaskAddPage.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/src/pages/TaskInfoPage.vue b/src/pages/TaskInfoPage.vue new file mode 100644 index 0000000..84eebc2 --- /dev/null +++ b/src/pages/TaskInfoPage.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/pages/TermsPage.vue b/src/pages/TermsPage.vue deleted file mode 100644 index e70761f..0000000 --- a/src/pages/TermsPage.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/src/pages/UserInfoPage.vue b/src/pages/UserInfoPage.vue new file mode 100644 index 0000000..fe5884c --- /dev/null +++ b/src/pages/UserInfoPage.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/pages/main/ChatsPage.vue b/src/pages/main/ChatsPage.vue new file mode 100644 index 0000000..95668eb --- /dev/null +++ b/src/pages/main/ChatsPage.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/pages/main/FilesPage.vue b/src/pages/main/FilesPage.vue new file mode 100644 index 0000000..1b27f20 --- /dev/null +++ b/src/pages/main/FilesPage.vue @@ -0,0 +1,504 @@ + + + + + diff --git a/src/pages/main/HeaderPage.vue b/src/pages/main/HeaderPage.vue new file mode 100644 index 0000000..4f7322d --- /dev/null +++ b/src/pages/main/HeaderPage.vue @@ -0,0 +1,170 @@ + + + + + + + + diff --git a/src/pages/main/MeetingsPage.vue b/src/pages/main/MeetingsPage.vue new file mode 100644 index 0000000..d3477b2 --- /dev/null +++ b/src/pages/main/MeetingsPage.vue @@ -0,0 +1,545 @@ + + + + + diff --git a/src/pages/main/TasksPage.vue b/src/pages/main/TasksPage.vue new file mode 100644 index 0000000..650fdaa --- /dev/null +++ b/src/pages/main/TasksPage.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/src/pages/main/UsersPage.vue b/src/pages/main/UsersPage.vue new file mode 100644 index 0000000..536172d --- /dev/null +++ b/src/pages/main/UsersPage.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index d77f8b4..a346060 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -6,18 +6,8 @@ import { createWebHistory, } from 'vue-router' import routes from './routes' -import { useAuthStore } from 'stores/auth' import { useProjectsStore } from 'stores/projects' -/* - * If not building with SSR mode, you can - * directly export the Router instantiation; - * - * The function below can be async too; either use - * async/await or return a Promise which resolves - * with the Router instance. - */ - export default defineRouter(function (/* { store, ssrContext } */) { const createHistory = process.env.SERVER ? createMemoryHistory @@ -32,30 +22,28 @@ export default defineRouter(function (/* { store, ssrContext } */) { // quasar.conf.js -> build -> publicPath history: createHistory(process.env.VUE_ROUTER_BASE), }) - - const publicPaths = ['/login', '/create-account', '/recovery-password'] Router.beforeEach(async (to) => { - const authStore = useAuthStore() + console.log(to) + if (to.name === 'settings') return; + const projectsStore = useProjectsStore() - // Инициализация хранилища перед проверкой - if (!authStore.isInitialized) { - await authStore.initialize() - } - - // Проверка авторизации для непубличных маршрутов - if (!publicPaths.includes(to.path)) { - if (!authStore.isAuthenticated) { - return { - path: '/login', - query: { redirect: to.fullPath } - } + if (to.params.id) { + const projectId = Number(to.params.id) + + if (!projectsStore.isInit) await projectsStore.init() + + const project = projectsStore.projectById(projectId) + if (!project) return { name: 'page404' } + + if (projectsStore.currentProjectId !== projectId) { + projectsStore.setCurrentProjectId(projectId) } - } - - // Редирект авторизованных пользователей с публичных маршрутов - if (publicPaths.includes(to.path) && authStore.isAuthenticated) { - return { path: '/' } + + } else { + if (!projectsStore.startProjectId) return { name: 'page404' } + projectsStore.setCurrentProjectId(projectsStore.startProjectId) + return { name: 'files', params: { id: projectsStore.startProjectId }} } }) @@ -67,11 +55,10 @@ export default defineRouter(function (/* { store, ssrContext } */) { if (window.history.length > 1) { Router.go(-1) } else { - await Router.push('/projects') + await Router.push({ name: 'main'}) } } } - Router.afterEach((to) => { const BackButton = window.Telegram?.WebApp?.BackButton; @@ -87,11 +74,6 @@ export default defineRouter(function (/* { store, ssrContext } */) { BackButton.offClick(handleBackButton as () => void) BackButton.onClick(handleBackButton as () => void) } - - if (!to.params.id) { - const projectsStore = useProjectsStore() - projectsStore.setCurrentProjectId(null) - } }) return Router diff --git a/src/router/routes.ts b/src/router/routes.ts index 108ce26..39d66c8 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -1,160 +1,83 @@ -import type { RouteRecordRaw, RouteLocationNormalized } from 'vue-router' -import { useProjectsStore } from '../stores/projects' - -const setProjectBeforeEnter = (to: RouteLocationNormalized) => { - const id = Number(to.params.id) - const projectsStore = useProjectsStore() - projectsStore.setCurrentProjectId( - !isNaN(id) && projectsStore.projectById(id) ? id : null - ) -} +import type { RouteRecordRaw } from 'vue-router' const routes: RouteRecordRaw[] = [ { path: '/', component: () => import('layouts/MainLayout.vue'), children: [ - { - path: '', - redirect: '/projects' - }, - { - name: 'projects', - path: '/projects', - component: () => import('pages/ProjectsPage.vue'), - meta: { hideBackButton: true } - }, - { - name: 'project_add', - path: '/project/add', - component: () => import('pages/ProjectCreatePage.vue') - }, - - { - name: 'project_info', - path: '/project/:id(\\d+)/info', - component: () => import('pages/ProjectInfoPage.vue'), - beforeEnter: setProjectBeforeEnter - }, - { - name: 'company_mask', - path: '/project/:id(\\d+)/company-mask', - component: () => import('pages/CompanyMaskPage.vue'), - beforeEnter: setProjectBeforeEnter - }, - { path: '/project/:id(\\d+)', - component: () => import('pages/ProjectPage.vue'), - beforeEnter: setProjectBeforeEnter, + component: () => import('pages/MainPage.vue'), + redirect: { name: 'files' }, + meta: { hideBackButton: true }, children: [ { - name: 'project', - path: '', - redirect: { name: 'chats' } + name: 'files', + path: 'files', + component: () => import('pages/main/FilesPage.vue'), + meta: { hideBackButton: true } + }, + { + name: 'tasks', + path: 'tasks', + component: () => import('pages/main/TasksPage.vue'), + meta: { hideBackButton: true } + }, + { + name: 'meetings', + path: 'meetings', + component: () => import('pages/main/MeetingsPage.vue'), + meta: { hideBackButton: true } + }, + { + name: 'users', + path: 'users', + component: () => import('src/pages/main/UsersPage.vue'), + meta: { hideBackButton: true } }, { name: 'chats', path: 'chats', - component: () => import('components/admin/project-page/ProjectPageChats.vue'), - meta: { backRoute: '/projects' } - }, - { - name: 'persons', - path: 'persons', - component: () => import('components/admin/project-page/ProjectPagePersons.vue'), - meta: { backRoute: '/projects' } - }, - { - name: 'companies', - path: 'companies', - component: () => import('components/admin/project-page/ProjectPageCompanies.vue'), - meta: { backRoute: '/projects' } + component: () => import('pages/main/ChatsPage.vue'), + meta: { hideBackButton: true } } ] }, { - name: 'company_info', - path: '/project/:id(\\d+)/company/:companyId', - component: () => import('pages/CompanyInfoPage.vue'), - beforeEnter: setProjectBeforeEnter + name: 'task_add', + path: '/project/:id(\\d+)/task/add', + component: () => import('pages/TaskAddPage.vue') }, { - name: 'person_info', - path: '/project/:id(\\d+)/person/:personId', - component: () => import('pages/PersonInfoPage.vue'), - beforeEnter: setProjectBeforeEnter + name: 'task_info', + path: '/project/:id(\\d+)/task/:taskId(\\d+)', + component: () => import('pages/TaskInfoPage.vue'), + }, + { + name: 'meeting_add', + path: '/project/:id(\\d+)/meeting/add', + component: () => import('pages/MeetingAddPage.vue') + }, + { + name: 'meeting_edit', + path: '/project/:id(\\d+)/meeting/:meetingId(\\d+)', + component: () => import('pages/MeetingEditPage.vue'), + }, + { + name: 'meeting_info', + path: '/project/:id(\\d+)/meeting/:meetingId(\\d+)', + component: () => import('pages/MeetingInfoPage.vue'), + }, + { + name: 'user_info', + path: '/project/:id(\\d+)/user/:userId', + component: () => import('pages/UserInfoPage.vue'), }, - { - name: 'account', - path: '/account', - component: () => import('pages/AccountPage.vue') - }, - { - name: 'create_account', - path: '/create-account', - component: () => import('src/pages/AccountCreatePage.vue') - }, - { - name: 'change_account_password', - path: '/change-password', - component: () => import('pages/AccountChangePasswordPage.vue') - }, - { - name: 'change_account_email', - path: '/change-email', - component: () => import('pages/AccountChangeEmailPage.vue') - }, - { - name: 'subscribe', - path: '/subscribe', - component: () => import('pages/SubscribePage.vue') - }, - { - name: 'terms', - path: '/terms-of-use', - component: () => import('pages/TermsPage.vue') - }, - { - name: 'privacy', - path: '/privacy', - component: () => import('pages/PrivacyPage.vue') - }, - { - name: 'your_company', - path: '/your-company', - component: () => import('src/pages/CompanyYourPage.vue') - }, - { - name: 'login', - path: '/login', - component: () => import('pages/LoginPage.vue') - }, - - { - name: 'recovery_password', - path: '/recovery-password', - component: () => import('src/pages/AccountForgotPasswordPage.vue') - }, - - { - name: 'add_company', - path: '/add-company', - component: () => import('src/pages/CompanyCreatePage.vue') - }, - - { - name: 'person_info', - path: '/person-info', - component: () => import('pages/PersonInfoPage.vue') - }, - - { name: 'settings', path: '/settings', component: () => import('pages/SettingsPage.vue') - } + } ] }, { @@ -163,5 +86,4 @@ const routes: RouteRecordRaw[] = [ } ] - -export default routes +export default routes \ No newline at end of file diff --git a/src/stores/auth.ts b/src/stores/auth.ts deleted file mode 100644 index a048f00..0000000 --- a/src/stores/auth.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' -import { api } from 'boot/axios' - -interface User { - id: string - email?: string - username: string - first_name?: string - last_name?: string - avatar?: string -} - -export const useAuthStore = defineStore('auth', () => { - // State - const user = ref(null) - const isInitialized = ref(false) - - // Getters - const isAuthenticated = computed(() => !!user.value) - - // Actions - const initialize = async () => { - try { - const { data } = await api.get('/customer/profile') - user.value = data - } catch (error) { - console.error(error) - user.value = null - } finally { - isInitialized.value = true - } - } - - const loginWithCredentials = async (email: string, password: string) => { - // будет переделано на беке - нужно сменить урл - await api.post('/api/admin/customer/login', { email, password }, { withCredentials: true }) - await initialize() - } - - const loginWithTelegram = async (initData: string) => { - await api.post('/api/admin/customer/login', { initData }, { withCredentials: true }) - await initialize() - } - - const logout = async () => { - try { - await api.get('/customer/logout', {}) - } finally { - user.value = null - // @ts-expect-ignore - // window.Telegram?.WebApp.close() - } - } - - return { - user, - isAuthenticated, - isInitialized, - initialize, - loginWithCredentials, - loginWithTelegram, - logout - } -}) \ No newline at end of file diff --git a/src/stores/chats.ts b/src/stores/chats.ts index 94e0ff8..a4ef668 100644 --- a/src/stores/chats.ts +++ b/src/stores/chats.ts @@ -1,36 +1,38 @@ -import { ref } from 'vue' +import { ref, computed } from 'vue' import { defineStore } from 'pinia' -import type { Chat } from '../types' +import { api } from 'boot/axios' +import { useProjectsStore } from 'stores/projects' +import type { Chat } from 'types/Chat' export const useChatsStore = defineStore('chats', () => { - const chats = ref([]) - - chats.value.push( - {id: 11, name: 'Аудит ИБ', logo: 'https://cdn.quasar.dev/img/avatar5.jpg', persons: 8, owner_id: 111}, - {id: 12, name: 'Разработка BI', logo: '', persons: 2, owner_id: 111}, - {id: 3, name: '-Обсуждение дашбордов', logo: '', description: 'Какой-то кратенькое описание', persons: 4, owner_id: 112}, - {id: 4, name: 'Расстрел нерадивых', logo: '', persons: 3, owner_id: 113}, - {id: 15, name: 'фыфыы Расстрел нерадивых', logo: '', persons: 5, owner_id: 112}, - {id: 16, name: 'Разработка BI', logo: '', persons: 6, owner_id: 114}, - {id: 17, name: '-Обсуждение дашбордов', logo: '', description: 'Какой-то кратенькое описание', persons: 58, owner_id: 111}, - {id: 18, name: 'Расстрел нерадивых', logo: '', persons: 3, owner_id: 112}, - {id: 19, name: 'фыфыы Расстрел нерадивых', logo: '', persons: 11, owner_id: 113}, - {id: 20, name: 'Разработка BI', logo: '', persons: 18, owner_id: 114}, - {id: 113, name: '-Обсуждение дашбордов', logo: '', description: 'Какой-то кратенькое описание', persons: 11, owner_id: 115}, - {id: 124, name: 'Расстрел нерадивых', logo: '', persons: 12, owner_id: 113}, - {id: 217, name: 'фыфыы Расстрел нерадивых', logo: '', persons: 5, owner_id: 112}, - {id: 2113, name: '-Обсуждение дашбордов', logo: '', description: 'Какой-то кратенькое описание', persons: 4, owner_id: 111}, - {id: 124, name: 'Расстрел нерадивых', logo: '', persons: 3, owner_id: 112}, - {id: 2117, name: 'фыфыы Расстрел нерадивых', logo: '', persons: 5, owner_id: 111}, - ) - function chatById (id :number) { + const chats = ref([]) + const isInit = ref(false) + + const projectsStore = useProjectsStore() + const currentProjectId = computed(() => projectsStore.currentProjectId) + + async function init () { + const response = await api.get('/project/' + currentProjectId.value + '/chat') + const chatsAPI = response.data.data + chats.value.push(...chatsAPI) + isInit.value = true + } + + function reset () { + chats.value = [] + isInit.value = false + } + + function chatById (id: number) { return chats.value.find(el =>el.id === id) } - function deleteChat (id :number) { - const idx = chats.value.findIndex(item => item.id === id) - chats.value.splice(idx, 1) - } - return { chats, deleteChat, chatById } + return { + chats, + isInit, + init, + reset, + chatById + } }) diff --git a/src/stores/companies.ts b/src/stores/companies.ts deleted file mode 100644 index 289431f..0000000 --- a/src/stores/companies.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ref } from 'vue' -import { defineStore } from 'pinia' -import type { Company, CompanyParams } from '../types' - -export const useCompaniesStore = defineStore('companies', () => { - const companies = ref([]) - - companies.value.push( - {id: 11, project_id: 11, name: 'Рога и копытца', logo: '', description: 'Монтажники вывески' }, - {id: 21, project_id: 12, name: 'ООО "Василек33"', logo: '' }, - {id: 13, project_id: 13, name: 'Откат и деньги', logo: '', description: 'Договариваются с администрацией' }, - ) - - function companyById (id :number) { - return companies.value.find(el =>el.id === id) - } - - function addCompany (company: CompanyParams) { - companies.value.push({ - id: Date.now(), - project_id: Date.now() * 1000, - ...company - }) - } - - function updateCompany (id :number, company: CompanyParams) { - const idx = companies.value.findIndex(item => item.id === id) - Object.assign(companies.value[idx] || {}, company) - } - - function deleteCompany (id :number) { - const idx = companies.value.findIndex(item => item.id === id) - companies.value.splice(idx, 1) - } - - return { companies, addCompany, updateCompany, deleteCompany, companyById } -}) diff --git a/src/stores/files.ts b/src/stores/files.ts new file mode 100644 index 0000000..9eacac1 --- /dev/null +++ b/src/stores/files.ts @@ -0,0 +1,51 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { api } from 'boot/axios' +import { useProjectsStore } from 'stores/projects' +import type { File } from 'types/File' + +export const useFilesStore = defineStore('files', () => { + + const files = ref([]) + const isInit = ref(false) + + const projectsStore = useProjectsStore() + const currentProjectId = computed(() => projectsStore.currentProjectId) + + async function init () { + const response = await api.get('/project/' + currentProjectId.value + '/file') + const filesAPI = response.data.data + files.value.push(...filesAPI) + isInit.value = true + } + + function reset () { + files.value = [] + isInit.value = false + } + + async function fileUrl (fileId: number) { + const response = api.get('/project/' + currentProjectId.value + '/file/' + fileId) + return (await response).data.data + } + + async function remove (fileId: number) { + const response = api.delete('/project/' + currentProjectId.value + '/file/' + fileId) + return (await response).data.data + } + + + function fileById (id: number) { + return files.value.find(el =>el.id === id) + } + + return { + files, + isInit, + init, + reset, + fileUrl, + remove, + fileById + } +}) diff --git a/src/stores/meetings.ts b/src/stores/meetings.ts new file mode 100644 index 0000000..9a280e9 --- /dev/null +++ b/src/stores/meetings.ts @@ -0,0 +1,86 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { api } from 'boot/axios' +import { useProjectsStore } from 'stores/projects' +import type { Meeting, MeetingParams } from 'types/Meeting' + +export const useMeetingsStore = defineStore('meetings', () => { + + const meetings = ref([]) + const isInit = ref(false) + + const projectsStore = useProjectsStore() + const currentProjectId = computed(() => projectsStore.currentProjectId) + + async function init () { + const response = await api.get('/project/' + currentProjectId.value + '/meeting') + const meetingsAPI = response.data.data + meetings.value.push(...meetingsAPI) + isInit.value = true + } + + function reset () { + meetings.value = [] + isInit.value = false + } + + async function add (meetingData: MeetingParams) { + const response = await api.post('/project/' + currentProjectId.value + '/meeting', meetingData) + const newMeetingAPI = response.data.data + meetings.value.push(newMeetingAPI) + return newMeetingAPI + } + + async function update (meetingId: number, meetingData: MeetingParams) { + const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, meetingData) + const meetingAPI = response.data.data + const idx = meetings.value.findIndex(item => item.id === meetingAPI.id) + if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI) + } + + async function updateParticipants (meetingId: number, participants: number[]) { + const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId + '/participant', participants) + const participantsAPI = response.data.data + const idx = meetings.value.findIndex(item => item.id === meetingId) + if (meetings.value[idx]) meetings.value[idx].participants = participantsAPI + } + + async function attachFiles (meetingId: number, files: number[]) { + const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId + '/attach', files) + const filesAPI = response.data.data + const idx = meetings.value.findIndex(item => item.id === meetingId) + if (meetings.value[idx]) meetings.value[idx].files = filesAPI + } + + async function setCancelStatus (meetingId: number, status: boolean) { + const response = await api.put('/project/' + currentProjectId.value + '/meeting/' + meetingId, { is_cancel: status }) + const meetingAPI = response.data.data + const idx = meetings.value.findIndex(item => item.id === meetingAPI.id) + if (meetings.value[idx]) Object.assign(meetings.value[idx], meetingAPI) + } + + async function remove (meetingId: number) { + const response = await api.delete('/project/' + currentProjectId.value + '/meeting/' + meetingId) + const meetingAPIid = response.data.data.id + const idx = meetings.value.findIndex(item => item.id === meetingAPIid) + meetings.value.splice(idx, 1) + } + + function meetingById (id :number) { + return meetings.value.find(el => el.id === id) + } + + return { + meetings, + isInit, + init, + reset, + add, + update, + updateParticipants, + attachFiles, + setCancelStatus, + remove, + meetingById + } +}) diff --git a/src/stores/projects.ts b/src/stores/projects.ts index e9b0c91..6af0459 100644 --- a/src/stores/projects.ts +++ b/src/stores/projects.ts @@ -1,77 +1,85 @@ -import { ref } from 'vue' +import { ref, watch } from 'vue' import { defineStore } from 'pinia' -import type { Project, ProjectParams } from '../types' +import { api } from 'boot/axios' + +import { useFilesStore } from 'stores/files' +import { useTasksStore } from 'stores/tasks' +import { useMeetingsStore } from 'stores/meetings' +import { useUsersStore } from 'stores/users' +import { useChatsStore } from 'stores/chats' + +import type { Project } from 'types/Project' export const useProjectsStore = defineStore('projects', () => { const projects = ref([]) const currentProjectId = ref(null) - - projects.value.push( - { id: 1, name: 'Тестовый проект', description: 'Пример тестового проекта - тут описание чего-то', logo: 'https://cdn.quasar.dev/img/boy-avatar.png', chats: 3, companies: 1, persons: 5, is_archive: false, logo_as_bg: false }, - { id: 2, name: 'Разделка бобра на куски', description: 'Пример тестового проекта - тут описание чего-то', logo: '', chats: 8, companies: 12, persons: 1, is_archive: false, logo_as_bg: false }, - { id: 3, name: 'Комплекс мер', description: '', logo: '', chats: 8, companies: 3, persons: 4, is_archive: true, logo_as_bg: false }, - { id: 4, name: 'Тестовый проект 2', description: 'Пример тестового проекта - тут описание чего-то', logo: '', chats: 12, companies: 11, persons: 15, is_archive: false, logo_as_bg: false }, - { id: 11, name: 'Тестовый проект 12', description: 'Пример тестового проекта - тут описание чего-то', logo: 'https://cdn.quasar.dev/img/boy-avatar.png', chats: 5, companies: 2, persons: 5, is_archive: false, logo_as_bg: false }, - { id: 12, name: 'Разделка бобра на куски 11 Ох как много кусков пипец каааак много - резать тяжело', description: '', logo: '', chats: 8, companies: 3, persons: 1, is_archive: false, logo_as_bg: false }, - { id: 13, name: 'Тестовый проект и что-то еще', description: 'Пример тестового проекта - тут описание чего-то Ох как много кусков пипец каааак много - резать тяжело Ох как много кусков пипец каааак много - резать тяжело', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 8, companies: 3, persons: 4, is_archive: false, logo_as_bg: true }, - { id: 14, name: 'Тестовый проект', description: 'Пример тестового проекта - тут описание чего-то', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 12, companies: 11, persons: 15, is_archive: false, logo_as_bg: false }, - { id: 112, name: 'Разделка бобра на куски 11 Ох как много кусков пипец каааак много - резать тяжело', description: '', logo: '', chats: 8, companies: 3, persons: 1, is_archive: false, logo_as_bg: false}, - { id: 113, name: 'Тестовый проект и что-то еще', description: 'Пример тестового проекта - тут описание чего-то Ох как много кусков пипец каааак много - резать тяжело Ох как много кусков пипец каааак много - резать тяжело', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 8, companies: 3, persons: 4, is_archive: false, logo_as_bg: false }, - { id: 114, name: 'Тестовый проект', description: 'Пример тестового проекта - тут описание чего-то', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 12, companies: 11, persons: 15, is_archive: true, logo_as_bg: false }, - { id: 1112, name: 'Разделка бобра на куски 11 Ох как много кусков пипец каааак много - резать тяжело', description: '', logo: '', chats: 8, companies: 3, persons: 1, is_archive: false, logo_as_bg: false }, - { id: 1113, name: 'Тестовый проект и что-то еще', description: 'Пример тестового проекта - тут описание чего-то Ох как много кусков пипец каааак много - резать тяжело Ох как много кусков пипец каааак много - резать тяжело', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 8, companies: 3, persons: 4, is_archive: false, logo_as_bg: false }, - { id: 1114, name: 'Тестовый проект', description: 'Пример тестового проекта - тут описание чего-то', logo: 'https://cdn.quasar.dev/img/mountains.jpg', chats: 12, companies: 11, persons: 15, is_archive: false, logo_as_bg: false }, - ) + const startProjectId = ref(null) + const isInit = ref(false) - function projectById (id :number) { + const filesStore = useFilesStore() + const tasksStore = useTasksStore() + const meetingsStore = useMeetingsStore() + const usersStore = useUsersStore() + const chatsStore = useChatsStore() + + async function init () { + const response = await api.get('/project') + const projectsAPI = response.data.data + projects.value.push(...projectsAPI) + isInit.value = true + } + + function reset () { + projects.value = [] + isInit.value = false + currentProjectId.value = null + } + + function projectById (id: number) { return projects.value.find(el =>el.id === id) } - - function addProject (project: ProjectParams) { - const newProject = { - id: Date.now(), - is_archive: false, - chats: 0, - persons: 0, - companies: 0, - ...project - } - projects.value.push(newProject) - return newProject - } - - function updateProject (id :number, project :Project) { - const idx = projects.value.findIndex(item => item.id === id) - Object.assign(projects.value[idx] || {}, project) - } - - function archiveProject (id :number, status :boolean) { - const idx = projects.value.findIndex(item => item.id === id) - if (projects.value[idx]) projects.value[idx].is_archive = status - } - - function deleteProject (id :number) { - const idx = projects.value.findIndex(item => item.id === id) - projects.value.splice(idx, 1) - } function setCurrentProjectId (id: number | null) { currentProjectId.value = id } - function getCurrentProject () { - return currentProjectId.value ? projectById(currentProjectId.value) : {} + function setStartProjectId (id: number | null) { + startProjectId.value = id } + async function initStores () { + resetStores() + if (!filesStore.isInit) await filesStore.init() + if (!tasksStore.isInit) await tasksStore.init() + if (!meetingsStore.isInit) await meetingsStore.init() + if (!usersStore.isInit) await usersStore.init() + if (!chatsStore.isInit) await chatsStore.init() + + } + + function resetStores () { + filesStore.reset() + tasksStore.reset() + meetingsStore.reset() + usersStore.reset() + chatsStore.reset() + } + + watch (currentProjectId, async (newId) => { + if (newId) await initStores(); else resetStores() + }, { flush: 'sync' }) + return { + init, + reset, + isInit, projects, currentProjectId, + startProjectId, projectById, - addProject, - updateProject, - archiveProject, - deleteProject, setCurrentProjectId, - getCurrentProject + setStartProjectId, + initStores, + resetStores } }) diff --git a/src/stores/settings.ts b/src/stores/settings.ts new file mode 100644 index 0000000..9453984 --- /dev/null +++ b/src/stores/settings.ts @@ -0,0 +1,155 @@ +import { defineStore } from 'pinia' +import { ref, computed, inject } from 'vue' +import { api } from 'boot/axios' +import { useI18n } from 'vue-i18n' +import { Lang } from 'quasar' +import type { WebApp } from '@twa-dev/types' + +interface AppSettings { + fontSize: number + locale: string +} + +const defaultFontSize = 16 +const minFontSize = 10 +const maxFontSize = 22 +const fontSizeStep = 2 + +const defaultSettings: AppSettings = { + fontSize: defaultFontSize, + locale: 'en-US' +} + +export const useSettingsStore = defineStore('settings', () => { + const { locale: i18nLocale } = useI18n() + + const settings = ref({ ...defaultSettings }) + const tg = inject('tg') + + const isInit = ref(false) + + const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize) + const canIncrease = computed(() => currentFontSize.value < maxFontSize) + const canDecrease = computed(() => currentFontSize.value > minFontSize) + + const supportLocale = [ + { value: 'en-US', label: 'English' }, + { value: 'ru-RU', label: 'Русский' } + ] + + const quasarLangMap: Record = { + 'en-US': 'en-US', + 'ru-RU': 'ru' + } + + const updateQuasarLang = async (locale: string) => { + const quasarLang = quasarLangMap[locale] || 'en-US' + + try { + const langModule = await import( + `../../node_modules/quasar/lang/${quasarLang}.js` + ) + Lang.set(langModule.default) + } catch (e) { + console.error('Quasar Error load locale:', quasarLang, e) + } + } + + + + const detectLocale = (): string => { + const localeMap = { + ru: 'ru-RU', + en: 'en-US' + } as const satisfies Record + + type LocaleCode = keyof typeof localeMap + + const normLocale = (locale?: string): string | undefined => { + if (!locale) return undefined + const code = locale.split('-')[0] as LocaleCode + return localeMap[code] ?? undefined + } + + const tgLang = tg?.initDataUnsafe?.user?.language_code + const normalizedTgLang = normLocale(tgLang) + + return normalizedTgLang ?? normLocale(navigator.language) ?? 'en-US' + } + + const updateCssVariable = () => { + document.documentElement.style.setProperty( + '--dynamic-font-size', + `${currentFontSize.value}px` + ) + } + + const applyLocale = async () => { + if (settings.value.locale && i18nLocale) { + i18nLocale.value = settings.value.locale + await updateQuasarLang(settings.value.locale) + } + } + + const updateLocale = async (newLocale: string) => { + if (i18nLocale) { + i18nLocale.value = newLocale + await updateQuasarLang(newLocale) + settings.value.locale = newLocale + await saveSettings() + } + } + + const saveSettings = async () => { + await api.put('/settings', { settings: settings.value }) + } + + const updateSettings = async (newSettings: Partial) => { + settings.value = { ...settings.value, ...newSettings } + updateCssVariable() + await applyLocale() + await saveSettings() + } + + const init = async () => { + try { + const response = await api.get('/settings') + settings.value = { + fontSize: response.data.data.settings.fontSize || defaultSettings.fontSize, + locale: response.data.data.settings.locale || detectLocale() + } + } catch { + settings.value.locale = detectLocale() + } + updateCssVariable() + await applyLocale() + isInit.value = true + } + + const clampFontSize = (size: number) => + Math.max(minFontSize, Math.min(size, maxFontSize)) + + const increaseFontSize = async () => { + const newSize = clampFontSize(currentFontSize.value + fontSizeStep) + await updateSettings({ fontSize: newSize }) + } + + const decreaseFontSize = async () => { + const newSize = clampFontSize(currentFontSize.value - fontSizeStep) + await updateSettings({ fontSize: newSize }) + } + + return { + settings, + supportLocale, + isInit, + currentFontSize, + canIncrease, + canDecrease, + init, + increaseFontSize, + decreaseFontSize, + updateSettings, + updateLocale + } +}) diff --git a/src/stores/tasks.ts b/src/stores/tasks.ts new file mode 100644 index 0000000..ba0efb6 --- /dev/null +++ b/src/stores/tasks.ts @@ -0,0 +1,78 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { api } from 'boot/axios' +import { useProjectsStore } from 'stores/projects' +import type { Task, TaskParams } from 'types/Task' + +export const useTasksStore = defineStore('tasks', () => { + + const tasks = ref([]) + const isInit = ref(false) + + const projectsStore = useProjectsStore() + const currentProjectId = computed(() => projectsStore.currentProjectId) + + async function init () { + const response = await api.get('/project/' + currentProjectId.value + '/task') + const tasksAPI = response.data.data + tasks.value.push(...tasksAPI) + isInit.value = true + } + + function reset () { + tasks.value = [] + isInit.value = false + } + + async function add (taskData: TaskParams) { + const response = await api.post('/project/' + currentProjectId.value + '/task', taskData) + const newTaskAPI = response.data.data + tasks.value.push(newTaskAPI) + return newTaskAPI + } + + async function update (taskId: number, taskData: TaskParams) { + const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId, taskData) + const taskAPI = response.data.data + const idx = tasks.value.findIndex(item => item.id === taskAPI.id) + if (tasks.value[idx]) Object.assign(tasks.value[idx], taskAPI) + } + + async function updateObservers (taskId: number, observers: number[]) { + const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId + '/observer', observers) + const observersAPI = response.data.data + const idx = tasks.value.findIndex(item => item.id === taskId) + if (tasks.value[idx]) tasks.value[idx].participants = observersAPI + } + + async function attachFiles (taskId: number, files: number[]) { + const response = await api.put('/project/' + currentProjectId.value + '/task/' + taskId + '/attach', files) + const filesAPI = response.data.data + const idx = tasks.value.findIndex(item => item.id === taskId) + if (tasks.value[idx]) tasks.value[idx].files = filesAPI + } + + async function remove (taskId: number) { + const response = await api.delete('/project/' + currentProjectId.value + '/task/' + taskId) + const taskAPIid = response.data.data.id + const idx = tasks.value.findIndex(item => item.id === taskAPIid ) + tasks.value.splice(idx, 1) + } + + function taskById (id :number) { + return tasks.value.find(el => el.id === id) + } + + return { + tasks, + isInit, + init, + reset, + add, + update, + updateObservers, + attachFiles, + remove, + taskById + } +}) diff --git a/src/stores/textSize.ts b/src/stores/textSize.ts deleted file mode 100644 index 9a4ed0a..0000000 --- a/src/stores/textSize.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { api } from 'boot/axios' -import { defineStore } from 'pinia' -import { ref, computed } from 'vue' - -interface FontSizeResponse { - fontSize: number -} - -interface FontSizeError { - message: string - code: number -} - -export const useTextSizeStore = defineStore('textSize', () => { - // State - const baseSize = ref(16) // Значение по умолчанию - const isLoading = ref(false) - const error = ref(null) - const isInitialized = ref(false) - - // Константы - const minFontSize = 12 - const maxFontSize = 20 - const fontSizeStep = 2 - - // Getters - const currentFontSize = computed(() => baseSize.value) - const canIncrease = computed(() => baseSize.value < maxFontSize) - const canDecrease = computed(() => baseSize.value > minFontSize) - - // Actions - const fetchFontSize = async () => { - try { - isLoading.value = true - const response = await api.get('customer/settings') - baseSize.value = clampFontSize(response.data.fontSize) - updateCssVariable() - } catch (err) { - handleError(err, 'Failed to fetch font size') - baseSize.value = 16 // Fallback к значению по умолчанию - throw err - } finally { - isLoading.value = false - } - } - - const updateFontSize = async (newSize: number) => { - try { - const validatedSize = clampFontSize(newSize) - - await api.put('customer/settings', { fontSize: validatedSize }) - - baseSize.value = validatedSize - updateCssVariable() - error.value = null - } catch (err) { - handleError(err, 'Failed to update font size') - throw err - } - } - - const increaseFontSize = async () => { - if (!canIncrease.value) return - await updateFontSize(baseSize.value + fontSizeStep) - } - - const decreaseFontSize = async () => { - if (!canDecrease.value) return - await updateFontSize(baseSize.value - fontSizeStep) - } - - // Helpers - const clampFontSize = (size: number): number => { - return Math.max(minFontSize, Math.min(size, maxFontSize)) - } - - const updateCssVariable = () => { - document.documentElement.style.setProperty( - '--dynamic-font-size', - `${baseSize.value}px` - ) - } - - const handleError = (err: unknown, defaultMessage: string) => { - const apiError = err as { response?: { data: { message: string; code: number } } } - error.value = { - message: apiError?.response?.data?.message || defaultMessage, - code: apiError?.response?.data?.code || 500 - } - console.error('FontSize Error:', error.value) - } - - // Инициализация при первом использовании - const initialize = async () => { - if (isInitialized.value) return - - try { - await fetchFontSize() - } catch { - // Оставляем значение по умолчанию - } finally { - isInitialized.value = true - } - } - - return { - baseSize, - currentFontSize, - minFontSize, - maxFontSize, - isLoading, - error, - canIncrease, - canDecrease, - fetchFontSize, - increaseFontSize, - decreaseFontSize, - updateFontSize, - initialize - } -}) \ No newline at end of file diff --git a/src/stores/users.ts b/src/stores/users.ts new file mode 100644 index 0000000..8529bcb --- /dev/null +++ b/src/stores/users.ts @@ -0,0 +1,54 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { api } from 'boot/axios' +import { useProjectsStore } from 'stores/projects' +import type { User } from 'types/User' + +export const useUsersStore = defineStore('users', () => { + + const users = ref([]) + const isInit = ref(false) + + const projectsStore = useProjectsStore() + const currentProjectId = computed(() => projectsStore.currentProjectId) + + async function init () { + const response = await api.get('/project/' + currentProjectId.value + '/user') + const usersAPI = response.data.data + users.value.push(...usersAPI) + isInit.value = true + } + + async function reload () { + reset() + await api.get('/project/' + currentProjectId.value + '/user/reload') + await init() + } + + function reset () { + users.value = [] + isInit.value = false + } + + function userById (id: number) { + return users.value.find(el =>el.id === id) + } + + function userNameById (id: number) { + const user = userById(id) + return user?.fullname + || [user?.firstname, user?.lastname].filter(Boolean).join(' ').trim() + || user?.username + || '---' + } + + return { + users, + isInit, + init, + reset, + reload, + userById, + userNameById + } +}) diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index d9a16e5..0000000 --- a/src/types.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { WebApp } from "@twa-dev/types" - -declare global { - interface Window { - Telegram: { - WebApp: WebApp - } - } -} - -interface ProjectParams { - name: string - description?: string - logo?: string - logo_as_bg: boolean -} - -interface Project extends ProjectParams { - id: number - is_archive: boolean - chats: number - companies: number - persons: number -} - -interface Chat { - id: number - // project_id: number - name: string - description?: string - logo?: string - persons: number - owner_id: number -} - -interface CompanyParams { - name: string - description?: string - address?: string - site?: string - phone?: string - email?: string - logo?: string -} - -interface Company extends CompanyParams { - id: number - project_id: number -} - -export type { - Project, - ProjectParams, - Chat, - Company, - CompanyParams -} diff --git a/src/types/Chat.ts b/src/types/Chat.ts new file mode 100644 index 0000000..f738c48 --- /dev/null +++ b/src/types/Chat.ts @@ -0,0 +1,19 @@ +interface Chat { + id: number + project_id: number + name: string | null + description: string | null + telegram_id: number + logo: string | null + is_channel: boolean + bot_can_ban: boolean + owner_id?: number + user_count: number + last_update_time: number + invite_link: string + [key: string]: unknown +} + +export type { + Chat +} diff --git a/src/types/File.ts b/src/types/File.ts new file mode 100644 index 0000000..7dcf80e --- /dev/null +++ b/src/types/File.ts @@ -0,0 +1,23 @@ +interface File { + id: number + project_id: number + origin_chat_id: number + origin_message_id: number + chat_id: number + message_id: number + file_id: number + filename: string + mime: string + caption: string + size: number + published_by: number + published: number + parent_type: 0 | 1 | 2 + parent_id: number + backup_state?: number + [key: string]: unknown +} + +export type { + File +} diff --git a/src/types/Meeting.ts b/src/types/Meeting.ts new file mode 100644 index 0000000..7f1f8f8 --- /dev/null +++ b/src/types/Meeting.ts @@ -0,0 +1,22 @@ +interface MeetingParams { + name: string + description: string + place: string + meet_date: number + chat_attach: number | null + participants: number[] + files: number[] + is_cancel: boolean +} + +interface Meeting extends MeetingParams { + id: number + project_id: number + created_by: number + [key: string]: unknown +} + +export type { + Meeting, + MeetingParams +} diff --git a/src/types/Project.ts b/src/types/Project.ts new file mode 100644 index 0000000..0944162 --- /dev/null +++ b/src/types/Project.ts @@ -0,0 +1,19 @@ +interface ProjectParams { + name: string + description: string + logo: string + is_logo_bg: boolean +} + +interface Project extends ProjectParams { + id: number + is_archived: boolean + chat_count: number + user_count: number + [key: string]: unknown +} + +export type { + Project, + ProjectParams +} diff --git a/src/types/Task.ts b/src/types/Task.ts new file mode 100644 index 0000000..72a52ae --- /dev/null +++ b/src/types/Task.ts @@ -0,0 +1,26 @@ +interface TaskParams { + name: string + description: string + assigned_to: number + priority: 0 | 1 | 2 | 3 + status: 1 | 5 + time_spent?: number + create_date: number + plan_date: number +} + +interface Task extends TaskParams { + id: number + project_id: number + created_by: number + closed_by: number | null + observers: number[] + files: number[] + close_date: number + [key: string]: unknown +} + +export type { + Task, + TaskParams +} diff --git a/src/types/User.ts b/src/types/User.ts new file mode 100644 index 0000000..a9b6b71 --- /dev/null +++ b/src/types/User.ts @@ -0,0 +1,22 @@ +interface User { + id: number + project_id: number + telegram_id: number + firstname: string | null + lastname: string | null + username: string | null + photo: string | null + phone: string + company_id: number + fullname: string + role: string + department: string + email: string + is_blocked: boolean + is_leave: boolean + [key: string]: unknown +} + +export type { + User +} diff --git a/todo.txt b/todo.txt index e313d65..88169a8 100644 --- a/todo.txt +++ b/todo.txt @@ -1,83 +1,34 @@ -0. Общее: +0. Общая панель: +- Панель выбора проекта +- Страница настроек +- Должна быть настройка: все проекты -1. Login: -+ Окно "Забыли пароль?" -+ Надпись "Неправильный логин или пароль" -+ Окно "Регистрация нового пользователя" -+ Верификация поля ввода e-mail (не делать - плохо выглядит) +1. Панель задач: +- Панель выбора задачи: +-- Фильтры +-- Выбор даты +- Панель конкретной задачи +- Панель добавить задачу +- Настройка Store Tasks -2. Account: -+ Работа с изображением логотипа компании -- Перенос аккаунта с телеграмм на логин/пароль -- Форма оплаты +2. Панель совещаний: +- Панель выбора совещаний: +-- Выбор даты +- Панель конкретного совещания +- Настройка Store Meetings -3. ProjectsPage: -+ Архивные проекты -+ (баг) Промотка шапки в конце прокрутки списка проектов -+ Добавить тень при прокрутке списка на заголовке "Проекты" -+ Окно добавить проект -+ При добавлении проекта проверять валидность, если не валидно то скрывать галку "Применить" +3. Панель файлов: +- Панель выбора файлов: +-- Фильтры +- Настройка Store Files -4.1 ProjectPage - Заголовок: -+ Анимация расширенной версии (плавное увеличение блока div) -+ Окно редактирования проекта -+ При изменении свойств проекта проверять валидность, если не валидно то скрывать галку "Применить" -+ Продумать backup (потом) -+ Окно отправки проекта в архив -+ Окно удаления проекта +4. Панель Контакты: +- Панель выбора контактов +- Панель конкретного контакта +- Настройка Store Users -4.2 ProjectPage - Чаты: -+ Окно прикрепления нового чата -+ Добавить диалог при слайдинге чата об подтверждении удаления и предупреждением. -+ Сделать стор с чатами -+ Настроить роутинг -+ У чатов добавить кол-во пользователей -- У чатов добавить указание владельца чата и его компанию -- Удаление чата свайпом и отключенные чаты. +5. Панель чаты: +- Панель выбора чата +- Настройка Store Chats -4.3 ProjectPage - Люди: -- Перечень сотрудников -+ Окно редактирования сотрудника -- При изменении сотрудников проверять валидность, если не валидно то скрывать галку "Применить" -- Сделать стор с персоналом -- Настроить роутинг -4.4 ProjectPage - Компании: -+ Перечень компаний -+ Окно редактирования компании -- При изменении компании проверять валидность, если не валидно то скрывать галку "Применить" -- Окно настройки видимости компаний - -4.5 ProjectPage - Маскировка: -- Сделать стор и настроить компоненты - -5. Settings: -- Роутинг -- Переключатель языков -+ Встроить в Телеграмм - -6. Лицензионное соглашение: -- Роутинг и заготовка -- Текст соглашения -- Встроить в Телеграмм - -BUGS: -+- 1. Прыгает кнопка fab при перещелкивании табов (при быстром переключении все равно прыгает, проблема установлена в q-page-sticky -как-то некорректно отрабатывается bottom и right) -+ 2. Верстка в шапке Projects плохая - переделать -- 3. Не хватает перевода местами -+ 4. При нажатии Back браузера скидывается активная табка. -+ 5. Криво работает удаление чата (полоски-бордюры) // дописывается стиль - -Need refactor -- 1. Слияение объектов разных типов, но с одинаковыми ключами (например, в updateProject через ObjectAssign) - -Current ToDo: -+ 1. pinia -+ 2. Реализовать функционал меню - редактирование проекта. (Бекап на потом) -+ 3. Архивные чаты и проекты. (Чаты отказался) -+4. Добавление компании. -+ 5. Удаление компании (слайдер), как в чате. -- 6. Страница аккаунта: -- 6.1 Переделать выбор платежей. -- 6.2 Окошко смены емейл аккаунта при входе с емейла. -- 7. Настроить git diff --git a/tsconfig.json b/tsconfig.json index 96fc57b..c77a8cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,6 @@ { "extends": "./.quasar/tsconfig.json", "compilerOptions": { - "baseUrl": ".", - "paths": { - "src/*": ["./src/*"], - "app/*": ["./src/*"], - "components/*": ["./src/components/*"], - "layouts/*": ["./src/layouts/*"], - "pages/*": ["./src/pages/*"], - "assets/*": ["./src/assets/*"], - "boot/*": ["./src/boot/*"], - "stores/*": ["./src/stores/*"] - }, "types": ["@twa-dev/types", "node"] }, "include": ["src/**/*", "types/**/*"]