From f91d58649dcff1e8a1fffd3688706af15c50e983 Mon Sep 17 00:00:00 2001 From: syns Date: Mon, 15 Jul 2024 01:32:01 +0700 Subject: [PATCH 01/32] [Feat] added log update and server in server-web (#2727) * style: refactor component interfaces * feat: added log server and log update * style: remove unused code --- .../src/locales/i18n/bg/translation.json | 28 ++- .../src/locales/i18n/en/translation.json | 19 +- apps/server-web/src/main/helpers/constant.ts | 25 ++- .../src/main/helpers/desktop-server.ts | 3 +- .../src/main/helpers/interfaces/i-constant.ts | 1 + .../src/main/helpers/interfaces/i-setting.ts | 0 .../src/main/helpers/interfaces/index.ts | 1 + .../main/helpers/services/libs/server-task.ts | 8 +- apps/server-web/src/main/main.ts | 178 +++++++++++++++--- apps/server-web/src/main/preload.ts | 8 +- apps/server-web/src/main/tray.ts | 16 +- apps/server-web/src/main/updater.ts | 6 +- apps/server-web/src/renderer/App.tsx | 2 + .../src/renderer/components/About.tsx | 7 +- .../src/renderer/components/General.tsx | 17 +- .../src/renderer/components/Popup.tsx | 10 +- .../src/renderer/components/Select.tsx | 17 +- .../src/renderer/components/Server.tsx | 16 +- .../src/renderer/components/SideBar.tsx | 40 ++-- .../src/renderer/components/Toast.tsx | 20 +- .../src/renderer/components/Updater.tsx | 150 ++++++++------- apps/server-web/src/renderer/libs/constant.ts | 20 ++ .../renderer/libs/interfaces/i-components.ts | 65 +++++++ .../src/renderer/libs/interfaces/i-setting.ts | 80 ++++++++ .../src/renderer/libs/interfaces/index.ts | 2 + apps/server-web/src/renderer/pages/Server.tsx | 101 ++++++++++ .../server-web/src/renderer/pages/Setting.tsx | 70 ++----- apps/web/app/constants.ts | 1 - 28 files changed, 649 insertions(+), 262 deletions(-) create mode 100644 apps/server-web/src/main/helpers/interfaces/i-constant.ts create mode 100644 apps/server-web/src/main/helpers/interfaces/i-setting.ts create mode 100644 apps/server-web/src/renderer/libs/interfaces/i-components.ts create mode 100644 apps/server-web/src/renderer/libs/interfaces/i-setting.ts create mode 100644 apps/server-web/src/renderer/libs/interfaces/index.ts create mode 100644 apps/server-web/src/renderer/pages/Server.tsx diff --git a/apps/server-web/src/locales/i18n/bg/translation.json b/apps/server-web/src/locales/i18n/bg/translation.json index 95b7f55a2..1ca895f37 100644 --- a/apps/server-web/src/locales/i18n/bg/translation.json +++ b/apps/server-web/src/locales/i18n/bg/translation.json @@ -10,7 +10,9 @@ "APP_QUIT": "Откажете се", "GENERAL": "Общ", "SERVER_STATUS_STOPPED": "Статус: Спряна", - "SERVER_STATUS_STARTED": "Статус: започна" + "SERVER_STATUS_STARTED": "Статус: започна", + "OPEN_WEB": "Отворете уеб в браузъра", + "SERVER_WINDOW": "Прозорец на сървъра" }, "FORM": { "FIELDS": { @@ -22,7 +24,15 @@ }, "BUTTON": { "SAVE_SETTING": "Запазване на настройката", - "OK": "Добре" + "OK": "Добре", + "YES": "да", + "NO": "Не", + "DOWNLOAD_NOW": "Свали сега", + "LATER": "По късно", + "CANCEL": "Отказ", + "CLOSE": "Близо", + "START": "Започнете", + "STOP": "Спри се" }, "LABELS": { "CHECKING": "Проверка", @@ -42,11 +52,21 @@ "AUTO_UPDATE_SUBTITLE": "Активирайте автоматичната проверка на актуализацията, за да стартирате заявка за проверка дали е налична нова версия и да уведомите", "AUTO_UPDATE_TOGLE": "Автоматична актуализация", "CHECK_UPDATE_TITLE": "Проверете и актуализирайте версията на приложението си", - "CHECK_UPDATE_SUBTITLE": "Налична е нова актуализация! Моля, щракнете върху бутона Изтегляне сега по-долу." + "CHECK_UPDATE_SUBTITLE": "Налична е нова актуализация! Моля, щракнете върху бутона Изтегляне сега по-долу.", + "LANGUAGES": "Езици" } }, "MESSAGE": { "SUCCESS": "Успех", - "ERROR": "Грешка" + "ERROR": "Грешка", + "WARNING": "Warning", + "INFO": "Информация", + "UPDATE_AVAILABLE": "Налична е нова актуализация! Моля, щракнете върху бутона Изтегляне сега по-долу.", + "EXIT_MESSAGE": "Мрежата на сървъра все още работи, сигурни ли сте, че ще излезете от приложението?", + "UPDATE_SUCCESS": "Актуализирайте успешно" + }, + "LANGUAGES": { + "en": "Английски", + "bg": "България" } } diff --git a/apps/server-web/src/locales/i18n/en/translation.json b/apps/server-web/src/locales/i18n/en/translation.json index 2e9d94f58..91ba9f7d5 100644 --- a/apps/server-web/src/locales/i18n/en/translation.json +++ b/apps/server-web/src/locales/i18n/en/translation.json @@ -10,7 +10,9 @@ "APP_QUIT": "Quit", "GENERAL": "General", "SERVER_STATUS_STOPPED": "Status: Stopped", - "SERVER_STATUS_STARTED": "Status: Started" + "SERVER_STATUS_STARTED": "Status: Started", + "OPEN_WEB": "Open Web In Browser", + "SERVER_WINDOW": "Server Window" }, "FORM": { "FIELDS": { @@ -27,7 +29,10 @@ "NO": "No", "DOWNLOAD_NOW": "Download Now", "LATER": "Later", - "CANCEL": "Cancel" + "CANCEL": "Cancel", + "CLOSE": "Close", + "START": "Start", + "STOP": "Stop" }, "LABELS": { "CHECKING": "Checking", @@ -47,7 +52,8 @@ "AUTO_UPDATE_SUBTITLE": "Enable automatice update check, in order to run a request to check if new version is available and notify", "AUTO_UPDATE_TOGLE": "Automatic Update", "CHECK_UPDATE_TITLE": "Check & Update your app version", - "CHECK_UPDATE_SUBTITLE": "New Update is available! Please click button Download Now below." + "CHECK_UPDATE_SUBTITLE": "New Update is available! Please click button Download Now below.", + "LANGUAGES": "Languages" } }, "MESSAGE": { @@ -56,6 +62,11 @@ "WARNING": "Warning", "INFO": "Info", "UPDATE_AVAILABLE": "New Update is available! Please click button Download Now below.", - "EXIT_MESSAGE": "Server web still running, Are you sure to exit the app ?" + "EXIT_MESSAGE": "Server web still running, Are you sure to exit the app ?", + "UPDATE_SUCCESS": "Update Successfully" + }, + "LANGUAGES": { + "en": "English", + "bg": "Bulgaria" } } diff --git a/apps/server-web/src/main/helpers/constant.ts b/apps/server-web/src/main/helpers/constant.ts index cab7ffd1e..2885630d1 100644 --- a/apps/server-web/src/main/helpers/constant.ts +++ b/apps/server-web/src/main/helpers/constant.ts @@ -1,3 +1,4 @@ +import { Channels } from './interfaces' export const EventLists = { webServerStarted: 'WEB_SERVER_STARTED', webServerStopped: 'WEB_SERVER_STOPPED', @@ -11,7 +12,9 @@ export const EventLists = { UPDATE_PROGRESS: 'UPDATE_PROGRESS', UPDATE_DOWNLOADED: 'UPDATE_DOWNLOADED', UPDATE_CANCELLED: 'UPDATE_CANCELLED', - CHANGE_LANGUAGE: 'CHANGE_LANGUAGE' + CHANGE_LANGUAGE: 'CHANGE_LANGUAGE', + OPEN_WEB: 'OPEN_WEB', + SERVER_WINDOW: 'SERVER_WINDOW' } export const SettingPageTypeMessage = { @@ -32,3 +35,23 @@ export const SettingPageTypeMessage = { updateSettingResponse: 'update-setting-response', updateCancel: 'update-cancel' } + +export const ServerPageTypeMessage = { + SERVER_STATUS: 'server-status', + SERVER_EXEC: 'server-exec' +} + +export const LOG_TYPES = { + UPDATE_LOG: 'UPDATE-LOG', + SERVER_LOG: 'SERVER-LOG' +} + +export const IPC_TYPES: { + SETTING_PAGE: Channels, + UPDATER_PAGE: Channels, + SERVER_PAGE: Channels +} = { + SETTING_PAGE: 'setting-page', + UPDATER_PAGE: 'updater-page', + SERVER_PAGE: 'server-page' +} diff --git a/apps/server-web/src/main/helpers/desktop-server.ts b/apps/server-web/src/main/helpers/desktop-server.ts index 0788f3816..06507071c 100644 --- a/apps/server-web/src/main/helpers/desktop-server.ts +++ b/apps/server-web/src/main/helpers/desktop-server.ts @@ -3,6 +3,7 @@ import { DesktopServerFactory } from './services/desktop-server-factory'; import EventEmitter from 'events'; import { Observer } from './services/utils'; import NotificationDesktop from '../windows/desktop-notifier'; +import { LOG_TYPES } from './constant'; // Define server states export enum ServerState { STOPPED = 'stopped', @@ -34,7 +35,7 @@ export class DesktopServer { mainWindow?: BrowserWindow, signal?: AbortSignal, ): Promise { - console.log('DesktopServer -> start'); + console.log(LOG_TYPES.SERVER_LOG, 'DesktopServer -> start'); try { if (this.state !== ServerState.STOPPED) { diff --git a/apps/server-web/src/main/helpers/interfaces/i-constant.ts b/apps/server-web/src/main/helpers/interfaces/i-constant.ts new file mode 100644 index 000000000..64cdc0211 --- /dev/null +++ b/apps/server-web/src/main/helpers/interfaces/i-constant.ts @@ -0,0 +1 @@ +export type Channels = 'setting-page' | 'ipc-renderer' | 'language-set' | 'updater-page' | 'server-page'; diff --git a/apps/server-web/src/main/helpers/interfaces/i-setting.ts b/apps/server-web/src/main/helpers/interfaces/i-setting.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/server-web/src/main/helpers/interfaces/index.ts b/apps/server-web/src/main/helpers/interfaces/index.ts index f7a793501..8f45b8de0 100644 --- a/apps/server-web/src/main/helpers/interfaces/index.ts +++ b/apps/server-web/src/main/helpers/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './i-server'; export * from './i-desktop-dialog'; +export * from './i-constant'; diff --git a/apps/server-web/src/main/helpers/services/libs/server-task.ts b/apps/server-web/src/main/helpers/services/libs/server-task.ts index 370cdb746..be4eae744 100644 --- a/apps/server-web/src/main/helpers/services/libs/server-task.ts +++ b/apps/server-web/src/main/helpers/services/libs/server-task.ts @@ -2,7 +2,7 @@ import { ChildProcessFactory, Observer } from '../utils'; import { BrowserWindow } from 'electron'; import { ServerConfig } from './server-config'; import EventEmitter from 'events'; -import { EventLists } from '../../constant'; +import { EventLists, LOG_TYPES } from '../../constant'; // import { Timeout } from '../../decorators'; export abstract class ServerTask { @@ -72,7 +72,7 @@ export abstract class ServerTask { const service = ChildProcessFactory.createProcess(this.processPath, this.args, signal); - console.log('Service created', service.pid); + console.log(LOG_TYPES.SERVER_LOG, 'Service created', service.pid); service.stdout?.on('data', (data: any) => { const msg = data.toString(); @@ -93,12 +93,12 @@ export abstract class ServerTask { }); service.stderr?.on('data', (data: any) => { - console.log('stderr:', data.toString()); + console.log(LOG_TYPES.SERVER_LOG, 'stderr:', data.toString()); this.loggerObserver.notify(data.toString()); }); service.on('disconnect', () => { - console.log('Webserver disconnected'); + console.log(LOG_TYPES.SERVER_LOG, 'Webserver disconnected'); if (this.eventEmmitter) { this.eventEmmitter.emit(EventLists.webServerStopped); } diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index c3b1cadfc..ebefb4b77 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -4,13 +4,18 @@ import { DesktopServer } from './helpers/desktop-server'; import { LocalStore } from './helpers/services/libs/desktop-store'; import { EventEmitter } from 'events'; import { defaultTrayMenuItem, _initTray, updateTrayMenu } from './tray'; -import { EventLists, SettingPageTypeMessage } from './helpers/constant'; +import { EventLists, SettingPageTypeMessage, ServerPageTypeMessage, LOG_TYPES, IPC_TYPES } from './helpers/constant'; import { resolveHtmlPath } from './util'; import Updater from './updater'; import { mainBindings } from 'i18next-electron-fs-backend'; import i18nextMainBackend from '../configs/i18n.mainconfig'; import fs from 'fs'; import { WebServer } from './helpers/interfaces'; +import Log from 'electron-log'; +console.log = Log.log; +Object.assign(console, Log.functions); + + const eventEmitter = new EventEmitter(); @@ -26,6 +31,48 @@ let isServerRun: boolean; let intervalUpdate: NodeJS.Timeout; let tray: Tray; let settingWindow: BrowserWindow | null = null; +let logWindow: BrowserWindow | null = null; + +Log.hooks.push((message:any, transport) => { + if (transport !== Log.transports.file) { + return message; + } + + // if (message[0]) { + // message[0] = `LOGS - ${message[0]}` + // } + + message.data = message.data.map((i: any) => { + if (typeof i === 'object') { + return JSON.stringify(i) + } + return i; + }) + + if (message.data[0] === LOG_TYPES.SERVER_LOG) { + if (logWindow) { + const msg = message.data.join(' '); + logWindow.webContents.send(IPC_TYPES.SERVER_PAGE, { + type: LOG_TYPES.SERVER_LOG, + msg + }); + } + } + + if (message.data[0] === LOG_TYPES.UPDATE_LOG) { + if (settingWindow) { + const msg = `${message.data.join(' ')}`; + settingWindow.webContents.send(IPC_TYPES.UPDATER_PAGE, { + type: LOG_TYPES.UPDATE_LOG, + msg + }) + } + } + + + return message; +}) + const updater = new Updater(eventEmitter, i18nextMainBackend); i18nextMainBackend.on('initialized', () => { const config = LocalStore.getStore('config'); @@ -92,12 +139,12 @@ const installExtensions = async () => { }; -const createWindow = async () => { +const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW') => { if (isDebug) { await installExtensions(); } - settingWindow = new BrowserWindow({ + const defaultOptionWindow = { show: false, width: 1024, height: 728, @@ -109,15 +156,31 @@ const createWindow = async () => { ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js'), }, - }); - const url = resolveHtmlPath('index.html', 'setting'); - settingWindow.loadURL(url); - - mainBindings(ipcMain, settingWindow, fs); - settingWindow.on('closed', () => { - settingWindow = null; - }); - + } + let url = ''; + switch (type) { + case 'SETTING_WINDOW': + settingWindow = new BrowserWindow(defaultOptionWindow); + url = resolveHtmlPath('index.html', 'setting'); + settingWindow.loadURL(url); + + mainBindings(ipcMain, settingWindow, fs); + settingWindow.on('closed', () => { + settingWindow = null; + }); + break; + case 'LOG_WINDOW': + logWindow = new BrowserWindow(defaultOptionWindow); + url = resolveHtmlPath('index.html', 'history-console') + logWindow.loadURL(url); + mainBindings(ipcMain, logWindow, fs); + logWindow.on('closed', () => { + logWindow = null; + }) + break; + default: + break; + } }; const runServer = async () => { @@ -182,7 +245,16 @@ const onInitApplication = () => { console.log(EventLists.webServerStarted) updateTrayMenu('SERVER_START', { enabled: false }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); updateTrayMenu('SERVER_STOP', { enabled: true }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + updateTrayMenu('OPEN_WEB', { enabled: true}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); updateTrayMenu('SERVER_STATUS', { label: 'MENU.SERVER_STATUS_STARTED' }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + if (logWindow) { + logWindow.webContents.send(IPC_TYPES.SERVER_PAGE, { + type: ServerPageTypeMessage.SERVER_STATUS, + data: { + isRun: true + } + }) + } isServerRun = true; }) @@ -190,13 +262,22 @@ const onInitApplication = () => { console.log(EventLists.webServerStopped); updateTrayMenu('SERVER_STOP', { enabled: false }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); updateTrayMenu('SERVER_START', { enabled: true }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + updateTrayMenu('OPEN_WEB', { enabled: false}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); updateTrayMenu('SERVER_STATUS', { label: 'MENU.SERVER_STATUS_STOPPED' }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + if (logWindow) { + logWindow.webContents.send(IPC_TYPES.SERVER_PAGE, { + type: ServerPageTypeMessage.SERVER_STATUS, + data: { + isRun: false + } + }) + } isServerRun = false; }) eventEmitter.on(EventLists.gotoSetting, async () => { if (!settingWindow) { - await createWindow() + await createWindow('SETTING_WINDOW'); } const serverSetting: WebServer = LocalStore.getStore('config'); console.log('setting data', serverSetting); @@ -210,32 +291,32 @@ const onInitApplication = () => { }) eventEmitter.on(EventLists.UPDATE_AVAILABLE, (data) => { - console.log('UPDATE_AVAILABLE', data); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_AVAILABLE', data); SendMessageToSettingWindow(SettingPageTypeMessage.updateAvailable, data); }) eventEmitter.on(EventLists.UPDATE_ERROR, (data) => { - console.log('UPDATE_ERROR', data); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_ERROR', data); SendMessageToSettingWindow(SettingPageTypeMessage.updateError, { message: JSON.stringify(data) }); }) eventEmitter.on(EventLists.UPDATE_NOT_AVAILABLE, (data) => { - console.log('UPDATE_NOT_AVAILABLE', data); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_NOT_AVAILABLE', data); SendMessageToSettingWindow(SettingPageTypeMessage.upToDate, data); }) eventEmitter.on(EventLists.UPDATE_PROGRESS, (data) => { - console.log('UPDATE_PROGRESS', data.percent); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_PROGRESS', data.percent); SendMessageToSettingWindow(SettingPageTypeMessage.downloadingUpdate, { percent: Math.floor(data.percent || 0) }); }) eventEmitter.on(EventLists.UPDATE_DOWNLOADED, (data) => { - console.log('UPDATE_DOWNLOADED', data); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_DOWNLOADED', data); SendMessageToSettingWindow(SettingPageTypeMessage.downloaded, data); }) eventEmitter.on(EventLists.UPDATE_CANCELLED, (data) => { - console.log('UPDATE_CANCELLED', data); + console.log(LOG_TYPES.UPDATE_LOG, 'UPDATE_CANCELLED', data); SendMessageToSettingWindow(SettingPageTypeMessage.updateCancel, data); }) @@ -250,7 +331,7 @@ const onInitApplication = () => { eventEmitter.on(EventLists.gotoAbout, async () => { if (!settingWindow) { - await createWindow(); + await createWindow('SETTING_WINDOW'); } const serverSetting = LocalStore.getStore('config'); settingWindow?.show(); @@ -262,6 +343,32 @@ const onInitApplication = () => { }, 100) }) }) + + eventEmitter.on(EventLists.SERVER_WINDOW, async () => { + if (!logWindow) { + await createWindow('LOG_WINDOW'); + } + const serverSetting = LocalStore.getStore('config'); + logWindow?.show(); + logWindow?.webContents.once('did-finish-load', () => { + setTimeout(() => { + logWindow?.webContents.send('languageSignal', serverSetting.general?.lang); + // SendMessageToSettingWindow(SettingPageTypeMessage.selectMenu, { key: 'about' }); + logWindow?.webContents.send(IPC_TYPES.SERVER_PAGE, { + type: ServerPageTypeMessage.SERVER_STATUS, + data: { + isRun: isServerRun + } + }) + }, 100) + }) + }) + + eventEmitter.on(EventLists.OPEN_WEB, () => { + const envConfig = getEnvApi(); + const url = `http://127.0.0.1:${envConfig?.PORT}` + shell.openExternal(url) + }) } (async () => { @@ -278,14 +385,14 @@ ipcMain.on('message', async (event, arg) => { event.reply('message', `${arg} World!`) }) -ipcMain.on('setting-page', (event, arg) => { +ipcMain.on(IPC_TYPES.SETTING_PAGE, (event, arg) => { console.log('main setting page', arg); switch (arg.type) { case SettingPageTypeMessage.saveSetting: LocalStore.updateConfigSetting({ server: arg.data }); - event.sender.send('setting-page', { type: SettingPageTypeMessage.mainResponse, data: true }); + event.sender.send(IPC_TYPES.SETTING_PAGE, { type: SettingPageTypeMessage.mainResponse, data: true }); break; case SettingPageTypeMessage.checkUpdate: updater.checkUpdate(); @@ -295,12 +402,19 @@ ipcMain.on('setting-page', (event, arg) => { break; case SettingPageTypeMessage.showVersion: const currentVersion = app.getVersion(); - event.sender.send('setting-page', { type: SettingPageTypeMessage.showVersion, data: currentVersion }) + event.sender.send(IPC_TYPES.SETTING_PAGE, { type: SettingPageTypeMessage.showVersion, data: currentVersion }) break; case SettingPageTypeMessage.langChange: event.sender.send('languageSignal', arg.data); eventEmitter.emit(EventLists.CHANGE_LANGUAGE, { code: arg.data }) break; + default: + break; + } +}) + +ipcMain.on(IPC_TYPES.UPDATER_PAGE, (event, arg) => { + switch (arg.type) { case SettingPageTypeMessage.updateSetting: LocalStore.updateConfigSetting({ general: { @@ -309,8 +423,24 @@ ipcMain.on('setting-page', (event, arg) => { } }) createIntervalAutoUpdate() - event.sender.send('setting-page', { type: SettingPageTypeMessage.updateSettingResponse, data: true }) + event.sender.send(IPC_TYPES.UPDATER_PAGE, { type: SettingPageTypeMessage.updateSettingResponse, data: true }) + break; + + default: break; + } +}) + +ipcMain.on(IPC_TYPES.SERVER_PAGE, (_, arg) => { + switch (arg.type) { + case ServerPageTypeMessage.SERVER_EXEC: + if (arg.data.isRun) { + eventEmitter.emit(EventLists.webServerStart) + } else { + eventEmitter.emit(EventLists.webServerStop) + } + break; + default: break; } diff --git a/apps/server-web/src/main/preload.ts b/apps/server-web/src/main/preload.ts index 7ab4c0903..a7989b0cb 100644 --- a/apps/server-web/src/main/preload.ts +++ b/apps/server-web/src/main/preload.ts @@ -1,8 +1,7 @@ // Disable no-unused-vars, broken for spread args /* eslint no-unused-vars: off */ -import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; - -export type Channels = 'setting-page' | 'ipc-renderer' | 'language-set'; +import { contextBridge, IpcRenderer, ipcRenderer, IpcRendererEvent } from 'electron'; +import { Channels } from './helpers/interfaces'; const electronHandler = { ipcRenderer: { @@ -24,6 +23,9 @@ const electronHandler = { once(channel: Channels, func: (...args: unknown[]) => void) { ipcRenderer.once(channel, (_event, ...args) => func(...args)); }, + removeEventListener(channel: Channels) { + ipcRenderer.removeAllListeners(channel) + } }, }; diff --git a/apps/server-web/src/main/tray.ts b/apps/server-web/src/main/tray.ts index 41fadb44d..11596d69c 100644 --- a/apps/server-web/src/main/tray.ts +++ b/apps/server-web/src/main/tray.ts @@ -1,5 +1,4 @@ import { app, NativeImage, nativeImage, Menu, Tray } from 'electron'; -import path from 'path'; import { EventEmitter } from 'events'; import { EventLists } from './helpers/constant'; import i18n from 'i18next'; @@ -14,6 +13,14 @@ export const _initTray = (contextMenu:any, icon:string): Tray => { export const defaultTrayMenuItem = (eventEmitter: EventEmitter) => { const contextMenu = [ + { + id: 'OPEN_WEB', + label: 'MENU.OPEN_WEB', + // enabled: false, + click () { + eventEmitter.emit(EventLists.OPEN_WEB) + } + }, { id: 'SERVER_STATUS', label: 'MENU.SERVER_STATUS_STOPPED', @@ -33,6 +40,13 @@ export const defaultTrayMenuItem = (eventEmitter: EventEmitter) => { eventEmitter.emit(EventLists.webServerStop); } }, + { + id: 'SERVER_WINDOW', + label: 'MENU.SERVER_WINDOW', + click () { + eventEmitter.emit(EventLists.SERVER_WINDOW); + } + }, { id: 'APP_SETTING', label: 'MENU.APP_SETTING', diff --git a/apps/server-web/src/main/updater.ts b/apps/server-web/src/main/updater.ts index 77539d78b..37e437b8f 100644 --- a/apps/server-web/src/main/updater.ts +++ b/apps/server-web/src/main/updater.ts @@ -1,5 +1,4 @@ import { autoUpdater } from 'electron-updater'; -import log from 'electron-log'; import EventEmitter from 'events'; import { EventLists } from './helpers/constant'; import { InfoMessagesBox } from './windows/dialog'; @@ -7,8 +6,7 @@ import i18n from 'i18next'; class AppUpdater { constructor(eventEmitter: EventEmitter, i18nextMainBackend: typeof i18n) { - log.transports.file.level = 'info'; - autoUpdater.logger = log; + autoUpdater.logger = console; autoUpdater.on('update-available', (info) => { eventEmitter.emit(EventLists.UPDATE_AVAILABLE, info); if (!autoUpdater.autoDownload) { @@ -48,6 +46,8 @@ class AppUpdater { autoUpdater.on('update-cancelled', (info) => { eventEmitter.emit(EventLists.UPDATE_CANCELLED, info); }) + + // autoUpdater.on('') // autoUpdater.checkForUpdatesAndNotify(); } diff --git a/apps/server-web/src/renderer/App.tsx b/apps/server-web/src/renderer/App.tsx index 40c981536..1718ea52c 100644 --- a/apps/server-web/src/renderer/App.tsx +++ b/apps/server-web/src/renderer/App.tsx @@ -3,6 +3,7 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import './App.css'; import { Setting } from './pages/Setting'; import i18next from 'i18next'; +import { ServerPage } from './pages/Server'; export default function App() { const [language, setLanguage] = useState('en'); @@ -16,6 +17,7 @@ export default function App() { } /> + } /> ); diff --git a/apps/server-web/src/renderer/components/About.tsx b/apps/server-web/src/renderer/components/About.tsx index be7030117..d95bb3f44 100644 --- a/apps/server-web/src/renderer/components/About.tsx +++ b/apps/server-web/src/renderer/components/About.tsx @@ -1,8 +1,7 @@ import { EverTeamsLogo } from './svgs'; -type Props = { - version: string; -}; -export const AboutComponent = (props: Props) => { +import { IAbout } from '../libs/interfaces'; + +export const AboutComponent = (props: IAbout) => { return (
diff --git a/apps/server-web/src/renderer/components/General.tsx b/apps/server-web/src/renderer/components/General.tsx index dc03bfe3b..a237ddc6e 100644 --- a/apps/server-web/src/renderer/components/General.tsx +++ b/apps/server-web/src/renderer/components/General.tsx @@ -1,15 +1,10 @@ import { Link } from 'react-router-dom'; -interface Languages { - code: string; - label: string; -} -type Props = { - langs: Languages[]; - onChange: (lang: any) => void; - lang: string; -}; -export function GeneralComponent(props: Props) { - const language = props.langs.find((lg) => lg.code === props.lang) || { +import { ILanguages, IGeneralSetting } from '../libs/interfaces'; + +export function GeneralComponent(props: IGeneralSetting) { + const language: ILanguages = props.langs.find( + (lg) => lg.code === props.lang, + ) || { code: 'en', label: 'English', }; diff --git a/apps/server-web/src/renderer/components/Popup.tsx b/apps/server-web/src/renderer/components/Popup.tsx index 0aead7aa0..f1d51c782 100644 --- a/apps/server-web/src/renderer/components/Popup.tsx +++ b/apps/server-web/src/renderer/components/Popup.tsx @@ -1,11 +1,7 @@ import { useTranslation } from 'react-i18next'; -type Props = { - isShowPopup: boolean; - modalAction: () => void; - type: 'success' | 'error' | 'none'; - message: string; -}; -export function Popup(props: Props) { +import { IPopupComponent } from '../libs/interfaces'; + +export function Popup(props: IPopupComponent) { const { t } = useTranslation(); return (
void; -}; +import { ISelectComponent } from '../libs/interfaces'; const SelectItem = forwardRef( ({ children, className, ...props }: any, forwardedRef) => { @@ -43,7 +32,7 @@ export const SelectComponent = ({ value, disabled, onValueChange, -}: Props) => { +}: ISelectComponent) => { const { t } = useTranslation(); return ( {items.map((item) => ( - {t(`FORM.LABELS.UPDATE_OPTIONS.${item.label}`)} + {t(`${item.label}`)} ))} diff --git a/apps/server-web/src/renderer/components/Server.tsx b/apps/server-web/src/renderer/components/Server.tsx index f72589144..1ec1d0b8e 100644 --- a/apps/server-web/src/renderer/components/Server.tsx +++ b/apps/server-web/src/renderer/components/Server.tsx @@ -1,24 +1,12 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { IServerSetting, IServerComponent } from '../libs/interfaces'; -interface IServerSetting { - PORT: number; - GAUZY_API_SERVER_URL: string; - NEXT_PUBLIC_GAUZY_API_SERVER_URL: string; -} - -type Props = { - serverSetting: IServerSetting; - saveSetting: (data: IServerSetting) => void; - Popup: JSX.Element; -}; - -export const ServerComponent = (props: Props) => { +export const ServerComponent = (props: IServerComponent) => { const { t } = useTranslation(); const [serverSetting, setServerSetting] = useState( props.serverSetting, ); - // useEffect(() => {}, []); const saveSetting = (e: any) => { e.preventDefault(); diff --git a/apps/server-web/src/renderer/components/SideBar.tsx b/apps/server-web/src/renderer/components/SideBar.tsx index ce543deef..09ee5b4df 100644 --- a/apps/server-web/src/renderer/components/SideBar.tsx +++ b/apps/server-web/src/renderer/components/SideBar.tsx @@ -1,24 +1,7 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -interface SideMenu { - displayName: string; - key: string; - isActive: boolean; -} - -interface Languages { - code: string; - label: string; -} - -type Props = { - children: string | JSX.Element | JSX.Element[]; - menus: SideMenu[]; - menuChange: (key: string) => void; - langs: Languages[]; - onLangChange: (lang: any) => void; - lang: string; -}; +import { ISidebarComponent } from '../libs/interfaces'; +import { SelectComponent } from './Select'; export function SideBar({ children, @@ -27,7 +10,7 @@ export function SideBar({ langs, lang, onLangChange, -}: Props) { +}: ISidebarComponent) { const { t } = useTranslation(); const language = langs.find((lg) => lg.code === lang) || { code: 'en', @@ -58,7 +41,20 @@ export function SideBar({
-
+ ({ + value: i.code, + label: `LANGUAGES.${i.code}`, + }))} + title={t('FORM.LABELS.LANGUAGES')} + defaultValue={language.code} + onValueChange={(lang) => { + onLangChange({ code: lang }); + }} + disabled={false} + value={language.code} + /> + {/*
-
+
*/}
{children}
diff --git a/apps/server-web/src/renderer/components/Toast.tsx b/apps/server-web/src/renderer/components/Toast.tsx index e63287440..11081c7b0 100644 --- a/apps/server-web/src/renderer/components/Toast.tsx +++ b/apps/server-web/src/renderer/components/Toast.tsx @@ -1,13 +1,7 @@ import * as React from 'react'; import * as Toast from '@radix-ui/react-toast'; -type Props = { - title: string; - message: string; - show: boolean; - autoClose: boolean; - timeout: number; - onClose: () => void; -}; +import { IToastComponent } from '../libs/interfaces'; +import { useTranslation } from 'react-i18next'; export const ToastComponent = ({ title, message, @@ -15,10 +9,10 @@ export const ToastComponent = ({ autoClose, timeout, onClose, -}: Props) => { +}: IToastComponent) => { const timerRef = React.useRef(0); + const { t } = useTranslation(); React.useEffect(() => { - // return () => clearTimeout(timerRef.current); if (autoClose) { clearTimeout(timerRef.current); timerRef.current = window.setTimeout(() => { @@ -35,10 +29,10 @@ export const ToastComponent = ({ duration={timeout} > - {title} + {t(title)} - {message} + {t(message)} - Close + {t('FORM.BUTTON.CLOSE')} diff --git a/apps/server-web/src/renderer/components/Updater.tsx b/apps/server-web/src/renderer/components/Updater.tsx index 5b6c2dd16..ada3401ff 100644 --- a/apps/server-web/src/renderer/components/Updater.tsx +++ b/apps/server-web/src/renderer/components/Updater.tsx @@ -4,37 +4,15 @@ import { SelectComponent } from './Select'; import { useEffect, useState } from 'react'; import { ToastComponent } from './Toast'; import { SettingPageTypeMessage } from '../../main/helpers/constant'; +import { DefaultRangeUpdateTimes } from '../libs/constant'; +import { IPC_TYPES, LOG_TYPES } from '../../main/helpers/constant'; +import { + IProgressComponent, + IRangeUpdates, + IUpdaterComponent, +} from '../libs/interfaces'; -interface UpdaterStates { - state: - | 'check-update' - | 'update-available' - | 'downloading' - | 'downloaded' - | 'error' - | 'not-started' - | 'up-to-date' - | 'cancel' - ; - data: any; - label: - | 'CHECKING' - | 'DOWNLOADING' - | 'QUIT_N_INSTALL' - | 'UP_TO_DATE' - | 'UPDATE_AVAILABLE' - | 'CHECK_FOR_UPDATE'; -} -type PropsProgress = { - updateStates: UpdaterStates; -}; - -type UpdateSetting = { - autoUpdate: boolean; - updateCheckPeriode: string; -}; - -const ProgressComponent = (props: PropsProgress) => { +const ProgressComponent = (props: IProgressComponent) => { const { t } = useTranslation(); return (
@@ -70,40 +48,11 @@ const ProgressComponent = (props: PropsProgress) => { ); }; -type Props = { - checkForUpdate: () => void; - loading: boolean; - updateStates: UpdaterStates; - Popup: JSX.Element; - data: UpdateSetting; - changeAutoUpdate: (data: UpdateSetting) => void; - saveSettingUpdate: (data: UpdateSetting) => void; -}; - -type RangeUpdates = { - value: string; - label: string; -}; -export const UpdaterComponent = (props: Props) => { +export const UpdaterComponent = (props: IUpdaterComponent) => { const { t } = useTranslation(); - const [rangeUpdate, setRangeUpdate] = useState([ - { - value: '30', - label: `30_MINUTES`, - }, - { - value: '60', - label: `A_HOURS`, - }, - { - value: '180', - label: `3_HOURS`, - }, - { - value: '1140', - label: `A_DAY`, - }, - ]); + const [rangeUpdate, _] = useState(DefaultRangeUpdateTimes); + + const [updateLogs, setUpdateLogs] = useState([]); const [toastShow, setToastShow] = useState(false); @@ -129,17 +78,22 @@ export const UpdaterComponent = (props: Props) => { }); }; - useEffect(() => { - window.electron.ipcRenderer.once('setting-page', (arg: any) => { - switch (arg.type) { - case SettingPageTypeMessage.updateSettingResponse: - setOpen(); - break; + const updaterEvent = (arg: any) => { + switch (arg.type) { + case SettingPageTypeMessage.updateSettingResponse: + setOpen(); + break; + case LOG_TYPES.UPDATE_LOG: + setUpdateLogs((prev) => [...prev, arg.msg]); + break; + default: + break; + } + }; - default: - break; - } - }); + useEffect(() => { + window.electron.ipcRenderer.removeEventListener(IPC_TYPES.UPDATER_PAGE); + window.electron.ipcRenderer.on(IPC_TYPES.UPDATER_PAGE, updaterEvent); }, []); return ( @@ -191,7 +145,10 @@ export const UpdaterComponent = (props: Props) => {
({ + ...i, + label: `FORM.LABELS.UPDATE_OPTIONS.${i.label}`, + }))} value={props.data.updateCheckPeriode} defaultValue={props.data.updateCheckPeriode} disabled={!props.data.autoUpdate} @@ -224,12 +181,53 @@ export const UpdaterComponent = (props: Props) => { )} {!props.loading && t(`FORM.LABELS.${props.updateStates.label}`)} +
+
+
+ + Update Logs + + + + + + +
+
+ {updateLogs.length > 0 && + updateLogs.map((ulog, i) => ( +
+ {ulog} +
+ ))} +
+
+
+
+
{props.Popup} void; +}; + +type IPopupComponent = { + isShowPopup: boolean; + modalAction: () => void; + type: 'success' | 'error' | 'none'; + message: string; +}; + +type IProgressComponent = { + updateStates: IUpdaterStates; +}; + +type IUpdaterComponent = { + checkForUpdate: () => void; + loading: boolean; + updateStates: IUpdaterStates; + Popup: JSX.Element; + data: IUpdateSetting; + changeAutoUpdate: (data: IUpdateSetting) => void; + saveSettingUpdate: (data: IUpdateSetting) => void; +}; + +type ISelectComponent = { + title: string; + items: ISelectItems[]; + defaultValue: string; + value: string; + disabled: boolean; + onValueChange: (val: string) => void; +}; + +type IServerComponent = { + serverSetting: IServerSetting; + saveSetting: (data: IServerSetting) => void; + Popup: JSX.Element; +}; + +type ISidebarComponent = { + children: string | JSX.Element | JSX.Element[]; + menus: ISideMenu[]; + menuChange: (key: string) => void; + langs: ILanguages[]; + onLangChange: (lang: any) => void; + lang: string; +}; + +export { + IToastComponent, + IProgressComponent, + IUpdaterComponent, + ISelectComponent, + IServerComponent, + ISidebarComponent, + IPopupComponent +} diff --git a/apps/server-web/src/renderer/libs/interfaces/i-setting.ts b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts new file mode 100644 index 000000000..4b47dd5e5 --- /dev/null +++ b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts @@ -0,0 +1,80 @@ +interface ISideMenu { + displayName: string; + key: string; + isActive: boolean; +} + +interface IUpdaterStates { + state: + | 'check-update' + | 'update-available' + | 'downloading' + | 'downloaded' + | 'error' + | 'not-started' + | 'up-to-date' + | 'cancel' + ; + data: any; + label: + | 'CHECKING' + | 'DOWNLOADING' + | 'QUIT_N_INSTALL' + | 'UP_TO_DATE' + | 'UPDATE_AVAILABLE' + | 'CHECK_FOR_UPDATE'; +} + +interface IServerSetting { + PORT: number; + GAUZY_API_SERVER_URL: string; + NEXT_PUBLIC_GAUZY_API_SERVER_URL: string; +} + +interface IPopup { + type: 'success' | 'error' | 'none'; + isShow: boolean; +} + +interface ILanguages { + code: string; + label: string; +} + +type IUpdateSetting = { + autoUpdate: boolean; + updateCheckPeriode: string; +}; + +type IAbout = { + version: string; +}; + +type IGeneralSetting = { + langs: ILanguages[]; + onChange: (lang: any) => void; + lang: string; +}; + +type ISelectItems = { + label: string; + value: string; +}; + +type IRangeUpdates = { + value: string; + label: string; +}; + +export { + ISideMenu, + IUpdaterStates, + IServerSetting, + IPopup, + ILanguages, + IUpdateSetting, + IAbout, + IGeneralSetting, + IRangeUpdates, + ISelectItems +} diff --git a/apps/server-web/src/renderer/libs/interfaces/index.ts b/apps/server-web/src/renderer/libs/interfaces/index.ts new file mode 100644 index 000000000..68a8d5aa7 --- /dev/null +++ b/apps/server-web/src/renderer/libs/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './i-setting'; +export * from './i-components'; diff --git a/apps/server-web/src/renderer/pages/Server.tsx b/apps/server-web/src/renderer/pages/Server.tsx new file mode 100644 index 000000000..ca5e83d43 --- /dev/null +++ b/apps/server-web/src/renderer/pages/Server.tsx @@ -0,0 +1,101 @@ +import { useState, useEffect } from 'react'; +import { ServerPageTypeMessage } from '../../main/helpers/constant'; +import { IPC_TYPES, LOG_TYPES } from '../../main/helpers/constant'; +import { EverTeamsLogo } from '../components/svgs'; +import { useTranslation } from 'react-i18next'; + +export function ServerPage() { + const [isRun, setIsRun] = useState(false); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + window.electron.ipcRenderer.removeEventListener(IPC_TYPES.SERVER_PAGE); + window.electron.ipcRenderer.on(IPC_TYPES.SERVER_PAGE, (arg: any) => { + switch (arg.type) { + case LOG_TYPES.SERVER_LOG: + setLogs((prev) => [...prev, arg.msg]); + break; + case ServerPageTypeMessage.SERVER_STATUS: + if (arg.data.isRun) { + setIsRun(true); + } else { + setIsRun(false); + } + setLoading(false); + break; + default: + break; + } + }); + }, []); + + const runServer = () => { + setLoading(true); + window.electron.ipcRenderer.sendMessage(IPC_TYPES.SERVER_PAGE, { + type: ServerPageTypeMessage.SERVER_EXEC, + data: { + isRun: !isRun, + }, + }); + }; + + return ( +
+
+
+ +
+
+ +
+
+
+ + Server Logs + + + + + + +
+
+ {logs.length > 0 && + logs.map((log, i) => ( +
+ {log} +
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/apps/server-web/src/renderer/pages/Setting.tsx b/apps/server-web/src/renderer/pages/Setting.tsx index 14440074f..87ef415f1 100644 --- a/apps/server-web/src/renderer/pages/Setting.tsx +++ b/apps/server-web/src/renderer/pages/Setting.tsx @@ -8,57 +8,17 @@ import { AboutComponent, GeneralComponent, } from '../components'; - -interface SideMenu { - displayName: string; - key: string; - isActive: boolean; -} - -interface UpdaterStates { - state: - | 'check-update' - | 'update-available' - | 'downloading' - | 'downloaded' - | 'error' - | 'not-started' - | 'up-to-date' - | 'cancel' - ; - data: any; - label: - | 'CHECKING' - | 'DOWNLOADING' - | 'QUIT_N_INSTALL' - | 'UP_TO_DATE' - | 'UPDATE_AVAILABLE' - | 'CHECK_FOR_UPDATE'; -} - -interface IServerSetting { - PORT: number; - GAUZY_API_SERVER_URL: string; - NEXT_PUBLIC_GAUZY_API_SERVER_URL: string; -} - -interface IPopup { - type: 'success' | 'error' | 'none'; - isShow: boolean; -} - -interface Languages { - code: string; - label: string; -} - -type UpdateSetting = { - autoUpdate: boolean; - updateCheckPeriode: string; -}; +import { + IUpdaterStates, + IUpdateSetting, + IServerSetting, + IPopup, + ILanguages, + ISideMenu, +} from '../libs/interfaces'; export function Setting() { - const [menus, setMenu] = useState([ + const [menus, setMenu] = useState([ { displayName: 'UPDATER', key: 'updater', @@ -76,12 +36,12 @@ export function Setting() { }, ]); - const [updateSetting, setUpdateSetting] = useState({ + const [updateSetting, setUpdateSetting] = useState({ autoUpdate: false, updateCheckPeriode: '180', }); - const [langs, setLangs] = useState([ + const [langs, setLangs] = useState([ { code: 'en', label: 'English', @@ -94,7 +54,7 @@ export function Setting() { const [lng, setLng] = useState('en'); - const [updateStates, setUpdateState] = useState({ + const [updateStates, setUpdateState] = useState({ state: 'not-started', data: null, label: 'CHECK_FOR_UPDATE', @@ -127,12 +87,12 @@ export function Setting() { setMenu(newMenu); }; - const changeLanguage = (lang: Languages) => { + const changeLanguage = (lang: ILanguages) => { sendingMessageToMain(lang.code, SettingPageTypeMessage.langChange); setLng(lang.code); }; - const saveSettingUpdate = (data: UpdateSetting) => { + const saveSettingUpdate = (data: IUpdateSetting) => { sendingMessageToMain(data, SettingPageTypeMessage.updateSetting); }; @@ -143,7 +103,7 @@ export function Setting() { }); }; - const updateDataSettingUpdate = (data: UpdateSetting) => { + const updateDataSettingUpdate = (data: IUpdateSetting) => { setUpdateSetting(data); }; diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index e9ba40c66..cb2e49e5f 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -36,7 +36,6 @@ export const RECAPTCHA_SITE_KEY = getNextPublicEnv( process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY ); export const RECAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY; - const basePath = process.env.GAUZY_API_SERVER_URL ? process.env.GAUZY_API_SERVER_URL : 'https://api.ever.team'; export const GAUZY_API_SERVER_URL = basePath + '/api'; From d59ec5825106b7c3f9bff1e7e6836308066f23e9 Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Mon, 15 Jul 2024 10:27:18 +0200 Subject: [PATCH 02/32] Update README.md From 10ccb5eed2d843acbac247790891b81e99a8c711 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Mon, 15 Jul 2024 10:52:01 +0200 Subject: [PATCH 03/32] fix: add custome fields on including project repository --- apps/web/app/services/client/api/auth/invite-accept.ts | 2 +- apps/web/app/services/client/api/organization-team.ts | 4 ++-- apps/web/app/services/server/requests/organization-team.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/app/services/client/api/auth/invite-accept.ts b/apps/web/app/services/client/api/auth/invite-accept.ts index e97106baa..bee527471 100644 --- a/apps/web/app/services/client/api/auth/invite-accept.ts +++ b/apps/web/app/services/client/api/auth/invite-accept.ts @@ -69,7 +69,7 @@ export function getAllOrganizationTeamAPI(params: ITeamRequestParams, bearer_tok 'members.employee.user', 'createdBy', 'projects', - 'projects.repository' + 'projects.customFields.repository' ]; // Construct search queries diff --git a/apps/web/app/services/client/api/organization-team.ts b/apps/web/app/services/client/api/organization-team.ts index 75e737fd9..733dbc9fa 100644 --- a/apps/web/app/services/client/api/organization-team.ts +++ b/apps/web/app/services/client/api/organization-team.ts @@ -31,7 +31,7 @@ export async function getOrganizationTeamsAPI(organizationId: string, tenantId: 'members.employee.user', 'createdBy', 'projects', - 'projects.repository' + 'projects.customFields.repository' ]; // Construct the query parameters including relations const queryParameters = { @@ -106,7 +106,7 @@ export async function getOrganizationTeamAPI(teamId: string, organizationId: str 'members.employee.user', 'createdBy', 'projects', - 'projects.repository' + 'projects.customFields.repository' ]; // Define base parameters including organization and tenant IDs, and date range diff --git a/apps/web/app/services/server/requests/organization-team.ts b/apps/web/app/services/server/requests/organization-team.ts index a62c2c6d9..8624b9fe2 100644 --- a/apps/web/app/services/server/requests/organization-team.ts +++ b/apps/web/app/services/server/requests/organization-team.ts @@ -92,7 +92,7 @@ export function getOrganizationTeamRequest( 'members.employee.user', 'createdBy', 'projects', - 'projects.repository' + 'projects.customFields.repository' ] }: ITeamRequestParams & { teamId: string }, bearer_token: string @@ -139,7 +139,7 @@ export function getAllOrganizationTeamRequest( 'members.employee.user', 'createdBy', 'projects', - 'projects.repository' + 'projects.customFields.repository' ] }: ITeamRequestParams, bearer_token: string From 7c6a36d7082b45b3616b9d6d340360adc1fd0c97 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Mon, 15 Jul 2024 15:19:38 +0200 Subject: [PATCH 04/32] fix: passcode signin issue --- .../app/[locale]/auth/passcode/component.tsx | 2 +- .../services/client/api/auth/invite-accept.ts | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/web/app/[locale]/auth/passcode/component.tsx b/apps/web/app/[locale]/auth/passcode/component.tsx index 05e51941a..87676b98d 100644 --- a/apps/web/app/[locale]/auth/passcode/component.tsx +++ b/apps/web/app/[locale]/auth/passcode/component.tsx @@ -335,7 +335,7 @@ function WorkSpaceScreen({ form, className }: { form: TAuthenticationPasscode } if (form.workspaces.length === 1 && currentTeams?.length === 1) { setSelectedTeam(currentTeams[0].team_id); } else { - const lastSelectedTeam = window.localStorage.getItem(LAST_WORSPACE_AND_TEAM) || currentTeams[0].team_id; + const lastSelectedTeam = window.localStorage.getItem(LAST_WORSPACE_AND_TEAM) || currentTeams[0]?.team_id; const lastSelectedWorkspace = form.workspaces.findIndex((workspace) => workspace.current_teams.find((team) => team.team_id === lastSelectedTeam) diff --git a/apps/web/app/services/client/api/auth/invite-accept.ts b/apps/web/app/services/client/api/auth/invite-accept.ts index bee527471..75b3be2c7 100644 --- a/apps/web/app/services/client/api/auth/invite-accept.ts +++ b/apps/web/app/services/client/api/auth/invite-accept.ts @@ -205,13 +205,24 @@ export function signInWorkspaceAPI(email: string, token: string) { * @returns */ export async function signInEmailConfirmGauzy(email: string, code: string) { - const loginResponse = await signInEmailCodeConfirmGauzy(email, code); + let loginResponse; + + try { + loginResponse = await signInEmailCodeConfirmGauzy(email, code); + } catch (error) { + console.error('Error in signInEmailCodeConfirmation:', error); + } if (loginResponse) { return loginResponse; } - return signInEmailConfirmAPI({ email, code }); + try { + const signinResponse = await signInEmailConfirmAPI({ email, code }); + return signinResponse; + } catch (error) { + return Promise.reject(error); + } } /** @@ -219,7 +230,12 @@ export async function signInEmailConfirmGauzy(email: string, code: string) { */ export async function signInWorkspaceGauzy(params: { email: string; token: string; teamId: string; code?: string }) { if (params.code) { - const loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code); + let loginResponse; + try { + loginResponse = await signInEmailCodeConfirmGauzy(params.email, params.code); + } catch (error) { + console.error('Error in signInWorkspaces', error); + } if (loginResponse) { return loginResponse; From cb5d3991f63e173b04c89d221dfe9a46d2fe7edd Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:37:44 +0200 Subject: [PATCH 05/32] [Feat] Filter Daily plans By date range (#2734) * feat: daily plan filter by date range * fix: error daily plan filter by date range al * fix: daily plan delete plan | open multiple popup * Delete duplicated file --------- Co-authored-by: Ruslan Konviser Co-authored-by: cedric karungu --- apps/web/app/helpers/date.ts | 13 ++++ apps/web/app/hooks/useDateRange.ts | 17 +++++ apps/web/app/hooks/useFilterDateRange.ts | 74 +++++++++++++++++++ apps/web/app/stores/daily-plan.ts | 68 ++++++++++++++++- .../features/task/daily-plan/future-tasks.tsx | 18 +++-- .../features/task/daily-plan/past-tasks.tsx | 7 +- .../web/lib/features/task/task-date-range.tsx | 71 ++++++++++++++++++ apps/web/lib/features/task/task-filters.tsx | 20 ++++- apps/web/lib/features/user-profile-plans.tsx | 29 +++++--- apps/web/tsconfig.json | 28 +++++-- 10 files changed, 319 insertions(+), 26 deletions(-) create mode 100644 apps/web/app/hooks/useDateRange.ts create mode 100644 apps/web/app/hooks/useFilterDateRange.ts create mode 100644 apps/web/lib/features/task/task-date-range.tsx diff --git a/apps/web/app/helpers/date.ts b/apps/web/app/helpers/date.ts index ace5da630..5aa529fd2 100644 --- a/apps/web/app/helpers/date.ts +++ b/apps/web/app/helpers/date.ts @@ -150,3 +150,16 @@ export const formatIntegerToHour = (number: number) => { return formattedHour; }; + + +export const isTestDateRange = (itemDate: Date, from?: Date, to?: Date) => { + if (from && to) { + return itemDate >= from && itemDate <= to; + } else if (from) { + return itemDate >= from; + } else if (to) { + return itemDate <= to; + } else { + return true; // or false, depending on your default logic + } +} diff --git a/apps/web/app/hooks/useDateRange.ts b/apps/web/app/hooks/useDateRange.ts new file mode 100644 index 000000000..38e3cec61 --- /dev/null +++ b/apps/web/app/hooks/useDateRange.ts @@ -0,0 +1,17 @@ +import { dateRangeAllPlanState, dateRangeFuturePlanState, dateRangePastPlanState } from "@app/stores"; +import { useRecoilState } from "recoil"; + +export const useDateRange = (tab: string | any) => { + const [dateFuture, setDateFuture] = useRecoilState(dateRangeFuturePlanState); + const [dateAllPlan, setDateAllPlan] = useRecoilState(dateRangeAllPlanState); + const [datePastPlan, setDatePastPlan] = useRecoilState(dateRangePastPlanState); + switch (tab) { + case 'Future Tasks': + return { date: dateFuture, setDate: setDateFuture }; + case 'Past Tasks': + return { date: datePastPlan, setDate: setDatePastPlan }; + case 'All Tasks': + default: + return { date: dateAllPlan, setDate: setDateAllPlan }; + } +} diff --git a/apps/web/app/hooks/useFilterDateRange.ts b/apps/web/app/hooks/useFilterDateRange.ts new file mode 100644 index 000000000..869d201bf --- /dev/null +++ b/apps/web/app/hooks/useFilterDateRange.ts @@ -0,0 +1,74 @@ +'use client'; + +import { IDailyPlan } from '@app/interfaces' +import { dateRangeAllPlanState, dateRangeFuturePlanState, dateRangePastPlanState, filteredAllPlanDataState, filteredFuturePlanDataState, filteredPastPlanDataState, originalAllPlanState, originalFuturePlanState, originalPastPlanDataState } from '@app/stores'; +import { useEffect, useMemo } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +/** + *custom filter the data with date range + * + * @export + * @param {IDailyPlan[]} itemsDailyPlan + * @param {('future' | 'past' | 'all')} [typeItems] + * @return {*} + */ +export function useFilterDateRange(itemsDailyPlan: IDailyPlan[], typeItems?: 'future' | 'past' | 'all') { + + const [dateAllPlan, setDateAllPlan] = useRecoilState(dateRangeAllPlanState); + const [datePastPlan, setDatePastPlan] = useRecoilState(dateRangePastPlanState); + const [dateFuture, setDateFuture] = useRecoilState(dateRangeFuturePlanState); + + const [originalAllPlanData, setOriginalAllPlanState] = useRecoilState(originalAllPlanState); + const [originalPastPlanData, setOriginalPastPlanData] = useRecoilState(originalPastPlanDataState); + const [originalFuturePlanData, setOriginalFuturePlanData] = useRecoilState(originalFuturePlanState); + + const filteredAllPlanData = useRecoilValue(filteredAllPlanDataState); + const filteredPastPlanData = useRecoilValue(filteredPastPlanDataState); + const filteredFuturePlanData = useRecoilValue(filteredFuturePlanDataState); + + // useEffect(() => { + // if (!itemsDailyPlan) return; + + // if (typeItems === 'future') { + // setOriginalFuturePlanData(itemsDailyPlan); + // } else if (typeItems === 'past') { + // setOriginalPastPlanData(itemsDailyPlan); + // } else if (typeItems === 'all') { + // setOriginalAllPlanState(itemsDailyPlan); + // } + // }, [itemsDailyPlan, dateFuture, datePastPlan, dateAllPlan, typeItems, setOriginalAllPlanState, setOriginalFuturePlanData, setOriginalAllPlanState]); + + const updateOriginalPlanData = useMemo(() => (data: IDailyPlan[]) => { + switch (typeItems) { + case 'future': + setOriginalFuturePlanData(data); + break; + case 'past': + setOriginalPastPlanData(data); + break; + case 'all': + setOriginalAllPlanState(data); + break; + default: + break; + } + }, [typeItems, setOriginalAllPlanState, setOriginalFuturePlanData, setOriginalPastPlanData]); + + useEffect(() => { + if (!itemsDailyPlan) return; + updateOriginalPlanData(itemsDailyPlan); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [updateOriginalPlanData, dateAllPlan, datePastPlan, dateFuture]); + + return { + filteredAllPlanData, + filteredPastPlanData, + filteredFuturePlanData, + originalAllPlanData, + originalFuturePlanData, + originalPastPlanData, + setDateAllPlan, + setDateFuture, + setDatePastPlan, + } +} diff --git a/apps/web/app/stores/daily-plan.ts b/apps/web/app/stores/daily-plan.ts index ab4b53e8b..1daea096c 100644 --- a/apps/web/app/stores/daily-plan.ts +++ b/apps/web/app/stores/daily-plan.ts @@ -1,5 +1,12 @@ -import { atom, selector } from 'recoil'; +import { atom, RecoilState, selector } from 'recoil'; import { IDailyPlan, PaginationResponse } from '@app/interfaces'; +import { addDays } from 'date-fns'; +import { DateRange } from 'react-day-picker'; +import { isTestDateRange } from '@app/helpers'; + +const today = new Date(); +const oneWeekAgo = new Date(); +oneWeekAgo.setDate(today.getDate() - 7); export const dailyPlanListState = atom>({ key: 'dailyPlanListState', @@ -44,3 +51,62 @@ export const activeDailyPlanState = selector({ return dailyPlans.items.find((plan) => plan.id === activeId) || dailyPlans.items[0] || null; } }); +const createDailyPlanCountFilterAtom = (key: string | any) => atom( + { + key, + default: 0 + } +) + +const createDailyPlanAtom = (key: string | any) => atom({ + key, + default: [], +}); + +const createDateRangeAtom = (key: string | any) => atom({ + key, + default: { + from: oneWeekAgo, + to: addDays(today, 3), + }, +}); + +const createFilteredDailyPlanDataSelector = (key: string | any, dateRangeState: RecoilState, originalDataState: RecoilState) => selector({ + key, + get: ({ get }) => { + const dateRange = get(dateRangeState); + const data = get(originalDataState); + if (!dateRange || !data.length) return data; + const { from, to } = dateRange; + return data.filter((plan) => { + const itemDate = new Date(plan.date); + return isTestDateRange(itemDate, from, to); + }); + }, +}); +export const dataDailyPlanCountFilterState = createDailyPlanCountFilterAtom('dataDailyPlanCountFilterState'); +export const dateRangePastPlanState = createDateRangeAtom('dateRangePastPlanState'); +export const dateRangeFuturePlanState = createDateRangeAtom('dateRangeFuturePlanState'); +export const dateRangeAllPlanState = createDateRangeAtom('dateRangeAllPlanState'); + +export const originalFuturePlanState = createDailyPlanAtom('originalFuturePlanState'); +export const originalAllPlanState = createDailyPlanAtom('originalAllPlanState'); +export const originalPastPlanDataState = createDailyPlanAtom('originalPastPlanDataState'); + +export const filteredPastPlanDataState = createFilteredDailyPlanDataSelector( + 'filteredPastPlanDataState', + dateRangePastPlanState, + originalPastPlanDataState +); + +export const filteredFuturePlanDataState = createFilteredDailyPlanDataSelector( + 'filteredFuturePlanDataState', + dateRangeFuturePlanState, + originalFuturePlanState +); + +export const filteredAllPlanDataState = createFilteredDailyPlanDataSelector( + 'filteredAllPlanDataState', + dateRangeAllPlanState, + originalAllPlanState +); diff --git a/apps/web/lib/features/task/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index ff4af85e1..00271eb1e 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -12,23 +12,26 @@ import { clsxm } from '@app/utils'; import { HorizontalSeparator } from 'lib/components'; import { useState } from 'react'; import { AlertPopup } from 'lib/components'; +import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); const canSeeActivity = useCanSeeActivityScreen(); const [popupOpen, setPopupOpen] = useState(false); + const { filteredFuturePlanData: filteredFuturePlanData } = useFilterDateRange(futurePlans, 'future'); + const [currentDelete, setCurrentDelete] = useState(""); const view = useRecoilValue(dailyPlanViewHeaderTabs); return (
- {futurePlans.length > 0 ? ( + {filteredFuturePlanData.length > 0 ? ( - {futurePlans.map((plan) => ( + {filteredFuturePlanData.map((plan, index) => ( setPopupOpen(true)} + onClick={() => { + setCurrentDelete(plan?.id ?? "") + setPopupOpen(true) + }} variant="outline" className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" > @@ -95,7 +101,9 @@ export function FutureTasks({ profile }: { profile: any }) { {/*button confirm*/} + + + + + +
+ ) +} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index 72b3d2c22..d8cff29ce 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -20,6 +20,8 @@ import { TaskLabelsDropdown, TaskPropertiesDropdown, TaskSizesDropdown, TaskStat import { useTranslations } from 'next-intl'; import { SettingFilterIcon } from 'assets/svg'; import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; +import { useDateRange } from '@app/hooks/useDateRange'; +import { TaskDatePickerWithRange } from './task-date-range'; type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; type ITabs = { @@ -174,9 +176,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); @@ -211,6 +213,7 @@ export type I_TaskFilter = ReturnType; type Props = { hook: I_TaskFilter; profile: I_UserProfilePage }; export function TaskFilter({ className, hook, profile }: IClassName & Props) { + return (
{ + setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || "Future Tasks") + }, [dailyPlanTab]) return (
@@ -404,7 +414,9 @@ export function TaskStatusFilter({ hook, employeeId }: { hook: I_TaskFilter; emp /> {hook.tab === 'dailyplan' && } - + {['Future Tasks', 'Past Tasks', 'All Tasks'].includes(dailyPlanTab) && ( + setDate(range)} label='Planned date' /> + )}
@@ -138,27 +142,31 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); + const [currentId, setCurrentId] = useState(""); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; const canSeeActivity = useCanSeeActivityScreen(); + const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); + const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; const view = useRecoilValue(dailyPlanViewHeaderTabs); + return (
- {filteredPlans?.length > 0 ? ( + {filterPlans?.length > 0 ? ( new Date(plan.date).toISOString().split('T')[0])[0]] + : [filterPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])[0]] } > - {filteredPlans?.map((plan) => ( + {filterPlans?.map((plan) => ( setPopupOpen(true)} + onClick={() => { + setCurrentId(plan.id ?? "") + setPopupOpen(true) + }} variant="outline" className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" > diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index ea316f538..9e750b5ec 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -2,7 +2,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "target": "ES2015", - "lib": ["dom", "dom.iterable", "esnext", "es2015"], + "lib": [ + "dom", + "dom.iterable", + "esnext", + "es2015" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -16,8 +21,12 @@ "jsx": "preserve", "baseUrl": ".", "paths": { - "@components/*": ["./components/*"], - "@app/*": ["./app/*"] + "@components/*": [ + "./components/*" + ], + "@app/*": [ + "./app/*" + ] }, "incremental": true, "plugins": [ @@ -26,6 +35,15 @@ } ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.js", + "**/*.jsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From c31986a6df5602446ecc1d2d198740b4ca707e2d Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Mon, 15 Jul 2024 18:25:18 +0200 Subject: [PATCH 06/32] fix: daily plan creation, filters and sort etc. --- .../web/app/services/client/api/daily-plan.ts | 6 +- .../services/server/requests/daily-plan.ts | 6 +- .../create-daily-plan-form-modal.tsx | 12 +-- .../task/daily-plan/outstanding-all.tsx | 75 +++++++++---------- .../web/lib/features/task/task-block-card.tsx | 6 +- apps/web/lib/features/task/task-card.tsx | 6 +- 6 files changed, 55 insertions(+), 56 deletions(-) diff --git a/apps/web/app/services/client/api/daily-plan.ts b/apps/web/app/services/client/api/daily-plan.ts index a99f1596e..6296dcfb3 100644 --- a/apps/web/app/services/client/api/daily-plan.ts +++ b/apps/web/app/services/client/api/daily-plan.ts @@ -14,7 +14,7 @@ export function getAllDayPlansAPI() { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks', 'employee.user']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, @@ -33,7 +33,7 @@ export function getMyDailyPlansAPI() { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, @@ -52,7 +52,7 @@ export function getDayPlansByEmployeeAPI(employeeId?: string) { const organizationId = getOrganizationIdCookie(); const tenantId = getTenantIdCookie(); - const relations = ['employee', 'tasks']; + const relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user']; const obj = { 'where[organizationId]': organizationId, diff --git a/apps/web/app/services/server/requests/daily-plan.ts b/apps/web/app/services/server/requests/daily-plan.ts index 11f451d4a..6357f7246 100644 --- a/apps/web/app/services/server/requests/daily-plan.ts +++ b/apps/web/app/services/server/requests/daily-plan.ts @@ -7,7 +7,7 @@ export function getAllDayPlans({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { organizationId: string; tenantId: string; @@ -36,7 +36,7 @@ export function getMyDailyPlansRequest({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { organizationId: string; tenantId: string; @@ -66,7 +66,7 @@ export function getDayPlansByEmployee({ organizationId, tenantId, bearer_token, - relations = ['employee', 'tasks'] + relations = ['employee', 'tasks', 'employee.user', 'tasks.members', 'tasks.members.user'] }: { employeeId: string; organizationId: string; diff --git a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx index 3264fe250..a60d3dfac 100644 --- a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx +++ b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import { useForm } from 'react-hook-form'; import { DailyPlanStatusEnum, IDailyPlanMode, IOrganizationTeamList, OT_Member } from '@app/interfaces'; import { useAuthenticateUser, useDailyPlan, useOrganizationTeams } from '@app/hooks'; -import { Avatar, Card, InputField, Modal, Text } from 'lib/components'; +import { Avatar, Card, Modal, Text } from 'lib/components'; import { imgTitle, tomorrowDate } from '@app/helpers'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import { cn } from 'lib/utils'; @@ -32,7 +32,7 @@ export function CreateDailyPlanFormModal({ employeeId?: string; chooseMember?: boolean; }) { - const { handleSubmit, reset, register } = useForm(); + const { handleSubmit, reset } = useForm(); const { user } = useAuthenticateUser(); const { activeTeam, activeTeamManagers } = useOrganizationTeams(); const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); @@ -50,7 +50,7 @@ export function CreateDailyPlanFormModal({ async (values: any) => { const toDay = new Date(); createDailyPlan({ - workTimePlanned: parseInt(values.workTimePlanned), + workTimePlanned: parseInt(values.workTimePlanned) || 0, taskId, date: planMode == 'today' @@ -104,14 +104,14 @@ export function CreateDailyPlanFormModal({ /> )} - + /> */} {planMode === 'custom' && ( diff --git a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx index c0ca5b92d..5a8aca31d 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -14,48 +14,47 @@ export function OutstandingAll({ profile }: OutstandingAll) { const { outstandingPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); const displayedTaskId = new Set(); + + const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); + return (
- {outstandingPlans?.length > 0 ? ( + {tasks && tasks?.length > 0 ? ( <> - {outstandingPlans?.map((plan) => ( - <> - {/* */} -
    - {plan?.tasks?.map((task) => { - //If the task is already displayed, skip it - if (displayedTaskId.has(task.id)) { - return null; - } - // Add the task to the Set to avoid displaying it again - displayedTaskId.add(task.id); - return view === 'CARDS' ? ( - - ) : ( - - ); - })} -
- - ))} + {/* */} +
    + {tasks?.map((task) => { + //If the task is already displayed, skip it + if (displayedTaskId.has(task.id)) { + return null; + } + // Add the task to the Set to avoid displaying it again + displayedTaskId.add(task.id); + return view === 'CARDS' ? ( + + ) : ( + + ); + })} +
) : ( diff --git a/apps/web/lib/features/task/task-block-card.tsx b/apps/web/lib/features/task/task-block-card.tsx index 3b6c7544b..30ab79b49 100644 --- a/apps/web/lib/features/task/task-block-card.tsx +++ b/apps/web/lib/features/task/task-block-card.tsx @@ -39,9 +39,9 @@ export default function TaskBlockCard(props: TaskItemProps) { const taskAssignee: ImageOverlapperProps[] = task.members?.map((member: any) => { return { - id: member.user.id, - url: member.user.imageUrl, - alt: member.user.firstName + id: member.user?.id, + url: member.user?.imageUrl, + alt: member.user?.firstName }; }); diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 192c035fd..6543e4ba1 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -132,9 +132,9 @@ export function TaskCard(props: Props) { const taskAssignee: ImageOverlapperProps[] = task?.members?.map((member: any) => { return { - id: member.user.id, - url: member.user.imageUrl, - alt: member.user.firstName + id: member.user?.id, + url: member.user?.imageUrl, + alt: member.user?.firstName }; }) || []; From da377da553d5d6429f439fdbf975dbb36e8413b9 Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Tue, 16 Jul 2024 13:09:14 +0200 Subject: [PATCH 07/32] feat: add today plan notification trigger --- apps/web/app/constants.ts | 1 + apps/web/app/hooks/features/useDailyPlan.ts | 52 ++++++++++++++++--- .../task/daily-plan/outstanding-all.tsx | 2 +- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index cb2e49e5f..f6a52203e 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -266,6 +266,7 @@ export const languagesFlags = [ // Local storage keys export const LAST_WORSPACE_AND_TEAM = 'last-workspace-and-team'; export const USER_SAW_OUTSTANDING_NOTIFICATION = 'user-saw-notif'; +export const TODAY_PLAN_ALERT_SHOWN_DATE = 'last-today-plan-alert-date'; // OAuth providers keys diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index d60318923..edc87d10e 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -1,16 +1,16 @@ 'use client'; -import { useRecoilState } from 'recoil'; -import { useCallback, useEffect } from 'react'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { useCallback, useEffect, useState } from 'react'; import { useQuery } from '../useQuery'; import { + activeTeamState, dailyPlanFetchingState, dailyPlanListState, employeePlansListState, myDailyPlanListState, profileDailyPlanListState, - taskPlans, - userState + taskPlans } from '@app/stores'; import { addTaskToPlanAPI, @@ -25,9 +25,22 @@ import { } from '@app/services/client/api'; import { ICreateDailyPlan, IDailyPlanTasksUpdate, IUpdateDailyPlan } from '@app/interfaces'; import { useFirstLoad } from '../useFirstLoad'; +import { useAuthenticateUser } from './useAuthenticateUser'; +import { TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; + +type TodayPlanNotificationParams = { + canBeSeen: boolean; + alreadySeen: boolean; +}; export function useDailyPlan() { - const [user] = useRecoilState(userState); + const [addTodayPlanTrigger, setAddTodayPlanTrigger] = useState({ + canBeSeen: false, + alreadySeen: false + }); + + const { user } = useAuthenticateUser(); + const activeTeam = useRecoilValue(activeTeamState); const { loading, queryCall } = useQuery(getDayPlansByEmployeeAPI); const { loading: getAllDayPlansLoading, queryCall: getAllQueryCall } = useQuery(getAllDayPlansAPI); @@ -241,10 +254,34 @@ export function useDailyPlan() { profileDailyPlans.items && [...profileDailyPlans.items].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + const currentUser = activeTeam?.members?.find((member) => member.employee.userId === user?.id); + useEffect(() => { getMyDailyPlans(); }, [getMyDailyPlans]); + useEffect(() => { + const checkAndShowAlert = () => { + if (activeTeam && currentUser) { + const lastAlertDate = localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const today = new Date().toISOString().split('T')[0]; + const totalMemberWorked = currentUser?.totalTodayTasks.reduce( + (previousValue, currentValue) => previousValue + currentValue.duration, + 0 + ); + const showTodayPlanTrigger = todayPlan && todayPlan.length > 0 && totalMemberWorked > 0; + if (lastAlertDate === today) { + setAddTodayPlanTrigger({ canBeSeen: !!showTodayPlanTrigger, alreadySeen: true }); + } + } + }; + + checkAndShowAlert(); + const intervalId = setInterval(checkAndShowAlert, 24 * 60 * 60 * 1000); // One day check and display + + return () => clearInterval(intervalId); + }, [activeTeam, currentUser, todayPlan]); + return { dailyPlan, setDailyPlan, @@ -292,6 +329,9 @@ export function useDailyPlan() { pastPlans, outstandingPlans, todayPlan, - sortedPlans + sortedPlans, + + addTodayPlanTrigger, + setAddTodayPlanTrigger }; } diff --git a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx index 5a8aca31d..998d7ff49 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -26,7 +26,7 @@ export function OutstandingAll({ profile }: OutstandingAll) {
    From 99854ce79beac239197d19335d35571199e8a2aa Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 17 Jul 2024 01:56:29 +0200 Subject: [PATCH 08/32] feat: soft Daily Plan triggers --- apps/web/app/hooks/features/useTimer.ts | 7 +- .../components/shared/timer/timer-card.tsx | 1 + .../plans-work-time-and-estimate.tsx | 169 ++++++++++++------ apps/web/lib/features/task/task-card.tsx | 1 + apps/web/lib/features/timer/timer.tsx | 1 + apps/web/locales/ar.json | 5 +- apps/web/locales/bg.json | 5 +- apps/web/locales/de.json | 5 +- apps/web/locales/en.json | 7 +- apps/web/locales/es.json | 5 +- apps/web/locales/fr.json | 5 +- apps/web/locales/he.json | 5 +- apps/web/locales/it.json | 5 +- apps/web/locales/nl.json | 5 +- apps/web/locales/pl.json | 5 +- apps/web/locales/pt.json | 5 +- apps/web/locales/ru.json | 5 +- apps/web/locales/zh.json | 5 +- 18 files changed, 159 insertions(+), 87 deletions(-) diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 432a452c1..5599f114e 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -205,11 +205,8 @@ export function useTimer() { // If require plan setting is activated, // check if the today plan has working time planned and all the tasks into the plan are estimated - let isPlanVerified = true; - if (requirePlan) { - isPlanVerified = - !!hasPlan?.workTimePlanned && !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0); - } + const isPlanVerified = + !!hasPlan?.workTimePlanned && !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0); const canRunTimer = user?.isEmailVerified && diff --git a/apps/web/components/shared/timer/timer-card.tsx b/apps/web/components/shared/timer/timer-card.tsx index 3511902db..10f2a9d08 100644 --- a/apps/web/components/shared/timer/timer-card.tsx +++ b/apps/web/components/shared/timer/timer-card.tsx @@ -60,6 +60,7 @@ const Timer = () => { open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} /> ); diff --git a/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx b/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx index 885d0698a..f2c4d1dd0 100644 --- a/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx +++ b/apps/web/lib/features/daily-plan/plans-work-time-and-estimate.tsx @@ -1,24 +1,28 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { PiWarningCircleFill } from 'react-icons/pi'; -import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import { DailyPlanStatusEnum, IDailyPlan, ITeamTask } from '@app/interfaces'; import { Card, InputField, Modal, Text, VerticalSeparator } from 'lib/components'; import { useTranslations } from 'use-intl'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { Button } from '@components/ui/button'; import { TaskEstimate } from '../task/task-estimate'; -import { useDailyPlan, useTeamTasks } from '@app/hooks'; +import { useAuthenticateUser, useDailyPlan, useTeamTasks } from '@app/hooks'; +import { ReloadIcon } from '@radix-ui/react-icons'; export function AddWorkTimeAndEstimatesToPlan({ open, closeModal, plan, - startTimer + startTimer, + hasPlan // employee }: { open: boolean; closeModal: () => void; startTimer: () => void; + hasPlan: boolean; plan?: IDailyPlan; + // employee?: OT_Member; }) { const t = useTranslations(); @@ -30,15 +34,18 @@ export function AddWorkTimeAndEstimatesToPlan({ const { updateDailyPlan } = useDailyPlan(); - const { tasks: $tasks } = useTeamTasks(); + const { tasks: $tasks, activeTeam } = useTeamTasks(); const tasks = $tasks.filter((task) => plan?.tasks?.some((t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0) ); const handleSubmit = () => { - if (workTimePlanned === 0 || typeof workTimePlanned !== 'number') return; - if (tasks.some((task) => task.estimate === 0)) return; + const requirePlan = activeTeam?.requirePlanToTrack; + if (requirePlan) { + if (workTimePlanned === 0 || typeof workTimePlanned !== 'number') return; + if (tasks.some((task) => task.estimate === 0)) return; + } updateDailyPlan({ workTimePlanned }, plan?.id ?? ''); startTimer(); @@ -47,59 +54,63 @@ export function AddWorkTimeAndEstimatesToPlan({ return ( - -
    -
    - - {t('timer.todayPlanSettings.TITLE')} - -
    -
    - - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * - - setworkTimePlanned(parseFloat(e.target.value))} - required - defaultValue={plan?.workTimePlanned ?? 0} - /> -
    + {hasPlan ? ( + +
    +
    + + {t('timer.todayPlanSettings.TITLE')} + +
    +
    + + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * + + setworkTimePlanned(parseFloat(e.target.value))} + required + defaultValue={plan?.workTimePlanned ?? 0} + /> +
    - {tasks.length > 0 && ( -
    - + {tasks.length > 0 && ( +
    + -
    - -

    {t('timer.todayPlanSettings.WARNING_PLAN_ESTIMATION')}

    +
    + +

    {t('timer.todayPlanSettings.WARNING_PLAN_ESTIMATION')}

    +
    + )} + +
    + +
    - )} - -
    - -
    -
    - + + ) : ( + + )} ); } @@ -146,3 +157,51 @@ export function UnEstimatedTask({ task }: { task: ITeamTask }) { ); } + +export function CreateTodayPlanPopup({ closeModal }: { closeModal: () => void }) { + const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); + const { user } = useAuthenticateUser(); + const { activeTeam } = useTeamTasks(); + const member = activeTeam?.members.find((member) => member.employee.userId === user?.id); + const onSubmit = useCallback( + async (values: any) => { + const toDay = new Date(); + createDailyPlan({ + workTimePlanned: parseInt(values.workTimePlanned) || 0, + date: toDay, + status: DailyPlanStatusEnum.OPEN, + tenantId: user?.tenantId ?? '', + employeeId: member?.employeeId, + organizationId: member?.organizationId + }).then(() => { + closeModal(); + }); + }, + [closeModal, createDailyPlan, member?.employeeId, member?.organizationId, user?.tenantId] + ); + + return ( + +
    +
    + + CREATE A PLAN FOR TODAY + + + You are creating a new plan for today +
    +
    + +
    +
    +
    + ); +} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 6543e4ba1..9b657343b 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -416,6 +416,7 @@ function TimerButtonCall({ open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} /> ); diff --git a/apps/web/lib/features/timer/timer.tsx b/apps/web/lib/features/timer/timer.tsx index d6245f893..fce5dc2ce 100644 --- a/apps/web/lib/features/timer/timer.tsx +++ b/apps/web/lib/features/timer/timer.tsx @@ -154,6 +154,7 @@ export function Timer({ className }: IClassName) { open={isOpen} plan={hasPlan} startTimer={startTimer} + hasPlan={!!hasPlan} />
    diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 17e85f377..2b7ff1272 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -197,7 +197,8 @@ "NOT_WORKING": "غير عامل", "WORKING": "يعمل", "PAUSED": "متوقف", - "ONLINE": "متصل" + "ONLINE": "متصل", + "SKIP_ADD_LATER": "أضف لاحقًا" }, "hotkeys": { "HELP": "مساعدة", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "لديك", "USER_LABEL": "مهام غير مكتملة، يرجى التحقق منها", - "OUTSTANDING_VIEW": "عرض المهام المعلقة", + "OUTSTANDING_VIEW": "معلق", "VIEW_BUTTON": "عرض" } }, diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 3b43adbba..2186a167a 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Не работи", "WORKING": "Работи", "PAUSED": "Пауза", - "ONLINE": "Онлайн" + "ONLINE": "Онлайн", + "SKIP_ADD_LATER": "Добави по-късно" }, "hotkeys": { "HELP": "Помощ", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Имате", "USER_LABEL": "незавършени задачи, моля, проверете", - "OUTSTANDING_VIEW": "Преглед на изчакващите задачи", + "OUTSTANDING_VIEW": "Изключителен", "VIEW_BUTTON": "Преглед" } }, diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index 1ce0d9d1e..ca856b96d 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Nicht arbeiten", "WORKING": "Arbeiten", "PAUSED": "Pausiert", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Später hinzufügen" }, "hotkeys": { "HELP": "Hilfe", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Sie haben", "USER_LABEL": "unvollständige Aufgaben, bitte überprüfen", - "OUTSTANDING_VIEW": "Ausstehende Ansicht", + "OUTSTANDING_VIEW": "Hervorragend", "VIEW_BUTTON": "Ansehen" } }, diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 1dc28e54d..07d45d2fb 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Not Working", "WORKING": "Working", "PAUSED": "Paused", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Add later" }, "hotkeys": { "HELP": "Help", @@ -229,9 +230,9 @@ "CONFIRM_ACCEPT_INVITATION": "Are you sure you want to accept the invitation?", "CONFIRM_REJECT_INVITATION": "Are you sure you want to reject the invitation?", "OUTSTANDING_NOTIFICATIONS": { - "SUBJECT": "You've", + "SUBJECT": "You have", "USER_LABEL": "uncompleted tasks, please check in", - "OUTSTANDING_VIEW": "Outstanding View", + "OUTSTANDING_VIEW": "Outstanding", "VIEW_BUTTON": "View" } }, diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index 257403ef4..818ac74d5 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -197,7 +197,8 @@ "NOT_WORKING": "No trabajando", "WORKING": "Trabajando", "PAUSED": "Pausado", - "ONLINE": "En línea" + "ONLINE": "En línea", + "SKIP_ADD_LATER": "Agregar más tarde" }, "hotkeys": { "HELP": "Ayuda", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Tienes", "USER_LABEL": "tareas incompletas, por favor revisa", - "OUTSTANDING_VIEW": "Vista pendiente", + "OUTSTANDING_VIEW": "Pendiente", "VIEW_BUTTON": "Ver" } }, diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 27c226ab1..6cd5044e2 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Non actif", "WORKING": "Actif", "PAUSED": "En pause", - "ONLINE": "En ligne" + "ONLINE": "En ligne", + "SKIP_ADD_LATER": "Ajouter plus tard" }, "hotkeys": { "HELP": "Aide", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Vous avez", "USER_LABEL": "des tâches incomplètes, veuillez vérifier", - "OUTSTANDING_VIEW": "Vue des tâches en attente", + "OUTSTANDING_VIEW": "En attente", "VIEW_BUTTON": "Voir" } }, diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 17de57399..d89cfbb2d 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -197,7 +197,8 @@ "NOT_WORKING": "לא עובד", "WORKING": "עובד", "PAUSED": "מושהה", - "ONLINE": "מחובר" + "ONLINE": "מחובר", + "SKIP_ADD_LATER": "הוסף מאוחר יותר" }, "hotkeys": { "HELP": "עזרה", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "יש לך", "USER_LABEL": "משימות שלא הושלמו, נא לבדוק", - "OUTSTANDING_VIEW": "תצוגה ממתינה", + "OUTSTANDING_VIEW": "יוצא דופן", "VIEW_BUTTON": "הצג" } }, diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 93262916f..3c7ff1fc9 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Non Lavora", "WORKING": "Lavora", "PAUSED": "In Pausa", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Aggiungi dopo" }, "hotkeys": { "HELP": "Aiuto", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Hai", "USER_LABEL": "compiti incompleti, controlla per favore", - "OUTSTANDING_VIEW": "Vista delle attività in sospeso", + "OUTSTANDING_VIEW": "Eccezionale", "VIEW_BUTTON": "Vedi" } }, diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index fad47a7c3..f6b88f6da 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Niet aan het werk", "WORKING": "Aan het werk", "PAUSED": "Gepauzeerd", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Later toevoegen" }, "hotkeys": { "HELP": "Help", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Je hebt", "USER_LABEL": "onvoltooide taken, controleer alsjeblieft", - "OUTSTANDING_VIEW": "Uitzicht op uitstaande taken", + "OUTSTANDING_VIEW": "Uitstekend", "VIEW_BUTTON": "Bekijk" } }, diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 1ec6b4080..21ec208c2 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Niepracujący", "WORKING": "Pracujący", "PAUSED": "Wstrzymany", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Dodaj później" }, "hotkeys": { "HELP": "Pomoc", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Masz", "USER_LABEL": "niezakończone zadania, proszę sprawdź", - "OUTSTANDING_VIEW": "Widok zadań do wykonania", + "OUTSTANDING_VIEW": "Wyjątkowy", "VIEW_BUTTON": "Zobacz" } }, diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index fef612fbe..3df8aeb73 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Não Trabalhando", "WORKING": "Trabalhando", "PAUSED": "Pausado", - "ONLINE": "Online" + "ONLINE": "Online", + "SKIP_ADD_LATER": "Adicionar depois" }, "hotkeys": { "HELP": "Ajuda", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "Você tem", "USER_LABEL": "tarefas não concluídas, por favor verifique", - "OUTSTANDING_VIEW": "Visualização de pendências", + "OUTSTANDING_VIEW": "Pendências", "VIEW_BUTTON": "Ver" } }, diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 51c727994..003e3c5fc 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -197,7 +197,8 @@ "NOT_WORKING": "Не работает", "WORKING": "Работает", "PAUSED": "Приостановлено", - "ONLINE": "Онлайн" + "ONLINE": "Онлайн", + "SKIP_ADD_LATER": "Добавить позже" }, "hotkeys": { "HELP": "Помощь", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "У вас есть", "USER_LABEL": "незавершенные задачи, пожалуйста, проверьте", - "OUTSTANDING_VIEW": "Просмотр незавершенных задач", + "OUTSTANDING_VIEW": "Выдающийся", "VIEW_BUTTON": "Просмотр" } }, diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 64d4ea750..fe4fd5a16 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -197,7 +197,8 @@ "NOT_WORKING": "不工作", "WORKING": "工作", "PAUSED": "暂停", - "ONLINE": "在线" + "ONLINE": "在线", + "SKIP_ADD_LATER": "稍后添加" }, "hotkeys": { "HELP": "帮助", @@ -231,7 +232,7 @@ "OUTSTANDING_NOTIFICATIONS": { "SUBJECT": "您有", "USER_LABEL": "未完成的任务,请检查", - "OUTSTANDING_VIEW": "待处理视图", + "OUTSTANDING_VIEW": "突出", "VIEW_BUTTON": "查看" } }, From e9eb607f7ae638248e5ae9d194ac7b85f408530b Mon Sep 17 00:00:00 2001 From: GloireMutaliko21 Date: Wed, 17 Jul 2024 10:32:04 +0200 Subject: [PATCH 09/32] refact: daily plan badge --- apps/web/app/helpers/plan-day-badge.ts | 10 +++-- apps/web/lib/components/kanban-card.tsx | 8 +++- .../user-team-active-task.tsx | 1 + .../features/task/daily-plan/past-tasks.tsx | 5 ++- .../features/task/task-all-status-type.tsx | 20 ++++++++-- .../web/lib/features/task/task-block-card.tsx | 8 +++- apps/web/lib/features/task/task-card.tsx | 12 ++++-- apps/web/lib/features/task/task-filters.tsx | 17 ++++---- .../team/user-team-block/task-info.tsx | 39 +++++++++++++++---- .../features/team/user-team-card/index.tsx | 15 +++++-- .../team/user-team-card/task-info.tsx | 18 +++++++-- apps/web/lib/features/user-profile-plans.tsx | 9 ++--- 12 files changed, 118 insertions(+), 44 deletions(-) diff --git a/apps/web/app/helpers/plan-day-badge.ts b/apps/web/app/helpers/plan-day-badge.ts index e5d9322da..70c1f92ed 100644 --- a/apps/web/app/helpers/plan-day-badge.ts +++ b/apps/web/app/helpers/plan-day-badge.ts @@ -1,7 +1,11 @@ import { IDailyPlan, ITeamTask } from '@app/interfaces'; import { formatDayPlanDate } from './date'; -export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): string | null => { +export const planBadgeContent = ( + plans: IDailyPlan[], + taskId: ITeamTask['id'], + tab?: 'default' | 'unassign' | 'dailyplan' +): string | null => { // Search a plan that contains a given task const plan = plans.find((plan) => plan.tasks?.some((task) => task.id === taskId)); @@ -12,8 +16,8 @@ export const planBadgeContent = (plans: IDailyPlan[], taskId: ITeamTask['id']): (pl) => pl.id !== plan.id && pl.tasks?.some((tsk) => tsk.id === taskId) ); - // If the task exists in other plans, the its planned many days - if (otherPlansWithTask.length > 0) { + // If the task exists in other plans, then its planned many days + if (otherPlansWithTask.length > 0 || tab === 'unassign') { return 'Planned'; } else { return `${formatDayPlanDate(plan.date, 'DD MMM YYYY')}`; diff --git a/apps/web/lib/components/kanban-card.tsx b/apps/web/lib/components/kanban-card.tsx index 98b38a2d9..e97005508 100644 --- a/apps/web/lib/components/kanban-card.tsx +++ b/apps/web/lib/components/kanban-card.tsx @@ -174,7 +174,13 @@ export default function Item(props: ItemProps) {
    - + diff --git a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx index ded3e360e..27a75ce65 100644 --- a/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx +++ b/apps/web/lib/features/all-teams/users-teams-card/user-team-active-task.tsx @@ -26,6 +26,7 @@ export default function UserTeamActiveTaskInfo({ member }: { member: OT_Member } memberInfo={memberInfo} className="flex-1 lg:px-4 px-2 overflow-y-hidden" publicTeam={false} + tab="default" /> ) : (
    --
    diff --git a/apps/web/lib/features/task/daily-plan/past-tasks.tsx b/apps/web/lib/features/task/daily-plan/past-tasks.tsx index 4055784de..683cb467f 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -1,6 +1,6 @@ import { formatDayPlanDate, yesterdayDate } from '@app/helpers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; -import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; +import { EmptyPlans, FilterTabs, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; import { useDailyPlan } from '@app/hooks'; import { useRecoilValue } from 'recoil'; @@ -10,7 +10,7 @@ import { clsxm } from '@app/utils'; import TaskBlockCard from '../task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; -export function PastTasks({ profile }: { profile: any }) { +export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { const { pastPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); @@ -63,6 +63,7 @@ export function PastTasks({ profile }: { profile: any }) { type="HORIZONTAL" taskBadgeClassName={`rounded-sm`} taskTitleClassName="mt-[0.0625rem]" + planMode={currentTab === 'Past Tasks' ? 'Past Tasks' : undefined} /> ) : ( diff --git a/apps/web/lib/features/task/task-all-status-type.tsx b/apps/web/lib/features/task/task-all-status-type.tsx index ce783a704..f4224ffd2 100644 --- a/apps/web/lib/features/task/task-all-status-type.tsx +++ b/apps/web/lib/features/task/task-all-status-type.tsx @@ -14,13 +14,18 @@ import { import { clsxm } from '@app/utils'; import { planBadgeContent } from '@app/helpers'; import { CalendarIcon } from '@radix-ui/react-icons'; +import { FilterTabs } from '../user-profile-plans'; export function TaskAllStatusTypes({ task, showStatus = false, toBlockCard = false, - className + className, + tab, + dayPlanTab }: { + tab?: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; task?: Nullable; showStatus?: boolean; toBlockCard?: boolean; @@ -94,10 +99,17 @@ export function TaskAllStatusTypes({ titleClassName={'text-[0.625rem] font-[500]'} /> )} - {planBadgeContent(dailyPlan.items, task?.id ?? '') && ( -
    + {planBadgeContent(dailyPlan.items, task?.id ?? '', tab) && ( +
    - {planBadgeContent(dailyPlan.items, task?.id ?? '')} + + {planBadgeContent(dailyPlan.items, task?.id ?? '', tab)} +
    )} {tags.map((tag, i) => { diff --git a/apps/web/lib/features/task/task-block-card.tsx b/apps/web/lib/features/task/task-block-card.tsx index 30ab79b49..7ba6f64f3 100644 --- a/apps/web/lib/features/task/task-block-card.tsx +++ b/apps/web/lib/features/task/task-block-card.tsx @@ -63,7 +63,13 @@ export default function TaskBlockCard(props: TaskItemProps) {
    - + diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 6543e4ba1..5d88b0043 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -160,6 +160,8 @@ export function TaskCard(props: Props) { className="px-4 w-full" taskBadgeClassName={clsxm(taskBadgeClassName)} taskTitleClassName={clsxm(taskTitleClassName)} + dayPlanTab={planMode} + tab={viewType} />
    @@ -248,7 +250,7 @@ export function TaskCard(props: Props) { )} */}
    - {' '} + {' '} {viewType === 'default' && ( <>
    @@ -427,8 +429,12 @@ function TaskInfo({ className, task, taskBadgeClassName, - taskTitleClassName + taskTitleClassName, + tab, + dayPlanTab }: IClassName & { + tab: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; task?: Nullable; taskBadgeClassName?: string; taskTitleClassName?: string; @@ -457,7 +463,7 @@ function TaskInfo({ )} {/* Task status */} - {task && } + {task && } {!task &&
    --
    }
    ); diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index d8cff29ce..c98e43020 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -23,7 +23,7 @@ import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; -type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; +export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; type ITabs = { tab: ITab; name: string; @@ -176,9 +176,9 @@ export function useTaskFilter(profile: I_UserProfilePage) { .every((k) => { return k === 'label' ? intersection( - statusFilters[k], - task['tags'].map((item) => item.name) - ).length === statusFilters[k].length + statusFilters[k], + task['tags'].map((item) => item.name) + ).length === statusFilters[k].length : statusFilters[k].includes(task[k]); }); }); @@ -213,7 +213,6 @@ export type I_TaskFilter = ReturnType; type Props = { hook: I_TaskFilter; profile: I_UserProfilePage }; export function TaskFilter({ className, hook, profile }: IClassName & Props) { - return (
    { - setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || "Future Tasks") - }, [dailyPlanTab]) + setDailyPlanTab(window.localStorage.getItem('daily-plan-tab') || 'Future Tasks'); + }, [dailyPlanTab]); return (
    @@ -415,7 +412,7 @@ export function TaskStatusFilter({ hook, employeeId }: { hook: I_TaskFilter; emp {hook.tab === 'dailyplan' && } {['Future Tasks', 'Past Tasks', 'All Tasks'].includes(dailyPlanTab) && ( - setDate(range)} label='Planned date' /> + setDate(range)} label="Planned date" /> )} diff --git a/apps/web/lib/features/team/user-team-block/task-info.tsx b/apps/web/lib/features/team/user-team-block/task-info.tsx index b92fd4543..93ab87cdb 100644 --- a/apps/web/lib/features/team/user-team-block/task-info.tsx +++ b/apps/web/lib/features/team/user-team-block/task-info.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { I_TeamMemberCardHook, I_TMCardTaskEditHook } from '@app/hooks'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; -import { TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; +import { FilterTabs, TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; import { useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; @@ -10,33 +9,49 @@ type Props = IClassName & { edition: I_TMCardTaskEditHook; memberInfo: I_TeamMemberCardHook; publicTeam?: boolean; + tab?: 'default' | 'unassign' | 'dailyplan'; + dayPlanTab?: FilterTabs; }; -export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { return (
    {/* task */}
    {edition.task && ( - + )} {!edition.task &&
    --
    }
    - {edition.task && } + {edition.task && ( + + )} {!edition.task &&
    --
    }
    ); } -export function TaskBlockInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskBlockInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { const t = useTranslations(); return (
    {/* task */}
    {edition.task && ( - + )} {!edition.task && (
    @@ -45,7 +60,15 @@ export function TaskBlockInfo({ className, memberInfo, edition, publicTeam }: Pr )}
    - {edition.task && } + {edition.task && ( + + )} {!edition.task &&
    _
    }
    ); diff --git a/apps/web/lib/features/team/user-team-card/index.tsx b/apps/web/lib/features/team/user-team-card/index.tsx index 5dbcb5f38..88bf8b117 100644 --- a/apps/web/lib/features/team/user-team-card/index.tsx +++ b/apps/web/lib/features/team/user-team-card/index.tsx @@ -195,13 +195,15 @@ export function UserTeamCard({ memberInfo={memberInfo} className="flex-1 lg:px-4 px-2 overflow-y-hidden" publicTeam={publicTeam} + tab="default" /> {isManagerConnectedUser != 1 ? (

    { - showActivityFilter('TICKET', memberInfo.member ?? null); setUserDetailAccordion(''); + showActivityFilter('TICKET', memberInfo.member ?? null); + setUserDetailAccordion(''); }} > {!showActivity ? ( @@ -254,7 +256,8 @@ export function UserTeamCard({

    {menu}
    {userDetailAccordion == memberInfo.memberUser?.id && - memberInfo.memberUser.id == profile.userProfile?.id && !showActivity? ( + memberInfo.memberUser.id == profile.userProfile?.id && + !showActivity ? (
    {canSeeActivity && ( @@ -300,7 +303,13 @@ export function UserTeamCard({
    - +
    diff --git a/apps/web/lib/features/team/user-team-card/task-info.tsx b/apps/web/lib/features/team/user-team-card/task-info.tsx index b96965fe4..36b291c9d 100644 --- a/apps/web/lib/features/team/user-team-card/task-info.tsx +++ b/apps/web/lib/features/team/user-team-card/task-info.tsx @@ -1,16 +1,18 @@ import { I_TeamMemberCardHook, I_TMCardTaskEditHook } from '@app/hooks'; import { IClassName } from '@app/interfaces'; import { clsxm } from '@app/utils'; -import { TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; +import { FilterTabs, TaskAllStatusTypes, TaskInput, TaskNameInfoDisplay } from 'lib/features'; import { useRouter } from 'next/navigation'; type Props = IClassName & { edition: I_TMCardTaskEditHook; memberInfo: I_TeamMemberCardHook; publicTeam?: boolean; + dayPlanTab?: FilterTabs; + tab?: 'default' | 'unassign' | 'dailyplan'; }; -export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) { +export function TaskInfo({ className, memberInfo, edition, publicTeam, tab, dayPlanTab }: Props) { return ( <> {!edition.task &&
    --
    } @@ -30,11 +32,19 @@ export function TaskInfo({ className, memberInfo, edition, publicTeam }: Props) )} > {edition.task && ( - + )}
    - {edition.task && } + {edition.task && ( + + )}
    {!edition.task &&
    --
    } diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index 1a0bccedd..b23168c25 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -22,7 +22,7 @@ import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from './task/task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; -type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; +export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; export function UserProfilePlans() { @@ -142,7 +142,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); - const [currentId, setCurrentId] = useState(""); + const [currentId, setCurrentId] = useState(''); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; @@ -153,7 +153,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const view = useRecoilValue(dailyPlanViewHeaderTabs); - return (
    {filterPlans?.length > 0 ? ( @@ -225,8 +224,8 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current //button open popup + } > {/*button confirm*/} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index a5f070681..1db976457 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -24,6 +24,7 @@ import { IDailyPlanMode, IDailyPlanTasksUpdate, IOrganizationTeamList, + IRemoveTaskFromManyPlans, ITeamTask, Nullable, OT_Member @@ -598,6 +599,12 @@ function TaskCardMenu({ plan={plan} />
    +
    + +
    ) : ( <> @@ -755,3 +762,23 @@ export function RemoveTaskFromPlan({ task, plan, member }: { task: ITeamTask; me ); } + +export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member; }) { + // const t = useTranslations(); + const { removeManyTaskPlans } = useDailyPlan(); + const data: IRemoveTaskFromManyPlans = { plansIds: [], employeeId: member?.employeeId }; + const onClick = () => { + removeManyTaskPlans(data, task.id ?? ''); + }; + return ( + + Remove from all plans + + ); +} diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index b23168c25..ac16b05e7 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -142,7 +142,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current let filteredPlans: IDailyPlan[] = []; const { deleteDailyPlan, deleteDailyPlanLoading, sortedPlans, todayPlan } = useDailyPlan(); const [popupOpen, setPopupOpen] = useState(false); - const [currentId, setCurrentId] = useState(''); + const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); filteredPlans = sortedPlans; if (currentTab === 'Today Tasks') filteredPlans = todayPlan; @@ -150,7 +150,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const canSeeActivity = useCanSeeActivityScreen(); const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; - const view = useRecoilValue(dailyPlanViewHeaderTabs); return ( @@ -165,7 +164,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current : [filterPlans?.map((plan) => new Date(plan.date).toISOString().split('T')[0])[0]] } > - {filterPlans?.map((plan) => ( + {filterPlans?.map((plan, index) => ( { - setCurrentId(plan.id ?? ''); - setPopupOpen(true); + setCurrentDeleteIndex(index) + setPopupOpen(prev => !prev); + }} variant="outline" className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" From 07ca32d949ff62bb3f347318667000cb5ea3158e Mon Sep 17 00:00:00 2001 From: "Gloire Mutaliko (Salva)" <86450367+GloireMutaliko21@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:37:40 +0200 Subject: [PATCH 11/32] [Fix] Profile | tab 'Assigned' values in Action menu (#2745) * feat: hide plan for today and for tomorrow on already existing plans * feat: dailyn plan creation disable dates already planned * feat: dailyn plan creation disable dates using profile user plans * fix: delete unncessary logs --- apps/web/app/hooks/features/useTimer.ts | 8 ++++++++ .../daily-plan/create-daily-plan-form-modal.tsx | 14 ++++++++++---- apps/web/lib/features/task/task-card.tsx | 17 ++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 5599f114e..6f1b28515 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -193,6 +193,11 @@ export function useTimer() { plan.tasks?.length > 0 ); + const tomorrow = moment().add(1, 'days'); + const hasPlanForTomorrow = myDailyPlans.items.find( + (plan) => moment(plan.date).format('YYYY-MM-DD') === tomorrow.format('YYYY-MM-DD') + ); + // Team setting that tells if each member must have a today plan for allowing tracking time const requirePlan = activeTeam?.requirePlanToTrack; @@ -420,6 +425,7 @@ export function useTimer() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -460,6 +466,7 @@ export function useTimerView() { startTimer, stopTimer, hasPlan, + hasPlanForTomorrow, canRunTimer, canTrack, isPlanVerified, @@ -491,6 +498,7 @@ export function useTimerView() { timerStatus, activeTeamTask, hasPlan, + hasPlanForTomorrow, disabled: !canRunTimer, canTrack, isPlanVerified, diff --git a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx index a60d3dfac..8491d203a 100644 --- a/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx +++ b/apps/web/lib/features/daily-plan/create-daily-plan-form-modal.tsx @@ -35,7 +35,9 @@ export function CreateDailyPlanFormModal({ const { handleSubmit, reset } = useForm(); const { user } = useAuthenticateUser(); const { activeTeam, activeTeamManagers } = useOrganizationTeams(); - const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); + const { createDailyPlan, createDailyPlanLoading, profileDailyPlans } = useDailyPlan(); + + const existingPlanDates = profileDailyPlans.items.map((plan) => new Date(plan.date)); const isManagerConnectedUser = activeTeamManagers.find((member) => member.employee?.user?.id == user?.id); @@ -119,7 +121,7 @@ export function CreateDailyPlanFormModal({ - + setDate(day ? day : new Date(tomorrowDate))} initialFocus - disabled={{ from: new Date(1970, 1, 1), to: tomorrowDate }} + disabled={[ + ...existingPlanDates, + { from: new Date(1970, 1, 1), to: tomorrowDate } + ]} /> diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 1db976457..64b03790d 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -500,6 +500,7 @@ function TaskCardMenu({ }, [memberInfo, task, viewType]); const canSeeActivity = useCanSeeActivityScreen(); + const { hasPlan, hasPlanForTomorrow } = useTimerView(); return ( @@ -556,6 +557,7 @@ function TaskCardMenu({ planMode="today" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasTodayPlan={hasPlan} />
  • @@ -563,6 +565,7 @@ function TaskCardMenu({ planMode="tomorow" taskId={task.id} employeeId={profile?.member?.employeeId ?? ''} + hasPlanForTomorrow={hasPlanForTomorrow} />
  • @@ -599,7 +602,7 @@ function TaskCardMenu({ plan={plan} />
  • -
    +
    - {planMode === 'today' && ( + {planMode === 'today' && !hasTodayPlan && ( {isPending ? ( @@ -711,7 +718,7 @@ export function PlanTask({ )} )} - {planMode === 'tomorow' && ( + {planMode === 'tomorow' && !hasPlanForTomorrow && ( {isPending ? ( @@ -763,7 +770,7 @@ export function RemoveTaskFromPlan({ task, plan, member }: { task: ITeamTask; me ); } -export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member; }) { +export function RemoveManyTaskFromPlan({ task, member }: { task: ITeamTask; member?: OT_Member }) { // const t = useTranslations(); const { removeManyTaskPlans } = useDailyPlan(); const data: IRemoveTaskFromManyPlans = { plansIds: [], employeeId: member?.employeeId }; From ef94bf08e2096483a6b2046d6aeb267b06f8ee96 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:37:57 +0200 Subject: [PATCH 12/32] [Task] Daily Plan | Drag and Drop (#2743) * feat: remove many daily plans * feat:Implement task cards Drag & Drop (Daily Plan) * fix: cspelling error * fix: cspelling error * fix: merge conflicts * fix: error drag drop null check --------- Co-authored-by: GloireMutaliko21 --- apps/extensions/components/popup/Timer.tsx | 2 +- apps/web/app/helpers/drag-and-drop.ts | 48 ++++ apps/web/app/helpers/index.ts | 1 + apps/web/app/interfaces/IDailyPlan.ts | 8 +- .../features/task/daily-plan/future-tasks.tsx | 240 +++++++++------- .../task/daily-plan/outstanding-all.tsx | 112 +++++--- .../task/daily-plan/outstanding-date.tsx | 149 ++++++---- .../features/task/daily-plan/past-tasks.tsx | 160 +++++++---- apps/web/lib/features/task/task-filters.tsx | 1 - .../lib/features/team-members-kanban-view.tsx | 6 +- .../lib/features/team-members-table-view.tsx | 18 +- apps/web/lib/features/user-profile-plans.tsx | 271 ++++++++++-------- 12 files changed, 642 insertions(+), 374 deletions(-) create mode 100644 apps/web/app/helpers/drag-and-drop.ts diff --git a/apps/extensions/components/popup/Timer.tsx b/apps/extensions/components/popup/Timer.tsx index 57341bed9..bd6c08c1e 100644 --- a/apps/extensions/components/popup/Timer.tsx +++ b/apps/extensions/components/popup/Timer.tsx @@ -25,7 +25,7 @@ const Timer: React.FC = ({ port }) => { ? new Date(msg.payload.timer * 1000).toISOString().substr(11, 8) : '00:00:00'; const totalWorkedTime = - msg.payload.totalWorked > 0 + msg.payload!.totalWorked > 0 ? new Date(msg.payload.totalWorked * 1000).toISOString().substr(11, 8) : '00:00:00'; setTimeString(taskWorkedTime); diff --git a/apps/web/app/helpers/drag-and-drop.ts b/apps/web/app/helpers/drag-and-drop.ts new file mode 100644 index 000000000..c1d3af554 --- /dev/null +++ b/apps/web/app/helpers/drag-and-drop.ts @@ -0,0 +1,48 @@ +import { IDailyPlan, ITeamTask } from "@app/interfaces"; +import { DropResult } from "react-beautiful-dnd"; + +export const handleDragAndDrop = (results: DropResult, plans: IDailyPlan[], setPlans: React.Dispatch>) => { + const { source, destination } = results; + + if (!destination || (source.droppableId === destination.droppableId && source.index === destination.index)) return; + + const newPlans = [...plans]; + + const planSourceIndex = newPlans.findIndex(plan => plan.id === source.droppableId); + const planDestinationIndex = newPlans.findIndex(plan => plan.id === destination.droppableId); + + const newSourceTasks = [...newPlans[planSourceIndex].tasks!]; + const newDestinationTasks = source.droppableId !== destination.droppableId + ? [...newPlans[planDestinationIndex].tasks!] + : newSourceTasks; + + const [deletedTask] = newSourceTasks.splice(source.index, 1); + newDestinationTasks.splice(destination.index, 0, deletedTask); + + newPlans[planSourceIndex] = { + ...newPlans[planSourceIndex], + tasks: newSourceTasks, + }; + newPlans[planDestinationIndex] = { + ...newPlans[planDestinationIndex], + tasks: newDestinationTasks, + }; + setPlans(newPlans); +}; + + +export const handleDragAndDropDailyOutstandingAll = ( + results: DropResult, + tasks: ITeamTask[], + setTasks: React.Dispatch> +) => { + const { source, destination } = results; + + if (!destination || (source.droppableId === destination.droppableId && source.index === destination.index)) return; + + const newTasks = [...tasks]; + const [movedTask] = newTasks.splice(source.index, 1); + newTasks.splice(destination.index, 0, movedTask); + + setTasks(newTasks); +}; diff --git a/apps/web/app/helpers/index.ts b/apps/web/app/helpers/index.ts index 5ec7a1bb8..de35a0e57 100644 --- a/apps/web/app/helpers/index.ts +++ b/apps/web/app/helpers/index.ts @@ -9,3 +9,4 @@ export * from './validations'; export * from './colors'; export * from './strings'; export * from './plan-day-badge'; +export * from './drag-and-drop' diff --git a/apps/web/app/interfaces/IDailyPlan.ts b/apps/web/app/interfaces/IDailyPlan.ts index b14d60521..5d91cd89a 100644 --- a/apps/web/app/interfaces/IDailyPlan.ts +++ b/apps/web/app/interfaces/IDailyPlan.ts @@ -9,10 +9,10 @@ export interface IDailyPlanBase extends IBasePerTenantAndOrganizationEntity { status: DailyPlanStatusEnum; } -export class IRemoveTaskFromManyPlans { +export interface IRemoveTaskFromManyPlans { employeeId?: IEmployee['id']; plansIds?: IDailyPlan['id'][]; - organizationId?: IOrganization['id'] + organizationId?: IOrganization['id']; } export interface IDailyPlan extends IDailyPlanBase, IRelationnalEmployee { @@ -23,11 +23,11 @@ export interface ICreateDailyPlan extends IDailyPlanBase, IRelationnalEmployee { taskId?: ITeamTask['id']; } -export interface IUpdateDailyPlan extends Partial, Pick { } +export interface IUpdateDailyPlan extends Partial, Pick {} export interface IDailyPlanTasksUpdate extends Pick, - IBasePerTenantAndOrganizationEntity { } + IBasePerTenantAndOrganizationEntity {} export enum DailyPlanStatusEnum { OPEN = 'open', diff --git a/apps/web/lib/features/task/daily-plan/future-tasks.tsx b/apps/web/lib/features/task/daily-plan/future-tasks.tsx index fdf80c665..f813116ea 100644 --- a/apps/web/lib/features/task/daily-plan/future-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/future-tasks.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate, tomorrowDate } from '@app/helpers'; +import { formatDayPlanDate, handleDragAndDrop, tomorrowDate } from '@app/helpers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; @@ -13,6 +13,8 @@ import { HorizontalSeparator } from 'lib/components'; import { useState } from 'react'; import { AlertPopup } from 'lib/components'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { IDailyPlan } from '@app/interfaces'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; export function FutureTasks({ profile }: { profile: any }) { const { deleteDailyPlan, deleteDailyPlanLoading, futurePlans } = useDailyPlan(); @@ -20,116 +22,150 @@ export function FutureTasks({ profile }: { profile: any }) { const [popupOpen, setPopupOpen] = useState(false); const { filteredFuturePlanData: filteredFuturePlanData } = useFilterDateRange(futurePlans, 'future'); const [currentDeleteIndex, setCurrentDeleteIndex] = useState(0); + const [futureDailyPlanTasks, setFutureDailyPlanTasks] = useState(filteredFuturePlanData); const view = useRecoilValue(dailyPlanViewHeaderTabs); return (
    - {filteredFuturePlanData.length > 0 ? ( - 0 ? ( + handleDragAndDrop(result, futureDailyPlanTasks, setFutureDailyPlanTasks)} > - {filteredFuturePlanData.map((plan, index) => ( - - -
    -
    - {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + + {futureDailyPlanTasks.map((plan, index) => ( + + +
    +
    + {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
    +
    - -
    - - - {/* Plan header */} - - - {/* Plan tasks list */} -
      - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
    - - {/* Delete Plan */} - {canSeeActivity ? ( -
    - { - setPopupOpen(prev => !prev) - setCurrentDeleteIndex(index) - }} - variant="outline" - className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" - > - Delete this plan - - - } - > - {/*button confirm*/} - - -
    - ) : ( - <> - )} -
    - - ))} - + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} +
    + ) : ( + + {(provided) => ( +
    + +
    + )} +
    + ) + )} + <>{provided.placeholder} + {canSeeActivity ? ( +
    + { + setPopupOpen((prev) => !prev); + setCurrentDeleteIndex(index); + }} + variant="outline" + className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" + > + Delete this plan + + } + > + {/*button confirm*/} + + {/*button cancel*/} + + +
    + ) : ( + <> + )} +
+ )} + + + + ))} + + ) : ( )} diff --git a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx index 998d7ff49..80e56fd96 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -6,6 +6,10 @@ import { useRecoilValue } from 'recoil'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; +import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; +import { useState } from 'react'; +import { ITeamTask } from '@app/interfaces'; +import { handleDragAndDropDailyOutstandingAll } from '@app/helpers'; interface OutstandingAll { profile: any; @@ -16,45 +20,89 @@ export function OutstandingAll({ profile }: OutstandingAll) { const displayedTaskId = new Set(); const tasks = outstandingPlans.map((plan) => plan.tasks).reduce((red, curr) => red?.concat(curr || []), []); + const [task, setTask] = useState(tasks!); return (
+ {tasks && tasks?.length > 0 ? ( <> - {/* */} -
    handleDragAndDropDailyOutstandingAll(result, task, setTask)} > - {tasks?.map((task) => { - //If the task is already displayed, skip it - if (displayedTaskId.has(task.id)) { - return null; - } - // Add the task to the Set to avoid displaying it again - displayedTaskId.add(task.id); - return view === 'CARDS' ? ( - - ) : ( - - ); - })} -
+ {/* */} + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
    + {tasks?.map((task, index) => { + //If the task is already displayed, skip it + if (displayedTaskId.has(task.id)) { + return null; + } + // Add the task to the Set to avoid displaying it again + displayedTaskId.add(task.id); + return view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} +
    + ) : ( + + {(provided) => ( +
    + +
    + )} +
    + ); + })} +
+ )} +
+ ) : ( diff --git a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx index 9eaa232ac..2623e624b 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate } from '@app/helpers'; +import { formatDayPlanDate, handleDragAndDrop } from '@app/helpers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { EmptyPlans, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; @@ -8,6 +8,9 @@ import TaskBlockCard from '../task-block-card'; import { clsxm } from '@app/utils'; import { useRecoilValue } from 'recoil'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; +import { useState } from 'react'; +import { IDailyPlan } from '@app/interfaces'; interface IOutstandingFilterDate { profile: any; @@ -15,64 +18,100 @@ interface IOutstandingFilterDate { export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) { const { outstandingPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); + const [outstandingTasks, setOutstandingTasks] = useState(outstandingPlans) return (
- {outstandingPlans?.length > 0 ? ( - new Date(plan.date).toISOString().split('T')[0])} - > - {outstandingPlans?.map((plan) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {outstandingTasks?.length > 0 ? ( + handleDragAndDrop(result, outstandingTasks, setOutstandingTasks)}> + new Date(plan.date).toISOString().split('T')[0])}> + {outstandingTasks?.map((plan) => ( + + +
+
+ {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
+
- -
- - - {/* Plan header */} - + + + {/* Plan header */} + + + {(provided) => ( +
    + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} - {/* Plan tasks list */} -
      - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
    - - - ))} - +
    + ) : ( + + {(provided) => ( +
    + +
    + )} +
    + ) + )} +
+ )} + {/* <>{provided.placeholder} */} + {/* Plan tasks list */} +
+
+ + ))} + + ) : ( )} diff --git a/apps/web/lib/features/task/daily-plan/past-tasks.tsx b/apps/web/lib/features/task/daily-plan/past-tasks.tsx index 683cb467f..412ef7d05 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -1,4 +1,4 @@ -import { formatDayPlanDate, yesterdayDate } from '@app/helpers'; +import { formatDayPlanDate, handleDragAndDrop, yesterdayDate } from '@app/helpers'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { EmptyPlans, FilterTabs, PlanHeader } from 'lib/features/user-profile-plans'; import { TaskCard } from '../task-card'; @@ -9,71 +9,119 @@ import { HorizontalSeparator } from 'lib/components'; import { clsxm } from '@app/utils'; import TaskBlockCard from '../task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { useState } from 'react'; +import { IDailyPlan } from '@app/interfaces'; +import { DragDropContext, Draggable, Droppable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any; currentTab?: FilterTabs }) { const { pastPlans } = useDailyPlan(); const view = useRecoilValue(dailyPlanViewHeaderTabs); const { filteredPastPlanData: filteredPastPlanData } = useFilterDateRange(pastPlans, 'past'); + const [pastTasks, setPastTasks] = useState(filteredPastPlanData); return (
- {filteredPastPlanData?.length > 0 ? ( - - {filteredPastPlanData?.map((plan) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {pastTasks?.length > 0 ? ( + handleDragAndDrop(result, pastPlans, setPastTasks)}> + + {pastTasks?.map((plan) => ( + + +
+
+ {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
+
- -
- - - {/* Plan header */} - - - {/* Plan tasks list */} -
    - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
-
- - ))} - + + + {/* Plan header */} + + + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => ( +
    + {plan.tasks?.map((task, index) => + view === 'CARDS' ? ( + + {(provided) => ( +
    + +
    + )} +
    + ) : ( + + {(provided) => ( +
    + +
    + )} +
    + ) + )} +
+ )} +
+
+ + ))} + + ) : ( )} diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index c98e43020..e2e1a8659 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -263,7 +263,6 @@ export function TaskFilter({ className, hook, profile }: IClassName & Props) { function InputFilters({ hook, profile }: Props) { const t = useTranslations(); const [loading, setLoading] = useState(false); - const osSpecificAssignTaskTooltipLabel = 'A'; return ( diff --git a/apps/web/lib/features/team-members-kanban-view.tsx b/apps/web/lib/features/team-members-kanban-view.tsx index 3433236d7..678966836 100644 --- a/apps/web/lib/features/team-members-kanban-view.tsx +++ b/apps/web/lib/features/team-members-kanban-view.tsx @@ -30,7 +30,7 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: const [columns, setColumn] = useState( Object.keys(kanbanBoardTasks).map((key) => { const columnInfo = kanbanColumns.find((item) => item.name === key); - return { id: columnInfo?.id, name: key, icon: columnInfo ? columnInfo.fullIconUrl : '',color: columnInfo?.color }; + return { id: columnInfo?.id, name: key, icon: columnInfo ? columnInfo.fullIconUrl : '', color: columnInfo?.color }; }) ); const fullWidth = useRecoilValue(fullWidthState); @@ -198,7 +198,9 @@ export const KanbanView = ({ kanbanBoardTasks, isLoading }: { kanbanBoardTasks: <> {Array.isArray(columns) && columns.length > 0 && ( - + {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
- []} - data={sortedTeamMembers} - noResultsMessage={{ - heading: 'No team members found', - content: 'Try adjusting your search or filter to find what you’re looking for.' - }} - /> + []} + data={sortedTeamMembers} + noResultsMessage={{ + heading: 'No team members found', + content: 'Try adjusting your search or filter to find what you’re looking for.' + }} + /> ); diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index ac16b05e7..6109c8810 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -1,5 +1,4 @@ 'use client'; - import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { useCanSeeActivityScreen, useDailyPlan, useUserProfilePage } from '@app/hooks'; @@ -21,6 +20,8 @@ import ViewsHeaderTabs from './task/daily-plan/views-header-tabs'; import { dailyPlanViewHeaderTabs } from '@app/stores/header-tabs'; import TaskBlockCard from './task/task-block-card'; import { useFilterDateRange } from '@app/hooks/useFilterDateRange'; +import { handleDragAndDrop } from '@app/helpers/drag-and-drop'; +import { DragDropContext, Droppable, Draggable, DroppableProvided, DroppableStateSnapshot } from 'react-beautiful-dnd'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; @@ -136,7 +137,12 @@ export function UserProfilePlans() {
); } - +/** + * + * + * @param {{ profile: any; currentTab?: FilterTabs }} { profile, currentTab = 'All Tasks' } + * @return {*} + */ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; currentTab?: FilterTabs }) { // Filter plans let filteredPlans: IDailyPlan[] = []; @@ -151,122 +157,163 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? filterAllPlanData : filteredPlans; const view = useRecoilValue(dailyPlanViewHeaderTabs); + const [plans, setPlans] = useState(filterPlans); return (
- {filterPlans?.length > 0 ? ( - new Date(plan.date).toISOString().split('T')[0])[0]] - } - > - {filterPlans?.map((plan, index) => ( - - -
-
- {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) + {Array.isArray(plans) && plans?.length > 0 ? ( + handleDragAndDrop(result, plans, setPlans)}> + new Date(plan.date).toISOString().split('T')[0])[0]] + } + > + {plans.map((plan, index) => ( + + +
+
+ {formatDayPlanDate(plan.date.toString())} ({plan.tasks?.length}) +
+
- -
- - - {/* Plan header */} - - - {/* Plan tasks list */} -
    - {plan.tasks?.map((task) => - view === 'CARDS' ? ( - - ) : ( - - ) - )} -
- - {/* Delete Plan */} - {currentTab === 'Today Tasks' && ( - <> - {canSeeActivity ? ( -
- { - setCurrentDeleteIndex(index) - setPopupOpen(prev => !prev); - - }} - variant="outline" - className="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light" - > - Delete this plan - - } - > - {/*button confirm*/} - + } + > + {/*button confirm*/} + + {/*button cancel*/} + + +
+ ) : ( + <> )} - Delete - - {/*button cancel*/} - - -
- ) : ( - <> + + )} + )} - - )} - -
- ))} -
+ + + + ))} + + ) : ( - + )}
); @@ -280,7 +327,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil // Get all tasks's estimations time const times = - plan.tasks?.map((task) => task.estimate).filter((time): time is number => typeof time === 'number') ?? []; + plan.tasks?.map((task) => task?.estimate).filter((time): time is number => typeof time === 'number') ?? []; let estimatedTime = 0; if (times.length > 0) estimatedTime = times.reduce((acc, cur) => acc + cur, 0) ?? 0; From ba9477c43f13229f1a486b76f3aefa9cded9370b Mon Sep 17 00:00:00 2001 From: Ushindi Gedeon Date: Wed, 17 Jul 2024 19:23:05 +0200 Subject: [PATCH 13/32] [Feature] Add manual time popup page (#2715) * [Feature]create a form to add time * remove undefined object error * Add key to options * Check if null * Replace the Add time button * Place the button between filter and Update the deign of the modal * Delete unused var * Change unfecognised word * Improve the design * Add reusable time picker component * Implement the custom time picker and add a gap of 10min * Update custom date picker * Update style and fix bug when selecting 0h * Fix bug and update styling * Install time picker * Install time picker * Remove TimeKeeper --------- Co-authored-by: cedric karungu --- .../app/[locale]/profile/[memberId]/page.tsx | 6 +- apps/web/lib/features/task/task-filters.tsx | 271 +++++++++++++++++- 2 files changed, 267 insertions(+), 10 deletions(-) diff --git a/apps/web/app/[locale]/profile/[memberId]/page.tsx b/apps/web/app/[locale]/profile/[memberId]/page.tsx index 3e3dd3ebf..c44a3d619 100644 --- a/apps/web/app/[locale]/profile/[memberId]/page.tsx +++ b/apps/web/app/[locale]/profile/[memberId]/page.tsx @@ -13,7 +13,7 @@ import { TaskFilter, Timer, TimerStatus, UserProfileTask, getTimerStatusValue, u import { MainHeader, MainLayout } from 'lib/layout'; import Link from 'next/link'; import React, { useCallback, useMemo, useState } from 'react'; -import { useTranslations } from 'next-intl'; +import { useTranslations } from 'next-intl' import stc from 'string-to-color'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -126,7 +126,6 @@ const Profile = React.memo(function ProfilePage({ params }: { params: { memberId {/* User Profile Detail */}
- {profileIsAuthUser && isTrackingEnabled && ( - - {/* Divider */} - {/*
*/} {hook.tab == 'worked' && canSeeActivity && ( diff --git a/apps/web/lib/features/task/task-filters.tsx b/apps/web/lib/features/task/task-filters.tsx index e2e1a8659..f098f23fc 100644 --- a/apps/web/lib/features/task/task-filters.tsx +++ b/apps/web/lib/features/task/task-filters.tsx @@ -6,7 +6,9 @@ import { useAuthenticateUser, useDailyPlan, useOrganizationTeams, - useOutsideClick + useOutsideClick, + useModal, + useTeamTasks } from '@app/hooks'; import { IClassName, ITeamTask } from '@app/interfaces'; import { clsxm } from '@app/utils'; @@ -14,16 +16,25 @@ import { Transition } from '@headlessui/react'; import { Button, InputField, Tooltip, VerticalSeparator } from 'lib/components'; import { SearchNormalIcon } from 'assets/svg'; import intersection from 'lodash/intersection'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState, FormEvent } from 'react'; import { TaskUnOrAssignPopover } from './task-assign-popover'; import { TaskLabelsDropdown, TaskPropertiesDropdown, TaskSizesDropdown, TaskStatusDropdown } from './task-status'; import { useTranslations } from 'next-intl'; import { SettingFilterIcon } from 'assets/svg'; import { DailyPlanFilter } from './daily-plan/daily-plan-filter'; +import { Modal, Divider } from 'lib/components'; +import api from '@app/services/client/axios'; +import { MdOutlineMoreTime } from "react-icons/md"; +import { IoIosTimer } from "react-icons/io"; +import { FiLoader } from "react-icons/fi"; +import { DatePicker } from '@components/ui/DatePicker'; +import { PencilSquareIcon } from '@heroicons/react/20/solid'; +import { FaRegCalendarAlt } from "react-icons/fa"; import { useDateRange } from '@app/hooks/useDateRange'; import { TaskDatePickerWithRange } from './task-date-range'; export type ITab = 'worked' | 'assigned' | 'unassigned' | 'dailyplan'; + type ITabs = { tab: ITab; name: string; @@ -263,6 +274,105 @@ export function TaskFilter({ className, hook, profile }: IClassName & Props) { function InputFilters({ hook, profile }: Props) { const t = useTranslations(); const [loading, setLoading] = useState(false); + const { tasks } = useTeamTasks(); + const { activeTeam } = useOrganizationTeams(); + const members = activeTeam?.members; + + const [date, setDate] = useState(''); + const [isBillable, setIsBillable] = useState(false); + const [startTime, setStartTime] = useState(''); + const [endTime, setEndTime] = useState(''); + const [team, setTeam] = useState(''); + const [task, setTask] = useState(''); + const [description, setDescription] = useState(''); + const [reason, setReason] = useState(''); + const [timeDifference, setTimeDifference] = useState(''); + const [errorMsg, setError] = useState(''); + const [loading1, setLoading1] = useState(false); + + const { isOpen, openModal, closeModal } = useModal(); + + useEffect(() => { + const now = new Date(); + const currentDate = now.toISOString().slice(0, 10); + const currentTime = now.toTimeString().slice(0, 5); + + setDate(currentDate); + setStartTime(currentTime); + setEndTime(currentTime); + }, []); + + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + const timeObject = { + date, + isBillable, + startTime, + endTime, + team, + task, + description, + reason, + timeDifference + }; + + if (date && startTime && endTime && team && task) { + setLoading1(true); + setError(''); + const postData = async () => { + try { + const response = await api.post('/add_time', timeObject); + if (response.data.message) { + setLoading1(false); + closeModal(); + } + + } catch (err) { + setError('Failed to post data'); + setLoading1(false); + } + }; + + postData(); + } else { + setError(`Please complete all required fields with a ${"*"}`) + } + }; + + const calculateTimeDifference = () => { + + const [startHours, startMinutes] = startTime.split(':').map(Number); + const [endHours, endMinutes] = endTime.split(':').map(Number); + + const startTotalMinutes = startHours * 60 + startMinutes; + const endTotalMinutes = endHours * 60 + endMinutes; + + const diffMinutes = endTotalMinutes - startTotalMinutes; + if (diffMinutes < 0) { + return; + } + + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + setTimeDifference(`${hours} Hours ${minutes} Minutes`); + }; + + useEffect(() => { + calculateTimeDifference(); + }, [endTime, startTime]); + + useEffect(() => { + if (task == '') { + setTask(tasks[0]?.id); + } + if (team == '') { + members && setTeam(members[0].id); + } + + }, [tasks, members]); + const osSpecificAssignTaskTooltipLabel = 'A'; return ( @@ -294,6 +404,12 @@ function InputFilters({ hook, profile }: Props) { {t('common.FILTER')} + {/* Assign task combobox */} {t('common.ASSIGN_TASK')} -
+
+ + +
+
+ +
+ + { + date ? +
+ + {date} +
+ : ( + + )} +
+ } + selected={new Date()} + onSelect={(dateI) => { + dateI && setDate(dateI.toDateString()); + }} + mode={'single'} + /> +
+
+ +
+ +
setIsBillable(!isBillable)} + style={isBillable ? { background: 'linear-gradient(to right, #3726a662, transparent)' } : { background: '#3726a662' }} + > +
+
+
+
+
+ + setStartTime(e.target.value)} + className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" + required + /> +
+ +
+ + setEndTime(e.target.value)} + className="w-full p-2 border text-[13px] font-bold border-gray-300 rounded-[10px]" + required + /> +
+
+ +
+ +
+ + {timeDifference} +
+
+ +
+ + +
+ +
+ + +
+ +
+ +