setModal(undefined)}/>);
+
+ return
+
+ {t('Section_Number')}
+
+ setNumber(e.currentTarget.value)} placeholder={t('Section_Number')} />
+
+
+
+ {t('Section_Name')}
+
+ setName(e.currentTarget.value)} placeholder={t('Section_Name')} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+}
diff --git a/app/protocols/client/views/Protocol.js b/app/protocols/client/views/Protocol.js
new file mode 100644
index 000000000000..31689fb11669
--- /dev/null
+++ b/app/protocols/client/views/Protocol.js
@@ -0,0 +1,131 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import {Box, Button, Field, Icon, Scrollable, Tabs} from '@rocket.chat/fuselage';
+
+import Page from '../../../../client/components/basic/Page';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useRoute, useRouteParameter } from '../../../../client/contexts/RouterContext';
+import { useEndpointData } from '../../../../client/hooks/useEndpointData';
+import { useFormatDate } from '../../../../client/hooks/useFormatDate';
+import { useMethod } from '../../../../client/contexts/ServerContext';
+import { Sections } from './Sections';
+import VerticalBar from "/client/components/basic/VerticalBar";
+import { AddSection } from './AddSection';
+import { AddItem } from './AddItem';
+import { EditSection } from './EditSection';
+import { EditItem } from './EditItem';
+
+export function ProtocolPage() {
+ const t = useTranslation();
+ const formatDate = useFormatDate();
+
+ const [cache, setCache] = useState();
+
+ const router = useRoute('protocol');
+ const protocolId = useRouteParameter('id');
+ const context = useRouteParameter('context');
+ const sectionId = useRouteParameter('sectionId');
+ const itemId = useRouteParameter('itemId');
+
+ const query = useMemo(() => ({
+ query: JSON.stringify({ _id: protocolId }),
+ }), [protocolId, cache]);
+
+ const data = useEndpointData('protocols.findOne', query) || {};
+
+ const title = t('Protocol').concat(' ').concat(data.num).concat(' ').concat(t('Date_to')).concat(' ').concat(formatDate(data.d));
+
+ const downloadProtocolParticipantsMethod = useMethod('downloadProtocolParticipants');
+
+ const downloadProtocolParticipants = (_id) => async (e) => {
+ e.preventDefault();
+ try {
+ const res = await downloadProtocolParticipantsMethod({ _id });
+ const url = window.URL.createObjectURL(new Blob([res]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', 'file.docx');
+ document.body.appendChild(link);
+ link.click();
+ } catch (e) {
+ console.error('[index.js].downloadProtocolParticipants :', e);
+ }
+ };
+
+ const onAddSectionClick = useCallback((context) => () => {
+ router.push({ id: protocolId, context: context });
+ }, [router]);
+
+ const onSectionClick = useCallback((_id) => () => {
+ router.push({
+ id: protocolId,
+ context: 'edit-section',
+ sectionId: _id,
+ });
+ }, [router]);
+
+ const onChange = useCallback(() => {
+ setCache(new Date());
+ }, []);
+
+ const close = useCallback(() => {
+ router.push({
+ id: protocolId,
+ });
+ }, [router]);
+
+ const onAddItemClick = useCallback((context, sectionId) => () => {
+ router.push({
+ id: protocolId,
+ context: context,
+ sectionId: sectionId
+ });
+ }, [router]);
+
+ const onItemClick = useCallback((sectionId, _id) => () => {
+ router.push({
+ id: protocolId,
+ context: 'edit-item',
+ sectionId: sectionId,
+ itemId: _id
+ });
+ }, [router]);
+
+ return
+
+
+
+
+
+
+
+ {data.place}
+
+
+
+
+
+
+
+ { context
+ &&
+
+ { context === 'new-section' && t('Section_Add') }
+ { context === 'new-item' && t('Item_Add') }
+ { context === 'edit-section' && t('Section_Info') }
+ { context === 'edit-item' && t('Item_Info') }
+
+
+ {context === 'new-section' && }
+ {context === 'new-item' && }
+ {context === 'edit-section' && }
+ {context === 'edit-item' && }
+ }
+ ;
+}
+
+ProtocolPage.displayName = 'ProtocolPage';
+
+export default ProtocolPage;
diff --git a/app/protocols/client/views/Protocols.js b/app/protocols/client/views/Protocols.js
new file mode 100644
index 000000000000..cc6c3f842060
--- /dev/null
+++ b/app/protocols/client/views/Protocols.js
@@ -0,0 +1,74 @@
+import React, { useMemo } from 'react';
+import { Box, Button, Icon, Table } from '@rocket.chat/fuselage';
+import { useMediaQuery } from '@rocket.chat/fuselage-hooks';
+
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { GenericTable, Th } from '../../../../client/components/GenericTable';
+import { useFormatDateAndTime } from '../../../../client/hooks/useFormatDateAndTime';
+import { useFormatDate } from '../../../../client/hooks/useFormatDate';
+import { useMethod } from '../../../../client/contexts/ServerContext';
+
+export function Protocols({
+ data,
+ sort,
+ onClick,
+ onEditClick,
+ onHeaderClick,
+ setParams,
+ params,
+}) {
+ const t = useTranslation();
+
+ const mediaQuery = useMediaQuery('(min-width: 768px)');
+
+ const downloadProtocolParticipantsMethod = useMethod('downloadProtocolParticipants');
+
+ const downloadProtocolParticipants = (_id) => async (e) => {
+ e.preventDefault();
+ try {
+ const res = await downloadProtocolParticipantsMethod({ _id });
+ const url = window.URL.createObjectURL(new Blob([res]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', 'file.docx');
+ document.body.appendChild(link);
+ link.click();
+ } catch (e) {
+ console.error('[index.js].downloadProtocolParticipants :', e);
+ }
+ };
+
+ const header = useMemo(() => [
+ {t('Protocol_Number')} | ,
+ {t('Protocol_Date')} | ,
+ mediaQuery && {t('Protocol_Place')} | ,
+ mediaQuery && {t('Created_at')} | ,
+ | ,
+ // |
+ ], [sort, mediaQuery]);
+
+ const formatDate = useFormatDate();
+ const formatDateAndTime = useFormatDateAndTime();
+
+ const renderRow = (protocol) => {
+ const { _id, d: date, num, place, ts } = protocol;
+ return
+ {num}
+ {formatDate(date)}
+ { mediaQuery && {place}}
+ { mediaQuery && {formatDateAndTime(ts)}}
+
+
+
+ {/**/}
+ {/* */}
+ {/**/}
+ ;
+ };
+
+ return ;
+}
diff --git a/app/protocols/client/views/Sections.js b/app/protocols/client/views/Sections.js
new file mode 100644
index 000000000000..96fa742f8260
--- /dev/null
+++ b/app/protocols/client/views/Sections.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import {Box, Button, Scrollable, Tile} from '@rocket.chat/fuselage';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { useFormatDate } from '../../../../client/hooks/useFormatDate';
+
+export function Sections({ data, onSectionClick, onAddItemClick, onItemClick }) {
+ const t = useTranslation();
+ const formatDate = useFormatDate();
+
+ const Item = (item) => <>
+
+ {item.num}. {item.name}
+ { item.responsible && {t('Item_Responsible')}: {item.responsible} }
+ { item.expireAt && {t('Item_ExpireAt')}: {formatDate(item.expireAt)} }
+
+ >;
+
+ const Section = (section) => <>
+
+ {section.num}. {section.name}
+
+
+ {(
+ section.items
+ ? section.items.map((props, index) => )
+ : <>>
+ )}
+
+
+ >;
+
+ return <>
+ {data && !data.length
+ ?
+ {t('No_data_found')}
+
+ : <>
+
+
+ {(
+ data
+ ? data.map((props, index) => )
+ : <>>
+ )}
+
+
+ >
+ }
+ >;
+}
diff --git a/app/protocols/client/views/index.js b/app/protocols/client/views/index.js
new file mode 100644
index 000000000000..034787e34209
--- /dev/null
+++ b/app/protocols/client/views/index.js
@@ -0,0 +1,104 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import { Button, Icon } from '@rocket.chat/fuselage';
+import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
+
+import Page from '../../../../client/components/basic/Page';
+import { useTranslation } from '../../../../client/contexts/TranslationContext';
+import { Protocols } from './Protocols';
+import { useRoute, useRouteParameter } from '../../../../client/contexts/RouterContext';
+import VerticalBar from '../../../../client/components/basic/VerticalBar';
+import { EditProtocol } from './EditProtocol';
+import { AddProtocol } from './AddProtocol';
+import { useEndpointData } from '../../../../client/hooks/useEndpointData';
+
+const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);
+
+const useQuery = ({ text, itemsPerPage, current }, [ column, direction ], cache) => useMemo(() => ({
+ // query: JSON.stringify({ desc: { $regex: text || '', $options: 'i' } }),
+ sort: JSON.stringify({ [column]: sortDir(direction) }),
+ ...itemsPerPage && { count: itemsPerPage },
+ ...current && { offset: current },
+ // TODO: remove cache. Is necessary for data invalidation
+}), [text, itemsPerPage, current, column, direction, cache]);
+
+export function ProtocolsPage() {
+ const t = useTranslation();
+
+ const routeName = 'protocols';
+
+ const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 });
+ const [sort, setSort] = useState(['d', 'desc']);
+ const [cache, setCache] = useState();
+
+ const debouncedParams = useDebouncedValue(params, 500);
+ const debouncedSort = useDebouncedValue(sort, 500);
+
+ const query = useQuery(debouncedParams, debouncedSort, cache);
+
+ const data = useEndpointData('protocols.list', query) || { result: [] };
+
+ const router = useRoute(routeName);
+
+ const context = useRouteParameter('context');
+ const id = useRouteParameter('id');
+
+ const onClick = (_id) => () => {
+ FlowRouter.go(`/protocol/${ _id }`);
+ };
+
+ const onEditClick = useCallback((_id) => () => {
+ router.push({
+ context: 'edit',
+ id: _id,
+ });
+ }, [router]);
+
+ const onHeaderClick = (id) => {
+ const [sortBy, sortDirection] = sort;
+
+ if (sortBy === id) {
+ setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
+ return;
+ }
+ setSort([id, 'asc']);
+ };
+
+ const handleHeaderButtonClick = useCallback((context) => () => {
+ router.push({ context });
+ }, [router]);
+
+ const close = useCallback(() => {
+ router.push({});
+ }, [router]);
+
+ const onChange = useCallback(() => {
+ setCache(new Date());
+ }, []);
+
+ return
+
+
+
+
+
+
+
+
+ { context
+ &&
+
+ { context === 'edit' && t('Protocol_Info') }
+ { context === 'new' && t('Protocol_Add') }
+
+
+ {context === 'edit' && }
+ {context === 'new' && }
+ }
+ ;
+}
+
+ProtocolsPage.displayName = 'ProtocolsPage';
+
+export default ProtocolsPage;
diff --git a/app/protocols/client/views/lib.js b/app/protocols/client/views/lib.js
new file mode 100644
index 000000000000..ad1e876a8eef
--- /dev/null
+++ b/app/protocols/client/views/lib.js
@@ -0,0 +1,88 @@
+// Here previousData will define if it is an update or a new entry
+export function validate(protocolData) {
+ const errors = [];
+
+ if (!protocolData.d) {
+ errors.push('Date');
+ }
+
+ if (!protocolData.num) {
+ errors.push('Number');
+ }
+
+ if (!protocolData.place) {
+ errors.push('Place');
+ }
+
+ return errors;
+}
+
+export function createProtocolData(date, number, place = '', previousData) {
+ const protocolData = {
+ };
+
+ if (previousData) {
+ protocolData._id = previousData._id;
+ }
+ protocolData.d = date;
+ protocolData.num = number;
+ protocolData.place = place;
+
+ return protocolData;
+}
+
+export function validateSectionData(sectionData) {
+ const errors = [];
+
+ if (!sectionData.num) {
+ errors.push('Number');
+ }
+
+ if (!sectionData.name) {
+ errors.push('Name');
+ }
+
+ return errors;
+}
+
+export function createSectionData(number, name = '', previousData) {
+ const sectionData = {
+ };
+
+ if (previousData) {
+ sectionData._id = previousData._id;
+ }
+ sectionData.num = number;
+ sectionData.name = name;
+
+ return sectionData;
+}
+
+export function validateItemData(itemData) {
+ const errors = [];
+
+ if (!itemData.num) {
+ errors.push('Number');
+ }
+
+ if (!itemData.name) {
+ errors.push('Name');
+ }
+
+ return errors;
+}
+
+export function createItemData(number, name, responsible, expireAt = '', previousData) {
+ const itemData = {
+ };
+
+ if (previousData) {
+ itemData._id = previousData._id;
+ }
+ itemData.num = number;
+ itemData.name = name;
+ itemData.responsible = responsible;
+ itemData.expireAt = expireAt;
+
+ return itemData;
+}
diff --git a/app/protocols/server/index.js b/app/protocols/server/index.js
new file mode 100644
index 000000000000..3e03e78e32cb
--- /dev/null
+++ b/app/protocols/server/index.js
@@ -0,0 +1,7 @@
+import './methods/insertOrUpdateProtocol';
+import './methods/insertOrUpdateSection';
+import './methods/insertOrUpdateItem';
+import './methods/deleteProtocol';
+import './methods/deleteSection';
+import './methods/deleteItem';
+import './methods/downloadProtocolParticipants';
diff --git a/app/protocols/server/methods/deleteItem.js b/app/protocols/server/methods/deleteItem.js
new file mode 100644
index 000000000000..2598c7d250f7
--- /dev/null
+++ b/app/protocols/server/methods/deleteItem.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ deleteItem(protocolId, sectionId, _id) {
+ let protocol = null;
+
+ if (hasPermission(this.userId, 'manage-protocols')) {
+ protocol = Protocols.findOneById(protocolId);
+ } else {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (protocol == null) {
+ throw new Meteor.Error('Protocol_Error_Invalid_Protocol', 'Invalid protocol', { method: 'deleteSection' });
+ }
+
+ Protocols.removeItemById(protocolId, sectionId, _id);
+
+ return true;
+ },
+});
diff --git a/app/protocols/server/methods/deleteProtocol.js b/app/protocols/server/methods/deleteProtocol.js
new file mode 100644
index 000000000000..a827bef40d45
--- /dev/null
+++ b/app/protocols/server/methods/deleteProtocol.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ deleteProtocol(protocolId) {
+ let protocol = null;
+
+ if (hasPermission(this.userId, 'manage-protocols')) {
+ protocol = Protocols.findOneById(protocolId);
+ } else {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (protocol == null) {
+ throw new Meteor.Error('Protocol_Error_Invalid_Protocol', 'Invalid protocol', { method: 'deleteProtocol' });
+ }
+
+ Protocols.removeById(protocolId);
+
+ return true;
+ },
+});
diff --git a/app/protocols/server/methods/deleteSection.js b/app/protocols/server/methods/deleteSection.js
new file mode 100644
index 000000000000..5353ef0d0448
--- /dev/null
+++ b/app/protocols/server/methods/deleteSection.js
@@ -0,0 +1,24 @@
+import { Meteor } from 'meteor/meteor';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ deleteSection(protocolId, _id) {
+ let protocol = null;
+
+ if (hasPermission(this.userId, 'manage-protocols')) {
+ protocol = Protocols.findOneById(protocolId);
+ } else {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (protocol == null) {
+ throw new Meteor.Error('Protocol_Error_Invalid_Protocol', 'Invalid protocol', { method: 'deleteSection' });
+ }
+
+ Protocols.removeSectionById(protocolId, _id);
+
+ return true;
+ },
+});
diff --git a/app/protocols/server/methods/downloadProtocolParticipants.js b/app/protocols/server/methods/downloadProtocolParticipants.js
new file mode 100644
index 000000000000..b9955509a617
--- /dev/null
+++ b/app/protocols/server/methods/downloadProtocolParticipants.js
@@ -0,0 +1,223 @@
+import { Meteor } from 'meteor/meteor';
+import s from 'underscore.string';
+import {
+ AlignmentType,
+ Document,
+ HeadingLevel,
+ Packer,
+ PageOrientation,
+ Paragraph,
+ Table,
+ TableCell,
+ TableRow,
+ TextRun, VerticalAlign, WidthType,
+} from 'docx';
+import moment from 'moment';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ async downloadProtocolParticipants({ _id }) {
+ if (!hasPermission(this.userId, 'manage-protocols')) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (!_id) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field _id is required', { method: 'downloadProtocolParticipants', field: '_id' });
+ }
+
+ const protocol = Protocols.findOne({ _id });
+
+ if (!protocol) {
+ throw new Meteor.Error('error-protocol-does-not-exists', `The protocol with _id: ${ _id } doesn't exist`, { method: 'downloadProtocolParticipants', field: '_id' });
+ }
+
+ const doc = new Document();
+
+ let usersRows = [
+ new TableRow({
+ tableHeader: true,
+ children: [
+ new TableCell({
+ children: [new Paragraph({ text: '№', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 5,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Участник', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Должность с указанием названия организации', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Контактное лицо', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Электронная почта', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Номер телефона', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: 'Заявлен', bold: true, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ ],
+ }),
+ ];
+ if (protocol.invitedUsers) {
+ usersRows = usersRows.concat(protocol.invitedUsers.map((value, index) => {
+ const contactFace = value.contactPersonFirstName ? `${ value.contactPersonLastName } ${ value.contactPersonFirstName } ${ value.contactPersonPatronymicName }`.trim() : '-';
+ return new TableRow({
+ children: [
+ new TableCell({
+ children: [new Paragraph({ text: `${ index + 1 }`, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 5,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: `${ value.lastName } ${ value.firstName } ${ value.patronymic }`.trim(), alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: `${ value.position }`, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: contactFace, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: `${ value.email }`, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: `${ value.phone }`, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ new TableCell({
+ children: [new Paragraph({ text: `${ moment(new Date(value.ts)).format('DD MMMM YYYY, HH:mm') }`, alignment: AlignmentType.CENTER })],
+ verticalAlign: VerticalAlign.CENTER,
+ alignment: AlignmentType.CENTER,
+ width: {
+ size: 19,
+ type: WidthType.PERCENTAGE,
+ },
+ }),
+ ],
+ });
+ },
+ ),
+ );
+ }
+
+
+ doc.addSection({
+ size: {
+ orientation: PageOrientation.LANDSCAPE,
+ },
+ children: [
+ new Paragraph({
+ children: [
+ new TextRun({
+ text: `Отчет сформирован ${ moment(new Date()).format('DD MMMM YYYY, HH:mm') }`,
+ }),
+ ],
+ alignment: AlignmentType.RIGHT,
+ }),
+ new Paragraph({
+ children: [
+ new TextRun({
+ text: 'Совещание',
+ }),
+ ],
+ heading: HeadingLevel.HEADING_1,
+ alignment: AlignmentType.CENTER,
+ }),
+ new Paragraph({
+ children: [
+ new TextRun({
+ text: `От ${ moment(protocol.d).format('DD MMMM YYYY, HH:mm') }`,
+ }),
+ ],
+ heading: HeadingLevel.HEADING_2,
+ alignment: AlignmentType.CENTER,
+ }),
+ new Table({
+ rows: usersRows,
+ width: {
+ size: 100,
+ type: WidthType.PERCENTAGE,
+ },
+ cantSplit: true,
+ }),
+ ],
+ });
+
+ const buffer = await Packer.toBuffer(doc);
+
+ return buffer;
+ },
+});
diff --git a/app/protocols/server/methods/insertOrUpdateItem.js b/app/protocols/server/methods/insertOrUpdateItem.js
new file mode 100644
index 000000000000..4c3788aaedb3
--- /dev/null
+++ b/app/protocols/server/methods/insertOrUpdateItem.js
@@ -0,0 +1,35 @@
+import { Meteor } from 'meteor/meteor';
+import s from 'underscore.string';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ insertOrUpdateItem(protocolId, sectionId, item) {
+ if (!hasPermission(this.userId, 'manage-protocols')) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (!protocolId) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field ProtocolId is required', { method: 'insertOrUpdateSection', field: 'ProtocolId' });
+ }
+
+ if (!sectionId) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field SectionId is required', { method: 'insertOrUpdateSection', field: 'SectionId' });
+ }
+
+ if (!s.trim(item.num)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Number is required', { method: 'insertOrUpdateSection', field: 'Number' });
+ }
+
+ if (!s.trim(item.name)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { method: 'insertOrUpdateSection', field: 'Name' });
+ }
+
+ if (!item._id) {
+ return Protocols.createItem(protocolId, sectionId, item)
+ }
+
+ return Protocols.updateItem(protocolId, sectionId, item);
+ },
+});
diff --git a/app/protocols/server/methods/insertOrUpdateProtocol.js b/app/protocols/server/methods/insertOrUpdateProtocol.js
new file mode 100644
index 000000000000..3fc60394c69d
--- /dev/null
+++ b/app/protocols/server/methods/insertOrUpdateProtocol.js
@@ -0,0 +1,48 @@
+import { Meteor } from 'meteor/meteor';
+import s from 'underscore.string';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ insertOrUpdateProtocol(protocolData) {
+ if (!hasPermission(this.userId, 'manage-protocols')) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (!protocolData.d) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Date is required', { method: 'insertOrUpdateProtocol', field: 'Date' });
+ }
+
+ if (!s.trim(protocolData.num)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Number is required', { method: 'insertOrUpdateProtocol', field: 'Number' });
+ }
+
+ if (!s.trim(protocolData.place)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Place is required', { method: 'insertOrUpdateProtocol', field: 'Place' });
+ }
+
+ if (!protocolData._id) {
+
+ const user = Meteor.user();
+
+ const createProtocol = {
+ ts: new Date(),
+ u: {
+ _id: user._id,
+ username: user.username,
+ },
+ d: new Date(protocolData.d),
+ num: protocolData.num,
+ place: protocolData.place,
+ sections: [],
+ };
+
+ const _id = Protocols.create(createProtocol);
+
+ return _id;
+ }
+
+ return Protocols.updateProtocol(protocolData._id, protocolData);
+ },
+});
diff --git a/app/protocols/server/methods/insertOrUpdateSection.js b/app/protocols/server/methods/insertOrUpdateSection.js
new file mode 100644
index 000000000000..69baa2150f91
--- /dev/null
+++ b/app/protocols/server/methods/insertOrUpdateSection.js
@@ -0,0 +1,31 @@
+import { Meteor } from 'meteor/meteor';
+import s from 'underscore.string';
+
+import { hasPermission } from '../../../authorization';
+import { Protocols } from '../../../models';
+
+Meteor.methods({
+ insertOrUpdateSection(protocolId, section) {
+ if (!hasPermission(this.userId, 'manage-protocols')) {
+ throw new Meteor.Error('not_authorized');
+ }
+
+ if (!protocolId) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field ProtocolId is required', { method: 'insertOrUpdateSection', field: 'ProtocolId' });
+ }
+
+ if (!s.trim(section.num)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Number is required', { method: 'insertOrUpdateSection', field: 'Number' });
+ }
+
+ if (!s.trim(section.name)) {
+ throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { method: 'insertOrUpdateSection', field: 'Name' });
+ }
+
+ if (!section._id) {
+ return Protocols.createSection(protocolId, section)
+ }
+
+ return Protocols.updateSection(protocolId, section);
+ },
+});
diff --git a/app/ui/client/views/app/home.js b/app/ui/client/views/app/home.js
index ce6574e6455e..a45cc8ad1619 100644
--- a/app/ui/client/views/app/home.js
+++ b/app/ui/client/views/app/home.js
@@ -153,7 +153,17 @@ const toolbarButtons = () => [
menu.close();
FlowRouter.go('/errands/charged_to_me');
},
- }];
+ },
+ {
+ name: t('Protocols'),
+ icon: 'errand',
+ //condition: () => hasPermission('manage-working-errand'),
+ action: () => {
+ menu.close();
+ FlowRouter.go('/protocols');
+ },
+ }
+];
Template.home.helpers({
title() {
diff --git a/client/routes.js b/client/routes.js
index 52071286d305..5f921924db70 100755
--- a/client/routes.js
+++ b/client/routes.js
@@ -200,6 +200,26 @@ FlowRouter.route('/manual-mail-sender', {
}],
});
+FlowRouter.route('/protocols/:context?/:id?', {
+ name: 'protocols',
+ action: () => {
+ renderRouteComponent(() => import('../app/protocols/client/views/index'), { template: 'main', region: 'center' });
+ },
+ triggersExit: [function() {
+ $('.main-content').addClass('rc-old');
+ }],
+});
+
+FlowRouter.route('/protocol/:id/:context?/:sectionId?/:itemId?', {
+ name: 'protocol',
+ action: () => {
+ renderRouteComponent(() => import('../app/protocols/client/views/Protocol'), { template: 'main', region: 'center' });
+ },
+ triggersExit: [function() {
+ $('.main-content').addClass('rc-old');
+ }],
+});
+
FlowRouter.route('/directory/:tab?', {
name: 'directory',
action: () => {
diff --git a/packages/rocketchat-i18n/i18n/ru.i18n.json b/packages/rocketchat-i18n/i18n/ru.i18n.json
index 1650bdc6bb4c..2c3987d6d5ba 100755
--- a/packages/rocketchat-i18n/i18n/ru.i18n.json
+++ b/packages/rocketchat-i18n/i18n/ru.i18n.json
@@ -24,7 +24,6 @@
"Access_not_authorized": "Неавторизованный доступ",
"Access_Token_URL": "Access Token URL",
"Accessing_permissions": "Права доступа",
- "Account": "Аккаунт",
"Account_SID": "SID учетной записи",
"Accounts": "Учётные записи",
"Accounts_Admin_Email_Approval_Needed_Default": "Зарегистрирован новый пользователь [name] ([email]) .
Пожалуйста, перейдите в раздел \"Администрирование -> Пользователи\" для его активации или удаления.
",
@@ -37,10 +36,7 @@
"Accounts_AllowedDomainsList_Description": "Список разрешенных доменов, разделенный запятыми ",
"Accounts_AllowEmailChange": "Разрешить изменять адрес электронной почты",
"Accounts_AllowEmailNotifications": "Разрешить уведомления по электронной почте",
- "Accounts_AllowOrganizationChange": "Разрешить смену организации",
"Accounts_AllowPasswordChange": "Разрешить смену пароля",
- "Accounts_AllowPositionChange": "Разрешить смену должности",
- "Accounts_AllowPhoneChange": "Разрешить смену номера телефона",
"Accounts_AllowRealNameChange": "Разрешить смену имени",
"Accounts_AllowUserAvatarChange": "Разрешить пользователю изменять аватар",
"Accounts_AllowUsernameChange": "Разрешить изменять логин",
@@ -741,20 +737,15 @@
"could-not-access-webdav": "Не удалось получить доступ к WebDAV",
"Council": "Мероприятие",
"Council_Add": "Добавить мероприятие",
- "Council_Add_Participant": "Добавить участника",
"Council_info": "Информация о мероприятии",
"Council_info_description": "Вы приглашены на мероприятие. Ознакомьтесь с его деталями.",
"Council_date": "Дата мероприятия",
- "Council_edit": "Редактирование мероприятия",
- "Council_edited": "Мероприятие отредактировано",
"Council_from": "Мероприятия от",
"Council_invite_error": "Приглашения на данное мероприятие не существует",
"Council_Added_Successfully": "Мероприятие успешно добавлено",
"Council_Delete_Warning": "Удаление мероприятия нельзя отменить",
- "Council_Delete_Participant_Warning": "Удалить участника?",
"Council_Error_Invalid_Council": "Некорректное мероприятие",
"Council_Has_Been_Deleted": "Мероприятие успешно удалено",
- "Council_Participant_Has_Been_Deleted": "Участник успешно удален",
"Council_Info": "Информация о мероприятии",
"Council_Invited_Users": "Участники",
"Council_Invited_Users_List": "Список участников мероприятия",
@@ -1168,7 +1159,6 @@
"Details": "Подробности",
"Different_Style_For_User_Mentions": "Другой стиль для упоминаний пользователей",
"Direct_message_someone": "Личная переписка с кем-нибудь",
- "Direct_Message": "Личное сообщение",
"Direct_Messages": "Личная переписка",
"Direct_Reply": "Прямой ответ",
"Direct_Reply_Advice": "Вы можете напрямую ответить на это письмо. Не изменяйте предыдущие сообщения в потоке.",
@@ -1272,9 +1262,7 @@
"Edit_Department": "Редактировать отдел",
"Edit_previous_message": "'%s' - редактировать предыдущее сообщение",
"Edit_Trigger": "Изменить триггер",
- "Edit_User": "Редактировать пользователя",
"edited": "отредактировано",
- "Editing": "Редактирование",
"Editing_room": "Редактирование комнаты",
"Editing_user": "Редактирование пользователя",
"Education": "Образование",
@@ -1496,7 +1484,6 @@
"except_pinned": "(за исключением тех, которые закреплены)",
"Execute_Synchronization_Now": "Выполнить синхронизацию сейчас",
"Exit_Full_Screen": "Выход из полноэкранного режима",
- "Export_Messages": "Экспорт сообщений",
"Export_My_Data": "Экспорт моих данных",
"expression": "Выражение",
"Extended": "Расширенное",
@@ -1600,7 +1587,6 @@
"FileUpload_Webdav_Proxy_Uploads": "Загрузка прокси",
"FileUpload_Webdav_Proxy_Uploads_Description": "Передача файлов прокси-сервера через ваш сервер вместо прямого доступа к URL-адресу актива",
"files": "файлы",
- "Files": "файлы",
"Files_only": "Удалять только прикрепленные файлы. Сообщения останутся",
"FileSize_KB": "__fileSize__ КБ",
"FileSize_MB": "__fileSize__ МБ",
@@ -1617,7 +1603,6 @@
"Food_and_Drink": "Еда и питьё",
"Footer": "Нижний колонтитул",
"Footer_Direct_Reply": "Подвал, когда прямой ответ включен",
- "For_example": "Например",
"For_more_details_please_check_our_docs": "Для получения дополнительной информации, пожалуйста, посмотрите нашу документацию",
"For_your_security_you_must_enter_your_current_password_to_continue": "Для вашей же безопасности, вы должны повторно ввести свой пароль, чтобы продолжить",
"force-delete-message": "Принудительное удаление сообщений",
@@ -1645,9 +1630,7 @@
"From_me": "От меня",
"From_Email": "Адрес электронной почты отправителя",
"From_email_warning": "Внимание: Поле От зависит от настроек вашего почтового сервера.",
- "Full_Name": "Полное имя",
"Full_Screen": "Полноэкранный",
- "Further": "Далее",
"Gaming": "Игорный бизнес",
"General": "Общие настройки",
"Get_link": "Получить ссылку",
@@ -1689,7 +1672,6 @@
"Group_mentions_disabled_x_members": "Групповые упоминания, для `@all` и `@here` были отключены для комнат с более чем __total__ участниками.",
"Group_mentions_only": "Только при упоминаниях в общих чатах",
"Hash": "Хэш",
- "Head_back": "Вернуться назад",
"Header": "Шапка",
"Header_and_Footer": "Шапка и подвал",
"Healthcare_and_Pharmaceutical": "Здравоохранение/Фармацевтика",
@@ -1731,7 +1713,6 @@
"if_they_are_from": "(если они из %s)",
"If_this_email_is_registered": "Если этот адрес электронной почты зарегистрирован, мы отправим на него инструкцию по сбросу пароля. Если вы не получили электронное сообщение, попробуйте снова позже.",
"If_you_are_sure_type_in_your_password": "Если вы уверены, введите пароль:",
- "Members": "Пользователи",
"Members_List": "Пользователи",
"If_you_are_sure_type_in_your_username": "Если вы уверены, введите ваш логин:",
"If_you_dont_have_one_send_an_email_to_omni_rocketchat_to_get_yours": "Если у вас нет одного, отправьте электронное сообщение на адрес [omni@rocket.chat] (mailto: omni@rocket.chat), чтобы получить свой.",
@@ -1792,7 +1773,6 @@
"Inclusive": "Включительно",
"Incoming_Livechats": "Входящие Livechat",
"Incoming_WebHook": "Входящий webhook",
- "Incorrect_phone_number_input": "Неккоректный ввод номера телефона",
"Industry": "Промышленность",
"Info": "Информация",
"initials_avatar": "Инициалы аватара",
@@ -1864,21 +1844,16 @@
"InternalHubot_Username_Description": "Должно быть действительным логином бота, зарегистрированным на сервере.",
"Invalid_confirm_pass": "Пароли не совпадают",
"Invalid_email": "Введен некорректный адрес электронной почты",
- "Invalid_login": "Логин не может быть пустым",
"Invalid_username": "Введенное имя пользователя недействительно.",
"Invalid_Import_File_Type": "Недействительный тип импортируемого файла.",
"Invalid_name": "Имя не может быть пустым",
"Invalid_notification_setting_s": "Неверная настройка уведомлений: %s",
"Invalid_or_expired_invite_token": "Недействительный или истекший срок действия жетона приглашения",
- "Invalid_organization": "Организация не может быть пустым",
"Invalid_pass": "Пароль не может быть пустым",
- "Invalid_position": "Должность не может быть пустым",
"Invalid_reason": "Причина присоединения не может быть пустой",
- "Invalid_Require_Name_For_Sign_Up": "Логин не может быть использован",
"Invalid_room_name": "%s недопустимое имя комнаты",
"Invalid_secret_URL_message": "Предоставленный URL-адрес недействителен.",
"Invalid_setting_s": "Неправильная настройка: %s",
- "Invalid_surname": "Фамилия не может быть пустым",
"Invalid_two_factor_code": "Неверный двухфакторный код",
"invisible": "невидимый",
"Invisible": "Невидимый",
@@ -1920,6 +1895,15 @@
"It_works": "Оно работает",
"italic": "Курсивный",
"italics": "курсив",
+ "Item_Add": "Добавить пункт",
+ "Item_Added_Successfully": "Пункт успешно добавлен",
+ "Item_Delete_Warning": "Удаление пункта не может быть отменено",
+ "Item_ExpireAt": "Срок",
+ "Item_Has_Been_Deleted": "Пункт успешно удален",
+ "Item_Info": "Информация о пункте",
+ "Item_Number": "Номер",
+ "Item_Name": "Наименование",
+ "Item_Responsible": "Ответственные",
"Items_per_page": "Элементов на странице",
"Mobex_sms_gateway_address": "Mobex SMS Gateway Адрес",
"Mobex_sms_gateway_address_desc": "IP или хост вашего сервиса Mobex с указанным портом. Например. `http: //192.168.1.1:1401` или` https: //www.example.com:1401`",
@@ -1948,7 +1932,6 @@
"Join_the_given_channel": "Присоединиться к этому каналу",
"Join_video_call": "Присоединиться к видеозвонку",
"Joined": "Участвую",
- "Joined_at": "Присоединился",
"Jump": "Перейти",
"Jump_to_first_unread": "Перейти к первому непрочитанному",
"Jump_to_message": "Перейти к сообщению",
@@ -2000,7 +1983,7 @@
"Launched_successfully": "Успешно запущен",
"Layout": "Внешний вид",
"Layout_Home_Body": "Текст на главной странице",
- "Layout_Home_Title": "Рабочий стол",
+ "Layout_Home_Title": "Заголовок на главной странице",
"Layout_Login_Terms": "Правила составления имения пользователя",
"Layout_Privacy_Policy": "Политика конфиденциальности",
"Layout_Sidenav_Footer": "Колонтитул навигационной панели",
@@ -2168,8 +2151,6 @@
"Loading_suggestion": "Загрузка предпочтений",
"Local_Domains": "Локальные домены",
"Local_Password": "Локальный пароль",
- "Local_Time": "Локальное время",
- "Local_Time_time": "Локальное время: __time__",
"Localization": "Язык",
"Log_Exceptions_to_Channel_Description": "Канал, который получит все захваченные исключения. Оставьте пустым, чтобы игнорировать исключения.",
"Log_Exceptions_to_Channel": "Логировать исключения на канале",
@@ -2422,7 +2403,6 @@
"N_new_messages": "%s новых сообщений",
"Name": "Имя",
"Name_cant_be_empty": "Имя не может быть пустым",
- "Name_login": "Логин",
"Name_of_agent": "Имя представителя",
"Name_optional": "Имя (опционально)",
"Name_Placeholder": "Пожалуйста, введите ваше имя...",
@@ -2452,19 +2432,14 @@
"New_visitor_navigation": "Новая навигация: __history__",
"Newer_than": "Новее, чем",
"Newer_than_may_not_exceed_Older_than": "«Новее, чем» не может превышать «Старше, чем»",
- "Nickname": "Никнейм",
"No_Limit": "Без Ограничений",
"No_available_agents_to_transfer": "Нет доступных сотрудников для передачи",
"No_channel_with_name_%s_was_found": "Канал с названием \"%s\" не найден!",
"No_channels_yet": "Вы пока не участвуете ни в одном канале.",
- "No_data_found": "Данные не найдены",
"No_direct_messages_yet": "Нет личных переписок.",
- "No_Discussions_found": "Обсуждения не найдены",
"No_emojis_found": "Не найдено Emoji",
"No_Encryption": "Без шифрования",
- "No_errands_yet": "Поручения не найдены",
"No_files_left_to_download": "Не осталось файлов для скачивания",
- "No_files_selected": "Файлы не выбраны",
"No_group_with_name_%s_was_found": "Закрытый канал с названием \"%s\" не найден!",
"No_groups_yet": "Вы не состоите ни в одной приватной группе.",
"No_integration_found": "Не найдена интеграция, соответствующая идентификатору",
@@ -2488,11 +2463,9 @@
"Normal": "Обычный",
"Not_authorized": "Не авторизован",
"Not_Available": "Не доступен",
- "Not_chosen": "Не выбрано",
"Not_found_or_not_allowed": "Не найден или владелец ограничил доступ",
"Not_following": "Не наблюдаю",
"Not_started": "Не начато",
- "Not_verified": "Не верифицирован",
"Nothing": "Ничего",
"Nothing_found": "Ничего не найдено",
"Not_Imported_Messages_Title": "Следующие сообщения не были успешно импортированы",
@@ -2562,7 +2535,6 @@
"or": "или",
"Or_talk_as_anonymous": "Или говорить как аноним",
"Order": "Сортировка",
- "Organization": "Организация",
"Organization_Email": "Электронная почта организации",
"Organization_Info": "Информация об организации",
"Organization_Name": "Название организации",
@@ -2646,7 +2618,6 @@
"Please_wait_while_your_profile_is_being_saved": "Пожалуйста, подождите, пока ваш профиль сохраняется...",
"Pool": "Pool",
"Port": "Порт",
- "Position": "Должность",
"post-readonly": "Сообщение только для чтения",
"post-readonly_description": "Разрешение на отправление сообщений на канале только для чтения",
"Post_as": "Отправить от имени",
@@ -2850,7 +2821,6 @@
"Role": "Роль",
"Role_Editing": "Редактировать роль",
"Role_removed": "Роль удалена",
- "Roles": "Роли пользователя",
"Room": "Комната",
"Room_announcement_changed_successfully": "Объявление комнаты успешно изменено",
"Room_archivation_state": "Статус",
@@ -2954,7 +2924,6 @@
"seconds": "секунды",
"Secret_token": "Секретный токен",
"Security": "Безопасность",
- "See_full_profile": "Посмотреть полный профиль",
"Select_a_department": "Выберите отдел",
"Select_a_user": "Выберите пользователя",
"Select_an_avatar": "Выберите аватар",
@@ -3041,8 +3010,7 @@
"Show_the_keyboard_shortcut_list": "Показывать список горячих клавиш",
"Showing_archived_results": "Показано %s архивных результатов
",
"Showing_online_users": "Показано: __total_showing__. Подключенных: __online__. Всего: __total__ пользователей",
- "Showing_results": "Показано результатов",
- "Showing_results %s - %s of %s": "Показано результатов %s - %s из %s",
+ "Showing_results": "Показано %s результатов
",
"Sidebar": "Боковая панель",
"Sidebar_list_mode": "Режим отображения списка каналов",
"Sign_in_to_start_talking": "Войдите, чтобы начать разговор",
@@ -3712,11 +3680,7 @@
"Wait_you": "Ожидаем вас на заседании:",
"Working_group": "Рабочая группа",
"Working_group_add": "Добавить участника",
- "Working_group_add_register": "Участник рабочей группы?",
- "Working_group_composition": "Справочник рабочих групп",
- "Working_group_composition_add": "Добавить рабочую группу",
- "Working_group_composition_added_successfully": "Рабочая группа успешно добавлена",
- "Working_group_composition_count": "Количество рабочих групп",
+ "Working_group_composition": "Состав рабочей группы",
"Working_group_composition_count_members": "Количество участников рабочей группы",
"Working_group_delete": "Удалить участника",
"Working_group_delete_warning": "Удаление участника рабочей группы нельзя отменить",
@@ -3725,7 +3689,6 @@
"Working_group_meeting": "Заседание рабочей группы",
"Working_group_meeting_add": "Добавить заседание",
"Working_group_meeting_count_members": "Количество участников",
- "Working_group_meeting_edit": "Редактировать заседание",
"Working_group_meeting_invited_users": "Участники рабочей группы",
"Working_group_meeting_list": "Список заседаний",
"Working_group_meeting_pinned_files": "Прикрепленные файлы",
diff --git a/server/importPackages.js b/server/importPackages.js
index 4224798288be..da79b8ea0234 100644
--- a/server/importPackages.js
+++ b/server/importPackages.js
@@ -119,3 +119,4 @@ import '../app/councils/server';
import '../app/working-group/server';
import '../app/working-group-meetings/server';
import '../app/manual-mail-sender/server';
+import '../app/protocols/server';