diff --git a/.cspell.json b/.cspell.json index a1cef8232..7281b6e66 100644 --- a/.cspell.json +++ b/.cspell.json @@ -8,8 +8,6 @@ "Accordian", "adipiscing", "aliqua", - "tblr", - "conver", "amet", "Andross", "animatable", @@ -23,6 +21,7 @@ "arrowleft", "asel", "Authentificate", + "authjs", "barcodes", "billrate", "binutils", @@ -48,6 +47,7 @@ "choos", "ciphertext", "classpath", + "Clik", "cloc", "clockcircleo", "cloudinary", @@ -62,13 +62,21 @@ "combx", "commitlint", "comparision", + "comparization", "compodoc", "consectetur", + "containerd", "contaniner", + "conver", "creatoe", + "dailyplan", "Darkmode", "datas", + "dataToDisplay", + "dearmor", + "deepscan", "Defaul", + "Desabled", "Descrption", "deserunt", "dimesions", @@ -79,6 +87,7 @@ "domutils", "DONT", "dotenv", + "dpkg", "Dropown", "dropwdown", "dummyimage", @@ -90,6 +99,7 @@ "embla", "Enderbury", "endregion", + "Entra", "Entypo", "envalid", "errr", @@ -115,6 +125,7 @@ "gauzystage", "gcloud", "Gitter", + "GlobalSkeleton", "gradlew", "graphicsmagick", "greenkeeper", @@ -170,9 +181,11 @@ "kanban", "Kanbanboard", "kanbandata", + "keyrings", "Kiritimati", "Kolkata", "Kosrae", + "Koyeb", "labore", "Lask", "lastest", @@ -191,6 +204,7 @@ "mathieudutour", "Mazen", "Metral", + "MICROSOFTENTRAID", "miliseconds", "millisencods", "mobx", @@ -200,7 +214,11 @@ "nextjs", "nimg", "nocheck", + "nodesource", + "nodistro", "noreferrer", + "Northflank", + "Notif", "Opena", "opentelemetry", "Ordereds", @@ -220,10 +238,12 @@ "phraseapp", "pkill", "Plaholder", + "plan", "plasmo", "plasmohq", "popperjs", "Pourtcent", + "prebuild", "precommit", "Pressable", "Pressible", @@ -236,6 +256,7 @@ "RECAPTCHA", "recieve", "Reconds", + "Relationnal", "Repobeats", "RESERVERD", "Rickert", @@ -249,6 +270,7 @@ "Settingfilter", "setuptools", "setwin", + "setwork", "shadcn", "shandow", "signin", @@ -259,9 +281,8 @@ "skey", "smalltext", "snyk", - "Sonner", "sonner", - "Notif", + "Sonner", "stackoverflow", "statsus", "statut", @@ -270,8 +291,8 @@ "stylelint", "stylesheet", "subsquently", - "svgs", "svgr", + "svgs", "Swith", "Syle", "Synk", @@ -281,6 +302,7 @@ "tanstack", "taskid", "taskstatus", + "tblr", "teamtask", "tempor", "testid", @@ -290,6 +312,7 @@ "tinvitations", "tnode", "Togger", + "tomorow", "Tongatapu", "tota", "TRANSFERT", @@ -326,29 +349,7 @@ "Xlarge", "xlcard", "xlight", - "yellowbox", - "Desabled", - "keyrings", - "dearmor", - "dpkg", - "containerd", - "nodistro", - "nodesource", - "Koyeb", - "Northflank", - "prebuild", - "dataToDisplay", - "GlobalSkeleton", - "dailyplan", - "tomorow", - "comparization", - "plan", - "setwork", - "Clik", - "Relationnal", - "authjs", - "MICROSOFTENTRAID", - "Entra" + "yellowbox" ], "useGitignore": true, "ignorePaths": [ diff --git a/.vscode/settings.json b/.vscode/settings.json index 5561567ae..170edab21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,43 +1,43 @@ { - "importSorter.generalConfiguration.sortOnBeforeSave": false, - "importSorter.sortConfiguration.joinImportPaths": false, - "cSpell.userWords": [], - "cSpell.enabled": true, - "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true, - "npm.packageManager": "yarn", - "prettier.trailingComma": "none", - "prettier.singleQuote": true, - "editor.formatOnSave": true, - "eslint.format.enable": true, - "editor.tabSize": 4, - "files.insertFinalNewline": true, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "never", - "source.sortMembers": "never", - "organizeImports": "never" - }, - "vsicons.presets.angular": true, - "deepscan.enable": true, - "cSpell.words": [], - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/node_modules": false, - "**/public/**/*.png": false, - "**/public/**/*.jpg": false, - "**/public/**/*.pdf": true - }, - "search.exclude": { - "**/node_modules": true, - "**/.idea": true, - "**/.idea/**/*.{xml,iml}": true, - "**/.iml": true, - "**/.yarn": true, - "**/yarn.lock": true - }, - "docwriter.style": "Auto-detect" + "importSorter.generalConfiguration.sortOnBeforeSave": false, + "importSorter.sortConfiguration.joinImportPaths": false, + "cSpell.userWords": [], + "cSpell.enabled": true, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "npm.packageManager": "yarn", + "prettier.trailingComma": "none", + "prettier.singleQuote": true, + "editor.formatOnSave": true, + "eslint.format.enable": true, + "editor.tabSize": 4, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "never", + "source.sortMembers": "never", + "organizeImports": "never" + }, + "vsicons.presets.angular": true, + "deepscan.enable": true, + "cSpell.words": ["Timepicker"], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/node_modules": false, + "**/public/**/*.png": false, + "**/public/**/*.jpg": false, + "**/public/**/*.pdf": true + }, + "search.exclude": { + "**/node_modules": true, + "**/.idea": true, + "**/.idea/**/*.{xml,iml}": true, + "**/.iml": true, + "**/.yarn": true, + "**/yarn.lock": true + }, + "docwriter.style": "Auto-detect" } diff --git a/apps/server-web/package.json b/apps/server-web/package.json index 626d9bcd5..2c9e42f39 100644 --- a/apps/server-web/package.json +++ b/apps/server-web/package.json @@ -51,7 +51,8 @@ "i18next-resources-to-backend": "^1.2.1", "react-i18next": "^14.1.0", "@radix-ui/react-switch": "^1.1.0", - "classnames": "^2.5.1" + "classnames": "^2.5.1", + "fast-glob": "^3.3.2" }, "devDependencies": { "electron": "28.1.0", diff --git a/apps/server-web/src/locales/i18n/bg/translation.json b/apps/server-web/src/locales/i18n/bg/translation.json index 1ca895f37..22c0e2529 100644 --- a/apps/server-web/src/locales/i18n/bg/translation.json +++ b/apps/server-web/src/locales/i18n/bg/translation.json @@ -63,7 +63,8 @@ "INFO": "Информация", "UPDATE_AVAILABLE": "Налична е нова актуализация! Моля, щракнете върху бутона Изтегляне сега по-долу.", "EXIT_MESSAGE": "Мрежата на сървъра все още работи, сигурни ли сте, че ще излезете от приложението?", - "UPDATE_SUCCESS": "Актуализирайте успешно" + "UPDATE_SUCCESS": "Актуализирайте успешно", + "SERVER_RUN_DIALOG": "Сървърната мрежа работи в момента, искате ли да рестартирате сървъра?" }, "LANGUAGES": { "en": "Английски", diff --git a/apps/server-web/src/locales/i18n/en/translation.json b/apps/server-web/src/locales/i18n/en/translation.json index 91ba9f7d5..1d506ae0e 100644 --- a/apps/server-web/src/locales/i18n/en/translation.json +++ b/apps/server-web/src/locales/i18n/en/translation.json @@ -63,7 +63,8 @@ "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 ?", - "UPDATE_SUCCESS": "Update Successfully" + "UPDATE_SUCCESS": "Update Successfully", + "SERVER_RUN_DIALOG": "Server web currently running, You want to restart the server ?" }, "LANGUAGES": { "en": "English", diff --git a/apps/server-web/src/main/helpers/constant.ts b/apps/server-web/src/main/helpers/constant.ts index 2885630d1..89889dd37 100644 --- a/apps/server-web/src/main/helpers/constant.ts +++ b/apps/server-web/src/main/helpers/constant.ts @@ -14,7 +14,8 @@ export const EventLists = { UPDATE_CANCELLED: 'UPDATE_CANCELLED', CHANGE_LANGUAGE: 'CHANGE_LANGUAGE', OPEN_WEB: 'OPEN_WEB', - SERVER_WINDOW: 'SERVER_WINDOW' + SERVER_WINDOW: 'SERVER_WINDOW', + RESTART_SERVER: 'RESTART_SERVER' } export const SettingPageTypeMessage = { @@ -33,7 +34,8 @@ export const SettingPageTypeMessage = { langChange: 'lang', updateSetting: 'update-setting', updateSettingResponse: 'update-setting-response', - updateCancel: 'update-cancel' + updateCancel: 'update-cancel', + restartServer: 'restart-server' } export const ServerPageTypeMessage = { diff --git a/apps/server-web/src/main/helpers/index.ts b/apps/server-web/src/main/helpers/index.ts index e1b9aad0a..f1642c4ed 100644 --- a/apps/server-web/src/main/helpers/index.ts +++ b/apps/server-web/src/main/helpers/index.ts @@ -1 +1,2 @@ -export * from './create-window' +export * from './create-window'; +export * from './replace-config'; diff --git a/apps/server-web/src/main/helpers/replace-config.ts b/apps/server-web/src/main/helpers/replace-config.ts new file mode 100644 index 000000000..98e12f9af --- /dev/null +++ b/apps/server-web/src/main/helpers/replace-config.ts @@ -0,0 +1,44 @@ +import fs from 'fs'; +import path from 'path'; +import fg from 'fast-glob'; +import os from 'os'; + +type EnvOptions = { + before: { + NEXT_PUBLIC_GAUZY_API_SERVER_URL?: string + }, + after: { + NEXT_PUBLIC_GAUZY_API_SERVER_URL?: string + } +} + +const scanAllFiles = async (files: string[], oldConfig: string, newConfig: string) => { + files.forEach((file) => { + if (path.extname(file) === '.js') { + try { + const data = fs.readFileSync(file, 'utf-8'); + const result = data.replace(new RegExp(oldConfig, 'g'), newConfig); + fs.writeFileSync(file, result, 'utf8'); + } catch (error) { + console.log('error replace', error); + } + } + }); +} +export const replaceConfig = async (folderPath: string, envOptions: EnvOptions) => { + try { + console.log('all files path', folderPath); + if (os.platform() === 'win32') { + folderPath = folderPath.replace(/\\/g, '/'); + } + console.log('final path', folderPath); + const NEXT_PUBLIC_GAUZY_API_SERVER_URL_BEFORE = `"NEXT_PUBLIC_GAUZY_API_SERVER_URL","${envOptions.before.NEXT_PUBLIC_GAUZY_API_SERVER_URL}"`; + const NEXT_PUBLIC_GAUZY_API_SERVER_URL_AFTER = `"NEXT_PUBLIC_GAUZY_API_SERVER_URL","${envOptions.after.NEXT_PUBLIC_GAUZY_API_SERVER_URL}"`; + const NEXT_PUBLIC_GAUZY_API_SERVER_URL_DEFAULT = `"NEXT_PUBLIC_GAUZY_API_SERVER_URL","https://api.ever.team"`; + const files = await fg(`${folderPath}/**/*`, { onlyFiles: true }); + await scanAllFiles(files, NEXT_PUBLIC_GAUZY_API_SERVER_URL_BEFORE, NEXT_PUBLIC_GAUZY_API_SERVER_URL_AFTER); + await scanAllFiles(files, NEXT_PUBLIC_GAUZY_API_SERVER_URL_DEFAULT, NEXT_PUBLIC_GAUZY_API_SERVER_URL_AFTER); + } catch (error) { + console.log('error on replacing file', error); + } +} diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index ebefb4b77..c0249ea0a 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -11,7 +11,11 @@ import { mainBindings } from 'i18next-electron-fs-backend'; import i18nextMainBackend from '../configs/i18n.mainconfig'; import fs from 'fs'; import { WebServer } from './helpers/interfaces'; +import { replaceConfig } from './helpers'; import Log from 'electron-log'; +import MenuBuilder from './menu'; + + console.log = Log.log; Object.assign(console, Log.functions); @@ -32,8 +36,10 @@ let intervalUpdate: NodeJS.Timeout; let tray: Tray; let settingWindow: BrowserWindow | null = null; let logWindow: BrowserWindow | null = null; +let SettingMenu: any = null; +let ServerWindowMenu: any = null; -Log.hooks.push((message:any, transport) => { +Log.hooks.push((message: any, transport) => { if (transport !== Log.transports.file) { return message; } @@ -167,7 +173,12 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW') => { mainBindings(ipcMain, settingWindow, fs); settingWindow.on('closed', () => { settingWindow = null; + SettingMenu = null }); + if (!SettingMenu) { + SettingMenu = new MenuBuilder(settingWindow); + } + SettingMenu.buildMenu(); break; case 'LOG_WINDOW': logWindow = new BrowserWindow(defaultOptionWindow); @@ -176,7 +187,12 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW') => { mainBindings(ipcMain, logWindow, fs); logWindow.on('closed', () => { logWindow = null; + ServerWindowMenu = null }) + if (!ServerWindowMenu) { + ServerWindowMenu = new MenuBuilder(logWindow); + } + ServerWindowMenu.buildMenu(); break; default: break; @@ -207,6 +223,16 @@ const stopServer = async () => { await desktopServer.stop(); }; +const restartServer = async () => { + await desktopServer.stop(); + const waitingForServerStop = setInterval(async () => { + if (!isServerRun) { + clearInterval(waitingForServerStop); + await runServer() + } + }, 1000) +} + const getEnvApi = () => { const setting: WebServer = LocalStore.getStore('config') return setting.server; @@ -237,15 +263,19 @@ const onInitApplication = () => { }) eventEmitter.on(EventLists.webServerStop, async () => { - isServerRun = false; await stopServer(); + isServerRun = false; + }) + + eventEmitter.on(EventLists.RESTART_SERVER, async () => { + await restartServer(); }) eventEmitter.on(EventLists.webServerStarted, () => { 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('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, { @@ -262,7 +292,7 @@ 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('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, { @@ -385,14 +415,35 @@ ipcMain.on('message', async (event, arg) => { event.reply('message', `${arg} World!`) }) -ipcMain.on(IPC_TYPES.SETTING_PAGE, (event, arg) => { +ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { console.log('main setting page', arg); switch (arg.type) { case SettingPageTypeMessage.saveSetting: + const existingConfig = getEnvApi(); LocalStore.updateConfigSetting({ server: arg.data }); - event.sender.send(IPC_TYPES.SETTING_PAGE, { type: SettingPageTypeMessage.mainResponse, data: true }); + const dirFiles = 'standalone/apps/web/.next'; + const devDirFilesPath = path.join(__dirname, resourceDir.webServer, dirFiles); + const packDirFilesPath = path.join(process.resourcesPath, 'release', 'app', 'dist', dirFiles) + const diFilesPath = isPack ? packDirFilesPath : devDirFilesPath; + await replaceConfig( + diFilesPath, + { + before: { + NEXT_PUBLIC_GAUZY_API_SERVER_URL: existingConfig?.NEXT_PUBLIC_GAUZY_API_SERVER_URL + }, + after: { + NEXT_PUBLIC_GAUZY_API_SERVER_URL: arg.data.NEXT_PUBLIC_GAUZY_API_SERVER_URL + } + } + ) + event.sender.send(IPC_TYPES.SETTING_PAGE, { + type: SettingPageTypeMessage.mainResponse, data: { + status: true, + isServerRun: isServerRun + } + }); break; case SettingPageTypeMessage.checkUpdate: updater.checkUpdate(); @@ -408,6 +459,9 @@ ipcMain.on(IPC_TYPES.SETTING_PAGE, (event, arg) => { event.sender.send('languageSignal', arg.data); eventEmitter.emit(EventLists.CHANGE_LANGUAGE, { code: arg.data }) break; + case SettingPageTypeMessage.restartServer: + eventEmitter.emit(EventLists.RESTART_SERVER) + break; default: break; } diff --git a/apps/server-web/src/main/menu.ts b/apps/server-web/src/main/menu.ts index ba0fb7709..7e788eef2 100644 --- a/apps/server-web/src/main/menu.ts +++ b/apps/server-web/src/main/menu.ts @@ -54,27 +54,13 @@ export default class MenuBuilder { buildDarwinTemplate(): MenuItemConstructorOptions[] { const subMenuAbout: DarwinMenuItemConstructorOptions = { - label: 'Electron', + label: app.getName(), submenu: [ { - label: 'About ElectronReact', + label: `About ${app.getName()}`, selector: 'orderFrontStandardAboutPanel:', }, { type: 'separator' }, - { label: 'Services', submenu: [] }, - { type: 'separator' }, - { - label: 'Hide ElectronReact', - accelerator: 'Command+H', - selector: 'hide:', - }, - { - label: 'Hide Others', - accelerator: 'Command+Shift+H', - selector: 'hideOtherApplications:', - }, - { label: 'Show All', selector: 'unhideAllApplications:' }, - { type: 'separator' }, { label: 'Quit', accelerator: 'Command+Q', @@ -84,22 +70,6 @@ export default class MenuBuilder { }, ], }; - const subMenuEdit: DarwinMenuItemConstructorOptions = { - label: 'Edit', - submenu: [ - { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, - { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, - { type: 'separator' }, - { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, - { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, - { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, - { - label: 'Select All', - accelerator: 'Command+A', - selector: 'selectAll:', - }, - ], - }; const subMenuViewDev: MenuItemConstructorOptions = { label: 'View', submenu: [ @@ -110,13 +80,6 @@ export default class MenuBuilder { this.mainWindow.webContents.reload(); }, }, - { - label: 'Toggle Full Screen', - accelerator: 'Ctrl+Command+F', - click: () => { - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); - }, - }, { label: 'Toggle Developer Tools', accelerator: 'Alt+Command+I', @@ -130,10 +93,17 @@ export default class MenuBuilder { label: 'View', submenu: [ { - label: 'Toggle Full Screen', - accelerator: 'Ctrl+Command+F', + label: 'Reload', + accelerator: 'Command+R', click: () => { - this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); + this.mainWindow.webContents.reload(); + }, + }, + { + label: 'Toggle Developer Tools', + accelerator: 'Alt+Command+I', + click: () => { + this.mainWindow.webContents.toggleDevTools(); }, }, ], @@ -148,7 +118,6 @@ export default class MenuBuilder { }, { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, { type: 'separator' }, - { label: 'Bring All to Front', selector: 'arrangeInFront:' }, ], }; const subMenuHelp: MenuItemConstructorOptions = { @@ -157,27 +126,21 @@ export default class MenuBuilder { { label: 'Learn More', click() { - shell.openExternal('https://electronjs.org'); + shell.openExternal('https://ever.team/'); }, }, { label: 'Documentation', click() { shell.openExternal( - 'https://github.com/electron/electron/tree/main/docs#readme', + 'https://github.com/ever-co/ever-teams/blob/develop/README.md', ); }, }, - { - label: 'Community Discussions', - click() { - shell.openExternal('https://www.electronjs.org/community'); - }, - }, { label: 'Search Issues', click() { - shell.openExternal('https://github.com/electron/electron/issues'); + shell.openExternal('https://github.com/ever-co/ever-teams/issues'); }, }, ], @@ -189,7 +152,7 @@ export default class MenuBuilder { ? subMenuViewDev : subMenuViewProd; - return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; + return [subMenuAbout, subMenuView, subMenuWindow, subMenuHelp]; } buildDefaultTemplate() { @@ -197,10 +160,6 @@ export default class MenuBuilder { { label: '&File', submenu: [ - { - label: '&Open', - accelerator: 'Ctrl+O', - }, { label: '&Close', accelerator: 'Ctrl+W', @@ -223,15 +182,6 @@ export default class MenuBuilder { this.mainWindow.webContents.reload(); }, }, - { - label: 'Toggle &Full Screen', - accelerator: 'F11', - click: () => { - this.mainWindow.setFullScreen( - !this.mainWindow.isFullScreen(), - ); - }, - }, { label: 'Toggle &Developer Tools', accelerator: 'Alt+Ctrl+I', @@ -241,15 +191,13 @@ export default class MenuBuilder { }, ] : [ - { - label: 'Toggle &Full Screen', - accelerator: 'F11', - click: () => { - this.mainWindow.setFullScreen( - !this.mainWindow.isFullScreen(), - ); - }, + { + label: 'Toggle &Developer Tools', + accelerator: 'Alt+Ctrl+I', + click: () => { + this.mainWindow.webContents.toggleDevTools(); }, + }, ], }, { @@ -258,27 +206,21 @@ export default class MenuBuilder { { label: 'Learn More', click() { - shell.openExternal('https://electronjs.org'); + shell.openExternal('https://ever.team/'); }, }, { label: 'Documentation', click() { shell.openExternal( - 'https://github.com/electron/electron/tree/main/docs#readme', + 'https://github.com/ever-co/ever-teams/blob/develop/README.md', ); }, }, - { - label: 'Community Discussions', - click() { - shell.openExternal('https://www.electronjs.org/community'); - }, - }, { label: 'Search Issues', click() { - shell.openExternal('https://github.com/electron/electron/issues'); + shell.openExternal('https://github.com/ever-co/ever-teams/issues'); }, }, ], diff --git a/apps/server-web/src/renderer/components/Popup.tsx b/apps/server-web/src/renderer/components/Popup.tsx index f1d51c782..0fdbb9629 100644 --- a/apps/server-web/src/renderer/components/Popup.tsx +++ b/apps/server-web/src/renderer/components/Popup.tsx @@ -46,6 +46,26 @@ export function Popup(props: IPopupComponent) { )} + {props.type === 'warning' && ( +
+
+ + + +
+
+ )} {props.type === 'error' && (
@@ -73,7 +93,7 @@ export function Popup(props: IPopupComponent) { className="text-lg leading-6 font-medium text-gray-900" id="modal-headline" > - {props.type == 'success' + {props.type == 'success' || 'warning' ? t('MESSAGE.SUCCESS') : t('MESSAGE.ERROR')} @@ -84,13 +104,21 @@ export function Popup(props: IPopupComponent) {
-
+
+ {props.closeAction && ( + + )}
diff --git a/apps/server-web/src/renderer/libs/interfaces/i-components.ts b/apps/server-web/src/renderer/libs/interfaces/i-components.ts index dbafae505..770bbb72a 100644 --- a/apps/server-web/src/renderer/libs/interfaces/i-components.ts +++ b/apps/server-web/src/renderer/libs/interfaces/i-components.ts @@ -12,8 +12,9 @@ type IToastComponent = { type IPopupComponent = { isShowPopup: boolean; modalAction: () => void; - type: 'success' | 'error' | 'none'; + type: 'success' | 'error' | 'none' | 'warning'; message: string; + closeAction?: () => void; }; type IProgressComponent = { diff --git a/apps/server-web/src/renderer/libs/interfaces/i-setting.ts b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts index 4b47dd5e5..0053f1eda 100644 --- a/apps/server-web/src/renderer/libs/interfaces/i-setting.ts +++ b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts @@ -32,8 +32,9 @@ interface IServerSetting { } interface IPopup { - type: 'success' | 'error' | 'none'; + type: 'success' | 'error' | 'none' | 'warning'; isShow: boolean; + isDialog: boolean; } interface ILanguages { diff --git a/apps/server-web/src/renderer/pages/Setting.tsx b/apps/server-web/src/renderer/pages/Setting.tsx index 87ef415f1..90b699ac7 100644 --- a/apps/server-web/src/renderer/pages/Setting.tsx +++ b/apps/server-web/src/renderer/pages/Setting.tsx @@ -16,6 +16,7 @@ import { ILanguages, ISideMenu, } from '../libs/interfaces'; +import { useTranslation } from 'react-i18next'; export function Setting() { const [menus, setMenu] = useState([ @@ -35,6 +36,7 @@ export function Setting() { isActive: false, }, ]); + const { t } = useTranslation(); const [updateSetting, setUpdateSetting] = useState({ autoUpdate: false, @@ -64,11 +66,13 @@ export function Setting() { const [popupServer, setPopupServer] = useState({ isShow: false, + isDialog: false, type: 'none', }); const [popupUpdater, setPopupUpdater] = useState({ isShow: false, + isDialog: false, type: 'none', }); @@ -136,6 +140,11 @@ export function Setting() { setPopupServer((prevData) => ({ ...prevData, isShow: !prevData.isShow })); }; + const restartServer = () => { + setPopupServer((prevData) => ({ ...prevData, isShow: !prevData.isShow })); + sendingMessageToMain({}, SettingPageTypeMessage.restartServer); + }; + const setPopupUpdaterState = () => { setPopupUpdater((prevData) => ({ ...prevData, isShow: !prevData.isShow })); }; @@ -147,12 +156,22 @@ export function Setting() { serverSetting={serverSetting} saveSetting={saveSetting} Popup={ - + popupServer.isDialog ? ( + + ) : ( + + ) } /> ); @@ -226,6 +245,7 @@ export function Setting() { setLoading(false); setPopupUpdater({ isShow: true, + isDialog: false, type: 'error', }); break; @@ -252,9 +272,16 @@ export function Setting() { }); break; case SettingPageTypeMessage.mainResponse: + let typeMessage: any; + if (arg.data.status && arg.data.isServerRun) { + typeMessage = 'warning'; + } else { + typeMessage = arg.data.status ? 'success' : 'error'; + } setPopupServer({ isShow: true, - type: arg.data ? 'success' : 'error', + type: typeMessage, + isDialog: arg.data.isServerRun, }); break; case SettingPageTypeMessage.showVersion: diff --git a/apps/web/app/[locale]/kanban/page.tsx b/apps/web/app/[locale]/kanban/page.tsx index a514c49a4..646b39716 100644 --- a/apps/web/app/[locale]/kanban/page.tsx +++ b/apps/web/app/[locale]/kanban/page.tsx @@ -8,9 +8,9 @@ import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Container, Divider } from 'lib/components'; import { KanbanView } from 'lib/features/team-members-kanban-view'; import { Footer, MainLayout } from 'lib/layout'; -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; -import { useParams } from 'next/navigation'; +import { useParams, useSearchParams } from 'next/navigation'; import ImageComponent, { ImageOverlapperProps } from 'lib/components/image-overlapper'; import Separator from '@components/ui/separator'; import HeaderTabs from '@components/pages/main/header-tabs'; @@ -55,11 +55,15 @@ const Kanban = () => { const fullWidth = useRecoilValue(fullWidthState); const currentLocale = params ? params.locale : null; const [activeTab, setActiveTab] = useState(KanbanTabs.TODAY); - const breadcrumbPath = [ - { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, - { title: activeTeam?.name || '', href: '/' }, - { title: t('common.KANBAN'), href: `/${currentLocale}/kanban` } - ]; + const employee = useSearchParams().get('employee'); + const breadcrumbPath = useMemo( + () => [ + { title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' }, + { title: activeTeam?.name || '', href: '/' }, + { title: t('common.KANBAN'), href: `/${currentLocale}/kanban` } + ], + [activeTeam?.name, currentLocale] + ); const activeTeamMembers = activeTeam?.members ? activeTeam.members : []; @@ -85,6 +89,22 @@ const Kanban = () => { value: issues as any, onValueChange: setIssues as any }); + + useEffect(() => { + const lastPath = breadcrumbPath.slice(-1)[0]; + if (employee) { + if (lastPath.title == 'Kanban') { + breadcrumbPath.push({ title: employee, href: `/${currentLocale}/kanban?employee=${employee}` }); + } else { + breadcrumbPath.pop(); + breadcrumbPath.push({ title: employee, href: `/${currentLocale}/kanban?employee=${employee}` }); + } + } else { + if (lastPath.title !== 'Kanban') { + breadcrumbPath.pop(); + } + } + }, [breadcrumbPath, currentLocale, employee]); return ( <> @@ -99,7 +119,11 @@ const Kanban = () => { className="h-[calc(100vh-_22px)]" >
-
+
@@ -122,7 +146,7 @@ const Kanban = () => {
- +
@@ -250,11 +274,12 @@ const Kanban = () => { )}
-
- - -
-
+
+ +
+
); diff --git a/apps/web/app/[locale]/page-component.tsx b/apps/web/app/[locale]/page-component.tsx index dc4e771ad..18ba41fa7 100644 --- a/apps/web/app/[locale]/page-component.tsx +++ b/apps/web/app/[locale]/page-component.tsx @@ -3,14 +3,14 @@ 'use client'; import React, { useEffect, useState } from 'react'; -import { useOrganizationTeams } from '@app/hooks'; +import { useDailyPlan, useOrganizationTeams, useUserProfilePage } from '@app/hooks'; import { clsxm } from '@app/utils'; import NoTeam from '@components/pages/main/no-team'; import { withAuthentication } from 'lib/app/authenticator'; import { Breadcrumb, Card } from 'lib/components'; import { AuthUserTaskInput, TeamInvitations, TeamMembers, Timer, UnverifiedEmail } from 'lib/features'; import { MainLayout } from 'lib/layout'; -import { IssuesView } from '@app/constants'; +import { DAILY_PLAN_SHOW_MODAL, IssuesView } from '@app/constants'; import { useNetworkState } from '@uidotdev/usehooks'; import Offline from '@components/pages/offline'; import { useTranslations } from 'next-intl'; @@ -31,11 +31,20 @@ import { PeoplesIcon } from 'assets/svg'; import TeamMemberHeader from 'lib/features/team-member-header'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@components/ui/resizable'; import { TeamOutstandingNotifications } from 'lib/features/team/team-outstanding-notifications'; +import { DailyPlanCompareEstimatedModal } from 'lib/features/daily-plan'; function MainPage() { const t = useTranslations(); - + const [isOpen, setIsOpen] = useState(false) + const { todayPlan } = useDailyPlan(); + const profile = useUserProfilePage(); const [headerSize, setHeaderSize] = useState(10); + const plan = todayPlan.find((plan) => plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])); + + const defaultOpenPopup = + typeof window !== 'undefined' + ? (window.localStorage.getItem(DAILY_PLAN_SHOW_MODAL)) || null + : new Date().toISOString().split('T')[0]; const { isTeamMember, isTrackingEnabled, activeTeam } = useOrganizationTeams(); const [fullWidth, setFullWidth] = useRecoilState(fullWidthState); @@ -54,6 +63,14 @@ function MainPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [path, setView]); + useEffect(() => { + window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); + if (defaultOpenPopup !== new Date().toISOString().split('T')[0] || defaultOpenPopup === null) { + setIsOpen(true) + } + }, [defaultOpenPopup, plan]) + + React.useEffect(() => { window && window?.localStorage.getItem('conf-fullWidth-mode'); setFullWidth(JSON.parse(window?.localStorage.getItem('conf-fullWidth-mode') || 'true')); @@ -64,6 +81,10 @@ function MainPage() { } return ( <> + setIsOpen((prev) => { + window.localStorage.setItem(DAILY_PLAN_SHOW_MODAL, new Date().toISOString().split('T')[0]); + return !prev; + })} todayPlan={todayPlan} profile={profile} />
{/*
*/} diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index f6a52203e..d58c076eb 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -267,6 +267,8 @@ export const languagesFlags = [ 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'; +export const ESTIMATE_POPUP_SHOWN_DATE = 'last-estimate-popup-date'; +export const DAILY_PLAN_SHOW_MODAL = 'daily-plan-modal'; // OAuth providers keys diff --git a/apps/web/app/helpers/daily-plan-estimated.ts b/apps/web/app/helpers/daily-plan-estimated.ts new file mode 100644 index 000000000..498648ed7 --- /dev/null +++ b/apps/web/app/helpers/daily-plan-estimated.ts @@ -0,0 +1,29 @@ +"use client" + +import { IDailyPlan } from "@app/interfaces"; + +export const dailyPlanCompareEstimated = (plans: IDailyPlan[]) => { + + const plan = plans.find((plan) => plan.date?.toString()?.startsWith(new Date()?.toISOString().split('T')[0])); + + const times = plan?.tasks?.map((task) => task?.estimate).filter((time): time is number => typeof time === 'number') ?? []; + const estimated = plan?.tasks?.map((task) => task.estimate! > 0); + + let estimatedTime = 0; + if (times.length > 0) estimatedTime = times.reduce((acc, cur) => acc + cur, 0) ?? 0; + + const workedTimes = + plan?.tasks?.map((task) => task.totalWorkedTime).filter((time): time is number => typeof time === 'number') ?? + []; + + let totalWorkTime = 0; + if (workedTimes?.length > 0) totalWorkTime = workedTimes.reduce((acc, cur) => acc + cur, 0) ?? 0; + + const result = estimated?.every(Boolean) ? estimatedTime - totalWorkTime : null; + + return { + result, + totalWorkTime, + estimatedTime + } +} diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index 434da0f59..8aa44d04e 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -1,10 +1,9 @@ 'use client'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { useCallback, useEffect, useState } from 'react'; +import { useRecoilState } from 'recoil'; +import { useCallback, useEffect } from 'react'; import { useQuery } from '../useQuery'; import { - activeTeamState, dailyPlanFetchingState, dailyPlanListState, employeePlansListState, @@ -27,22 +26,11 @@ import { import { ICreateDailyPlan, IDailyPlanTasksUpdate, IRemoveTaskFromManyPlans, 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 type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; export function useDailyPlan() { - 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); @@ -66,7 +54,6 @@ export function useDailyPlan() { const [dailyPlanFetching, setDailyPlanFetching] = useRecoilState(dailyPlanFetchingState); const { firstLoadData: firstLoadDailyPlanData, firstLoad } = useFirstLoad(); - useEffect(() => { if (firstLoad) { setDailyPlanFetching(loading); @@ -78,10 +65,8 @@ export function useDailyPlan() { if (response.data.items.length) { const { items, total } = response.data; setDailyPlan({ items, total }); - } }); - }, [getAllQueryCall, setDailyPlan]); const getMyDailyPlans = useCallback(() => { @@ -297,37 +282,10 @@ 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]; - - if (currentUser?.totalTodayTasks) { - 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, @@ -378,9 +336,6 @@ export function useDailyPlan() { pastPlans, outstandingPlans, todayPlan, - sortedPlans, - - addTodayPlanTrigger, - setAddTodayPlanTrigger + sortedPlans }; } diff --git a/apps/web/app/hooks/features/useKanban.ts b/apps/web/app/hooks/features/useKanban.ts index 42596ba2e..de81ed715 100644 --- a/apps/web/app/hooks/features/useKanban.ts +++ b/apps/web/app/hooks/features/useKanban.ts @@ -6,6 +6,7 @@ import { ITaskStatusItemList, ITeamTask } from '@app/interfaces'; import { useTeamTasks } from './useTeamTasks'; import { IKanban } from '@app/interfaces/IKanban'; import { TStatusItem } from 'lib/features'; +import { useSearchParams } from 'next/navigation'; export function useKanban() { const [loading, setLoading] = useState(true); const [searchTasks, setSearchTasks] = useState(''); @@ -22,6 +23,7 @@ export function useKanban() { const { tasks: newTask, tasksFetching, updateTask } = useTeamTasks(); const [priority, setPriority] = useState([]); const [sizes, setSizes] = useState([]); + const employee = useSearchParams().get('employee'); useEffect(() => { if (!taskStatusHook.loading && !tasksFetching) { let kanban = {}; @@ -44,6 +46,13 @@ export function useKanban() { }) .filter((task: ITeamTask) => { return epics.length ? epics.includes(task.id) : true; + }) + .filter((task: ITeamTask) => { + if (employee) { + return task.members.map((el) => el.fullName).includes(employee as string); + } else { + return task; + } }); const getTasksByStatus = (status: string | undefined) => { @@ -62,7 +71,7 @@ export function useKanban() { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [taskStatusHook.loading, tasksFetching, newTask, searchTasks, priority, sizes, labels, epics, issues]); + }, [taskStatusHook.loading, tasksFetching, newTask, searchTasks, priority, sizes, labels, epics, issues, employee]); /** * collapse or show kanban column diff --git a/apps/web/app/hooks/features/useTimer.ts b/apps/web/app/hooks/features/useTimer.ts index 6f1b28515..38b298b69 100644 --- a/apps/web/app/hooks/features/useTimer.ts +++ b/apps/web/app/hooks/features/useTimer.ts @@ -210,8 +210,11 @@ 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 - const isPlanVerified = - !!hasPlan?.workTimePlanned && !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0); + const isPlanVerified = requirePlan + ? hasPlan && + hasPlan?.workTimePlanned > 0 && + !!hasPlan?.tasks?.every((task) => task.estimate && task.estimate > 0) + : true; 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 10f2a9d08..1a100e3f5 100644 --- a/apps/web/components/shared/timer/timer-card.tsx +++ b/apps/web/components/shared/timer/timer-card.tsx @@ -1,3 +1,4 @@ +import { ESTIMATE_POPUP_SHOWN_DATE, TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; import { pad } from '@app/helpers/number'; import { useModal } from '@app/hooks'; import { useTaskStatistics } from '@app/hooks/features/useTaskStatistics'; @@ -27,11 +28,15 @@ const Timer = () => { const { closeModal, isOpen, openModal } = useModal(); const timerHanlder = () => { + const currentDate = new Date().toISOString().split('T')[0]; + const lastPopupDate = window && window?.localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const lastPopupEstimates = window && window?.localStorage.getItem(ESTIMATE_POPUP_SHOWN_DATE); + if (timerStatusFetching || !canRunTimer) return; if (timerStatus?.running) { stopTimer(); } else { - if (!isPlanVerified) { + if (!isPlanVerified || lastPopupDate !== currentDate || lastPopupEstimates !== currentDate) { openModal(); } else { startTimer(); diff --git a/apps/web/components/ui/scroll-area.tsx b/apps/web/components/ui/scroll-area.tsx new file mode 100644 index 000000000..c43925b00 --- /dev/null +++ b/apps/web/components/ui/scroll-area.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "lib/utils" + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/apps/web/lib/components/image-overlapper.tsx b/apps/web/lib/components/image-overlapper.tsx index 7f5223b7b..1f20eb588 100644 --- a/apps/web/lib/components/image-overlapper.tsx +++ b/apps/web/lib/components/image-overlapper.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import Image from 'next/image'; import Link from 'next/link'; @@ -17,6 +17,7 @@ import { TaskAvatars } from 'lib/features'; import { FaCheck } from 'react-icons/fa6'; import TeamMember from 'lib/components/team-member'; import { IEmployee } from '@app/interfaces'; +import { Url } from 'next/dist/shared/lib/router/router'; export interface ImageOverlapperProps { id: string; @@ -42,7 +43,8 @@ export default function ImageOverlapper({ arrowData = null, hasActiveMembers = false, assignTaskButtonCall = false, - hasInfo = '' + hasInfo = '', + onAvatarClickRedirectTo = 'profile' }: { images: ImageOverlapperProps[]; radius?: number; @@ -54,6 +56,7 @@ export default function ImageOverlapper({ hasActiveMembers?: boolean; assignTaskButtonCall?: boolean; hasInfo?: string; + onAvatarClickRedirectTo?: 'kanbanTasks' | 'profile'; }) { // Split the array into two arrays based on the display number const firstArray = images?.slice(0, displayImageCount); @@ -84,6 +87,26 @@ export default function ImageOverlapper({ } }; + const onRedirect = useCallback( + (image: ImageOverlapperProps): Url => { + switch (onAvatarClickRedirectTo) { + case 'kanbanTasks': + return { + pathname: '/kanban', + query: { + employee: activeTeam?.members.find((el) => el.employee.userId === image.id)?.employee + .fullName + } + }; + case 'profile': + return { pathname: `/profile/${image.id}`, query: { name: image.alt } }; + default: + return {}; + } + }, + [activeTeam?.members, onAvatarClickRedirectTo] + ); + const onCLickValidate = () => { setValidate(!validate); closeModal(); @@ -212,7 +235,7 @@ export default function ImageOverlapper({ className="relative " > {firstArray.map((image, index) => ( - +
{ return ( diff --git a/apps/web/lib/components/index.ts b/apps/web/lib/components/index.ts index 90dfd5b2b..38faad35d 100644 --- a/apps/web/lib/components/index.ts +++ b/apps/web/lib/components/index.ts @@ -21,6 +21,7 @@ export * from './separator'; export * from './color-picker'; export * from './no-data'; export * from './pagination'; +export * from './time-picker' export * from './inputs/input'; export * from './inputs/auth-code-input'; diff --git a/apps/web/lib/components/inputs/auth-code-input.tsx b/apps/web/lib/components/inputs/auth-code-input.tsx index a087b2165..7a8a2b4e2 100644 --- a/apps/web/lib/components/inputs/auth-code-input.tsx +++ b/apps/web/lib/components/inputs/auth-code-input.tsx @@ -188,26 +188,22 @@ export const AuthCodeInputField = forwardRef( }; const handleOnPaste = (e: React.ClipboardEvent) => { + e.preventDefault(); const pastedValue = e.clipboardData.getData('Text'); let currentInput = 0; for (let i = 0; i < pastedValue.length; i++) { const pastedCharacter = pastedValue.charAt(i); - const currentValue = inputsRef.current[currentInput].value; if (pastedCharacter.match(inputProps.pattern)) { - if (!currentValue) { - inputsRef.current[currentInput].value = pastedCharacter; - if (inputsRef.current[currentInput].nextElementSibling !== null) { - (inputsRef.current[currentInput].nextElementSibling as HTMLInputElement).focus(); - currentInput++; - } + inputsRef.current[currentInput].value = pastedCharacter; + if (inputsRef.current[currentInput].nextElementSibling !== null) { + (inputsRef.current[currentInput].nextElementSibling as HTMLInputElement).focus(); + currentInput++; } } } sendResult(); - - e.preventDefault(); }; const handleAutoComplete = (code: string) => { diff --git a/apps/web/lib/components/modal.tsx b/apps/web/lib/components/modal.tsx index c1af3fc71..33ea9eed9 100644 --- a/apps/web/lib/components/modal.tsx +++ b/apps/web/lib/components/modal.tsx @@ -13,6 +13,7 @@ type Props = { closeModal: () => void; className?: string; alignCloseIcon?: boolean; + showCloseIcon?: boolean; } & PropsWithChildren; export function Modal({ @@ -23,7 +24,8 @@ export function Modal({ titleClass, description, className, - alignCloseIcon + alignCloseIcon, + showCloseIcon = true }: Props) { const refDiv = useRef(null); @@ -50,20 +52,22 @@ export function Modal({ > {title && {title}} {description && {description}} -
- close -
+ {showCloseIcon && ( +
+ close +
+ )} {children}
diff --git a/apps/web/lib/components/sidebar-accordian.tsx b/apps/web/lib/components/sidebar-accordian.tsx index 39e6a2e1e..7d63cb34e 100644 --- a/apps/web/lib/components/sidebar-accordian.tsx +++ b/apps/web/lib/components/sidebar-accordian.tsx @@ -28,9 +28,8 @@ export const SidebarAccordian = ({ children, title, className, wrapperClassName, {children && ( diff --git a/apps/web/lib/components/time-picker/index.tsx b/apps/web/lib/components/time-picker/index.tsx new file mode 100644 index 000000000..dba5217c5 --- /dev/null +++ b/apps/web/lib/components/time-picker/index.tsx @@ -0,0 +1,184 @@ +"use client" +import React, { useCallback, useState } from 'react' +import { TimerIcon } from "lucide-react" +import { cn } from "lib/utils" +import { Button } from "@components/ui/button" + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@components/ui/popover" +import { clsxm } from '@app/utils' + +export type TimePickerValue = { + hours: string; + minute: string; + meridiem: 'AM' | 'PM'; +}; + +interface IPopoverTimePicker { + defaultValue?: TimePickerValue; + onChange?: (value: TimePickerValue) => void; +} + +export function TimePicker({ onChange, defaultValue }: IPopoverTimePicker) { + const [time, setTime] = useState({ + hours: defaultValue?.hours, + minute: defaultValue?.minute, + meridiem: defaultValue?.meridiem, + }); + + const handleTimeChange = (newTime: any) => { + setTime(newTime); + onChange!(newTime) + }; + return ( + + + + + + + + + ); +} + + + +const TimePickerInput = ({ onTimeChange }: { onTimeChange: (_: any) => void }) => { + const [time, setTime] = useState({ + hours: 0, + minutes: 0, + meridiem: true, + }); + + const handleHoursClick = useCallback((index: number) => { + setTime((prev) => { + const newTime = { ...prev, hours: index }; + onTimeChange({ + ...newTime, + hours: String(index + 1).padStart(2, '0'), + minute: String(newTime.minutes).padStart(2, '0'), + meridiem: newTime.meridiem ? 'PM' : 'AM', + }); + return newTime; + }); + }, [onTimeChange]); + + const handleMinutesClick = useCallback((index: number) => { + setTime((prev) => { + const newTime = { ...prev, minutes: index }; + onTimeChange({ + ...newTime, + hours: String(newTime.hours + 1).padStart(2, '0'), + minute: String(index).padStart(2, '0'), + meridiem: newTime.meridiem ? 'PM' : 'AM', + }); + return newTime; + }); + }, [onTimeChange]); + + const handleMeridiemClick = useCallback((isAM: any) => { + setTime((prev) => { + const newTime = { ...prev, meridiem: isAM }; + onTimeChange({ + ...newTime, + hours: String(newTime.hours + 1).padStart(2, '0'), + minute: String(newTime.minutes).padStart(2, '0'), + meridiem: isAM ? 'PM' : 'AM', + }); + return newTime; + }); + }, [onTimeChange]); + + const renderButtons = (length: number, clickHandler: any, selectedIndex: number) => { + return Array.from({ length }, (_, index) => ( + clickHandler(index)} // deepscan-disable-line + variant={selectedIndex === index ? 'default' : 'outline'} + /> + )); + }; + + return ( +
+
+
+ {renderButtons(12, handleHoursClick, time.hours)} +
+
+ {renderButtons(60, handleMinutesClick, time.minutes)} +
+
+ handleMeridiemClick(!time.meridiem)} // deepscan-disable-line + variant={time.meridiem ? 'outline' : 'default'} + title={'AM'} + /> + handleMeridiemClick(!time.meridiem)} // deepscan-disable-line REACT_INEFFICIENT_PURE_COMPONENT_PROP + variant={time.meridiem ? 'default' : 'outline'} + title={'PM'} + /> +
+
+
+ ); +}; + +export default TimePickerInput; + + + +interface TimerPickerButtonProps { + title?: string; + className?: string; + onClick?: () => void; + loading?: boolean; + variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null; +} + +// eslint-disable-next-line react/display-name +const TimerPickerButton: React.FC = React.memo(({ + title = '', + className = 'border-none border-gray-100 ', + onClick = () => null, + loading = false, + variant = 'default', +}) => { + return ( + + ); +}); diff --git a/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx b/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx new file mode 100644 index 000000000..5e40b0d9d --- /dev/null +++ b/apps/web/lib/features/daily-plan/daily-plan-compare-estimate-modal.tsx @@ -0,0 +1,168 @@ +"use client" + +import { Card, Modal, Text, Button, TimePicker, TimePickerValue } from 'lib/components' +import { PiWarningCircleFill } from "react-icons/pi"; +import React, { useState } from 'react' +import Separator from '@components/ui/separator'; +import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import { TaskNameInfoDisplay } from '../task/task-displays'; +import { clsxm } from '@app/utils'; +import { TaskEstimateInput } from '../team/user-team-card/task-estimate'; +import { useTeamMemberCard, useTimer, useTMCardTaskEdit } from '@app/hooks'; +import { dailyPlanCompareEstimated } from '@app/helpers/daily-plan-estimated'; +import { secondsToTime } from '@app/helpers'; + + +export function DailyPlanCompareEstimatedModal({ + open, + closeModal, + todayPlan, + profile +}: { open: boolean, closeModal: () => void, todayPlan: IDailyPlan[], profile: any }) { + + const { estimatedTime } = dailyPlanCompareEstimated(todayPlan); + const { h: dh, m: dm } = secondsToTime(estimatedTime || 0); + const { startTimer } = useTimer() + const hour = dh.toString()?.padStart(2, '0'); + const minute = dm.toString()?.padStart(2, '0'); + const [times, setTimes] = useState({ + hours: '--', + meridiem: 'PM', + minute: '--' + }) + const onClick = () => { + startTimer(); + window.localStorage.setItem('daily-plan-modal', new Date().toISOString().split('T')[0]); + } + return ( + +
+ +
+ +
+
+ { + setTimes(value); + console.log(times) + + }} + /> + +
+
+ {todayPlan.map((plan, i) => { + return
+ {plan.tasks?.map((data, index) => { + return + })} +
+ })} +
+
+
+ + Please correct planned work hours or re-estimate task(s) +
+ +
+
+
+
+ ) +} +export function DailyPlanTask({ task, profile }: { task?: ITeamTask, profile: any }) { + const taskEdition = useTMCardTaskEdit(task); + const member = task?.selectedTeam?.members.find((member) => { + return member?.employee?.user?.id === profile?.userProfile?.id + }); + + const memberInfo = useTeamMemberCard(member); + return ( +
+
+ +
+ +
+ +
+
+ ); +} + + +export function DailyPlanCompareActionButton({ closeModal, onClick, loading }: { closeModal?: () => void, onClick?: () => void, loading?: boolean }) { + return ( +
+ + +
+ ) +} + + +export function DailyPlanCompareHeader() { + return ( + <> +
+ + TODAY'S PLAN + +
+
+
+ + Add planned working hours + + * + +
+
+ + ) +} + + +export function DailyPlanWorkTimeInput() { + return ( + <> +
+ + Tasks with no time estimations + + + * + +
+ + ) +} diff --git a/apps/web/lib/features/daily-plan/index.ts b/apps/web/lib/features/daily-plan/index.ts new file mode 100644 index 000000000..198a4d36b --- /dev/null +++ b/apps/web/lib/features/daily-plan/index.ts @@ -0,0 +1,4 @@ +export * from './plans-work-time-and-estimate'; +export * from './add-task-to-plan'; +export * from './daily-plan-compare-estimate-modal' +export * from './create-daily-plan-form-modal' 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 f2c4d1dd0..cfd0bb1d5 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,13 +1,14 @@ import { useCallback, useEffect, useState } from 'react'; import { PiWarningCircleFill } from 'react-icons/pi'; -import { DailyPlanStatusEnum, IDailyPlan, ITeamTask } from '@app/interfaces'; +import { IDailyPlan, ITeamTask, IUser } 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 { useAuthenticateUser, useDailyPlan, useTeamTasks } from '@app/hooks'; +import { useAuthenticateUser, useAuthTeamTasks, useDailyPlan, useTeamTasks } from '@app/hooks'; import { ReloadIcon } from '@radix-ui/react-icons'; +import { ESTIMATE_POPUP_SHOWN_DATE, TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; export function AddWorkTimeAndEstimatesToPlan({ open, @@ -15,15 +16,12 @@ export function AddWorkTimeAndEstimatesToPlan({ plan, startTimer, hasPlan - // employee }: { open: boolean; closeModal: () => void; startTimer: () => void; hasPlan: boolean; plan?: IDailyPlan; - - // employee?: OT_Member; }) { const t = useTranslations(); const [workTimePlanned, setworkTimePlanned] = useState(plan?.workTimePlanned); @@ -31,17 +29,30 @@ export function AddWorkTimeAndEstimatesToPlan({ useEffect(() => { if (typeof workTimePlanned === 'string') setworkTimePlanned(parseFloat(workTimePlanned)); }, [workTimePlanned]); + const { user } = useAuthenticateUser(); - const { updateDailyPlan } = useDailyPlan(); + const { updateDailyPlan, todayPlan: hasPlanToday } = useDailyPlan(); const { tasks: $tasks, activeTeam } = useTeamTasks(); + const requirePlan = activeTeam?.requirePlanToTrack; + const currentUser = activeTeam?.members?.find((member) => member.employee.userId === user?.id); + + const tasksEstimated = plan?.tasks?.some((t) => typeof t?.estimate === 'number' && t?.estimate <= 0); + + const tasks = $tasks.filter((task) => { + return plan?.tasks?.some((t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0); + }); - const tasks = $tasks.filter((task) => - plan?.tasks?.some((t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0) + const currentDate = new Date().toISOString().split('T')[0]; + const lastPopupDate = window && window?.localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const lastPopupEstimates = window && window?.localStorage.getItem(ESTIMATE_POPUP_SHOWN_DATE); + + const hasWorkedToday = currentUser?.totalTodayTasks.reduce( + (previousValue, currentValue) => previousValue + currentValue.duration, + 0 ); const handleSubmit = () => { - const requirePlan = activeTeam?.requirePlanToTrack; if (requirePlan) { if (workTimePlanned === 0 || typeof workTimePlanned !== 'number') return; if (tasks.some((task) => task.estimate === 0)) return; @@ -52,77 +63,124 @@ export function AddWorkTimeAndEstimatesToPlan({ closeModal(); }; - return ( - - {hasPlan ? ( - -
-
- - {t('timer.todayPlanSettings.TITLE')} - -
-
- - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')} * - - setworkTimePlanned(parseFloat(e.target.value))} - required - defaultValue={plan?.workTimePlanned ?? 0} - /> -
- - {tasks.length > 0 && ( -
- - -
- -

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

-
+ const handleCloseModal = useCallback(() => { + closeModal(); + // startTimer(); + localStorage.setItem(ESTIMATE_POPUP_SHOWN_DATE, currentDate); + }, [closeModal, currentDate]); + + const Content = () => { + if (hasWorkedToday && hasWorkedToday > 0) { + if ((!hasPlanToday || hasPlanToday.length === 0) && (!lastPopupDate || lastPopupDate !== currentDate)) { + return ; + } else { + if ( + (tasksEstimated || !plan?.workTimePlanned || plan?.workTimePlanned <= 0) && + (!lastPopupEstimates || lastPopupEstimates !== currentDate) + ) { + return ( +
+
+ + {t('timer.todayPlanSettings.TITLE')} + + {hasPlan && plan?.workTimePlanned && plan?.workTimePlanned <= 0 && ( +
+ + {t('timer.todayPlanSettings.WORK_TIME_PLANNED')}{' '} + * + + + setworkTimePlanned(parseFloat(e.target.value))} + required + defaultValue={plan?.workTimePlanned ?? 0} + /> +
+ )} + {tasksEstimated && ( +
+ + +
+ +

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

+
+
+ )} +
+
+ +
- )} - -
- -
-
- - ) : ( - - )} + ); + } + } + } + return null; + }; + + return ( + + + + ); } -function UnEstimatedTasks({ dailyPlan }: { dailyPlan?: IDailyPlan }) { +function UnEstimatedTasks({ + requirePlan, + hasPlan, + dailyPlan, + user +}: { + requirePlan: boolean; + hasPlan: boolean; + dailyPlan?: IDailyPlan; + user?: IUser; +}) { const t = useTranslations(); const { tasks: $tasks } = useTeamTasks(); - - const tasks = $tasks.filter((task) => - dailyPlan?.tasks?.some((t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0) - ); + const { assignedTasks } = useAuthTeamTasks(user); + + let tasks: ITeamTask[] = []; + if (hasPlan) { + tasks = $tasks.filter((task) => + dailyPlan?.tasks?.some( + (t) => task?.id === t.id && typeof task?.estimate === 'number' && task?.estimate <= 0 + ) + ); + } else { + if (!requirePlan) { + tasks = assignedTasks.filter((task) => typeof task?.estimate === 'number' && task?.estimate <= 0); + } + } return (
@@ -158,44 +216,33 @@ 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] - ); +export function CreateTodayPlanPopup({ closeModal, currentDate }: { closeModal: () => void; currentDate: string }) { + const t = useTranslations(); + const { createDailyPlanLoading } = useDailyPlan(); + + const handleCloseModal = useCallback(() => { + closeModal(); + // startTimer(); + localStorage.setItem(TODAY_PLAN_ALERT_SHOWN_DATE, currentDate); + }, [closeModal, currentDate]); return (
- CREATE A PLAN FOR TODAY + {t('dailyPlan.CREATE_A_PLAN_FOR_TODAY')} - You are creating a new plan for today + {t('dailyPlan.TODAY_PLAN_SUB_TITLE')} + {t('dailyPlan.DAILY_PLAN_DESCRIPTION')}
- + {(provided) => ( @@ -79,7 +79,7 @@ export function FutureTasks({ profile }: { profile: any }) { {...provided.dragHandleProps} style={{ ...provided.draggableProps.style, - marginBottom: 8 + marginBottom: 4 }} >
)} 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 80e56fd96..d6722ad5f 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-all.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-all.tsx @@ -63,7 +63,7 @@ export function OutstandingAll({ profile }: OutstandingAll) { {...provided.dragHandleProps} style={{ ...provided.draggableProps.style, - marginBottom: 8 + marginBottom: 4 }} >
)} 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 2623e624b..b83ca2b0a 100644 --- a/apps/web/lib/features/task/daily-plan/outstanding-date.tsx +++ b/apps/web/lib/features/task/daily-plan/outstanding-date.tsx @@ -40,7 +40,7 @@ export function OutstandingFilterDate({ profile }: IOutstandingFilterDate) {
- + {/* Plan header */} 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 6fc9a3983..988266346 100644 --- a/apps/web/lib/features/task/daily-plan/past-tasks.tsx +++ b/apps/web/lib/features/task/daily-plan/past-tasks.tsx @@ -47,7 +47,7 @@ export function PastTasks({ profile, currentTab = 'Past Tasks' }: { profile: any
- + {/* Plan header */}
)} diff --git a/apps/web/lib/features/task/task-card.tsx b/apps/web/lib/features/task/task-card.tsx index 64b03790d..4702cafb6 100644 --- a/apps/web/lib/features/task/task-card.tsx +++ b/apps/web/lib/features/task/task-card.tsx @@ -57,6 +57,7 @@ import { CreateDailyPlanFormModal } from '../daily-plan/create-daily-plan-form-m import { AddTaskToPlan } from '../daily-plan/add-task-to-plan'; import { AddWorkTimeAndEstimatesToPlan } from '../daily-plan/plans-work-time-and-estimate'; import { ReloadIcon } from '@radix-ui/react-icons'; +import { ESTIMATE_POPUP_SHOWN_DATE, TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; type Props = { active?: boolean; @@ -392,11 +393,15 @@ function TimerButtonCall({ ]); const timerHanlderStartStop = useCallback(() => { + const currentDate = new Date().toISOString().split('T')[0]; + const lastPopupDate = window && window?.localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const lastPopupEstimates = window && window?.localStorage.getItem(ESTIMATE_POPUP_SHOWN_DATE); + if (timerStatusFetching || !canRunTimer) return; if (timerStatus?.running) { stopTimer(); } else { - if (!isPlanVerified) { + if (!isPlanVerified || lastPopupDate !== currentDate || lastPopupEstimates !== currentDate) { openModal(); } else { startTimer(); diff --git a/apps/web/lib/features/timer/timer.tsx b/apps/web/lib/features/timer/timer.tsx index fce5dc2ce..32ebe6545 100644 --- a/apps/web/lib/features/timer/timer.tsx +++ b/apps/web/lib/features/timer/timer.tsx @@ -16,6 +16,7 @@ import { import { HotkeysEvent } from 'hotkeys-js'; import { useCallback, useMemo } from 'react'; import { AddWorkTimeAndEstimatesToPlan } from '../daily-plan/plans-work-time-and-estimate'; +import { ESTIMATE_POPUP_SHOWN_DATE, TODAY_PLAN_ALERT_SHOWN_DATE } from '@app/constants'; export function Timer({ className }: IClassName) { const t = useTranslations(); @@ -38,11 +39,15 @@ export function Timer({ className }: IClassName) { } = useTimerView(); const timerHanlderStartStop = useCallback(() => { + const currentDate = new Date().toISOString().split('T')[0]; + const lastPopupDate = window && window?.localStorage.getItem(TODAY_PLAN_ALERT_SHOWN_DATE); + const lastPopupEstimates = window && window?.localStorage.getItem(ESTIMATE_POPUP_SHOWN_DATE); + if (timerStatusFetching || !canRunTimer) return; if (timerStatus?.running) { stopTimer(); } else { - if (!isPlanVerified) { + if (!isPlanVerified || lastPopupDate !== currentDate || lastPopupEstimates !== currentDate) { openModal(); } else { startTimer(); diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index 0e6faa0f6..a8d8a78a2 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -180,8 +180,6 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current if (currentTab === 'Today Tasks') filteredPlans = todayPlan; const canSeeActivity = useCanSeeActivityScreen(); - // const { filteredAllPlanData: filterAllPlanData } = useFilterDateRange(filteredPlans, 'all'); - // const filterPlans: IDailyPlan[] = currentTab === 'All Tasks' ? : filteredPlans; const view = useRecoilValue(dailyPlanViewHeaderTabs); const [plans, setPlans] = useState(filteredPlans); @@ -215,7 +213,7 @@ function AllPlans({ profile, currentTab = 'All Tasks' }: { profile: any; current
- +
)} @@ -381,7 +380,7 @@ export function PlanHeader({ plan, planMode }: { plan: IDailyPlan; planMode: Fil return (
{/* Planned Time */} diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index f240c8951..b66b3e500 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "خطة الغد", "PLAN_FOR_SOME_DAY": "خطة ليوم ما", "ADD_TASK_TO_PLAN": "أضف هذه المهمة إلى الخطة", - "REMOVE_FROM_THIS_PLAN": "إزالة من هذه الخطة" + "REMOVE_FROM_THIS_PLAN": "إزالة من هذه الخطة", + "CREATE_A_PLAN_FOR_TODAY": "إنشاء خطة لليوم", + "TODAY_PLAN_SUB_TITLE": "لا توجد خطة لليوم", + "DAILY_PLAN_DESCRIPTION": "'الخطة اليومية' تساعد في تنظيم عملية العمل لتحقيق أفضل النتائج" }, "form": { "NAME_PLACEHOLDER": "أدخل اسمك", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 98ee52b97..5970bb2ae 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "План за утре", "PLAN_FOR_SOME_DAY": "План за някой ден", "ADD_TASK_TO_PLAN": "Добави тази задача към плана", - "REMOVE_FROM_THIS_PLAN": "Премахни от този план" + "REMOVE_FROM_THIS_PLAN": "Премахни от този план", + "CREATE_A_PLAN_FOR_TODAY": "Създайте план за днес", + "TODAY_PLAN_SUB_TITLE": "Няма план за днес", + "DAILY_PLAN_DESCRIPTION": "'Дневният план' помага да се организира работният процес за постигане на най-добри резултати" }, "form": { "NAME_PLACEHOLDER": "Въведете името си", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index fbf4b6bfa..7349dcf23 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan für morgen", "PLAN_FOR_SOME_DAY": "Plan für irgendeinen Tag", "ADD_TASK_TO_PLAN": "Diese Aufgabe zum Plan hinzufügen", - "REMOVE_FROM_THIS_PLAN": "Aus diesem Plan entfernen" + "REMOVE_FROM_THIS_PLAN": "Aus diesem Plan entfernen", + "CREATE_A_PLAN_FOR_TODAY": "Erstellen Sie einen Plan für heute", + "TODAY_PLAN_SUB_TITLE": "Es gibt keinen Plan für heute", + "DAILY_PLAN_DESCRIPTION": "'Tagesplan' hilft, den Arbeitsprozess zu organisieren, um die besten Ergebnisse zu erzielen" }, "form": { "NAME_PLACEHOLDER": "Ihren Namen eingeben", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 81f888ea4..a08dc0bc4 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan for tomorrow", "PLAN_FOR_SOME_DAY": "Plan for some day", "ADD_TASK_TO_PLAN": "Add this task to a plan", - "REMOVE_FROM_THIS_PLAN": "Remove from this plan" + "REMOVE_FROM_THIS_PLAN": "Remove from this plan", + "CREATE_A_PLAN_FOR_TODAY": "Create a Plan for Today", + "TODAY_PLAN_SUB_TITLE": " There is no plan for Today", + "DAILY_PLAN_DESCRIPTION": "'Daily Plan' helps organize the work process to achieve the best results" }, "form": { "NAME_PLACEHOLDER": "Enter your name", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index bcdfab492..721947be3 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan para mañana", "PLAN_FOR_SOME_DAY": "Plan para algún día", "ADD_TASK_TO_PLAN": "Agregar esta tarea a un plan", - "REMOVE_FROM_THIS_PLAN": "Eliminar de este plan" + "REMOVE_FROM_THIS_PLAN": "Eliminar de este plan", + "CREATE_A_PLAN_FOR_TODAY": "Crear un plan para hoy", + "TODAY_PLAN_SUB_TITLE": "No hay plan para hoy", + "DAILY_PLAN_DESCRIPTION": "'Plan diario' ayuda a organizar el proceso de trabajo para lograr los mejores resultados" }, "form": { "NAME_PLACEHOLDER": "Ingresa tu nombre", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index 04222d68b..be3260e5a 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan pour demain", "PLAN_FOR_SOME_DAY": "Plan pour une date", "ADD_TASK_TO_PLAN": "Ajouter cette tâche au plan", - "REMOVE_FROM_THIS_PLAN": "Retirer de ce plan" + "REMOVE_FROM_THIS_PLAN": "Retirer de ce plan", + "CREATE_A_PLAN_FOR_TODAY": "Créer un plan pour aujourd'hui", + "TODAY_PLAN_SUB_TITLE": "Il n'y a pas de plan pour aujourd'hui", + "DAILY_PLAN_DESCRIPTION": "'Plan quotidien' aide à organiser le processus de travail pour obtenir les meilleurs résultats" }, "form": { "NAME_PLACEHOLDER": "Entrez votre nom", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 1f75be567..c1ff34b50 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "תוכנית למחר", "PLAN_FOR_SOME_DAY": "תוכנית ליום כלשהו", "ADD_TASK_TO_PLAN": "הוסף משימה זו לתוכנית", - "REMOVE_FROM_THIS_PLAN": "הסר מתוכנית זו" + "REMOVE_FROM_THIS_PLAN": "הסר מתוכנית זו", + "CREATE_A_PLAN_FOR_TODAY": "צור תוכנית להיום", + "TODAY_PLAN_SUB_TITLE": "אין תוכנית להיום", + "DAILY_PLAN_DESCRIPTION": "'תוכנית יומית' עוזרת לארגן את תהליך העבודה להשגת התוצאות הטובות ביותר" }, "form": { "NAME_PLACEHOLDER": "הכנס את השם שלך", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 8a332efec..e2f293952 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Piano per domani", "PLAN_FOR_SOME_DAY": "Piano per un giorno", "ADD_TASK_TO_PLAN": "Aggiungi questa attività al piano", - "REMOVE_FROM_THIS_PLAN": "Rimuovi da questo piano" + "REMOVE_FROM_THIS_PLAN": "Rimuovi da questo piano", + "CREATE_A_PLAN_FOR_TODAY": "Crea un piano per oggi", + "TODAY_PLAN_SUB_TITLE": "Non c'è nessun piano per oggi", + "DAILY_PLAN_DESCRIPTION": "'Piano giornaliero' aiuta a organizzare il processo di lavoro per ottenere i migliori risultati" }, "form": { "NAME_PLACEHOLDER": "Inserisci il tuo nome", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index 557c8e470..09aa6bbd5 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan voor morgen", "PLAN_FOR_SOME_DAY": "Plan voor een dag", "ADD_TASK_TO_PLAN": "Voeg deze taak toe aan een plan", - "REMOVE_FROM_THIS_PLAN": "Verwijder uit dit plan" + "REMOVE_FROM_THIS_PLAN": "Verwijder uit dit plan", + "CREATE_A_PLAN_FOR_TODAY": "Maak een plan voor vandaag", + "TODAY_PLAN_SUB_TITLE": "Er is geen plan voor vandaag", + "DAILY_PLAN_DESCRIPTION": "'Dagelijkse planning' helpt om het werkproces te organiseren om de beste resultaten te behalen" }, "form": { "NAME_PLACEHOLDER": "Voer uw naam in", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 72797e308..b250d0bd4 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plan na jutro", "PLAN_FOR_SOME_DAY": "Plan na pewien dzień", "ADD_TASK_TO_PLAN": "Dodaj tę zadanie do planu", - "REMOVE_FROM_THIS_PLAN": "Usuń z tego planu" + "REMOVE_FROM_THIS_PLAN": "Usuń z tego planu", + "CREATE_A_PLAN_FOR_TODAY": "Utwórz plan na dziś", + "TODAY_PLAN_SUB_TITLE": "Brak planu na dziś", + "DAILY_PLAN_DESCRIPTION": "'Plan dnia' pomaga zorganizować proces pracy, aby osiągnąć najlepsze wyniki" }, "form": { "NAME_PLACEHOLDER": "Wprowadź swoje imię", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 34eb0cf9f..c37ed080a 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "Plano para amanhã", "PLAN_FOR_SOME_DAY": "Plano para algum dia", "ADD_TASK_TO_PLAN": "Adicionar esta tarefa ao plano", - "REMOVE_FROM_THIS_PLAN": "Remover deste plano" + "REMOVE_FROM_THIS_PLAN": "Remover deste plano", + "CREATE_A_PLAN_FOR_TODAY": "Criar um plano para hoje", + "TODAY_PLAN_SUB_TITLE": "Não há plano para hoje", + "DAILY_PLAN_DESCRIPTION": "'Plano diário' ajuda a organizar o processo de trabalho para obter os melhores resultados" }, "form": { "NAME_PLACEHOLDER": "Digite seu nome", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 1efca274d..5e7c31c81 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "План на завтра", "PLAN_FOR_SOME_DAY": "План на какой-то день", "ADD_TASK_TO_PLAN": "Добавить эту задачу в план", - "REMOVE_FROM_THIS_PLAN": "Убрать из этого плана" + "REMOVE_FROM_THIS_PLAN": "Убрать из этого плана", + "CREATE_A_PLAN_FOR_TODAY": "Создать план на сегодня", + "TODAY_PLAN_SUB_TITLE": "На сегодня нет плана", + "DAILY_PLAN_DESCRIPTION": "'Ежедневный план' помогает организовать рабочий процесс для достижения наилучших результатов" }, "form": { "NAME_PLACEHOLDER": "Введите ваше имя", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index c1ed90f27..b547c5d2f 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -579,7 +579,10 @@ "PLAN_FOR_TOMORROW": "明日计划", "PLAN_FOR_SOME_DAY": "未来计划", "ADD_TASK_TO_PLAN": "添加此任务到计划", - "REMOVE_FROM_THIS_PLAN": "从计划中移除" + "REMOVE_FROM_THIS_PLAN": "从计划中移除", + "CREATE_A_PLAN_FOR_TODAY": "为今天制定计划", + "TODAY_PLAN_SUB_TITLE": "今天没有计划", + "DAILY_PLAN_DESCRIPTION": "'每日计划' 有助于组织工作流程以实现最佳结果" }, "form": { "NAME_PLACEHOLDER": "输入您的姓名", diff --git a/yarn.lock b/yarn.lock index 6c6e56058..fdabdabc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9714,14 +9714,6 @@ buffer@~5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" -builder-util-runtime@9.2.3: - version "9.2.3" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" - integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== - dependencies: - debug "^4.3.4" - sax "^1.2.4" - builder-util-runtime@9.2.4: version "9.2.4" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" @@ -9730,6 +9722,14 @@ builder-util-runtime@9.2.4: debug "^4.3.4" sax "^1.2.4" +builder-util-runtime@9.2.5: + version "9.2.5" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz#0afdffa0adb5c84c14926c7dd2cf3c6e96e9be83" + integrity sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg== + dependencies: + debug "^4.3.4" + sax "^1.2.4" + builder-util@24.13.1: version "24.13.1" resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816" @@ -12618,11 +12618,11 @@ electron-to-chromium@^1.4.477: integrity sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg== electron-updater@^6.1.4: - version "6.1.8" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" - integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== + version "6.3.0" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.0.tgz#13a5c3c3f0b2b114fe33181e24a8270096734b3e" + integrity sha512-3Xlezhk+dKaSQrOnkQNqCGiuGSSUPO9BV9TQZ4Iig6AyTJ4FzJONE5gFFc382sY53Sh9dwJfzKsA3DxRHt2btw== dependencies: - builder-util-runtime "9.2.3" + builder-util-runtime "9.2.5" fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" @@ -22848,7 +22848,7 @@ semver@7.5.3: dependencies: lru-cache "^6.0.0" -semver@7.5.4, semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +semver@7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -22860,18 +22860,11 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1: +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.6, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -semver@^7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" - send@0.18.0, send@latest: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -23710,7 +23703,16 @@ string-to-color@^2.2.2: lodash.words "^4.2.0" rgb-hex "^3.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -23820,7 +23822,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -25824,7 +25833,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -25842,6 +25851,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"