diff --git a/.scripts/electron-desktop-environment/concrete-environment-content/desktop-server-web-environment-content.ts b/.scripts/electron-desktop-environment/concrete-environment-content/desktop-server-web-environment-content.ts index c3dca9e68..bdec1d835 100644 --- a/.scripts/electron-desktop-environment/concrete-environment-content/desktop-server-web-environment-content.ts +++ b/.scripts/electron-desktop-environment/concrete-environment-content/desktop-server-web-environment-content.ts @@ -18,6 +18,8 @@ export class DesktopServerWebEnvironmentContent implements IContentGenerator { GAUZY_API_SERVER_URL: '${variable.GAUZY_API_SERVER_URL}', NEXT_PUBLIC_GAUZY_API_SERVER_URL: '${variable.NEXT_PUBLIC_GAUZY_API_SERVER_URL}', DESKTOP_WEB_SERVER_HOSTNAME: '${variable.DESKTOP_WEB_SERVER_HOSTNAME}', + TERM_OF_SERVICE: '${variable.TERM_OF_SERVICE}', + PRIVACY_POLICY: '${variable.PRIVACY_POLICY}' `; } } diff --git a/.scripts/env.ts b/.scripts/env.ts index 1265b2170..b62dad23f 100644 --- a/.scripts/env.ts +++ b/.scripts/env.ts @@ -21,6 +21,8 @@ export type Env = Readonly<{ GAUZY_API_SERVER_URL: string; NEXT_PUBLIC_GAUZY_API_SERVER_URL: string; DESKTOP_WEB_SERVER_HOSTNAME: string; + TERM_OF_SERVICE: string; + PRIVACY_POLICY: string; }> export const env = cleanEnv(process.env, { @@ -31,7 +33,7 @@ export const env = cleanEnv(process.env, { default: 'https://github.com/ever-co/ever-teams' }), PLATFORM_LOGO: str({ - default: 'src/resources/icons/platform-logo.png' + default: 'https://app.ever.team/assets/ever-teams.png' }), DESKTOP_WEB_SERVER_APP_NAME: str({ default: 'ever-teams-server-web' @@ -69,5 +71,11 @@ export const env = cleanEnv(process.env, { DESKTOP_WEB_SERVER_HOSTNAME: str({ default: '0.0.0.0', // let's use the same one for now for all envs desc: 'WARNING: Using 0.0.0.0 binds to all network interfaces. Use with caution in production.' + }), + TERM_OF_SERVICE: str({ + default: 'https://ever.team/tos' + }), + PRIVACY_POLICY: str({ + default: 'https://ever.team/privacy' }) }); diff --git a/apps/server-web/src/main/helpers/constant.ts b/apps/server-web/src/main/helpers/constant.ts index dc547d227..b347ec018 100644 --- a/apps/server-web/src/main/helpers/constant.ts +++ b/apps/server-web/src/main/helpers/constant.ts @@ -6,6 +6,7 @@ export const EventLists = { webServerStop: 'WEB_SERVER_STOP', gotoSetting: 'GO_TO_SETTING', gotoAbout: 'GO_TO_ABOUT', + OPEN_WINDOW: 'OPEN_WINDOW', UPDATE_AVAILABLE: 'UPDATE_AVAILABLE', UPDATE_ERROR: 'UPDATE_ERROR', UPDATE_NOT_AVAILABLE: 'UPDATE_NOT_AVAILABLE', @@ -19,7 +20,8 @@ export const EventLists = { CHANGE_THEME: 'CHANGE_THEME', SETUP_WINDOW: 'SETUP_WINDOW', SETTING_WINDOW_DEV: 'SETTING_WINDOW_DEV', - SERVER_WINDOW_DEV: 'SERVER_WINDOW_DEV' + SERVER_WINDOW_DEV: 'SERVER_WINDOW_DEV', + WINDOW_EVENT: 'WINDOW_EVENT' } export const SettingPageTypeMessage = { @@ -40,7 +42,13 @@ export const SettingPageTypeMessage = { updateSettingResponse: 'update-setting-response', updateCancel: 'update-cancel', restartServer: 'restart-server', - themeChange: 'theme-change' + themeChange: 'theme-change', + linkAction: 'link-action' +} + +export const APP_LINK = { + TERM_OF_SERVICE: 'TERM_OF_SERVICE', + PRIVACY_POLICY: 'PRIVACY_POLICY' } export const ServerPageTypeMessage = { @@ -63,3 +71,44 @@ export const IPC_TYPES: { UPDATER_PAGE: 'updater-page', SERVER_PAGE: 'server-page' } + +export const WindowOptions = { + SETTING_WINDOW: { + width: 1024, + height: 728, + hashPath: 'setting' + }, + LOG_WINDOW: { + width: 1024, + height: 728, + hashPath: 'history-console' + }, + SETUP_WINDOW: { + width: 1024, + height: 728, + hashPath: 'setup' + }, + ABOUT_WINDOW: { + width: 300, + height: 250, + hashPath: 'about' + } +} + +export const WindowTypes: { + SETTING_WINDOW: 'SETTING_WINDOW', + LOG_WINDOW: 'LOG_WINDOW', + SETUP_WINDOW: 'SETUP_WINDOW', + ABOUT_WINDOW: 'ABOUT_WINDOW' +} = { + SETTING_WINDOW: 'SETTING_WINDOW', + LOG_WINDOW: 'LOG_WINDOW', + SETUP_WINDOW: 'SETUP_WINDOW', + ABOUT_WINDOW: 'ABOUT_WINDOW' +} + +export const WINDOW_EVENTS: { + CLOSE: 'close' +} = { + CLOSE: 'close' +} diff --git a/apps/server-web/src/main/helpers/interfaces/i-events.ts b/apps/server-web/src/main/helpers/interfaces/i-events.ts new file mode 100644 index 000000000..9a7c9d459 --- /dev/null +++ b/apps/server-web/src/main/helpers/interfaces/i-events.ts @@ -0,0 +1,5 @@ +import { IWindowTypes } from "./i-window"; + +export interface IOpenWindow { + windowType: IWindowTypes +} diff --git a/apps/server-web/src/main/helpers/interfaces/i-window.ts b/apps/server-web/src/main/helpers/interfaces/i-window.ts new file mode 100644 index 000000000..da48c33ad --- /dev/null +++ b/apps/server-web/src/main/helpers/interfaces/i-window.ts @@ -0,0 +1,8 @@ +import { Menu } from "electron"; + +export type IWindowTypes = 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDOW' | 'ABOUT_WINDOW' + +export interface IAppWindow { + windowType: IWindowTypes, + menu: Menu +} diff --git a/apps/server-web/src/main/helpers/interfaces/index.ts b/apps/server-web/src/main/helpers/interfaces/index.ts index de47665c2..7428c1e03 100644 --- a/apps/server-web/src/main/helpers/interfaces/index.ts +++ b/apps/server-web/src/main/helpers/interfaces/index.ts @@ -2,3 +2,5 @@ export * from './i-server'; export * from './i-desktop-dialog'; export * from './i-constant'; export * from './i-menu'; +export * from './i-window'; +export * from './i-events'; diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index acc556fde..16404ec94 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -4,18 +4,16 @@ 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, ServerPageTypeMessage, LOG_TYPES, IPC_TYPES } from './helpers/constant'; -import { resolveHtmlPath } from './util'; +import { EventLists, SettingPageTypeMessage, ServerPageTypeMessage, LOG_TYPES, IPC_TYPES, WindowTypes, APP_LINK, WINDOW_EVENTS } from './helpers/constant'; import Updater from './updater'; -import { mainBindings } from 'i18next-electron-fs-backend'; import i18nextMainBackend from '../configs/i18n.mainconfig'; -import fs from 'fs'; -import { WebServer, AppMenu, ServerConfig } from './helpers/interfaces'; +import { WebServer, AppMenu, ServerConfig, IWindowTypes, IOpenWindow } from './helpers/interfaces'; import { clearDesktopConfig } from './helpers'; import Log from 'electron-log'; import MenuBuilder from './menu'; import { config } from '../configs/config'; import { debounce } from 'lodash'; +import WindowFactory from './windows/window-factory'; console.log = Log.log; @@ -40,7 +38,40 @@ let tray: Tray; let settingWindow: BrowserWindow | null = null; let logWindow: BrowserWindow | null = null; let setupWindow: BrowserWindow | any = null; -const appMenu = new MenuBuilder(eventEmitter) +let aboutWindow: BrowserWindow | null = null; +const appMenu = new MenuBuilder(eventEmitter); + +const handleCloseWindow = (windowTypes: IWindowTypes) => { + switch (windowTypes) { + case WindowTypes.SETTING_WINDOW: + settingWindow = null; + break; + case WindowTypes.SETUP_WINDOW: + setupWindow = null; + break; + case WindowTypes.LOG_WINDOW: + logWindow = null; + break; + case WindowTypes.ABOUT_WINDOW: + aboutWindow = null; + break; + default: + break; + } +} + +const handleLinkAction = (linkType: string) => { + switch (linkType) { + case APP_LINK.TERM_OF_SERVICE: + shell.openExternal(config.TERM_OF_SERVICE); + break; + case APP_LINK.PRIVACY_POLICY: + shell.openExternal(config.PRIVACY_POLICY); + break; + default: + break; + } +} Log.hooks.push((message: any, transport) => { if (transport !== Log.transports.file) { @@ -111,6 +142,10 @@ const getAssetPath = (...paths: string[]): string => { return path.join(RESOURCES_PATH, ...paths); }; +const preloadPath: string = app.isPackaged +? path.join(__dirname, 'preload.js') +: path.join(__dirname, '../../.erb/dll/preload.js'); + console.log(__dirname); if (isProd) { @@ -145,6 +180,12 @@ if (isDebug) { require('electron-debug')(); } +const windowFactory = new WindowFactory( + preloadPath, + 'icons/icon.png', + eventEmitter +) + const installExtensions = async () => { const installer = require('electron-devtools-installer'); const forceDownload = !!process.env.UPGRADE_EXTENSIONS; @@ -159,63 +200,40 @@ const installExtensions = async () => { }; -const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDOW') => { +const createWindow = async (windowType: IWindowTypes): Promise => { if (isDebug) { await installExtensions(); } + return windowFactory.buildWindow({windowType, menu: appMenu.buildTemplateMenu(windowType, i18nextMainBackend)}); +}; - const defaultOptionWindow = { - title: app.name, - frame: true, - show: false, - width: 1024, - height: 728, - icon: getAssetPath('icons/icon.png'), - maximizable: false, - resizable: false, - webPreferences: { - preload: app.isPackaged - ? path.join(__dirname, 'preload.js') - : path.join(__dirname, '../../.erb/dll/preload.js'), - }, - } - 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; - }); - Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) - 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; - }) - Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) - break; - case 'SETUP_WINDOW': - setupWindow = new BrowserWindow(defaultOptionWindow); - url = resolveHtmlPath('index.html', 'setup'); - setupWindow?.loadURL(url); - mainBindings(ipcMain, setupWindow, fs); - Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenu.initialMenu(), i18nextMainBackend)); - setupWindow.on('closed', () => { - setupWindow = null; - }) +const handleOpenWindow = async (data: IOpenWindow) => { + let browserWindow: BrowserWindow | null = null; + const serverSetting = LocalStore.getStore('config'); + switch (data.windowType) { + case WindowTypes.ABOUT_WINDOW: + if (aboutWindow) { + browserWindow = aboutWindow + } else { + browserWindow = await createWindow(data.windowType) + } break; default: break; } -}; + if (browserWindow) { + browserWindow?.show(); + browserWindow?.webContents.once('did-finish-load', () => { + setTimeout(() => { + browserWindow?.webContents.send('languageSignal', serverSetting.general?.lang); + browserWindow?.webContents.send(IPC_TYPES.SETTING_PAGE, { + data: {...serverSetting, appName: app.name, version: app.getVersion()}, + type: SettingPageTypeMessage.loadSetting, + }); + }, 50) + }) + } +} const runServer = async () => { console.log('Run the Server...'); @@ -275,9 +293,9 @@ const onInitApplication = () => { if (i18nextMainBackend.isInitialized && storeConfig.general?.setup) { trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); updateTrayMenu('none', {}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); - Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) + Menu.setApplicationMenu(appMenu.buildTemplateMenu(WindowTypes.LOG_WINDOW, i18nextMainBackend)) } else { - Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenu.initialMenu(), i18nextMainBackend)) + Menu.setApplicationMenu(appMenu.buildTemplateMenu(WindowTypes.SETUP_WINDOW, i18nextMainBackend)) } }, 250)); @@ -326,7 +344,7 @@ const onInitApplication = () => { eventEmitter.on(EventLists.gotoSetting, async () => { if (!settingWindow) { - await createWindow('SETTING_WINDOW'); + settingWindow = await createWindow(WindowTypes.SETTING_WINDOW); } const serverSetting: WebServer = LocalStore.getStore('config'); console.log('setting data', serverSetting); @@ -389,25 +407,12 @@ const onInitApplication = () => { setupWindow?.webContents.send('themeSignal', { type: SettingPageTypeMessage.themeChange, data }); }) - eventEmitter.on(EventLists.gotoAbout, async () => { - if (!settingWindow) { - await createWindow('SETTING_WINDOW'); - } - const serverSetting = LocalStore.getStore('config'); - settingWindow?.show(); - settingWindow?.webContents.once('did-finish-load', () => { - setTimeout(() => { - SendMessageToSettingWindow(SettingPageTypeMessage.loadSetting, serverSetting); - settingWindow?.webContents.send('languageSignal', serverSetting.general?.lang); - SendMessageToSettingWindow(SettingPageTypeMessage.selectMenu, { key: 'about' }); - }, 100) - }) - }) + eventEmitter.on(EventLists.OPEN_WINDOW, handleOpenWindow) eventEmitter.on(EventLists.SERVER_WINDOW, async () => { if (!logWindow) { initTrayMenu() - await createWindow('LOG_WINDOW'); + logWindow = await createWindow(WindowTypes.LOG_WINDOW); } const serverSetting = LocalStore.getStore('config'); logWindow?.show(); @@ -438,6 +443,16 @@ const onInitApplication = () => { eventEmitter.on(EventLists.SERVER_WINDOW_DEV, () => { logWindow?.webContents.toggleDevTools(); }) + + eventEmitter.on(EventLists.WINDOW_EVENT, (data) => { + switch (data.eventType) { + case WINDOW_EVENTS.CLOSE: + handleCloseWindow(data.windowType) + break; + default: + break; + } + }) } const initTrayMenu = () => { @@ -480,7 +495,7 @@ const initTrayMenu = () => { eventEmitter.emit(EventLists.SERVER_WINDOW); } else { if (!setupWindow) { - await createWindow('SETUP_WINDOW'); + setupWindow = await createWindow(WindowTypes.SETUP_WINDOW); } if (setupWindow) { setupWindow?.show(); @@ -569,6 +584,10 @@ ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { createIntervalAutoUpdate() event.sender.send(IPC_TYPES.UPDATER_PAGE, { type: SettingPageTypeMessage.updateSettingResponse, data: true }) break; + case SettingPageTypeMessage.linkAction: + console.log(arg) + handleLinkAction(arg.data.linkType) + break; default: break; } diff --git a/apps/server-web/src/main/menu.ts b/apps/server-web/src/main/menu.ts index d997814a9..00c816f8b 100644 --- a/apps/server-web/src/main/menu.ts +++ b/apps/server-web/src/main/menu.ts @@ -5,9 +5,9 @@ import { } from 'electron'; import { config } from '../configs/config'; import { EventEmitter } from 'events'; -import { EventLists } from './helpers/constant'; +import { EventLists, WindowTypes } from './helpers/constant'; import i18n from 'i18next'; -import { AppMenu } from './helpers/interfaces'; +import { AppMenu, IWindowTypes } from './helpers/interfaces'; export default class MenuBuilder { eventEmitter: EventEmitter @@ -27,7 +27,7 @@ export default class MenuBuilder { id: 'MENU_APP_ABOUT', label: `MENU_APP.APP_ABOUT`, click: () => { - this.eventEmitter.emit(EventLists.gotoAbout) + this.eventEmitter.emit(EventLists.OPEN_WINDOW, { windowType: WindowTypes.ABOUT_WINDOW}) } }, { type: 'separator' }, @@ -125,14 +125,6 @@ export default class MenuBuilder { ] } - buildDefaultTemplate(menuItems: any, i18nextMainBackend: typeof i18n) { - return Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, menuItems)); - } - - buildInitialTemplate(menuItems: any, i18nextMainBackend: typeof i18n) { - return Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, menuItems)); - } - updateAppMenu(menuItem: string, context: { label?: string, enabled?: boolean}, contextMenuItems: any, i18nextMainBackend: typeof i18n) { const menuIdx:number = contextMenuItems.findIndex((item: any) => item.id === menuItem); if (menuIdx > -1) { @@ -143,24 +135,38 @@ export default class MenuBuilder { const newMenu = [...contextMenuItems]; Menu.setApplicationMenu(Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, newMenu))) } -} + } -translateAppMenu(i18nextMainBackend: typeof i18n, contextMenu: any) { - return contextMenu.map((menu: any) => { - const menuCopied = {...menu}; - if (menuCopied.label) { - menuCopied.label = i18nextMainBackend.t(menuCopied.label); - } - if (menuCopied.submenu && menuCopied.submenu.length) { - menuCopied.submenu = menuCopied.submenu.map((sm: any) => { - const submenu = {...sm}; - if (submenu.label) { - submenu.label = i18nextMainBackend.t(submenu.label) - } - return submenu; - }) + translateAppMenu(i18nextMainBackend: typeof i18n, contextMenu: any) { + return contextMenu.map((menu: any) => { + const menuCopied = {...menu}; + if (menuCopied.label) { + menuCopied.label = i18nextMainBackend.t(menuCopied.label); + } + if (menuCopied.submenu && menuCopied.submenu.length) { + menuCopied.submenu = menuCopied.submenu.map((sm: any) => { + const submenu = {...sm}; + if (submenu.label) { + submenu.label = i18nextMainBackend.t(submenu.label) + } + return submenu; + }) + } + return menuCopied; + }) + } + + getWindowMenu(windowType: IWindowTypes) { + switch (windowType) { + case WindowTypes.SETUP_WINDOW: + return this.initialMenu(); + default: + return this.defaultMenu(); } - return menuCopied; - }) -} + } + + buildTemplateMenu(windowType: IWindowTypes, i18nextMainBackend: typeof i18n) { + const menu = this.getWindowMenu(windowType) + return Menu.buildFromTemplate(this.translateAppMenu(i18nextMainBackend, menu)); + } } diff --git a/apps/server-web/src/main/windows/window-factory.ts b/apps/server-web/src/main/windows/window-factory.ts new file mode 100644 index 000000000..c07d39084 --- /dev/null +++ b/apps/server-web/src/main/windows/window-factory.ts @@ -0,0 +1,82 @@ + +import { BrowserWindow, app, BrowserWindowConstructorOptions, ipcMain, Menu} from 'electron'; +import { resolveHtmlPath } from '../util'; +import { mainBindings } from 'i18next-electron-fs-backend'; +import fs from 'fs'; +import { EventEmitter } from 'events'; +import { EventLists, WindowOptions, WindowTypes, WINDOW_EVENTS } from '../helpers/constant'; +import { IAppWindow, IWindowTypes } from '../helpers/interfaces'; + +export default class WindowsFactory { + private preloadPath: string; + private iconPath: string; + private eventEmitter: EventEmitter; + constructor( + preloadPath: string, + iconPath: string, + eventEmitter: EventEmitter + ) { + this.preloadPath = preloadPath; + this.iconPath = iconPath; + this.eventEmitter = eventEmitter; + } + + + defaultOptionWindow(): BrowserWindowConstructorOptions { + return { + title: app.name, + frame: true, + show: false, + icon: this.iconPath, + maximizable: false, + resizable: false, + width: 1024, + height: 728, + webPreferences: { + preload: this.preloadPath, + nodeIntegration: false, + contextIsolation: true, + sandbox: true, + webSecurity: true + } + } + } + + createWindow( + width: number, + height: number, + hashPath: string, + menu: Menu + ): BrowserWindow { + const windowOptions: BrowserWindowConstructorOptions = this.defaultOptionWindow(); + windowOptions.width = width; + windowOptions.height = height; + let browserWindow = new BrowserWindow(windowOptions); + const url = resolveHtmlPath('index.html', hashPath); + browserWindow.loadURL(url); + mainBindings(ipcMain, browserWindow, fs); + Menu.setApplicationMenu(menu); + return browserWindow; + } + + buildWindow({windowType, menu}: IAppWindow): BrowserWindow { + const options = this.windowCustomOptions(windowType); + const browserWindow = this.createWindow( + options.width, + options.height, + options.hashPath, + menu + ) + browserWindow.on(WINDOW_EVENTS.CLOSE, () => { + this.eventEmitter.emit(EventLists.WINDOW_EVENT, { + windowType: WindowTypes[windowType], + eventType: WINDOW_EVENTS.CLOSE + }) + }) + return browserWindow; + } + + windowCustomOptions(windowType: IWindowTypes) { + return WindowOptions[windowType]; + } +} diff --git a/apps/server-web/src/renderer/App.tsx b/apps/server-web/src/renderer/App.tsx index dbeebce43..15d3ac4d3 100644 --- a/apps/server-web/src/renderer/App.tsx +++ b/apps/server-web/src/renderer/App.tsx @@ -6,6 +6,7 @@ import i18next from 'i18next'; import { ThemeProvider, useTheme } from './ThemeContext'; import SetupPage from './pages/Setup'; import { ServerPage } from './pages/Server'; +import AboutPage from './pages/About'; export default function App() { const [language, setLanguage] = useState('en'); @@ -38,6 +39,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 0a1260f48..bbf17fc15 100644 --- a/apps/server-web/src/renderer/components/About.tsx +++ b/apps/server-web/src/renderer/components/About.tsx @@ -1,16 +1,58 @@ import { EverTeamsLogo } from './svgs'; import { IAbout } from '../libs/interfaces'; +import { + APP_LINK, + IPC_TYPES, + SettingPageTypeMessage, +} from '../../main/helpers/constant'; +import { Link } from 'react-router-dom'; export const AboutComponent = (props: IAbout) => { + const handleLinkClick = (linkType: string) => { + window.electron.ipcRenderer.sendMessage(IPC_TYPES.SETTING_PAGE, { + type: SettingPageTypeMessage.linkAction, + data: { + linkType, + }, + }); + }; return ( -
-
-
+
+
+
-

- V {props.version} -

+

+ Version v{props.version} +

+
+
+

+ Copyright © 2024-Present{' '} + Ever Co. LTD +

+

+ All rights reserved. +

+

+ { + handleLinkClick(APP_LINK.TERM_OF_SERVICE); + }} + > + Terms Of Service + + | + { + handleLinkClick(APP_LINK.PRIVACY_POLICY); + }} + > + Privacy Policy + +

); diff --git a/apps/server-web/src/renderer/components/SideBar.tsx b/apps/server-web/src/renderer/components/SideBar.tsx index f0b94c3d7..3660ef47d 100644 --- a/apps/server-web/src/renderer/components/SideBar.tsx +++ b/apps/server-web/src/renderer/components/SideBar.tsx @@ -13,7 +13,7 @@ export function SideBar({ const { t } = useTranslation(); return (
-
+
    {menus.length > 0 && @@ -21,7 +21,7 @@ export function SideBar({
  • { menuChange(menu.key); }} diff --git a/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx b/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx index 3df194cda..157485615 100644 --- a/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx +++ b/apps/server-web/src/renderer/components/svgs/EverTeamsLogo.tsx @@ -1,12 +1,100 @@ -import Logo from '../../../resources/icons/platform-logo.png'; -export const EverTeamsLogo = () => { +import { config } from '../../../configs/config'; +import { IClassName } from '../../libs/interfaces'; +import { clsxm } from '../../libs/utils/clsxm'; + +const PLATFORM_LOGO = config.PLATFORM_LOGO; + +type Props = IClassName<{ + dash?: boolean; + color?: 'auto' | 'default' | 'white' | 'white-black' | 'black-white' | 'dark'; +}>; + +export function EverTeamsLogo({ className, dash, color = 'auto' }: Props) { return ( - EverTeams Logo + <> + {PLATFORM_LOGO ? ( + + ) : ( + + )} + ); -}; +} diff --git a/apps/server-web/src/renderer/pages/About.tsx b/apps/server-web/src/renderer/pages/About.tsx new file mode 100644 index 000000000..18a9954df --- /dev/null +++ b/apps/server-web/src/renderer/pages/About.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from 'react'; +import { EverTeamsLogo } from '../components/svgs'; +import { + APP_LINK, + IPC_TYPES, + SettingPageTypeMessage, +} from '../../main/helpers/constant'; +import { Link } from 'react-router-dom'; + +const AboutPage = () => { + const [aboutApp, setAboutApp] = useState<{ + name: string; + version: string; + }>({ + name: 'Web Server', + version: '0.1.0', + }); + + const handleLinkClick = (linkType: string) => { + window.electron.ipcRenderer.sendMessage(IPC_TYPES.SETTING_PAGE, { + type: SettingPageTypeMessage.linkAction, + data: { + linkType, + }, + }); + }; + + useEffect(() => { + window.electron.ipcRenderer.removeEventListener(IPC_TYPES.SETTING_PAGE); + window.electron.ipcRenderer.on(IPC_TYPES.SETTING_PAGE, (arg: any) => { + switch (arg.type) { + case SettingPageTypeMessage.loadSetting: + setAboutApp({ + name: arg.data.appName, + version: arg.data.version, + }); + break; + default: + break; + } + }); + }); + return ( +
    +
    +
    + +
    +

    + {aboutApp.name} +

    +

    + Version v{aboutApp.version} +

    +
    +
    +

    + Copyright © 2024-Present{' '} + Ever Co. LTD +

    +

    + All rights reserved. +

    +

    + { + handleLinkClick(APP_LINK.TERM_OF_SERVICE); + }} + > + Terms Of Service + + | + { + handleLinkClick(APP_LINK.PRIVACY_POLICY); + }} + > + Privacy Policy + +

    +
    +
    + ); +}; + +export default AboutPage; diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index ca450efd9..3893337aa 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -1,45 +1,68 @@ import React from 'react' -import { useTeamTasks, useTimelogFilterOptions } from '@/app/hooks'; -import { ITaskIssue } from '@/app/interfaces'; +import { useOrganizationProjects, useOrganizationTeams, useTeamTasks, useTimelogFilterOptions } from '@/app/hooks'; +import { TimeLogType, TimerSource } from '@/app/interfaces'; import { clsxm } from '@/app/utils'; import { Modal } from '@/lib/components' -import { CustomSelect, TaskStatus, taskIssues } from '@/lib/features'; +import { CustomSelect, TaskNameInfoDisplay } from '@/lib/features'; import { Item, ManageOrMemberComponent, getNestedValue } from '@/lib/features/manual-time/manage-member-component'; import { TranslationHooks, useTranslations } from 'next-intl'; import { ToggleButton } from './EditTaskModal'; -import { PlusIcon } from '@radix-ui/react-icons'; +import { PlusIcon, ReloadIcon } from '@radix-ui/react-icons'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@components/ui/accordion'; import { DatePickerFilter } from './TimesheetFilterDate'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@components/ui/select'; +import { useTimesheet } from '@/app/hooks/features/useTimesheet'; export interface IAddTaskModalProps { isOpen: boolean; closeModal: () => void; } +interface Shift { + startTime: string; + endTime: string; + totalHours: string; + dateFrom: Date | string, +} + export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { + const { tasks } = useTeamTasks(); const { generateTimeOptions } = useTimelogFilterOptions(); - const timeOptions = generateTimeOptions(15); + const { organizationProjects } = useOrganizationProjects(); + const { activeTeam } = useOrganizationTeams(); + const { createTimesheet, loadingCreateTimesheet } = useTimesheet({}); + + const timeOptions = generateTimeOptions(5); const t = useTranslations(); - const { activeTeam } = useTeamTasks(); - const [notes, setNotes] = React.useState(''); - const [task, setTasks] = React.useState('') - const [isBillable, setIsBillable] = React.useState(true); - const [dateRange, setDateRange] = React.useState<{ from: Date | null }>({ - from: new Date(), + const [formState, setFormState] = React.useState({ + notes: '', + isBillable: true, + taskId: '', + employeeId: '', + projectId: '', + shifts: [ + { startTime: '', endTime: '', totalHours: '00:00h', dateFrom: new Date() }, + ] as Shift[], }); - const handleFromChange = (fromDate: Date | null) => { - setDateRange((prev) => ({ ...prev, from: fromDate })); + + const updateFormState = (field: keyof typeof formState, value: any) => { + setFormState((prevState) => ({ + ...prevState, + [field]: value, + })); }; + const projectItemsLists = { - Project: activeTeam?.projects ?? [], + Project: organizationProjects || [], }; const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { - // Handle value changes + if (!values.Project) return; + updateFormState('projectId', values.Project.id); }; + const selectedValues = { Project: null, }; - const handleChange = (field: string, selectedItem: Item | null) => { + const handleChange = () => { // Handle field changes }; @@ -54,59 +77,100 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { }, ]; + const handleAddTimesheet = async () => { + const payload = { + isBillable: formState.isBillable, + description: formState.notes, + projectId: formState.projectId, + logType: TimeLogType.MANUAL as any, + source: TimerSource.BROWSER as any, + taskId: formState.taskId, + employeeId: formState.employeeId + } + const createUtcDate = (baseDate: Date, time: string): Date => { + const [hours, minutes] = time.split(':').map(Number); + return new Date(Date.UTC(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), hours, minutes)); + }; + + try { + await Promise.all(formState.shifts.map(async (shift) => { + const baseDate = shift.dateFrom instanceof Date ? shift.dateFrom : new Date(shift.dateFrom ?? new Date()); + const startedAt = createUtcDate(baseDate, shift.startTime.toString().slice(0, 5)); + const stoppedAt = createUtcDate(baseDate, shift.endTime.toString().slice(0, 5)); + await createTimesheet({ + ...payload, + startedAt, + stoppedAt, + }); + })); + closeModal(); + } catch (error) { + console.error('Failed to create timesheet:', error); + } + } + return ( -
    +
    - setTasks(e.target?.value)} - className="w-full p-2 border font-normal border-slate-300 dark:border-slate-600 dark:bg-dark--theme-light rounded-md" - placeholder='Bug for creating calendar view' - required + updateFormState('taskId', value.id)} + classNameGroup='h-[40vh]' + ariaLabel='Task issues' + className='w-full font-medium' + options={tasks} + renderOption={(option) => ( +
    + +
    + )} /> +
    -
    +
    items)} - renderOption={(option) => ( + className='w-full font-medium' + options={activeTeam?.members as any} + onChange={(value: any) => updateFormState('employeeId', value.id)} + renderOption={(option: any) => (
    - - {option} + {option.employee.fullName}
    )} />
    updateFormState('shifts', e)} + shifts={formState.shifts} t={t} - dateRange={dateRange} timeOptions={timeOptions} - handleFromChange={handleFromChange} /> + />
    @@ -127,13 +191,13 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { }
    setIsBillable(true)} + isActive={formState.isBillable} + onClick={() => updateFormState('isBillable', true)} label={t('pages.timesheet.BILLABLE.YES')} /> setIsBillable(false)} + isActive={!formState.isBillable} + onClick={() => updateFormState('isBillable', false)} label={t('pages.timesheet.BILLABLE.NO')} />
    @@ -142,14 +206,14 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
    {t('common.NOTES')}