From df5fb615410f408f72556d6dc43cc9a24b90f0fa Mon Sep 17 00:00:00 2001 From: lightii Date: Mon, 27 Mar 2023 12:07:51 +0800 Subject: [PATCH 0001/1171] ggpull --- yarn.lock | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/yarn.lock b/yarn.lock index 994ad34138..19096d8ef3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12918,6 +12918,17 @@ http-proxy-middleware@^2.0.0, http-proxy-middleware@^2.0.1: is-plain-obj "^3.0.0" micromatch "^4.0.2" +http-proxy-middleware@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -12936,6 +12947,11 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +http@^0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/http/-/http-0.0.1-security.tgz#3aac09129d12dc2747bbce4157afde20ad1f7995" + integrity sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g== + https-proxy-agent@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz#35f7da6c48ce4ddbfa264891ac593ee5ff8671e6" From 337b9610872f88a5d28ff60a13f96a62741bd8ea Mon Sep 17 00:00:00 2001 From: Ariunzaya Date: Thu, 18 May 2023 21:26:19 +0800 Subject: [PATCH 0002/1171] new chat pop up --- .../plugin-chats-ui/src/components/Widget.tsx | 6 +- .../src/components/chats/ChatList.tsx | 298 ++++++++++-------- packages/plugin-chats-ui/src/styles.ts | 2 +- 3 files changed, 171 insertions(+), 135 deletions(-) diff --git a/packages/plugin-chats-ui/src/components/Widget.tsx b/packages/plugin-chats-ui/src/components/Widget.tsx index f001a7a37a..4e391ba1ae 100644 --- a/packages/plugin-chats-ui/src/components/Widget.tsx +++ b/packages/plugin-chats-ui/src/components/Widget.tsx @@ -3,7 +3,6 @@ import { Link } from 'react-router-dom'; // erxes import Icon from '@erxes/ui/src/components/Icon'; import { OverlayTrigger, Popover } from 'react-bootstrap'; -import { Tabs, TabTitle } from '@erxes/ui/src/components/tabs'; // local import ChatList from '../containers/chats/ChatList'; import WidgetChatWindow from '../containers/WidgetChatWindow'; @@ -42,7 +41,10 @@ const Widget = () => { const popoverChat = ( - handleActive(_chatId)} /> + handleActive(_chatId)} + /> See all diff --git a/packages/plugin-chats-ui/src/components/chats/ChatList.tsx b/packages/plugin-chats-ui/src/components/chats/ChatList.tsx index d5630b05c8..2725473144 100644 --- a/packages/plugin-chats-ui/src/components/chats/ChatList.tsx +++ b/packages/plugin-chats-ui/src/components/chats/ChatList.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; // erxes import FormControl from '@erxes/ui/src/components/form/Control'; import { IUser } from '@erxes/ui/src/auth/types'; @@ -15,141 +15,175 @@ type Props = { handleClickItem?: (chatId: string) => void; }; +type State = { + searchValue: string; + filteredChats: any; + pinnedChatIds: any; + activeChatIds: string[]; +}; + const LOCALSTORAGE_KEY = 'erxes_pinned_chats'; +const LOCALSTORAGE_KEY_ACTIVE = 'erxes_active_chats'; -const ChatList = (props: Props) => { - const { chats, currentUser, chatId, hasOptions, isWidget } = props; - const [searchValue, setSearchValue] = useState(''); - const [filteredChats, setFilteredChats] = useState([]); - const [pinnedChatIds, setPinnedChatIds] = useState( - JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY) || '[]') - ); - - const handlePin = (_chatId: string) => { - if (checkPinned(_chatId)) { - updatePinned(pinnedChatIds.filter(c => c !== _chatId)); - } else { - updatePinned([...pinnedChatIds, _chatId]); - } - }; - - const updatePinned = (_chats: any[]) => { - setPinnedChatIds(_chats); - - localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(_chats)); - }; - - const checkPinned = (_chatId: string) => { - return pinnedChatIds.indexOf(_chatId) !== -1; - }; - - const handleSearch = (event: any) => { - setSearchValue(event.target.value); - setFilteredChats( - chats.filter(item => { - let name = ''; - - if (item.type == 'direct') { - const users: any[] = item.participantUsers || []; - const user: any = - users.length > 1 - ? users.filter(u => u._id !== currentUser._id)[0] - : users[0]; - name = user.details.fullName || user.email; - } else { - name = item.name; - } - - return name.toLowerCase().includes(searchValue.toLowerCase()); - }) - ); - }; - - const renderPinnedChats = () => { - if (pinnedChatIds.length !== 0) { - return ( - <> - Pinned - - {chats.map( - c => - checkPinned(c._id) && ( - - ) - )} - - +class ChatList extends React.Component { + constructor(props) { + super(props); + + this.state = { + searchValue: '', + filteredChats: [], + pinnedChatIds: JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY) || '[]'), + activeChatIds: JSON.parse( + localStorage.getItem(LOCALSTORAGE_KEY_ACTIVE) || '[]' + ) + }; + } + + componentDidUpdate(prevProps) { + const { chats } = this.props; + + if ( + chats[0].lastMessage.createdAt !== + prevProps.chats[0].lastMessage.createdAt + ) { + localStorage.setItem( + LOCALSTORAGE_KEY_ACTIVE, + JSON.stringify([...this.state.activeChatIds, chats[0]._id]) ); } - }; - - const renderChats = () => ( - <> - Recent - - {chats.map( - c => - !checkPinned(c._id) && ( - - ) - )} - - - ); - - const renderFilteredChats = () => { - return filteredChats.map(c => ( - - )); - }; - - return ( - - - { + if (checkPinned(_chatId)) { + updatePinned(this.state.pinnedChatIds.filter(c => c !== _chatId)); + } else { + updatePinned([...this.state.pinnedChatIds, _chatId]); + } + }; + + const updatePinned = (_chats: any[]) => { + this.setState({ pinnedChatIds: _chats }); + + localStorage.setItem(LOCALSTORAGE_KEY, JSON.stringify(_chats)); + }; + + const checkPinned = (_chatId: string) => { + return this.state.pinnedChatIds.indexOf(_chatId) !== -1; + }; + + const handleSearch = (event: any) => { + this.setState({ searchValue: event.target.value }); + this.setState({ + filteredChats: chats.filter(item => { + let name = ''; + + if (item.type === 'direct') { + const users: any[] = item.participantUsers || []; + const user: any = + users.length > 1 + ? users.filter(u => u._id !== currentUser._id)[0] + : users[0]; + name = user.details.fullName || user.email; + } else { + name = item.name; + } + + return name + .toLowerCase() + .includes(this.state.searchValue.toLowerCase()); + }) + }); + }; + + const renderPinnedChats = () => { + if (this.state.pinnedChatIds.length !== 0) { + return ( + <> + Pinned + + {chats.map( + c => + checkPinned(c._id) && ( + + ) + )} + + + ); + } + }; + + const renderChats = () => ( + <> + Recent + + {chats.map( + c => + !checkPinned(c._id) && ( + + ) + )} + + + ); + + const renderFilteredChats = () => { + return this.state.filteredChats.map(c => ( + - - {searchValue.length === 0 ? ( - <> - {renderPinnedChats()} - {renderChats()} - - ) : ( - renderFilteredChats() - )} - - ); -}; + )); + }; + + return ( + + + + + {this.state.searchValue.length === 0 ? ( + <> + {renderPinnedChats()} + {renderChats()} + + ) : ( + renderFilteredChats() + )} + + ); + } +} export default ChatList; diff --git a/packages/plugin-chats-ui/src/styles.ts b/packages/plugin-chats-ui/src/styles.ts index 976d0e3297..3a542f1d58 100644 --- a/packages/plugin-chats-ui/src/styles.ts +++ b/packages/plugin-chats-ui/src/styles.ts @@ -169,7 +169,7 @@ export const WidgetChatWindowHeader = styled.div` justify-content: space-between; align-items: center; background-color: #f9f9f9; - padding: 0 ${dimensions.unitSpacing}px; + padding: ${dimensions.unitSpacing}px; border-bottom: 2px solid ${colors.borderPrimary}; i { From d7f8a9f2a3d90940d4da67c7d3bc26bc1f3f9365 Mon Sep 17 00:00:00 2001 From: Nandinbold Norovsambuu Date: Tue, 30 May 2023 17:33:24 +0800 Subject: [PATCH 0003/1171] Client portal improve notifications (#4438) Co-authored-by: soyombo --- .../main/components/notifications/List.tsx | 89 ++++++++++---- .../main/components/notifications/Row.tsx | 42 +++++-- .../main/containers/notifications/List.tsx | 49 ++++++-- client-portal/modules/styles/notifications.ts | 115 ++++++++++++++++++ client-portal/modules/types.ts | 9 +- .../src/afterMutations/cards.ts | 13 +- .../resolvers/clientPortalNotification.ts | 14 +++ .../src/graphql/resolvers/index.ts | 2 + .../mutations/clientPortalNotifications.ts | 13 +- .../schema/clientPortalNotifications.ts | 2 +- packages/plugin-clientportal-api/src/utils.ts | 18 ++- 11 files changed, 305 insertions(+), 61 deletions(-) create mode 100644 client-portal/modules/styles/notifications.ts create mode 100644 packages/plugin-clientportal-api/src/graphql/resolvers/clientPortalNotification.ts diff --git a/client-portal/modules/main/components/notifications/List.tsx b/client-portal/modules/main/components/notifications/List.tsx index 761beb6d68..4af3be35dd 100644 --- a/client-portal/modules/main/components/notifications/List.tsx +++ b/client-portal/modules/main/components/notifications/List.tsx @@ -1,14 +1,20 @@ -import { INotification, IUser } from "../../../types"; -import { NotificationHeader, NotificationList } from "../../../styles/main"; +import { INotification, IUser } from '../../../types'; +import { NotificationList } from '../../../styles/main'; -import Alert from "../../../utils/Alert"; -import EmptyState from "../../../common/form/EmptyState"; -import Modal from "../../../common/Modal"; -import NotificationDetail from "../../containers/notifications/Detail"; -import React from "react"; -import Row from "./Row"; -import Spinner from "../../../common/Spinner"; -import { Wrapper } from "../../../styles/tasks"; +import EmptyState from '../../../common/form/EmptyState'; +import Modal from '../../../common/Modal'; +import NotificationDetail from '../../containers/notifications/Detail'; +import React, { useState } from 'react'; +import Row from './Row'; +import Spinner from '../../../common/Spinner'; +import { + NotificationSeeAll, + MarkAllRead, + NotificationWrapper, + TabContainer, + TabCaption +} from '../../../styles/notifications'; +import { __ } from '../../../../utils'; type Props = { currentUser: IUser; @@ -16,17 +22,41 @@ type Props = { count: number; loading: boolean; refetch?: () => void; - onClickNotification: (notificationId: string) => void; + markAsRead: (notificationIds?: string[]) => void; + markAllAsRead: any; + showNotifications: (requireRead: boolean) => void; }; const List = (props: Props) => { - const { notifications, loading } = props; + const { + notifications, + loading, + count, + markAsRead, + markAllAsRead, + showNotifications + } = props; + const [currentTab, setCurrentTab] = useState('Recent'); const [showModal, setShowModal] = React.useState(false); const [selectedNotificationId, setSelectedNotificationId] = React.useState( - "" + '' ); + const onTabClick = currTab => { + setCurrentTab(currTab); + }; + + const recentOnClick = () => { + onTabClick('Recent'); + showNotifications(false); + }; + + const unreadOnClick = () => { + onTabClick('Unread'); + showNotifications(true); + }; + const renderContent = () => { if (loading) { return ; @@ -43,19 +73,26 @@ const List = (props: Props) => { } const onClick = (notificationId: string) => { - props.onClickNotification(notificationId); + markAsRead([notificationId]); setSelectedNotificationId(notificationId); setShowModal(true); }; return ( - <> + {notifications.map((notif, key) => ( ))} + + {__('See all')} + + + {__('Mark all as read')}{' '} + + ( { onClose={() => setShowModal(false)} isOpen={showModal} /> - + ); }; return ( <> - -
Notifications
- 0 New -
- {renderContent()} + + + {__('Recent')} + + + {__('Unread')} + + + {renderContent()} ); }; diff --git a/client-portal/modules/main/components/notifications/Row.tsx b/client-portal/modules/main/components/notifications/Row.tsx index c850014002..f8adbd7683 100644 --- a/client-portal/modules/main/components/notifications/Row.tsx +++ b/client-portal/modules/main/components/notifications/Row.tsx @@ -1,10 +1,11 @@ -import { CreatedDate, InfoSection } from "../../../styles/main"; +import { CreatedDate, InfoSection } from '../../../styles/main'; -import { INotification } from "../../../types"; -import { Label } from "../../../common/form/styles"; -import React from "react"; -import classNames from "classnames"; -import dayjs from "dayjs"; +import { INotification } from '../../../types'; +import React from 'react'; +import classNames from 'classnames'; +import dayjs from 'dayjs'; +import NameCard from '../../../common/nameCard/NameCard'; +import { AvatarSection, CreatedUser } from '../../../styles/notifications'; type Props = { notification: INotification; @@ -23,12 +24,37 @@ const Row = (props: Props) => { const classes = classNames({ unread: !notification.isRead }); + const renderCreatedUser = () => { + const { createdUser, content } = notification; + + let name = 'system'; + + if (createdUser) { + name = createdUser.details + ? createdUser.details.fullName || '' + : createdUser.username || createdUser.email; + } + + const getCardType = (content.split(' ')[0] || '').toLocaleLowerCase(); + + const createTitle = `has updated ${getCardType}`; + return ( + + {name} {createTitle} + + ); + }; + return (
  • + + + - + {renderCreatedUser()} + {notification.content || 'New notification'} - {dayjs(notification.createdAt).format("DD MMM YYYY, HH:mm")} + {dayjs(notification.createdAt).format('DD MMM YYYY, HH:mm')}
  • diff --git a/client-portal/modules/main/containers/notifications/List.tsx b/client-portal/modules/main/containers/notifications/List.tsx index 4995f8ac1b..4c97eb7168 100644 --- a/client-portal/modules/main/containers/notifications/List.tsx +++ b/client-portal/modules/main/containers/notifications/List.tsx @@ -5,9 +5,8 @@ import Notifications from '../../components/notifications/List'; import { IUser, NotificationsCountQueryResponse, - NotificationsQueryResponse, + NotificationsQueryResponse } from '../../../types'; -import { useRouter } from 'next/router'; type Props = { count: number; @@ -40,25 +39,44 @@ const notificationsQuery = gql` createdAt isRead title + content + createdUser { + username + details { + fullName + avatar + } + } } } `; const markAsReadMutation = gql` - mutation ClientPortalNotificationsMarkAsRead($ids: [String]) { - clientPortalNotificationsMarkAsRead(_ids: $ids) + mutation ClientPortalNotificationsMarkAsRead( + $ids: [String] + $markAll: Boolean + ) { + clientPortalNotificationsMarkAsRead(_ids: $ids, markAll: $markAll) } `; function NotificationsContainer(props: Props) { const [markAsReadMutaion] = useMutation(markAsReadMutation); - const onClickNotification = (notificationId: string) => { + const markAsRead = (ids: string[]) => { markAsReadMutaion({ variables: { - ids: [notificationId], - }, - }) + ids + } + }); + }; + + const markAllAsRead = () => { + markAsReadMutaion({ + variables: { + markAll: true + } + }); }; const notificationsResponse = useQuery( @@ -66,14 +84,17 @@ function NotificationsContainer(props: Props) { { skip: !props.currentUser, variables: { - requireRead: props.requireRead, page: 1, - perPage: 10, + perPage: 10 }, - fetchPolicy: 'network-only', + fetchPolicy: 'network-only' } ); + const showNotifications = (requireRead: boolean) => { + notificationsResponse.refetch({ requireRead }); + }; + const notifications = (notificationsResponse.data && notificationsResponse.data.clientPortalNotifications) || @@ -87,8 +108,10 @@ function NotificationsContainer(props: Props) { ...props, notifications, loading: notificationsResponse.loading, - onClickNotification, - refetch, + markAsRead, + showNotifications, + markAllAsRead, + refetch }; return ; diff --git a/client-portal/modules/styles/notifications.ts b/client-portal/modules/styles/notifications.ts new file mode 100644 index 0000000000..e9dc7bc907 --- /dev/null +++ b/client-portal/modules/styles/notifications.ts @@ -0,0 +1,115 @@ +import { colors, dimensions, typography } from '.'; +import styled from 'styled-components'; +import styledTS from 'styled-components-ts'; + +const NotificationWrapper = styled.div` + position: relative; + padding-bottom: 30px; + border-top: 1px solid ${colors.borderPrimary}; +`; + +const NotificationSeeAll = styled.div` + position: absolute; + bottom: 0; + width: 100%; + border-top: 1px solid ${colors.borderPrimary}; + height: 30px; + + font-size: 13px !important; + color: rgb(23, 133, 252) !important; + + a { + padding: 5px ${dimensions.coreSpacing}px; + display: block; + text-align: left; + } +`; + +const MarkAllRead = styled.a` + position: relative; + cursor: pointer; + + font-size: 13px !important; + color: rgb(23, 133, 252) !important; + + padding: 5px ${dimensions.coreSpacing}px; + float: right; +`; + +const CreatedUser = styledTS<{ isList?: boolean }>(styled.div)` + font-weight: 600; + max-width: ${props => props.isList && '80%'}; + + span { + padding-left: ${dimensions.unitSpacing - 5}px; + font-weight: normal; + } +`; + +const AvatarSection = styled.div` + margin-right: ${dimensions.unitSpacing + 5}px; + position: relative; +`; + +const TabContainer = styledTS<{ grayBorder?: boolean; full?: boolean }>( + styled.div +)` + border-bottom: 1px solid + ${props => (props.grayBorder ? colors.borderDarker : colors.borderPrimary)}; + margin-bottom: -1px; + position: relative; + z-index: 2; + display: flex; + justify-content: ${props => props.full && 'space-evenly'}; + flex-shrink: 0; + height: ${dimensions.headerSpacing}px; + + font-size: 0.675rem !important; + +`; + +const TabCaption = styled.span` + cursor: pointer; + display: flex; + color: ${colors.textSecondary}; + font-weight: ${typography.fontWeightRegular}; + padding: 15px ${dimensions.coreSpacing}px; + position: relative; + transition: all ease 0.3s; + line-height: 18px; + text-align: center; + align-items: center; + + &:hover { + color: ${colors.textPrimary}; + } + + i { + margin-right: 3px; + } + + &.active { + color: ${colors.textPrimary}; + font-weight: 500; + + &:before { + border-bottom: 3px solid ${colors.colorSecondary}; + content: ''; + width: 100%; + position: absolute; + z-index: 1; + left: 0; + bottom: -1px; + } + } +`; + +export { + NotificationSeeAll, + NotificationWrapper, + MarkAllRead, + CreatedUser, + AvatarSection, + TabCaption, + TabContainer +}; diff --git a/client-portal/modules/types.ts b/client-portal/modules/types.ts index 8a80bc354c..697f759337 100644 --- a/client-portal/modules/types.ts +++ b/client-portal/modules/types.ts @@ -136,7 +136,7 @@ export type Config = { taskPublicBoardId?: string; ticketLabel?: string; dealLabel?: string; - purchaseLabel?:string; + purchaseLabel?: string; taskLabel?: string; taskStageId?: string; ticketStageId?: string; @@ -144,7 +144,7 @@ export type Config = { purchaseStageId?: string; ticketPipelineId?: string; dealPipelineId?: string; - purchasePipelineId?:string; + purchasePipelineId?: string; taskPipelineId?: string; kbToggle?: boolean; @@ -152,7 +152,7 @@ export type Config = { ticketToggle?: boolean; taskToggle?: boolean; dealToggle?: boolean; - purchaseToggle?:boolean; + purchaseToggle?: boolean; styles?: { bodyColor?: string; @@ -230,7 +230,7 @@ export interface IUser { details?: IUserDetails; type: string; companyName: string; - + username?: string; notificationSettings?: INotifcationSettings; } @@ -311,6 +311,7 @@ export interface INotification { link: string; isRead: boolean; createdAt: Date; + createdUser: IUser; } export type Topic = { diff --git a/packages/plugin-clientportal-api/src/afterMutations/cards.ts b/packages/plugin-clientportal-api/src/afterMutations/cards.ts index ecbd70e491..856e913831 100644 --- a/packages/plugin-clientportal-api/src/afterMutations/cards.ts +++ b/packages/plugin-clientportal-api/src/afterMutations/cards.ts @@ -3,10 +3,11 @@ import { sendCardsMessage } from '../messageBroker'; import { sendNotification } from '../utils'; export const cardUpdateHandler = async (models: IModels, subdomain, params) => { - const { type } = params; + const { type, object } = params; const cardType = type.split(':')[1]; + const prevStageId = object.stageId; const card = params.updatedDocument; const oldCard = params.object; const destinationStageId = card.stageId || ''; @@ -40,9 +41,17 @@ export const cardUpdateHandler = async (models: IModels, subdomain, params) => { defaultValue: null }); + const prevStage = await sendCardsMessage({ + subdomain, + action: 'stages.findOne', + data: { _id: prevStageId }, + isRPC: true, + defaultValue: null + }); + content = `${cardType.charAt(0).toUpperCase() + cardType.slice(1)} ${ card.name - } has been moved to ${stage.name} stage`; + } has been moved from ${prevStage.name} to ${stage.name} stage`; if (newStatus !== oldStatus && newStatus === 'archived') { content = `Your ${cardType} named ${card.name} has been archived`; diff --git a/packages/plugin-clientportal-api/src/graphql/resolvers/clientPortalNotification.ts b/packages/plugin-clientportal-api/src/graphql/resolvers/clientPortalNotification.ts new file mode 100644 index 0000000000..c2a598d76b --- /dev/null +++ b/packages/plugin-clientportal-api/src/graphql/resolvers/clientPortalNotification.ts @@ -0,0 +1,14 @@ +import { IContext } from '../../connectionResolver'; +import { sendCoreMessage } from '../../messageBroker'; + +export default { + async createdUser(clientPortalNotification, {}, { subdomain }: IContext) { + return sendCoreMessage({ + subdomain, + action: 'users.findOne', + data: { _id: clientPortalNotification.createdUser }, + isRPC: true, + defaultValue: {} + }); + } +}; diff --git a/packages/plugin-clientportal-api/src/graphql/resolvers/index.ts b/packages/plugin-clientportal-api/src/graphql/resolvers/index.ts index 2df6f68ece..07438bb215 100644 --- a/packages/plugin-clientportal-api/src/graphql/resolvers/index.ts +++ b/packages/plugin-clientportal-api/src/graphql/resolvers/index.ts @@ -1,6 +1,7 @@ import customScalars from '@erxes/api-utils/src/customScalars'; import { ClientPortalUser } from './clientPortalUser'; +import ClientPortalNotification from './clientPortalNotification'; import ClientPortalComment from './comment'; import Mutation from './mutations'; import Query from './queries'; @@ -10,6 +11,7 @@ const resolvers: any = { Mutation, Query, + ClientPortalNotification, ClientPortalUser, ClientPortalComment }; diff --git a/packages/plugin-clientportal-api/src/graphql/resolvers/mutations/clientPortalNotifications.ts b/packages/plugin-clientportal-api/src/graphql/resolvers/mutations/clientPortalNotifications.ts index ad55b546a3..06d3f46a23 100644 --- a/packages/plugin-clientportal-api/src/graphql/resolvers/mutations/clientPortalNotifications.ts +++ b/packages/plugin-clientportal-api/src/graphql/resolvers/mutations/clientPortalNotifications.ts @@ -6,9 +6,18 @@ import { sendNotification } from '../../../utils'; const notificationMutations = { async clientPortalNotificationsMarkAsRead( _root, - { _ids }: { _ids: string[] }, + { _ids, markAll }: { _ids: string[]; markAll: boolean }, { models, cpUser }: IContext ) { + let cpNotifIds = _ids; + + // mark all notifs as read + if (markAll) { + cpNotifIds = ( + await models.ClientPortalNotifications.find({ isRead: false }) + ).map(notif => notif._id); + } + if (!cpUser) { throw new Error('You are not logged in'); } @@ -17,7 +26,7 @@ const notificationMutations = { clientPortalNotificationRead: { userId: cpUser._id } }); - await models.ClientPortalNotifications.markAsRead(_ids, cpUser._id); + await models.ClientPortalNotifications.markAsRead(cpNotifIds, cpUser._id); return 'marked'; }, diff --git a/packages/plugin-clientportal-api/src/graphql/schema/clientPortalNotifications.ts b/packages/plugin-clientportal-api/src/graphql/schema/clientPortalNotifications.ts index f85024aa9f..0a443117a3 100644 --- a/packages/plugin-clientportal-api/src/graphql/schema/clientPortalNotifications.ts +++ b/packages/plugin-clientportal-api/src/graphql/schema/clientPortalNotifications.ts @@ -60,7 +60,7 @@ export const queries = ` `; export const mutations = ` - clientPortalNotificationsMarkAsRead (_ids: [String]) : String + clientPortalNotificationsMarkAsRead (_ids: [String], markAll: Boolean) : String clientPortalNotificationsRemove(_ids: [String]) : JSON clientPortalUserUpdateNotificationSettings( diff --git a/packages/plugin-clientportal-api/src/utils.ts b/packages/plugin-clientportal-api/src/utils.ts index 004da22ff7..9aea7484bd 100644 --- a/packages/plugin-clientportal-api/src/utils.ts +++ b/packages/plugin-clientportal-api/src/utils.ts @@ -415,7 +415,7 @@ export const sendAfterMutation = async ( export const getCards = async ( type: 'ticket' | 'deal' | 'task' | 'purchase', context: IContext, - _args: any + args: any ) => { const { subdomain, models, cpUser } = context; if (!cpUser) { @@ -487,12 +487,10 @@ export const getCards = async ( const stageIds = stages.map(stage => stage._id); - //гоё засаарай АМЖИЛТ kiss - let oneStageId = ''; - if ('stageId' in _args) { - if (stageIds.includes(_args.stageId)) { - oneStageId = _args.stageId; + if (args.stageId) { + if (stageIds.includes(args.stageId)) { + oneStageId = args.stageId; } else { oneStageId = 'noneId'; } @@ -504,10 +502,10 @@ export const getCards = async ( data: { _id: { $in: cardIds }, stageId: oneStageId ? oneStageId : { $in: stageIds }, - ...(_args?.priority && { priority: _args?.priority || [] }), - ...(_args?.labelIds && { labelIds: _args?.labelIds || [] }), - ...(_args?.closeDateType && { closeDateType: _args?.closeDateType }), - ...(_args?.userIds && { assignedUserIds: _args?.userIds || [] }) + ...(args?.priority && { priority: args?.priority || [] }), + ...(args?.labelIds && { labelIds: args?.labelIds || [] }), + ...(args?.closeDateType && { closeDateType: args?.closeDateType }), + ...(args?.userIds && { assignedUserIds: args?.userIds || [] }) }, isRPC: true, defaultValue: [] From ab56b68c267b272972e119e3873d047bcfb6ad86 Mon Sep 17 00:00:00 2001 From: Tuguldur Ch Date: Wed, 31 May 2023 14:01:21 +0800 Subject: [PATCH 0004/1171] update(cards):get customFieldsData --- packages/plugin-cards-api/src/graphql/schema/task.ts | 1 + packages/plugin-cards-api/src/graphql/schema/ticket.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/plugin-cards-api/src/graphql/schema/task.ts b/packages/plugin-cards-api/src/graphql/schema/task.ts index e449255c13..39f878c518 100644 --- a/packages/plugin-cards-api/src/graphql/schema/task.ts +++ b/packages/plugin-cards-api/src/graphql/schema/task.ts @@ -9,6 +9,7 @@ import { export const types = ({ contacts, tags }) => ` type TaskListItem { + customFieldsData:JSON, ${commonListTypes} } diff --git a/packages/plugin-cards-api/src/graphql/schema/ticket.ts b/packages/plugin-cards-api/src/graphql/schema/ticket.ts index 0f38c5cb0d..bece8961db 100644 --- a/packages/plugin-cards-api/src/graphql/schema/ticket.ts +++ b/packages/plugin-cards-api/src/graphql/schema/ticket.ts @@ -9,6 +9,7 @@ import { export const types = ({ contacts, tags }) => ` type TicketListItem { + customFieldsData:JSON, ${commonListTypes} } From c1b395b86dec6a2fb76ce02d4e528bcac11f8f5c Mon Sep 17 00:00:00 2001 From: Tuguldur Ch Date: Wed, 31 May 2023 17:27:34 +0800 Subject: [PATCH 0005/1171] update(cards):get field type in customFieldsData --- .../customResolvers/commonListItem.ts | 34 ++++++++++++++++++- .../src/graphql/schema/task.ts | 2 +- .../src/graphql/schema/ticket.ts | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/plugin-cards-api/src/graphql/resolvers/customResolvers/commonListItem.ts b/packages/plugin-cards-api/src/graphql/resolvers/customResolvers/commonListItem.ts index 83f9d89ccd..35250b74df 100644 --- a/packages/plugin-cards-api/src/graphql/resolvers/customResolvers/commonListItem.ts +++ b/packages/plugin-cards-api/src/graphql/resolvers/customResolvers/commonListItem.ts @@ -1,5 +1,5 @@ import { IContext } from '../../../connectionResolver'; -import { sendCoreMessage } from '../../../messageBroker'; +import { sendCoreMessage, sendFormsMessage } from '../../../messageBroker'; import { IItemCommonFields } from '../../../models/definitions/boards'; export default { @@ -28,5 +28,37 @@ export default { isRPC: true, defaultValue: [] }); + }, + async customPropertiesData( + item: IItemCommonFields, + _args, + { user, subdomain } + ) { + const customFieldsData = (item?.customFieldsData as any[]) || []; + + const fieldIds = customFieldsData.map(customField => customField.field); + + if (!fieldIds?.length) { + return customFieldsData; + } + + const fields = await sendFormsMessage({ + subdomain, + action: 'fields.find', + data: { + query: { _id: { $in: fieldIds } } + }, + isRPC: true, + defaultValue: [] + }); + + for (const customFieldData of customFieldsData) { + const field = fields.find(field => field._id === customFieldData.field); + if (field) { + customFieldData.type = field.type; + } + } + + return customFieldsData; } }; diff --git a/packages/plugin-cards-api/src/graphql/schema/task.ts b/packages/plugin-cards-api/src/graphql/schema/task.ts index 39f878c518..3fccec36cc 100644 --- a/packages/plugin-cards-api/src/graphql/schema/task.ts +++ b/packages/plugin-cards-api/src/graphql/schema/task.ts @@ -9,7 +9,7 @@ import { export const types = ({ contacts, tags }) => ` type TaskListItem { - customFieldsData:JSON, + customPropertiesData:JSON, ${commonListTypes} } diff --git a/packages/plugin-cards-api/src/graphql/schema/ticket.ts b/packages/plugin-cards-api/src/graphql/schema/ticket.ts index bece8961db..91469b0ee3 100644 --- a/packages/plugin-cards-api/src/graphql/schema/ticket.ts +++ b/packages/plugin-cards-api/src/graphql/schema/ticket.ts @@ -9,7 +9,7 @@ import { export const types = ({ contacts, tags }) => ` type TicketListItem { - customFieldsData:JSON, + customPropertiesData:JSON, ${commonListTypes} } From e88674056baa6bdaf0ece6afcfff3fc523443be2 Mon Sep 17 00:00:00 2001 From: munkhsaikhan Date: Mon, 5 Jun 2023 11:27:32 +0800 Subject: [PATCH 0006/1171] approve number format input --- .../erxes-ui/src/components/form/Control.tsx | 18 +------- .../erxes-ui/src/components/form/Form.tsx | 7 +++- .../src/components/form/NumberInput.tsx | 42 +++++++++++++++++++ packages/erxes-ui/src/utils/core.tsx | 6 +++ 4 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 packages/erxes-ui/src/components/form/NumberInput.tsx diff --git a/packages/erxes-ui/src/components/form/Control.tsx b/packages/erxes-ui/src/components/form/Control.tsx index 0614b1f323..8a4c3ccb89 100644 --- a/packages/erxes-ui/src/components/form/Control.tsx +++ b/packages/erxes-ui/src/components/form/Control.tsx @@ -12,7 +12,7 @@ import { Column } from '@erxes/ui/src/styles/main'; import ProgressBar from '../ProgressBar'; import React from 'react'; import Textarea from './Textarea'; -import { numberFormatter, numberParser } from '../../utils/core'; +import NumberInput from './NumberInput'; type Props = { children?: React.ReactNode; @@ -211,23 +211,9 @@ class FormControl extends React.Component { } if (props.type === 'number' && props.useNumberFormat) { - const onChangeNumber = e => { - if (e.target.value === '') { - attributes.onChange(e); - } else if (/^[0-9.,]+$/.test(e.target.value)) { - e.target.value = numberParser(e.target.value, props.fixed); - attributes.onChange(e); - } - }; - return ( - + {errorMessage} ); diff --git a/packages/erxes-ui/src/components/form/Form.tsx b/packages/erxes-ui/src/components/form/Form.tsx index 371f194258..e8d9d40ca7 100644 --- a/packages/erxes-ui/src/components/form/Form.tsx +++ b/packages/erxes-ui/src/components/form/Form.tsx @@ -115,7 +115,12 @@ class Form extends React.Component { return {__('Invalid link')}; } - if (value && props.type === 'number' && !validator.isFloat(value)) { + if ( + value && + props.type === 'number' && + !validator.isFloat(value) && + !/^[0-9.,-]+$/.test(value) + ) { return ( {__('Invalid number format! Please enter a valid number')} diff --git a/packages/erxes-ui/src/components/form/NumberInput.tsx b/packages/erxes-ui/src/components/form/NumberInput.tsx new file mode 100644 index 0000000000..a23bf895d0 --- /dev/null +++ b/packages/erxes-ui/src/components/form/NumberInput.tsx @@ -0,0 +1,42 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Input } from './styles'; +import { numberFormatter, numberParser } from '../../utils/core'; + +let cursorPosition = 0; + +function NumberInput({ value, onChange, fixed, ...props }: any, ref: any) { + const currentRef = useRef(null); + const [numberValue, setNumberValue] = useState(value || props.defaultValue); + + function onChangeValue(e: any) { + if (e.target.value === '') { + onChange(e); + setNumberValue(e.target.value); + } else if (/^[0-9.,-]+$/.test(e.target.value)) { + cursorPosition = e.target.value.length - e.target.selectionStart; + e.target.value = numberParser(e.target.value, fixed); + setNumberValue(e.target.value); + onChange(e); + } + } + + useEffect(() => { + if (currentRef.current) { + let position = currentRef.current.value.length - cursorPosition; + currentRef.current.selectionStart = position; + currentRef.current.selectionEnd = position; + } + }, [numberValue]); + + return ( + + ); +} + +export default React.forwardRef(NumberInput); diff --git a/packages/erxes-ui/src/utils/core.tsx b/packages/erxes-ui/src/utils/core.tsx index a9e42b5164..f2a154baec 100644 --- a/packages/erxes-ui/src/utils/core.tsx +++ b/packages/erxes-ui/src/utils/core.tsx @@ -540,6 +540,12 @@ export function numberFormatter(value, fixed) { } export function numberParser(value, fixed) { + if (value === '-') return '-'; + if (RegExp('-', 'g').test(value)) { + value = value.replace(RegExp('-', 'g'), ''); + value = `-${value}`; + } + value = value!.replace(/(,*)/g, ''); if (value?.includes('.')) { From 3bfb4dfaa8615b13100f4a38730723078b645872 Mon Sep 17 00:00:00 2001 From: baterdeneTS <125258377+hitchikermn@users.noreply.github.com> Date: Mon, 5 Jun 2023 11:35:47 +0800 Subject: [PATCH 0007/1171] Loan exiration (#199) --- packages/plugin-loans-api/src/configs.ts | 8 +- packages/plugin-loans-api/src/exporter.ts | 75 ++++++++++ packages/plugin-loans-api/src/forms.ts | 62 ++++++++ .../src/graphql/resolvers/contract.ts | 14 ++ .../src/graphql/schema/contract.ts | 3 + packages/plugin-loans-api/src/imports.ts | 82 +++++++++++ .../plugin-loans-api/src/messageBroker.ts | 2 +- .../src/models/contractTypes.ts | 8 +- .../src/models/definitions/constants.ts | 8 ++ .../src/models/definitions/contracts.ts | 13 +- .../src/models/definitions/schedules.ts | 28 ++++ .../src/models/insuranceTypes.ts | 1 + .../src/models/utils/scheduleUtils.ts | 105 ++++++++++++-- .../src/models/utils/transactionUtils.ts | 91 +++++++++--- .../collaterals/components/CategoryList.tsx | 2 +- .../components/CollateralsList.tsx | 3 +- packages/plugin-loans-ui/src/configs.js | 2 +- packages/plugin-loans-ui/src/constants.ts | 9 +- .../components/ContractTypeDetailInfo.tsx | 4 +- .../components/ContractTypeDetails.tsx | 3 +- .../components/ContractTypeForm.tsx | 17 ++- .../components/ContractTypesList.tsx | 9 +- .../components/JournalsSettings.tsx | 4 +- .../containers/ContractTypeForm.tsx | 6 +- .../components/collaterals/CollateralItem.tsx | 3 +- .../collaterals/CollateralsManager.tsx | 8 +- .../components/common/BasicInfoSection.tsx | 8 +- .../components/common/DetailInfo.tsx | 8 +- .../contracts/components/detail/CloseForm.tsx | 22 ++- .../components/detail/CollateralsSection.tsx | 7 +- .../components/detail/ContractDetails.tsx | 2 +- .../components/detail/RightSidebar.tsx | 2 +- .../components/list/ContractForm.tsx | 99 ++++++++----- .../contracts/components/list/ContractRow.tsx | 2 + .../components/list/ContractsList.tsx | 9 +- .../contracts/components/list/RightMenu.tsx | 26 ++-- .../components/schedules/ScheduleRow.tsx | 3 +- .../components/schedules/ScheduleSection.tsx | 7 +- .../components/schedules/SchedulesList.tsx | 3 +- .../src/contracts/containers/ContractForm.tsx | 5 +- .../contracts/containers/detail/CloseForm.tsx | 7 +- .../src/contracts/graphql/mutations.ts | 2 + .../src/contracts/graphql/queries.ts | 1 + .../plugin-loans-ui/src/contracts/types.ts | 2 + .../components/InsuranceTypeForm.tsx | 15 +- .../components/InsuranceTypesList.tsx | 10 +- .../containers/InsuranceTypeForm.tsx | 6 +- .../src/invoices/components/InvoiceForm.tsx | 4 +- .../src/invoices/components/PerInvoice.tsx | 2 +- .../src/invoices/containers/InvoiceForm.tsx | 5 +- packages/plugin-loans-ui/src/locales/mn.json | 133 +++++++++++++++++- .../components/PeriodLockDetailInfo.tsx | 4 +- .../components/PeriodLockDetails.tsx | 3 +- .../periodLocks/components/PeriodLockForm.tsx | 11 +- .../components/PeriodLocksList.tsx | 8 +- .../periodLocks/containers/PeriodLockForm.tsx | 6 +- .../settings/components/HolidaySettings.tsx | 6 +- .../settings/components/PerHolidayBonus.tsx | 12 +- .../src/settings/components/PerUndueBonus.tsx | 14 +- .../src/settings/components/Sidebar.tsx | 8 +- .../src/settings/components/UndueSettings.tsx | 8 +- .../transactions/components/ChangeTrForm.tsx | 2 +- .../src/transactions/components/RightMenu.tsx | 8 +- .../components/TransactionForm.tsx | 34 ++--- .../components/TransactionList.tsx | 4 +- .../components/TransactionRow.tsx | 4 +- .../transactions/containers/ChangeTrForm.tsx | 5 +- .../containers/TransactionForm.tsx | 6 +- scripts/pluginsMap.js | 2 +- 69 files changed, 869 insertions(+), 236 deletions(-) create mode 100644 packages/plugin-loans-api/src/exporter.ts create mode 100644 packages/plugin-loans-api/src/forms.ts create mode 100644 packages/plugin-loans-api/src/imports.ts diff --git a/packages/plugin-loans-api/src/configs.ts b/packages/plugin-loans-api/src/configs.ts index c6a1ea60d6..188b446f90 100644 --- a/packages/plugin-loans-api/src/configs.ts +++ b/packages/plugin-loans-api/src/configs.ts @@ -3,6 +3,9 @@ import resolvers from './graphql/resolvers'; import { generateModels } from './connectionResolver'; import { initBroker } from './messageBroker'; import documents from './documents'; +import forms from './forms'; +import imports from './imports'; +import exporter from './exporter'; import * as permissions from './permissions'; import { checkContractScheduleAnd } from './cronjobs/contractCronJobs'; @@ -44,6 +47,9 @@ export default { cronjobs: { handleDailyJob: checkContractScheduleAnd }, - documents + documents, + forms, + imports, + exporter } }; diff --git a/packages/plugin-loans-api/src/exporter.ts b/packages/plugin-loans-api/src/exporter.ts new file mode 100644 index 0000000000..fd21e2ec38 --- /dev/null +++ b/packages/plugin-loans-api/src/exporter.ts @@ -0,0 +1,75 @@ +import { generateModels, IModels } from './connectionResolver'; +import { IMPORT_EXPORT_TYPES } from './imports'; +import * as moment from 'moment'; + +const prepareData = async ( + models: IModels, + _subdomain: string, + _query: any +): Promise => { + let data: any[] = []; + + const productsFilter: any = {}; + + data = await models.Contracts.find(productsFilter).lean(); + + return data; +}; + +export const fillValue = async ( + models: IModels, + subdomain: string, + column: string, + item: any +): Promise => { + let value = item[column]; + + switch (column) { + case 'createdAt': + value = moment(value).format('YYYY-MM-DD'); + break; + + default: + break; + } + + return value || '-'; +}; + +export default { + importExportTypes: IMPORT_EXPORT_TYPES, + + prepareExportData: async ({ subdomain, data }) => { + const models = await generateModels(subdomain); + const { columnsConfig } = data; + const docs = [] as any; + const headers = [] as any; + const excelHeader = [] as any; + + try { + const results = await prepareData(models, subdomain, data); + + for (const column of columnsConfig) { + headers.push(column); + } + + for (const item of results) { + const result = {}; + + for (const column of headers) { + const value = await fillValue(models, subdomain, column, item); + + result[column] = value || '-'; + } + + docs.push(result); + } + for (const header of headers) { + excelHeader.push(header); + } + } catch (e) { + return { error: e.message }; + } + return { docs, excelHeader }; + } +}; diff --git a/packages/plugin-loans-api/src/forms.ts b/packages/plugin-loans-api/src/forms.ts new file mode 100644 index 0000000000..0d1c9cfee3 --- /dev/null +++ b/packages/plugin-loans-api/src/forms.ts @@ -0,0 +1,62 @@ +import { generateFieldsFromSchema } from '@erxes/api-utils/src'; +import { generateModels } from './connectionResolver'; + +export default { + types: [{ description: 'Loan Contact', type: 'contract' }], + fields: async ({ subdomain, data }) => { + const models = await generateModels(subdomain); + + const schema = models.Contracts.schema as any; + + let fields: Array<{ + _id: number; + name: string; + group?: string; + label?: string; + type?: string; + validation?: string; + options?: string[]; + selectOptions?: Array<{ label: string; value: string }>; + }> = []; + + if (schema) { + fields = [...fields, ...(await generateFieldsFromSchema(schema, ''))]; + + for (const name of Object.keys(schema.paths)) { + const path = schema.paths[name]; + + if (path.schema) { + fields = [ + ...fields, + ...(await generateFieldsFromSchema(path.schema, `${name}.`)) + ]; + } + } + } + + return fields; + }, + + systemFields: ({ data: { groupId } }) => + [ + { field: 'number', label: 'Number' }, + { field: 'status', label: 'Status' }, + { field: 'classification', label: 'Classification' }, + { field: 'description', label: 'Description' }, + { field: 'marginAmount', label: 'MarginAmount' }, + { field: 'leaseAmount', label: 'LeaseAmount' }, + { field: 'tenor', label: 'Tenor' }, + { field: 'interestRate', label: 'Interest rate' }, + { field: 'unduePercent', label: 'Undue percent' }, + { field: 'repayment', label: 'Repayment' }, + { field: 'startDate', label: 'Start date' }, + { field: 'scheduleDays', label: 'Schedule days' } + ].map(e => ({ + text: e.label, + type: e.field, + groupId, + contentType: `loans:contract`, + canHide: false, + isDefinedByErxes: true + })) +}; diff --git a/packages/plugin-loans-api/src/graphql/resolvers/contract.ts b/packages/plugin-loans-api/src/graphql/resolvers/contract.ts index 4929b17a2c..519ef540a6 100644 --- a/packages/plugin-loans-api/src/graphql/resolvers/contract.ts +++ b/packages/plugin-loans-api/src/graphql/resolvers/contract.ts @@ -191,6 +191,20 @@ const Contracts = { })) > 0 ); }, + async expiredDays(contract: IContractDocument, {}, { models }: IContext) { + const today = getFullDate(new Date()); + const expiredSchedule = await models.Schedules.findOne({ + contractId: contract._id, + scheduleDidStatus: { $ne: SCHEDULE_STATUS.DONE }, + isDefault: true + }).sort({ payDate: 1 }); + + const paymentDate = getFullDate(expiredSchedule?.payDate as Date); + const days = Math.ceil( + (today.getTime() - paymentDate.getTime()) / (1000 * 3600 * 24) + ); + return days > 0 ? days : 0; + }, async loanBalanceAmount( contract: IContractDocument, {}, diff --git a/packages/plugin-loans-api/src/graphql/schema/contract.ts b/packages/plugin-loans-api/src/graphql/schema/contract.ts index 77a59d3b63..ceac11a239 100644 --- a/packages/plugin-loans-api/src/graphql/schema/contract.ts +++ b/packages/plugin-loans-api/src/graphql/schema/contract.ts @@ -38,6 +38,7 @@ export const types = () => ` feeAmount: Float tenor: Float unduePercent: Float + undueCalcType: String interestRate: Float repayment: String startDate: Date @@ -85,6 +86,7 @@ export const types = () => ` nextPayment:Float payedAmountSum:Float loanBalanceAmount:Float + expiredDays:Float } @@ -162,6 +164,7 @@ const commonFields = ` feeAmount: Float tenor: Float unduePercent: Float + undueCalcType: String interestRate: Float repayment: String startDate: Date diff --git a/packages/plugin-loans-api/src/imports.ts b/packages/plugin-loans-api/src/imports.ts new file mode 100644 index 0000000000..7d9aa5ec8f --- /dev/null +++ b/packages/plugin-loans-api/src/imports.ts @@ -0,0 +1,82 @@ +import { generateModels } from './connectionResolver'; +import { IContract } from './models/definitions/contracts'; + +export const IMPORT_EXPORT_TYPES = [ + { + text: 'Loan Contract', + contentType: 'contract', + icon: 'server-alt' + } +]; + +export default { + importExportTypes: IMPORT_EXPORT_TYPES, + + insertImportItems: async ({ + subdomain, + data + }: { + data: { docs: [IContract] }; + subdomain: string; + }) => { + const models = await generateModels(subdomain); + + const { docs } = data; + + let updated = 0; + const objects: any = []; + + try { + for (const doc of docs) { + if (doc.number) { + const contract = await models.Contracts.findOne({ code: doc.number }); + + if (contract) { + await models.Contracts.updateOne( + { _id: contract._id }, + { $set: { ...doc } } + ); + updated++; + } else { + const insertedProduct = await models.Contracts.create(doc); + + objects.push(insertedProduct); + } + } else { + const insertedProduct = await models.Contracts.create(doc); + + objects.push(insertedProduct); + } + } + + return { objects, updated }; + } catch (e) { + return { error: e.message }; + } + }, + + prepareImportDocs: async ({ subdomain, data }) => { + const { result, properties } = data; + + const bulkDoc: any = []; + + // Iterating field values + for (const fieldValue of result) { + let colIndex: number = 0; + + var res = {}; + for (const property of properties) { + const value = (fieldValue[colIndex] || '').toString(); + if (property.name === 'classification') + res[property.name] = value.toUpperCase(); + else res[property.name] = value; + + colIndex++; + } + + bulkDoc.push(res); + } + + return bulkDoc; + } +}; diff --git a/packages/plugin-loans-api/src/messageBroker.ts b/packages/plugin-loans-api/src/messageBroker.ts index 97cba14956..5a5c971b90 100644 --- a/packages/plugin-loans-api/src/messageBroker.ts +++ b/packages/plugin-loans-api/src/messageBroker.ts @@ -9,7 +9,7 @@ export const initBroker = async cl => { export const sendMessageBroker = async ( args: ISendMessageArgs, - name: 'core' | 'cards' | 'reactions' | 'contacts' | 'products' + name: 'core' | 'cards' | 'reactions' | 'contacts' | 'products' | 'forms' ): Promise => { return sendMessage({ client, diff --git a/packages/plugin-loans-api/src/models/contractTypes.ts b/packages/plugin-loans-api/src/models/contractTypes.ts index e9bfd05112..29265954c2 100644 --- a/packages/plugin-loans-api/src/models/contractTypes.ts +++ b/packages/plugin-loans-api/src/models/contractTypes.ts @@ -1,4 +1,5 @@ import { + IContractType, IContractTypeDocument, contractTypeSchema } from './definitions/contractTypes'; @@ -28,7 +29,12 @@ export const loadContractTypeClass = (models: IModels) => { /** * Create a insuranceType */ - public static async createContractType(doc) { + public static async createContractType(doc: IContractType) { + if (!doc.code) throw new Error('Code is required'); + if (!doc.number) throw new Error('Start Number is required'); + if (!doc.vacancy) throw new Error('Vacancy is required'); + if (!doc.name) throw new Error('Name is required'); + return models.ContractTypes.create(doc); } diff --git a/packages/plugin-loans-api/src/models/definitions/constants.ts b/packages/plugin-loans-api/src/models/definitions/constants.ts index 7226d8c5d0..74e5fd5274 100644 --- a/packages/plugin-loans-api/src/models/definitions/constants.ts +++ b/packages/plugin-loans-api/src/models/definitions/constants.ts @@ -11,6 +11,14 @@ export const CONTRACT_STATUS = { ALL: ['draft', 'normal', 'bad', 'closed'] }; +export const UNDUE_CALC_TYPE = { + FROMAMOUNT: 'fromAmount', + FROMINTEREST: 'fromInterest', + FROMTOTALPAYMENT: 'fromTotalPayment', + FROMENDAMOUNT: 'fromEndAmount', + ALL: ['fromAmount', 'fromInterest', 'fromTotalPayment', 'fromEndAmount'] +}; + export const CONTRACT_CLASSIFICATION = { NORMAL: 'NORMAL', EXPIRED: 'EXPIRED', diff --git a/packages/plugin-loans-api/src/models/definitions/contracts.ts b/packages/plugin-loans-api/src/models/definitions/contracts.ts index 890aabb863..270a974b1e 100644 --- a/packages/plugin-loans-api/src/models/definitions/contracts.ts +++ b/packages/plugin-loans-api/src/models/definitions/contracts.ts @@ -94,6 +94,7 @@ export interface IContract { isExpired?: boolean; repaymentDate?: Date; + undueCalcType?: string; dealId?: string; } @@ -213,7 +214,7 @@ export const contractSchema = schemaHooksWrapper( type: String, enum: REPAYMENT_TYPE.map(option => option.value), required: true, - label: 'Type', + label: 'Schedule Type', selectOptions: REPAYMENT_TYPE }), startDate: field({ type: Date, label: 'Rate Start Date' }), @@ -322,13 +323,17 @@ export const contractSchema = schemaHooksWrapper( isExpired: field({ type: Boolean, optional: true, - label: - 'when contract expired of payment date then this field will be true' + label: 'Is Expired' }), repaymentDate: field({ type: Date, optional: true, - label: 'contract payment date of schedule' + label: 'Repayment' + }), + undueCalcType: field({ + type: String, + optional: true, + label: 'Undue Calc Type' }), dealId: field({ type: String, diff --git a/packages/plugin-loans-api/src/models/definitions/schedules.ts b/packages/plugin-loans-api/src/models/definitions/schedules.ts index 0fe1779773..0e4a004571 100644 --- a/packages/plugin-loans-api/src/models/definitions/schedules.ts +++ b/packages/plugin-loans-api/src/models/definitions/schedules.ts @@ -34,6 +34,10 @@ export interface ISchedule { didTotal: number; surplus?: number; + scheduleDidPayment?: number; + scheduleDidInterest?: number; + scheduleDidStatus?: 'done' | 'less' | 'pending'; + transactionIds?: string[]; isDefault: boolean; } @@ -121,6 +125,30 @@ export const scheduleSchema = schemaHooksWrapper( optional: true }), surplus: field({ type: Number, min: 0, label: 'Surplus', optional: true }), + scheduleDidPayment: field({ + type: Number, + min: 0, + label: 'scheduleDidPayment', + optional: true + }), + scheduleDidInterest: field({ + type: Number, + min: 0, + label: 'scheduleDidInterest', + optional: true + }), + scheduleDidStatus: field({ + type: String, + enum: [ + SCHEDULE_STATUS.DONE, + SCHEDULE_STATUS.LESS, + SCHEDULE_STATUS.PENDING + ], + min: 0, + label: 'scheduleDidInterest', + default: SCHEDULE_STATUS.PENDING, + optional: true + }), transactionIds: field({ type: [String], diff --git a/packages/plugin-loans-api/src/models/insuranceTypes.ts b/packages/plugin-loans-api/src/models/insuranceTypes.ts index 3c81254b0d..69432bec16 100644 --- a/packages/plugin-loans-api/src/models/insuranceTypes.ts +++ b/packages/plugin-loans-api/src/models/insuranceTypes.ts @@ -37,6 +37,7 @@ export const loadInsuranceTypeClass = (models: IModels) => { * Create a insuranceType */ public static async createInsuranceType(doc: IInsuranceType) { + if (!doc.companyId) throw new Error('Company is required'); return models.InsuranceTypes.create(doc); } diff --git a/packages/plugin-loans-api/src/models/utils/scheduleUtils.ts b/packages/plugin-loans-api/src/models/utils/scheduleUtils.ts index cfcda19aea..66ff973d07 100644 --- a/packages/plugin-loans-api/src/models/utils/scheduleUtils.ts +++ b/packages/plugin-loans-api/src/models/utils/scheduleUtils.ts @@ -466,6 +466,7 @@ export const generatePendingSchedules = async ( ) => { let changeDoc = {}; + //this preMainSchedule is payment is less but if prev schedule is done and const preMainSchedule: any = !updatedSchedule.isDefault && (await models.Schedules.findOne({ @@ -506,6 +507,7 @@ export const generatePendingSchedules = async ( return; } + // if undue payed less than must pay undue then this section will be true if ( !!updatedSchedule.didUndue && !!updatedSchedule.undue && @@ -538,6 +540,7 @@ export const generatePendingSchedules = async ( return; } + //this diff is payment diff payed greater is less let diff = (updatedSchedule.didPayment || 0) + (updatedSchedule.didInterestEve || 0) + @@ -546,15 +549,83 @@ export const generatePendingSchedules = async ( (updatedSchedule.interestEve || 0) - (updatedSchedule.interestNonce || 0); - let preSchedule = updatedSchedule; - let schedule = pendingSchedules[0]; - let balance = updatedSchedule.balance; + let preSchedule = updatedSchedule; //current schedule + let schedule = pendingSchedules[0]; //feature schedule + let balance = updatedSchedule.balance; //current balance + + //must pay payment paid greater than or less than payed let paymentBalance = (updatedSchedule.payment || 0) - (updatedSchedule.didPayment || 0); - let interestEve = 0; - let interestNonce = 0; - let index = 0; + + let interestEve = 0; //this is for calculate interestEve for between schedules + let interestNonce = 0; //this is for calculate interestNonce for between schedules + let index = 0; //this index for while loop for correction future schedules let payment = 0; + + let updatePrevScheduleReactions: any = []; //this updatePrevScheduleReactions variable for transaction reaction + let updatePrevSchedulesBulk: any = []; + + //undoneSchedules this list is undone default schedules + const undoneSchedules = await models.Schedules.find({ + contractId: contract._id, + payDate: { $lt: tr.payDate }, + scheduleDidStatus: { $ne: SCHEDULE_STATUS.DONE }, + isDefault: true + }) + .sort({ payDate: 1 }) + .lean(); + + if (undoneSchedules.length > 0) { + undoneSchedules.map((schedule: IScheduleDocument) => { + let changeDoc = { + scheduleDidPayment: schedule.scheduleDidPayment || 0, + scheduleDidInterest: schedule.scheduleDidInterest || 0, + scheduleDidStatus: SCHEDULE_STATUS.PENDING + }; + + let sumPayment = + (updatedSchedule.didPayment || 0) + (changeDoc.scheduleDidPayment || 0); + + if (updatedSchedule.didPayment && sumPayment >= (schedule.payment || 0)) { + changeDoc.scheduleDidPayment = schedule.payment || 0; + changeDoc.scheduleDidStatus = SCHEDULE_STATUS.DONE; + } else { + changeDoc.scheduleDidPayment = sumPayment; + changeDoc.scheduleDidStatus = SCHEDULE_STATUS.LESS; + } + + let sumInterest = + (updatedSchedule.didInterestEve || 0) + + (updatedSchedule.didInterestNonce || 0) + + (changeDoc.scheduleDidInterest || 0); + + if ( + (updatedSchedule.didInterestEve || 0) + + (updatedSchedule.didInterestNonce || 0) > + 0 && + sumInterest >= + (schedule.didInterestEve || 0) + (schedule.didInterestNonce || 0) + ) { + changeDoc.scheduleDidInterest = + (schedule.interestEve || 0) + (schedule.interestNonce || 0); + } else { + changeDoc.scheduleDidInterest = sumInterest; + } + + updatePrevScheduleReactions.push({ + scheduleId: updatedSchedule._id, + preData: { ...getChanged({ ...schedule }, { ...changeDoc }) } + }); + + updatePrevSchedulesBulk.push({ + updateOne: { + filter: { _id: schedule._id }, + update: { $set: { ...changeDoc } } + } + }); + }); + } + if ( paymentBalance < 0 && (tr.payment || 0) > 0 && @@ -585,7 +656,7 @@ export const generatePendingSchedules = async ( (await models.Transactions.updateOne( { _id: tr._id }, { - $set: { reactions: trReaction } + $set: { reactions: [...trReaction, ...updatePrevScheduleReactions] } } )); } @@ -605,7 +676,9 @@ export const generatePendingSchedules = async ( interestRate: contract.interestRate, dayOfMonth: diffNonce }); + payment = schedule.payment || 0; + if (paymentBalance < 0) { payment = payment + paymentBalance; if (payment < 0) { @@ -634,9 +707,13 @@ export const generatePendingSchedules = async ( (await models.Transactions.updateOne( { _id: tr._id }, { - $set: { reactions: trReaction } + $set: { reactions: [...trReaction, ...updatePrevScheduleReactions] } } )); + + //updatePrevSchedulesBulk this update section must be update schedule Payment is done + updatePrevSchedulesBulk.length > 0 && + (await models.Schedules.bulkWrite(updatePrevSchedulesBulk)); return; } @@ -664,9 +741,10 @@ export const generatePendingSchedules = async ( (await models.Transactions.updateOne( { _id: tr._id }, { - $set: { reactions: trReaction } + $set: { reactions: [...trReaction, ...updatePrevScheduleReactions] } } )); + return; } @@ -693,9 +771,10 @@ export const generatePendingSchedules = async ( (await models.Transactions.updateOne( { _id: tr._id }, { - $set: { reactions: trReaction } + $set: { reactions: [...trReaction, ...updatePrevScheduleReactions] } } )); + return; } @@ -874,11 +953,15 @@ export const generatePendingSchedules = async ( }); } + //updatePrevSchedulesBulk this update section must be update schedule Payment is done + updatePrevSchedulesBulk.length > 0 && + (await models.Schedules.bulkWrite(updatePrevSchedulesBulk)); + tr._id && (await models.Transactions.updateOne( { _id: tr._id }, { - $set: { reactions: trReaction } + $set: { reactions: [...trReaction, ...updatePrevScheduleReactions] } } )); await models.Schedules.bulkWrite(bulkOps); diff --git a/packages/plugin-loans-api/src/models/utils/transactionUtils.ts b/packages/plugin-loans-api/src/models/utils/transactionUtils.ts index ea2ec6ebce..8f906bec8f 100644 --- a/packages/plugin-loans-api/src/models/utils/transactionUtils.ts +++ b/packages/plugin-loans-api/src/models/utils/transactionUtils.ts @@ -1,5 +1,10 @@ import { IModels } from '../../connectionResolver'; -import { INVOICE_STATUS, SCHEDULE_STATUS } from '../definitions/constants'; +import { + INVOICE_STATUS, + SCHEDULE_STATUS, + UNDUE_CALC_TYPE +} from '../definitions/constants'; +import { IContractDocument } from '../definitions/contracts'; import { IScheduleDocument } from '../definitions/schedules'; import { ICalcDivideParams, @@ -53,6 +58,54 @@ export const getAOESchedules = async ( return { preSchedule, nextSchedule }; }; + +/** + * @param preSchedule must pay default schedule + * @param contract + * @param unduePercent + * @param diff + * @returns calculatedUndue + */ +export const calcUndue = async ( + preSchedule: IScheduleDocument, + contract: IContractDocument, + unduePercent, + diff: number +): Promise => { + let result = 0; + + switch (contract.undueCalcType) { + case UNDUE_CALC_TYPE.FROMAMOUNT: + result = Math.round((preSchedule.payment || 0) * unduePercent * diff); + break; + + case UNDUE_CALC_TYPE.FROMINTEREST: + result = Math.round( + ((preSchedule.balance * contract.interestRate) / 100 / 365) * + unduePercent * + diff + ); + break; + + case UNDUE_CALC_TYPE.FROMENDAMOUNT: + result = Math.round(preSchedule.balance * unduePercent * diff); + break; + + case UNDUE_CALC_TYPE.FROMTOTALPAYMENT: + result = Math.round(preSchedule.total * unduePercent * diff); + break; + + default: + result = Math.round( + ((preSchedule.balance * contract.interestRate) / 100 / 365) * + unduePercent * + diff + ); + break; + } + return result; +}; + /** * this method generate loan payment data * @param models @@ -137,10 +190,11 @@ export const getCalcedAmounts = async ( contract ); - result.undue = Math.round( - ((preSchedule.balance * contract.interestRate) / 100 / 365) * - unduePercent * - getDiffDay(prePayDate, trDate) + result.undue = await calcUndue( + preSchedule, + contract, + unduePercent, + getDiffDay(prePayDate, trDate) ); const { diffEve, diffNonce } = getDatesDiffMonth(prePayDate, trDate); result.interestEve = calcInterest({ @@ -258,10 +312,11 @@ export const getCalcedAmounts = async ( contract ); - result.undue += Math.round( - ((preSchedule.balance * contract.interestRate) / 100 / 365) * - unduePercent * - getDiffDay(prePayDate, trDate) + result.undue += await calcUndue( + preSchedule, + contract, + unduePercent, + getDiffDay(prePayDate, trDate) ); } return result; @@ -301,10 +356,11 @@ export const getCalcedAmounts = async ( contract ); - result.undue += Math.round( - ((preSchedule.balance * contract.interestRate) / 100 / 365) * - unduePercent * - getDiffDay(prePayDate, trDate) + result.undue += await calcUndue( + preSchedule, + contract, + unduePercent, + getDiffDay(prePayDate, trDate) ); } @@ -321,10 +377,11 @@ export const getCalcedAmounts = async ( contract ); - result.undue = Math.round( - ((preSchedule.balance * contract.interestRate) / 100 / 365) * - unduePercent * - getDiffDay(nextPayDate, trDate) + result.undue = await calcUndue( + preSchedule, + contract, + unduePercent, + getDiffDay(prePayDate, trDate) ); const { diffEve, diffNonce } = getDatesDiffMonth(prePayDate, trDate); diff --git a/packages/plugin-loans-ui/src/collaterals/components/CategoryList.tsx b/packages/plugin-loans-ui/src/collaterals/components/CategoryList.tsx index dbfb663d72..2bb749a342 100644 --- a/packages/plugin-loans-ui/src/collaterals/components/CategoryList.tsx +++ b/packages/plugin-loans-ui/src/collaterals/components/CategoryList.tsx @@ -1,5 +1,4 @@ import { - __, DataWithLoader, Icon, router, @@ -10,6 +9,7 @@ import { import { IProductCategory } from '@erxes/ui-products/src/types'; import React from 'react'; import { Link } from 'react-router-dom'; +import { __ } from 'coreui/utils'; import { SidebarListItem } from '../styles'; diff --git a/packages/plugin-loans-ui/src/collaterals/components/CollateralsList.tsx b/packages/plugin-loans-ui/src/collaterals/components/CollateralsList.tsx index 8691f21a21..8de6024fdd 100755 --- a/packages/plugin-loans-ui/src/collaterals/components/CollateralsList.tsx +++ b/packages/plugin-loans-ui/src/collaterals/components/CollateralsList.tsx @@ -1,5 +1,4 @@ import { - __, BarItems, DataWithLoader, FormControl, @@ -21,6 +20,7 @@ import Sidebar from './Sidebar'; import { can } from '@erxes/ui/src/utils/core'; import withConsumer from '../../withConsumer'; import { IUser } from '@erxes/ui/src/auth/types'; +import { __ } from 'coreui/utils'; interface IProps extends IRouterProps { collaterals: ICollateral[]; @@ -169,7 +169,6 @@ class CollateralsList extends React.Component { header={ can(row.permission, currentUser) )} diff --git a/packages/plugin-loans-ui/src/configs.js b/packages/plugin-loans-ui/src/configs.js index a98786b791..ccb58792a7 100644 --- a/packages/plugin-loans-ui/src/configs.js +++ b/packages/plugin-loans-ui/src/configs.js @@ -13,7 +13,7 @@ module.exports = { }, menus: [ { - text: 'Contracts', + text: 'Loan Contract', url: '/erxes-plugin-loan/contract-list', icon: 'icon-medal', location: 'mainNavigation', diff --git a/packages/plugin-loans-ui/src/constants.ts b/packages/plugin-loans-ui/src/constants.ts index eec472cfea..469c5bd58a 100644 --- a/packages/plugin-loans-ui/src/constants.ts +++ b/packages/plugin-loans-ui/src/constants.ts @@ -1,21 +1,22 @@ +import { __ } from 'coreui/utils'; export const menuContracts = [ { - title: 'Contracts', + title: __('Contracts'), link: '/erxes-plugin-loan/contract-list', permission: 'showContracts' }, { - title: 'Collaterals', + title: __('Collaterals'), link: '/erxes-plugin-loan/collateral-list', permission: 'showCollaterals' }, { - title: 'Transactions', + title: __('Transactions'), link: '/erxes-plugin-loan/transaction-list', permission: 'showTransactions' }, { - title: 'PeriodLocks', + title: __('PeriodLocks'), link: '/erxes-plugin-loan/periodLock-list', permission: 'showPeriodLocks' } diff --git a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetailInfo.tsx b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetailInfo.tsx index ce403d97e3..7bb2a2c0df 100755 --- a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetailInfo.tsx +++ b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetailInfo.tsx @@ -1,5 +1,4 @@ import { - __, Alert, Button, confirm, @@ -12,6 +11,7 @@ import { SidebarCounter, SidebarList } from '@erxes/ui/src'; +import { __ } from 'coreui/utils'; import Dropdown from 'react-bootstrap/Dropdown'; import { Action, Name } from '../../contracts/styles'; import React from 'react'; @@ -80,7 +80,7 @@ class DetailInfo extends React.Component { {contractType.name} } size="lg" content={content} diff --git a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetails.tsx b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetails.tsx index d24a886cda..d1f7d94e66 100644 --- a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetails.tsx +++ b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeDetails.tsx @@ -1,4 +1,5 @@ -import { __, Wrapper } from '@erxes/ui/src'; +import { Wrapper } from '@erxes/ui/src'; +import { __ } from 'coreui/utils'; import React from 'react'; import { IContractTypeDetail } from '../types'; diff --git a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeForm.tsx b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeForm.tsx index dc2d7255e4..61b512aad7 100755 --- a/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeForm.tsx +++ b/packages/plugin-loans-ui/src/contractTypes/components/ContractTypeForm.tsx @@ -1,5 +1,4 @@ import { - __, Button, ControlLabel, Form, @@ -14,6 +13,7 @@ import { IProductCategory } from '@erxes/ui-products/src/types'; import Select from 'react-select-plus'; import { IButtonMutateProps, IFormProps } from '@erxes/ui/src/types'; import React from 'react'; +import { __ } from 'coreui/utils'; import { IContractType, IContractTypeDoc } from '../types'; @@ -67,7 +67,7 @@ class ContractTypeForm extends React.Component { renderFormGroup = (label, props) => { return ( - {label} + {__(label)} ); @@ -96,29 +96,34 @@ class ContractTypeForm extends React.Component { {this.renderFormGroup('Code', { ...formProps, name: 'code', + required: true, defaultValue: contractType.code || '' })} {this.renderFormGroup('Name', { ...formProps, name: 'name', + required: true, defaultValue: contractType.name || '' })} {this.renderFormGroup('Start Number', { ...formProps, name: 'number', + required: true, defaultValue: contractType.number || '' })} {this.renderFormGroup('After vacancy count', { ...formProps, name: 'vacancy', + required: true, type: 'number', - defaultValue: contractType.vacancy || 0, + defaultValue: contractType.vacancy || 1, max: 20 })} {this.renderFormGroup('Undue Percent', { ...formProps, name: 'unduePercent', - defaultValue: contractType.unduePercent || '' + defaultValue: contractType.unduePercent || '', + type: 'number' })} {__('Lease Type')}: @@ -140,7 +145,7 @@ class ContractTypeForm extends React.Component { - Allow Product Categories + {__('Allow Product Categories')} { /> - {this.renderFormGroup('debt', { + {this.renderFormGroup('Debt', { ...formProps, type: 'number', name: 'debt', @@ -529,7 +556,7 @@ class ContractForm extends React.Component { onChange: this.onChangeField, onClick: this.onFieldClick })} - {this.renderFormGroup('debt Tenor', { + {this.renderFormGroup('Debt Tenor', { ...formProps, type: 'number', name: 'debtTenor', @@ -540,7 +567,7 @@ class ContractForm extends React.Component { onClick: this.onFieldClick })} - {this.renderFormGroup('debt Limit', { + {this.renderFormGroup('Debt Limit', { ...formProps, type: 'number', name: 'debtLimit', @@ -553,9 +580,9 @@ class ContractForm extends React.Component { - Closed Contract + {__('Closed Contract')} { {__('Branches')} - Relation Expert + {__('Relation Expert')} { - Leasing Expert + {__('Leasing Expert')} { - Risk Expert + {__('Risk Expert')} { - Weekends + {__('Weekends')} + this.onChangeRangeFilter('startDate', date)} + name={this.state.dateRange.startDate} + onChange={date => + this.onChangeRangeFilter(this.state.dateRange.startDate, date) + } placeholder={'Start date'} dateFormat={'YYYY-MM-DD'} /> this.onChangeRangeFilter('endDate', date)} + onChange={date => + this.onChangeRangeFilter(this.state.dateRange.endDate, date) + } dateFormat={'YYYY-MM-DD'} /> diff --git a/packages/ui-cards/src/boards/constants.ts b/packages/ui-cards/src/boards/constants.ts index 751c8a843d..36ac73df0b 100644 --- a/packages/ui-cards/src/boards/constants.ts +++ b/packages/ui-cards/src/boards/constants.ts @@ -4,6 +4,13 @@ export const STORAGE_PIPELINE_KEY = 'erxesCurrentPipelineId'; export const PRIORITIES = ['Critical', 'High', 'Medium', 'Low']; +export const DATERANGES = [ + { name: 'Created date', value: 'createdAt' }, + { name: 'Stage changed date', value: 'stageChangedDate' }, + { name: 'Start date', value: 'startDate' }, + { name: 'Close date', value: 'closeDate' } +]; + export const TEXT_COLORS = [ '#fff', '#fefefe', diff --git a/packages/ui-cards/src/boards/containers/MainActionBar.tsx b/packages/ui-cards/src/boards/containers/MainActionBar.tsx index f7b26910ed..e8a03551d1 100644 --- a/packages/ui-cards/src/boards/containers/MainActionBar.tsx +++ b/packages/ui-cards/src/boards/containers/MainActionBar.tsx @@ -44,7 +44,15 @@ const FILTER_PARAMS = [ 'assignedToMe', 'closeDateType', 'startDate', - 'endDate' + 'endDate', + 'createdStartDate', + 'createdEndDate', + 'stateChangedStartDate', + 'stateChangedEndDate', + 'startDateStartDate', + 'startDateEndDate', + 'closeDateStartDate', + 'closeDateEndDate' ]; const generateQueryParams = ({ location }) => { diff --git a/packages/ui-cards/src/boards/types.ts b/packages/ui-cards/src/boards/types.ts index 992540db68..494e47ea56 100644 --- a/packages/ui-cards/src/boards/types.ts +++ b/packages/ui-cards/src/boards/types.ts @@ -409,6 +409,14 @@ export interface IFilterParams extends ISavedConformity { tagIds?: string[]; branchIds: string[]; departmentIds: string[]; + createdEndDate: Date; + createdStartDate: Date; + stateChangedStartDate: Date; + stateChangedEndDate: Date; + startDateStartDate: Date; + startDateEndDate: Date; + closeDateStartDate: Date; + closeDateEndDate: Date; } export interface INonFilterParams { diff --git a/packages/ui-cards/src/deals/graphql/queries.ts b/packages/ui-cards/src/deals/graphql/queries.ts index 02ad14e2ba..ac4e935f78 100644 --- a/packages/ui-cards/src/deals/graphql/queries.ts +++ b/packages/ui-cards/src/deals/graphql/queries.ts @@ -30,7 +30,15 @@ const commonParams = ` $noSkipArchive: Boolean $branchIds:[String] $departmentIds:[String] - ${conformityQueryFields} + ${conformityQueryFields}, + $createdStartDate: Date, + $createdEndDate: Date, + $stateChangedStartDate: Date + $stateChangedEndDate: Date + $startDateStartDate: Date + $startDateEndDate: Date + $closeDateStartDate: Date + $closeDateEndDate: Date `; const commonParamDefs = ` @@ -58,7 +66,15 @@ const commonParamDefs = ` noSkipArchive: $noSkipArchive branchIds: $branchIds, departmentIds: $departmentIds, - ${conformityQueryFieldDefs} + ${conformityQueryFieldDefs}, + createdStartDate: $createdStartDate, + createdEndDate: $createdEndDate, + stateChangedStartDate: $stateChangedStartDate + stateChangedEndDate: $stateChangedEndDate + startDateStartDate: $startDateStartDate + startDateEndDate: $startDateEndDate + closeDateStartDate: $closeDateStartDate + closeDateEndDate: $closeDateEndDate `; export const dealFields = ` diff --git a/packages/ui-cards/src/deals/options.ts b/packages/ui-cards/src/deals/options.ts index 1c0b933e74..dbbda48e9e 100644 --- a/packages/ui-cards/src/deals/options.ts +++ b/packages/ui-cards/src/deals/options.ts @@ -1,36 +1,36 @@ -import { toArray } from "../boards/utils"; -import DealEditForm from "./components/DealEditForm"; -import DealItem from "./components/DealItem"; -import { mutations, queries } from "./graphql"; +import { toArray } from '../boards/utils'; +import DealEditForm from './components/DealEditForm'; +import DealItem from './components/DealItem'; +import { mutations, queries } from './graphql'; const options = { EditForm: DealEditForm, Item: DealItem, - title: "Deal", - type: "deal", + title: 'Deal', + type: 'deal', queriesName: { - itemsQuery: "deals", - itemsTotalCountQuery: "dealsTotalCount", - detailQuery: "dealDetail", - archivedItemsQuery: "archivedDeals", - archivedItemsCountQuery: "archivedDealsCount", + itemsQuery: 'deals', + itemsTotalCountQuery: 'dealsTotalCount', + detailQuery: 'dealDetail', + archivedItemsQuery: 'archivedDeals', + archivedItemsCountQuery: 'archivedDealsCount' }, mutationsName: { - addMutation: "dealsAdd", - editMutation: "dealsEdit", - removeMutation: "dealsRemove", - changeMutation: "dealsChange", - watchMutation: "dealsWatch", - archiveMutation: "dealsArchive", - copyMutation: "dealsCopy", - updateTimeTrackMutation: "updateTimeTrack", + addMutation: 'dealsAdd', + editMutation: 'dealsEdit', + removeMutation: 'dealsRemove', + changeMutation: 'dealsChange', + watchMutation: 'dealsWatch', + archiveMutation: 'dealsArchive', + copyMutation: 'dealsCopy', + updateTimeTrackMutation: 'updateTimeTrack' }, queries: { itemsQuery: queries.deals, itemsTotalCountQuery: queries.dealsTotalCount, detailQuery: queries.dealDetail, archivedItemsQuery: queries.archivedDeals, - archivedItemsCountQuery: queries.archivedDealsCount, + archivedItemsCountQuery: queries.archivedDealsCount }, mutations: { addMutation: mutations.dealsAdd, @@ -40,18 +40,32 @@ const options = { watchMutation: mutations.dealsWatch, archiveMutation: mutations.dealsArchive, copyMutation: mutations.dealsCopy, - updateTimeTrackMutation: ``, + updateTimeTrackMutation: `` }, texts: { - addText: "Add a deal", - updateSuccessText: "You successfully updated a deal", - deleteSuccessText: "You successfully deleted a deal", - changeSuccessText: "You successfully changed a deal", - copySuccessText: "You successfully copied a deal", + addText: 'Add a deal', + updateSuccessText: 'You successfully updated a deal', + deleteSuccessText: 'You successfully deleted a deal', + changeSuccessText: 'You successfully changed a deal', + copySuccessText: 'You successfully copied a deal' }, isMove: true, getExtraParams: (queryParams: any) => { - const { priority, productIds, userIds, startDate, endDate } = queryParams; + const { + priority, + productIds, + userIds, + startDate, + endDate, + createdStartDate, + createdEndDate, + stateChangedStartDate, + stateChangedEndDate, + startDateStartDate, + startDateEndDate, + closeDateStartDate, + closeDateEndDate + } = queryParams; const extraParams: any = {}; if (priority) { @@ -70,12 +84,42 @@ const options = { extraParams.endDate = endDate; } + if (createdStartDate) { + extraParams.createdStartDate = createdStartDate; + } + + if (createdEndDate) { + extraParams.createdEndDate = createdEndDate; + } + + if (stateChangedStartDate) { + extraParams.stateChangedStartDate = stateChangedStartDate; + } + + if (stateChangedEndDate) { + extraParams.stateChangedEndDate = stateChangedEndDate; + } + if (startDateStartDate) { + extraParams.startDateStartDate = startDateStartDate; + } + + if (startDateEndDate) { + extraParams.startDateEndDate = startDateEndDate; + } + if (closeDateStartDate) { + extraParams.closeDateStartDate = closeDateStartDate; + } + + if (closeDateEndDate) { + extraParams.closeDateEndDate = closeDateEndDate; + } + if (productIds) { extraParams.productIds = toArray(productIds); } return extraParams; - }, + } }; export default options; diff --git a/packages/ui-cards/src/tasks/graphql/queries.ts b/packages/ui-cards/src/tasks/graphql/queries.ts index baa8c8531f..7d672bed47 100644 --- a/packages/ui-cards/src/tasks/graphql/queries.ts +++ b/packages/ui-cards/src/tasks/graphql/queries.ts @@ -25,6 +25,14 @@ const commonParams = ` $branchIds:[String] $departmentIds:[String] ${conformityQueryFields} + $createdStartDate: Date + $createdEndDate: Date + $stateChangedStartDate: Date + $stateChangedEndDate: Date + $startDateStartDate: Date + $startDateEndDate: Date + $closeDateStartDate: Date + $closeDateEndDate: Date `; const commonParamDefs = ` @@ -48,6 +56,14 @@ const commonParamDefs = ` branchIds: $branchIds, departmentIds: $departmentIds, ${conformityQueryFieldDefs} + createdStartDate: $createdStartDate + createdEndDate: $createdEndDate + stateChangedStartDate: $stateChangedStartDate + stateChangedEndDate: $stateChangedEndDate + startDateStartDate: $startDateStartDate + startDateEndDate: $startDateEndDate + closeDateStartDate: $closeDateStartDate + closeDateEndDate: $closeDateEndDate `; const tasks = ` diff --git a/packages/ui-cards/src/tasks/options.ts b/packages/ui-cards/src/tasks/options.ts index 9da0717559..eba075cde2 100644 --- a/packages/ui-cards/src/tasks/options.ts +++ b/packages/ui-cards/src/tasks/options.ts @@ -1,36 +1,36 @@ -import { toArray } from "../boards/utils"; -import TaskEditForm from "./components/TaskEditForm"; -import TaskItem from "./components/TaskItem"; -import { mutations, queries } from "./graphql"; +import { toArray } from '../boards/utils'; +import TaskEditForm from './components/TaskEditForm'; +import TaskItem from './components/TaskItem'; +import { mutations, queries } from './graphql'; const options = { EditForm: TaskEditForm, Item: TaskItem, - type: "task", - title: "Task", + type: 'task', + title: 'Task', queriesName: { - itemsQuery: "tasks", - itemsTotalCountQuery: "tasksTotalCount", - detailQuery: "taskDetail", - archivedItemsQuery: "archivedTasks", - archivedItemsCountQuery: "archivedTasksCount", + itemsQuery: 'tasks', + itemsTotalCountQuery: 'tasksTotalCount', + detailQuery: 'taskDetail', + archivedItemsQuery: 'archivedTasks', + archivedItemsCountQuery: 'archivedTasksCount' }, mutationsName: { - addMutation: "tasksAdd", - editMutation: "tasksEdit", - removeMutation: "tasksRemove", - changeMutation: "tasksChange", - watchMutation: "tasksWatch", - archiveMutation: "tasksArchive", - copyMutation: "tasksCopy", - updateTimeTrackMutation: "updateTimeTrack", + addMutation: 'tasksAdd', + editMutation: 'tasksEdit', + removeMutation: 'tasksRemove', + changeMutation: 'tasksChange', + watchMutation: 'tasksWatch', + archiveMutation: 'tasksArchive', + copyMutation: 'tasksCopy', + updateTimeTrackMutation: 'updateTimeTrack' }, queries: { itemsQuery: queries.tasks, itemsTotalCountQuery: queries.tasksTotalCount, detailQuery: queries.taskDetail, archivedItemsQuery: queries.archivedTasks, - archivedItemsCountQuery: queries.archivedTasksCount, + archivedItemsCountQuery: queries.archivedTasksCount }, mutations: { addMutation: mutations.tasksAdd, @@ -39,18 +39,31 @@ const options = { changeMutation: mutations.tasksChange, watchMutation: mutations.tasksWatch, archiveMutation: mutations.tasksArchive, - copyMutation: mutations.tasksCopy, + copyMutation: mutations.tasksCopy }, texts: { - addText: "Add a task", - updateSuccessText: "You successfully updated a task", - deleteSuccessText: "You successfully deleted a task", - copySuccessText: "You successfully copied a task", - changeSuccessText: "You successfully changed a task", + addText: 'Add a task', + updateSuccessText: 'You successfully updated a task', + deleteSuccessText: 'You successfully deleted a task', + copySuccessText: 'You successfully copied a task', + changeSuccessText: 'You successfully changed a task' }, isMove: true, getExtraParams: (queryParams: any) => { - const { priority, userIds, startDate, endDate } = queryParams; + const { + priority, + userIds, + startDate, + endDate, + createdStartDate, + createdEndDate, + stateChangedStartDate, + stateChangedEndDate, + startDateStartDate, + startDateEndDate, + closeDateStartDate, + closeDateEndDate + } = queryParams; const extraParams: any = {}; if (priority) { @@ -69,8 +82,38 @@ const options = { extraParams.endDate = endDate; } + if (createdStartDate) { + extraParams.createdStartDate = createdStartDate; + } + + if (createdEndDate) { + extraParams.createdEndDate = createdEndDate; + } + + if (stateChangedStartDate) { + extraParams.stateChangedStartDate = stateChangedStartDate; + } + + if (stateChangedEndDate) { + extraParams.stateChangedEndDate = stateChangedEndDate; + } + if (startDateStartDate) { + extraParams.startDateStartDate = startDateStartDate; + } + + if (startDateEndDate) { + extraParams.startDateEndDate = startDateEndDate; + } + if (closeDateStartDate) { + extraParams.closeDateStartDate = closeDateStartDate; + } + + if (closeDateEndDate) { + extraParams.closeDateEndDate = closeDateEndDate; + } + return extraParams; - }, + } }; export default options; diff --git a/packages/ui-cards/src/tickets/graphql/queries.ts b/packages/ui-cards/src/tickets/graphql/queries.ts index 054ad56ead..df8f4b384e 100644 --- a/packages/ui-cards/src/tickets/graphql/queries.ts +++ b/packages/ui-cards/src/tickets/graphql/queries.ts @@ -25,6 +25,14 @@ const commonParams = ` $branchIds:[String] $departmentIds:[String] ${conformityQueryFields} + $createdStartDate: Date + $createdEndDate: Date + $stateChangedStartDate: Date + $stateChangedEndDate: Date + $startDateStartDate: Date + $startDateEndDate: Date + $closeDateStartDate: Date + $closeDateEndDate: Date `; const commonParamDefs = ` @@ -48,6 +56,14 @@ const commonParamDefs = ` branchIds: $branchIds, departmentIds: $departmentIds, ${conformityQueryFieldDefs} + createdStartDate: $createdStartDate + createdEndDate: $createdEndDate + stateChangedStartDate: $stateChangedStartDate + stateChangedEndDate: $stateChangedEndDate + startDateStartDate: $startDateStartDate + startDateEndDate: $startDateEndDate + closeDateStartDate: $closeDateStartDate + closeDateEndDate: $closeDateEndDate `; export const ticketFields = ` diff --git a/packages/ui-cards/src/tickets/options.ts b/packages/ui-cards/src/tickets/options.ts index 8abb243024..adf7be0ccf 100644 --- a/packages/ui-cards/src/tickets/options.ts +++ b/packages/ui-cards/src/tickets/options.ts @@ -1,35 +1,35 @@ -import { toArray } from "../boards/utils"; -import TicketEditForm from "./components/TicketEditForm"; -import TicketItem from "./components/TicketItem"; -import { mutations, queries } from "./graphql"; +import { toArray } from '../boards/utils'; +import TicketEditForm from './components/TicketEditForm'; +import TicketItem from './components/TicketItem'; +import { mutations, queries } from './graphql'; const options = { EditForm: TicketEditForm, Item: TicketItem, - type: "ticket", - title: "Ticket", + type: 'ticket', + title: 'Ticket', queriesName: { - itemsQuery: "tickets", - itemsTotalCountQuery: "ticketsTotalCount", - detailQuery: "ticketDetail", - archivedItemsQuery: "archivedTickets", - archivedItemsCountQuery: "archivedTicketsCount", + itemsQuery: 'tickets', + itemsTotalCountQuery: 'ticketsTotalCount', + detailQuery: 'ticketDetail', + archivedItemsQuery: 'archivedTickets', + archivedItemsCountQuery: 'archivedTicketsCount' }, mutationsName: { - addMutation: "ticketsAdd", - editMutation: "ticketsEdit", - removeMutation: "ticketsRemove", - changeMutation: "ticketsChange", - watchMutation: "ticketsWatch", - archiveMutation: "ticketsArchive", - copyMutation: "ticketsCopy", + addMutation: 'ticketsAdd', + editMutation: 'ticketsEdit', + removeMutation: 'ticketsRemove', + changeMutation: 'ticketsChange', + watchMutation: 'ticketsWatch', + archiveMutation: 'ticketsArchive', + copyMutation: 'ticketsCopy' }, queries: { itemsQuery: queries.tickets, itemsTotalCountQuery: queries.ticketsTotalCount, detailQuery: queries.ticketDetail, archivedItemsQuery: queries.archivedTickets, - archivedItemsCountQuery: queries.archivedTicketsCount, + archivedItemsCountQuery: queries.archivedTicketsCount }, mutations: { addMutation: mutations.ticketsAdd, @@ -38,18 +38,32 @@ const options = { changeMutation: mutations.ticketsChange, watchMutation: mutations.ticketsWatch, archiveMutation: mutations.ticketsArchive, - copyMutation: mutations.ticketsCopy, + copyMutation: mutations.ticketsCopy }, texts: { - addText: "Add a ticket", - updateSuccessText: "You successfully updated a ticket", - deleteSuccessText: "You successfully deleted a ticket", - copySuccessText: "You successfully copied a ticket", - changeSuccessText: "You successfully changed a ticket", + addText: 'Add a ticket', + updateSuccessText: 'You successfully updated a ticket', + deleteSuccessText: 'You successfully deleted a ticket', + copySuccessText: 'You successfully copied a ticket', + changeSuccessText: 'You successfully changed a ticket' }, isMove: true, getExtraParams: (queryParams: any) => { - const { priority, source, userIds, startDate, endDate } = queryParams; + const { + priority, + source, + userIds, + startDate, + endDate, + createdStartDate, + createdEndDate, + stateChangedStartDate, + stateChangedEndDate, + startDateStartDate, + startDateEndDate, + closeDateStartDate, + closeDateEndDate + } = queryParams; const extraParams: any = {}; if (priority) { @@ -72,8 +86,38 @@ const options = { extraParams.endDate = endDate; } + if (createdStartDate) { + extraParams.createdStartDate = createdStartDate; + } + + if (createdEndDate) { + extraParams.createdEndDate = createdEndDate; + } + + if (stateChangedStartDate) { + extraParams.stateChangedStartDate = stateChangedStartDate; + } + + if (stateChangedEndDate) { + extraParams.stateChangedEndDate = stateChangedEndDate; + } + if (startDateStartDate) { + extraParams.startDateStartDate = startDateStartDate; + } + + if (startDateEndDate) { + extraParams.startDateEndDate = startDateEndDate; + } + if (closeDateStartDate) { + extraParams.closeDateStartDate = closeDateStartDate; + } + + if (closeDateEndDate) { + extraParams.closeDateEndDate = closeDateEndDate; + } + return extraParams; - }, + } }; export default options; From 531180cb3f3bdaccfbc0e5300f92ab3683c10445 Mon Sep 17 00:00:00 2001 From: Gerelsukh <48400228+Gerelsukh@users.noreply.github.com> Date: Tue, 6 Jun 2023 11:33:14 +0800 Subject: [PATCH 0017/1171] feat(segment) add dropdown on segments list --- .../src/components/SidebarFilter.tsx | 144 ++++++++++++++---- .../ui-segments/src/containers/Filter.tsx | 3 +- 2 files changed, 115 insertions(+), 32 deletions(-) diff --git a/packages/ui-segments/src/components/SidebarFilter.tsx b/packages/ui-segments/src/components/SidebarFilter.tsx index 7ff1fd13f9..302f00872c 100755 --- a/packages/ui-segments/src/components/SidebarFilter.tsx +++ b/packages/ui-segments/src/components/SidebarFilter.tsx @@ -13,6 +13,10 @@ import Icon from '@erxes/ui/src/components/Icon'; import { Link } from 'react-router-dom'; import React from 'react'; import { __ } from '@erxes/ui/src/utils'; +import { + ChildList, + ToggleIcon +} from '@erxes/ui/src/components/filterableList/styles'; type Props = { currentSegment?: string; @@ -22,9 +26,41 @@ type Props = { counts?: any; segments: ISegment[]; loading: boolean; + treeView?: boolean; +}; + +type State = { + key: string; + parentFieldIds: { [key: string]: boolean }; }; -class Segments extends React.Component { +class Segments extends React.Component { + constructor(props) { + super(props); + + this.state = { + key: '', + parentFieldIds: {} + }; + } + + groupByParent = (array: any[]) => { + const key = 'subOf'; + + return array.reduce((rv, x) => { + (rv[x[key]] = rv[x[key]] || []).push(x); + + return rv; + }, {}); + }; + + onToggle = (id: string, isOpen: boolean) => { + const parentFieldIds = this.state.parentFieldIds; + parentFieldIds[id] = !isOpen; + + this.setState({ parentFieldIds }); + }; + renderCancelBtn() { const { currentSegment, removeSegment } = this.props; @@ -75,40 +111,86 @@ class Segments extends React.Component { } renderData() { - const { counts, segments, currentSegment } = this.props; - const orderedSegments: ISegment[] = []; + const { counts, segments, currentSegment, treeView } = this.props; + const { key } = this.state; - segments.forEach(segment => { - if (!segment.subOf) { - orderedSegments.push(segment, ...segment.getSubSegments); + const renderFieldItem = (segment: any, isOpen?: boolean) => { + // filter items by key + if (key && segment.name.toLowerCase().indexOf(key) < 0) { + return false; } - }); - return ( - - {orderedSegments.map(segment => ( -
  • + - - {segment.subOf ? '\u00a0\u00a0' : null} - {' '} - {segment.name} - {counts[segment._id]} - -
  • - ))} -
    - ); + {segment.subOf ? '\u00a0\u00a0' : null} + {' '} + {segment.name} + {counts[segment._id]} + + + ); + }; + + const renderContent = () => { + if (!treeView) { + return segments.map(field => { + return renderFieldItem(field); + }); + } + + const subFields = segments.filter(f => f.subOf); + const parents = segments.filter(f => !f.subOf); + + const groupByParent = this.groupByParent(subFields); + + const renderTree = field => { + const childrens = groupByParent[field._id]; + + if (childrens) { + const isOpen = this.state.parentFieldIds[field._id]; + + return ( + + + + + + + {renderFieldItem(field, isOpen)} + {isOpen && + childrens.map(childField => { + return renderTree(childField); + })} + + + ); + } + + return renderFieldItem(field); + }; + + return parents.map(field => { + return renderTree(field); + }); + }; + + return {renderContent()}; } render() { diff --git a/packages/ui-segments/src/containers/Filter.tsx b/packages/ui-segments/src/containers/Filter.tsx index 8d20e7a861..f944ce2d28 100755 --- a/packages/ui-segments/src/containers/Filter.tsx +++ b/packages/ui-segments/src/containers/Filter.tsx @@ -40,7 +40,8 @@ const FilterContainer = (props: FinalProps) => { setSegment, removeSegment, segments: segmentsQuery.segments || [], - loading: segmentsQuery.loading + loading: segmentsQuery.loading, + treeView: true }; return ; From 72bf9e607cb9dad5152a3e8476a838a9ba05659d Mon Sep 17 00:00:00 2001 From: Anu-Ujin Bat-Ulzii Date: Tue, 6 Jun 2023 13:31:21 +0800 Subject: [PATCH 0018/1171] some improvement on cp detail --- .../modules/card/components/Detail.tsx | 109 +++++++++--------- client-portal/modules/styles/cards.ts | 17 ++- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/client-portal/modules/card/components/Detail.tsx b/client-portal/modules/card/components/Detail.tsx index e660b27b62..ea1ddc7f62 100644 --- a/client-portal/modules/card/components/Detail.tsx +++ b/client-portal/modules/card/components/Detail.tsx @@ -1,4 +1,5 @@ import { + Card, CommentContainer, CommentContent, CommentWrapper, @@ -183,68 +184,70 @@ export default class CardDetail extends React.Component< - Back + Back
    -

    {item.name}

    - - Labels -
    - {!labels || labels.length === 0 ? ( - No labels at the moment! + +

    {item.name}

    + + Labels +
    + {!labels || labels.length === 0 ? ( + No labels at the moment! + ) : ( + (labels || []).map((label) => ( + + )) + )} +
    +
    + + Description + {description ? ( + ) : ( - (labels || []).map((label) => ( - - )) + No description at the moment! )} -
    -
    - - Description - {description ? ( - - ) : ( - No description at the moment! - )} - + - - Attachments - No attachments at the moment! - + + Attachments + No attachments at the moment! + - Comments - -