From 31507e2fc021bb6df0dfe350d1b45cca70ecd3e7 Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:49:22 +0200 Subject: [PATCH 1/5] [Fix]: Update Timesheet Functionality and Load All Projects (#3413) * fix: update timesheet functionality and load all projects * fix: cspell --- .../[memberId]/components/AddTaskModal.tsx | 8 ++- .../[memberId]/components/CalendarView.tsx | 6 +- .../components/CompactTimesheetComponent.tsx | 4 +- .../[memberId]/components/EditTaskModal.tsx | 60 ++++++++++++++----- .../components/FilterWithStatus.tsx | 3 +- .../components/TimeSheetFilterPopover.tsx | 8 ++- .../[memberId]/components/TimesheetCard.tsx | 2 +- .../[locale]/timesheet/[memberId]/page.tsx | 8 ++- apps/web/app/hooks/features/useTimesheet.ts | 2 +- apps/web/lib/components/Kanban.tsx | 8 +-- 10 files changed, 72 insertions(+), 37 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index ca450efd9..514305b6c 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useTeamTasks, useTimelogFilterOptions } from '@/app/hooks'; +import { useOrganizationProjects, useTimelogFilterOptions } from '@/app/hooks'; import { ITaskIssue } from '@/app/interfaces'; import { clsxm } from '@/app/utils'; import { Modal } from '@/lib/components' @@ -17,20 +17,22 @@ export interface IAddTaskModalProps { } export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { const { generateTimeOptions } = useTimelogFilterOptions(); + const { organizationProjects } = useOrganizationProjects(); + const timeOptions = generateTimeOptions(15); 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 handleFromChange = (fromDate: Date | null) => { setDateRange((prev) => ({ ...prev, from: fromDate })); }; const projectItemsLists = { - Project: activeTeam?.projects ?? [], + Project: organizationProjects ?? [], }; const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx index 686961708..6d31d52e8 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/CalendarView.tsx @@ -102,7 +102,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati
- + {status === 'DENIED' ? 'REJECTED' : status} ({rows.length}) @@ -141,7 +141,7 @@ const CalendarDataView = ({ data, t }: { data?: GroupedTimesheet[], t: Translati ( ); -const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full" }: +const ImageWithLoader = ({ imageUrl, alt, className = "w-6 h-6 rounded-full font-bold" }: { imageUrl: string; alt: string; className?: string }) => { const [isLoading, setIsLoading] = React.useState(true); return (
{isLoading && (
- +
)} ({ date: dataTimesheet.timesheet?.startedAt ? new Date(dataTimesheet.timesheet.startedAt) : new Date(), @@ -45,13 +48,12 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo })); }; const [timesheetData, setTimesheetData] = useState({ - isBillable: dataTimesheet.isBillable, + isBillable: dataTimesheet.isBillable ?? true, projectId: dataTimesheet.project?.id || '', notes: dataTimesheet.description || '', }); - const memberItemsLists = { - Project: activeTeam?.projects as [], + Project: organizationProjects, }; const handleSelectedValuesChange = (values: { [key: string]: Item | null }) => { setTimesheetData((prev) => ({ @@ -60,7 +62,7 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo })); }; const selectedValues = { - Teams: null, + Project: dataTimesheet.project, }; const handleChange = (field: string, selectedItem: Item | null) => { // Handle field changes @@ -94,19 +96,40 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo ...timeRange.endTime.split(':').map(Number) ) ); - await updateTimesheet({ - id: dataTimesheet.timesheetId, + const payload = { + id: dataTimesheet.id, isBillable: timesheetData.isBillable, employeeId: dataTimesheet.employeeId, logType: dataTimesheet.logType, source: dataTimesheet.source, - startedAt: startedAt, - stoppedAt: stoppedAt, + startedAt, + stoppedAt, tenantId: dataTimesheet.tenantId, organizationId: dataTimesheet.organizationId, description: timesheetData.notes, projectId: timesheetData.projectId, - }); + organizationTeamId: dataTimesheet.organizationTeamId ?? null, + organizationContactId: dataTimesheet.organizationContactId ?? null, + } + updateTimesheet({ ...payload }) + .then(() => { + toast({ + title: 'Modification Confirmed', + description: "The timesheet has been successfully modified.", + variant: 'default', + className: 'bg-green-50 text-green-600 border-green-500 z-[10000px]', + action: Undo + }); + closeModal() + }).catch((error) => { + toast({ + title: 'Error during modification', + description: `An error occurred: ${error}. The timesheet could not be modified.`, + variant: 'destructive', + className: 'bg-red-50 text-red-600 border-red-500 z-[10000px]' + }); + closeModal() + }); }, [dateRange, timeRange, timesheetData, dataTimesheet, updateTimesheet]); const fields = [ @@ -137,8 +160,8 @@ export function EditTaskModal({ isOpen, closeModal, dataTimesheet }: IEditTaskMo
* -
diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx index 8e4c98a06..48834ec8a 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/FilterWithStatus.tsx @@ -51,8 +51,7 @@ export function FilterWithStatus({ className={clsxm( 'flex flex-nowrap h-[2.2rem] items-center bg-[#e2e8f0aa] dark:bg-gray-800 rounded-xl ', className - )} - > + )}> {buttonData.map(({ label, count, icon }, index) => (
{ const { toggleColumn } = useKanban(); const { isOpen, closeModal, openModal } = useModal(); - + const t = useTranslations() return ( <> {title && ( @@ -417,7 +417,7 @@ const KanbanDraggableHeader = ({ className="hover:font-medium p-1.5 text-sm cursor-pointer" onClick={() => createTask()} > - Create Task + {t('common.CREATE_TASK')}
Date: Wed, 11 Dec 2024 13:02:09 +0700 Subject: [PATCH 2/5] Fix/server web about page (#3409) * fix: rafactoring function create window * fix: about page and about window server web * fix: remove unused code * fix: server web about link action * fix: cspell error * fix: added enum event Window * fix: added enum event Window --- .../desktop-server-web-environment-content.ts | 2 + .scripts/env.ts | 10 +- apps/server-web/src/main/helpers/constant.ts | 53 +++++- .../src/main/helpers/interfaces/i-events.ts | 5 + .../src/main/helpers/interfaces/i-window.ts | 8 + .../src/main/helpers/interfaces/index.ts | 2 + apps/server-web/src/main/main.ts | 167 ++++++++++-------- apps/server-web/src/main/menu.ts | 64 ++++--- .../src/main/windows/window-factory.ts | 82 +++++++++ apps/server-web/src/renderer/App.tsx | 2 + .../src/renderer/components/About.tsx | 54 +++++- .../src/renderer/components/SideBar.tsx | 4 +- .../components/svgs/EverTeamsLogo.tsx | 108 +++++++++-- apps/server-web/src/renderer/pages/About.tsx | 88 +++++++++ 14 files changed, 525 insertions(+), 124 deletions(-) create mode 100644 apps/server-web/src/main/helpers/interfaces/i-events.ts create mode 100644 apps/server-web/src/main/helpers/interfaces/i-window.ts create mode 100644 apps/server-web/src/main/windows/window-factory.ts create mode 100644 apps/server-web/src/renderer/pages/About.tsx 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; From 75af5ca7494bc365bbc8d2d89235ba5a2e72d438 Mon Sep 17 00:00:00 2001 From: Innocent-akim Date: Thu, 12 Dec 2024 15:16:44 +0200 Subject: [PATCH 3/5] feat: add multi-language translations for task-related terms and project actions --- .../[memberId]/components/AddTaskModal.tsx | 2 +- .../[memberId]/components/TimesheetAction.tsx | 6 ++--- .../[locale]/timesheet/[memberId]/page.tsx | 6 ++--- apps/web/components/nav-projects.tsx | 8 +++---- .../details-section/blocks/task-progress.tsx | 22 +++++++++---------- apps/web/components/sidebar-opt-in-form.tsx | 6 +++-- apps/web/lib/components/Kanban.tsx | 8 +++---- apps/web/locales/ar.json | 13 +++++++++++ apps/web/locales/bg.json | 13 +++++++++++ apps/web/locales/de.json | 13 +++++++++++ apps/web/locales/en.json | 13 +++++++++++ apps/web/locales/es.json | 13 +++++++++++ apps/web/locales/fr.json | 13 +++++++++++ apps/web/locales/he.json | 13 +++++++++++ apps/web/locales/it.json | 13 +++++++++++ apps/web/locales/nl.json | 13 +++++++++++ apps/web/locales/pl.json | 13 +++++++++++ apps/web/locales/pt.json | 13 +++++++++++ apps/web/locales/ru.json | 13 +++++++++++ apps/web/locales/zh.json | 13 +++++++++++ 20 files changed, 199 insertions(+), 28 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index 514305b6c..90c9d2bc9 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -60,7 +60,7 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) { diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx index 1909dcd13..b22d01e2c 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/TimesheetAction.tsx @@ -27,7 +27,7 @@ export const TimesheetButton = ({ className, icon, onClick, title, disabled }: I } -export type StatusType = "Pending" | "Approved" | "Denied"; +export type StatusType = "PENDING" | "APPROVED" | "Denied"; export type StatusAction = "Deleted" | "Approved" | "Denied"; @@ -36,12 +36,12 @@ export type StatusAction = "Deleted" | "Approved" | "Denied"; export const getTimesheetButtons = (status: StatusType, t: TranslationHooks, disabled: boolean, onClick: (action: StatusAction) => void) => { const buttonsConfig: Record = { - Pending: [ + PENDING: [ { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_APPROVE_SELECTED'), action: "Approved" }, { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" }, { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" } ], - Approved: [ + APPROVED: [ { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_REJECT_SELECTED'), action: "Denied" }, { icon: , title: t('pages.timesheet.TIMESHEET_ACTION_DELETE_SELECTED'), action: "Deleted" } ], diff --git a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx index 4051a0bc0..5dd010ff2 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/page.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/page.tsx @@ -145,7 +145,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb
    } classNameIcon="bg-[#FBB650] shadow-[#fbb75095]" @@ -153,7 +153,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb /> } classNameIcon="bg-[#3D5A80] shadow-[#3d5a809c] " @@ -164,7 +164,7 @@ const TimeSheet = React.memo(function TimeSheetPage({ params }: { params: { memb .map(entry => entry.employee.id) .filter((id, index, array) => array.indexOf(id) === index) .length} - title="Members Worked" + title={t('common.MEMBERS_WORKED')} description="People worked since last time" icon={} classNameIcon="bg-[#30B366] shadow-[#30b3678f]" diff --git a/apps/web/components/nav-projects.tsx b/apps/web/components/nav-projects.tsx index e9d9d3c52..bb0001da6 100644 --- a/apps/web/components/nav-projects.tsx +++ b/apps/web/components/nav-projects.tsx @@ -41,7 +41,7 @@ export function NavProjects({ const t = useTranslations(); return userManagedTeams && userManagedTeams.length > 0 ? ( - Projects + {t('sidebar.PROJECTS')} {projects && projects.length ? ( <> @@ -67,16 +67,16 @@ export function NavProjects({ > - View Project + {t('common.VIEW_PROJECT')} - Share Project + {t('common.SHARE_PROJECT')} - Delete Project + {t('common.DELETE_PROJECT')} diff --git a/apps/web/components/pages/task/details-section/blocks/task-progress.tsx b/apps/web/components/pages/task/details-section/blocks/task-progress.tsx index 3623721be..dc71bcbd2 100644 --- a/apps/web/components/pages/task/details-section/blocks/task-progress.tsx +++ b/apps/web/components/pages/task/details-section/blocks/task-progress.tsx @@ -75,14 +75,14 @@ const TaskProgress = () => { const matchingMembers: | OT_Member[] | undefined = activeTeam?.members.filter((member) => - task?.members.some((taskMember) => taskMember.id === member.employeeId) - ); + task?.members.some((taskMember) => taskMember.id === member.employeeId) + ); const usersTaskArray: | ITasksTimesheet[] | undefined = matchingMembers - ?.flatMap((obj) => obj.totalWorkedTasks) - .filter((taskObj) => taskObj?.id === task?.id); + ?.flatMap((obj) => obj.totalWorkedTasks) + .filter((taskObj) => taskObj?.id === task?.id); const usersTotalTimeInSeconds: number | undefined = usersTaskArray?.reduce( (totalDuration, item) => totalDuration + item.duration, @@ -100,9 +100,9 @@ const TaskProgress = () => { const remainingTime: number = task?.estimate === null || - task?.estimate === 0 || - task?.estimate === undefined || - usersTotalTimeInSeconds === undefined + task?.estimate === 0 || + task?.estimate === undefined || + usersTotalTimeInSeconds === undefined ? 0 : task?.estimate - usersTotalTimeInSeconds; @@ -121,7 +121,7 @@ const TaskProgress = () => { isAuthUser={true} activeAuthTask={true} showPercents={true} - // memberInfo={memberInfo} + // memberInfo={memberInfo} /> @@ -169,7 +169,7 @@ const TaskProgress = () => { } className="text-xs" > - Show More + {t('common.SHOW_MORE')}
    )} @@ -201,8 +201,8 @@ const IndividualMembersTotalTime = ({ const matchingMembers: | OT_Member[] | undefined = activeTeam?.members.filter((member) => - task?.members.some((taskMember) => taskMember.id === member.employeeId) - ); + task?.members.some((taskMember) => taskMember.id === member.employeeId) + ); const findUserTotalWorked = (user: OT_Member, id: string | undefined) => { return ( diff --git a/apps/web/components/sidebar-opt-in-form.tsx b/apps/web/components/sidebar-opt-in-form.tsx index 670acd645..e66195efb 100644 --- a/apps/web/components/sidebar-opt-in-form.tsx +++ b/apps/web/components/sidebar-opt-in-form.tsx @@ -9,8 +9,10 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@/component import { ToastAction } from './ui/toast'; import { toast } from './ui/use-toast'; import { useState } from 'react'; +import { useTranslations } from 'next-intl'; export function SidebarOptInForm() { + const t = useTranslations(); const { state } = useSidebar(); const [isLoading, setLoading] = useState(false); const subscribeFormSchema = z @@ -73,9 +75,9 @@ export function SidebarOptInForm() {
    - Subscribe to our newsletter + {t('common.SUBSCRIBE_NEWSLETTER')} - Opt-in to receive updates and news about Ever Teams. + {t('common.OPT_IN_UPDATES')} diff --git a/apps/web/lib/components/Kanban.tsx b/apps/web/lib/components/Kanban.tsx index af8606d8d..fb5df3592 100644 --- a/apps/web/lib/components/Kanban.tsx +++ b/apps/web/lib/components/Kanban.tsx @@ -299,13 +299,13 @@ export const EmptyKanbanDroppable = ({ className="hover:font-medium p-1.5 text-sm cursor-pointer" onClick={() => toggleColumn(title, false)} > - Collapse Column + {t('common.COLLAPSE_COLUMN')}
- Edit Status + {t('common.EDIT_STATUS')}
@@ -423,10 +423,10 @@ const KanbanDraggableHeader = ({ className="hover:font-medium p-1.5 text-sm cursor-pointer" onClick={() => toggleColumn(title, true)} > - Collapse Column + {t('common.COLLAPSE_COLUMN')}
- Edit Status + {t('common.EDIT_STATUS')}
diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index eca9c7ba9..5f2c47efd 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -1,6 +1,19 @@ { "TITLE": "إيفر تيمز", "common": { + "PENDING_TASKS": "المهام المعلقة", + "SHOW_MORE": "عرض المزيد", + "TASKS": "المهام", + "MEN_HOURS": "ساعات العمل", + "MEMBERS_WORKED": "الأعضاء الذين عملوا", + "COLLAPSE_COLUMN": "طي العمود", + "EDIT_STATUS": "تعديل الحالة", + "ADD_TIME_ENTRY": "إضافة إدخال وقت", + "OPT_IN_UPDATES": "اشترك لتلقي التحديثات والأخبار حول Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "اشترك في النشرة الإخبارية", + "VIEW_PROJECT": "عرض المشروع", + "SHARE_PROJECT": "مشاركة المشروع", + "DELETE_PROJECT": "حذف المشروع", "DELETE_CONFIRMATION": "هل أنت متأكد أنك تريد الحذف؟", "IRREVERSIBLE_ACTION_WARNING": "هذا الإجراء لا رجعة فيه. ستفقد جميع البيانات ذات الصلة.", "SHIFT_TIMING": "توقيت العمل", diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 0cdd2c793..ce51bec75 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Бюджет на проекта" }, "common": { + "PENDING_TASKS": "Чакащи задачи", + "SHOW_MORE": "Покажи още", + "TASKS": "Задачи", + "MEN_HOURS": "Човеко-часове", + "MEMBERS_WORKED": "Работили членове", + "COLLAPSE_COLUMN": "Свий колона", + "EDIT_STATUS": "Редактирай статус", + "ADD_TIME_ENTRY": "Добавяне на запис на време", + "OPT_IN_UPDATES": "Абонирайте се за актуализации и новини за Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Абонирайте се за нашия бюлетин", + "VIEW_PROJECT": "Преглед на проекта", + "SHARE_PROJECT": "Споделяне на проекта", + "DELETE_PROJECT": "Изтриване на проекта", "DELETE_CONFIRMATION": "Сигурни ли сте, че искате да изтриете това?", "IRREVERSIBLE_ACTION_WARNING": "Това действие е необратимо. Всички свързани данни ще бъдат изгубени.", "SHIFT_TIMING": "Смяна на времето", diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index 72fea4a02..09b5a8192 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Projektbudget" }, "common": { + "PENDING_TASKS": "Ausstehende Aufgaben", + "SHOW_MORE": "Mehr anzeigen", + "TASKS": "Aufgaben", + "MEN_HOURS": "Mannstunden", + "MEMBERS_WORKED": "Mitglieder gearbeitet", + "COLLAPSE_COLUMN": "Spalte einklappen", + "EDIT_STATUS": "Status bearbeiten", + "ADD_TIME_ENTRY": "Zeiteintrag hinzufügen", + "OPT_IN_UPDATES": "Abonnieren Sie Updates und Nachrichten zu Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Abonnieren Sie unseren Newsletter", + "VIEW_PROJECT": "Projekt anzeigen", + "SHARE_PROJECT": "Projekt teilen", + "DELETE_PROJECT": "Projekt löschen", "DELETE_CONFIRMATION": "Sind Sie sicher, dass Sie dies löschen möchten?", "IRREVERSIBLE_ACTION_WARNING": "Diese Aktion ist irreversibel. Alle zugehörigen Daten gehen verloren.", "SHIFT_TIMING": "Schichtzeiten", diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index a01c1cc56..37c2d34eb 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Project Budget" }, "common": { + "PENDING_TASKS": "Pending Tasks", + "SHOW_MORE": "Show More", + "TASKS": "Tasks", + "MEN_HOURS": "Men Hours", + "MEMBERS_WORKED": "Members Worked", + "COLLAPSE_COLUMN": "Collapse Column", + "EDIT_STATUS": "Edit Status", + "ADD_TIME_ENTRY": "Add Time Entry", + "OPT_IN_UPDATES": "Opt-in to receive updates and news about Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Subscribe to our newsletter", + "VIEW_PROJECT": "View Project", + "SHARE_PROJECT": "Share Project", + "DELETE_PROJECT": "Delete Project", "DELETE_CONFIRMATION": "Are you sure you want to delete this?", "IRREVERSIBLE_ACTION_WARNING": "This action is irreversible. All related data will be lost.", "SHIFT_TIMING": "Shift Timing", diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index e9b0cadd0..4e9f06126 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Presupuesto del Proyecto" }, "common": { + "PENDING_TASKS": "Tareas pendientes", + "SHOW_MORE": "Mostrar más", + "TASKS": "Tareas", + "MEN_HOURS": "Horas Hombre", + "MEMBERS_WORKED": "Miembros que trabajaron", + "COLLAPSE_COLUMN": "Colapsar columna", + "EDIT_STATUS": "Editar estado", + "ADD_TIME_ENTRY": "Agregar entrada de tiempo", + "OPT_IN_UPDATES": "Opta por recibir actualizaciones y noticias sobre Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Suscríbete a nuestro boletín", + "VIEW_PROJECT": "Ver proyecto", + "SHARE_PROJECT": "Compartir proyecto", + "DELETE_PROJECT": "Eliminar proyecto", "DELETE_CONFIRMATION": "¿Estás seguro de que deseas eliminar esto?", "IRREVERSIBLE_ACTION_WARNING": "Esta acción es irreversible. Todos los datos relacionados se perderán.", "SHIFT_TIMING": "Horario de turno", diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index aa682d277..408e52567 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Budget du projet" }, "common": { + "PENDING_TASKS": "Tâches en attente", + "SHOW_MORE": "Voir plus", + "TASKS": "Tâches", + "MEN_HOURS": "Heures Homme", + "MEMBERS_WORKED": "Membres ayant travaillé", + "COLLAPSE_COLUMN": "Réduire la colonne", + "EDIT_STATUS": "Modifier le statut", + "ADD_TIME_ENTRY": "Ajouter une entrée de temps", + "OPT_IN_UPDATES": "Acceptez de recevoir des mises à jour et des nouvelles sur Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Abonnez-vous à notre newsletter", + "VIEW_PROJECT": "Voir le projet", + "SHARE_PROJECT": "Partager le projet", + "DELETE_PROJECT": "Supprimer le projet", "DELETE_CONFIRMATION": "Êtes-vous sûr de vouloir supprimer cela ?", "IRREVERSIBLE_ACTION_WARNING": "Cette action est irréversible. Toutes les données associées seront perdues.", "SHIFT_TIMING": "Temps de poste", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index c1305fe5f..22fcdc8be 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "תקציב פרויקט" }, "common": { + "PENDING_TASKS": "משימות ממתינות", + "SHOW_MORE": "הצג עוד", + "TASKS": "משימות", + "MEN_HOURS": "שעות עבודה", + "MEMBERS_WORKED": "חברים שעבדו", + "COLLAPSE_COLUMN": "כווץ עמודה", + "EDIT_STATUS": "ערוך סטטוס", + "ADD_TIME_ENTRY": "הוסף כניסת זמן", + "OPT_IN_UPDATES": "בחר לקבל עדכונים וחדשות על Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "הירשם לניוזלטר שלנו", + "VIEW_PROJECT": "צפה בפרויקט", + "SHARE_PROJECT": "שתף פרויקט", + "DELETE_PROJECT": "מחק פרויקט", "DELETE_CONFIRMATION": "האם אתה בטוח שברצונך למחוק?", "IRREVERSIBLE_ACTION_WARNING": "פעולה זו בלתי הפיכה. כל הנתונים הקשורים יימחקו.", "SHIFT_TIMING": "זמן משמרת", diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 02be4b0a7..a675053bb 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Budget di progetto" }, "common": { + "PENDING_TASKS": "Compiti in sospeso", + "SHOW_MORE": "Mostra di più", + "TASKS": "Compiti", + "MEN_HOURS": "Ore Uomo", + "MEMBERS_WORKED": "Membri che hanno lavorato", + "COLLAPSE_COLUMN": "Comprimi colonna", + "EDIT_STATUS": "Modifica stato", + "ADD_TIME_ENTRY": "Aggiungi voce di tempo", + "OPT_IN_UPDATES": "Acconsenti a ricevere aggiornamenti e notizie su Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Iscriviti alla nostra newsletter", + "VIEW_PROJECT": "Visualizza progetto", + "SHARE_PROJECT": "Condividi progetto", + "DELETE_PROJECT": "Elimina progetto", "DELETE_CONFIRMATION": "Sei sicuro di voler eliminare questo elemento?", "IRREVERSIBLE_ACTION_WARNING": "Questa azione è irreversibile. Tutti i dati correlati andranno persi.", "SHIFT_TIMING": "Orario di turno", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index aa792b40a..75d00076d 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Projectbudget" }, "common": { + "PENDING_TASKS": "In afwachting Taken", + "SHOW_MORE": "Meer weergeven", + "TASKS": "Taken", + "MEN_HOURS": "Manuren", + "MEMBERS_WORKED": "Leden gewerkt", + "COLLAPSE_COLUMN": "Kolom samenvouwen", + "EDIT_STATUS": "Status bewerken", + "ADD_TIME_ENTRY": "Tijdinvoer toevoegen", + "OPT_IN_UPDATES": "Kies ervoor om updates en nieuws over Ever Teams te ontvangen.", + "SUBSCRIBE_NEWSLETTER": "Abonneer je op onze nieuwsbrief", + "VIEW_PROJECT": "Project bekijken", + "SHARE_PROJECT": "Project delen", + "DELETE_PROJECT": "Project verwijderen", "DELETE_CONFIRMATION": "Weet u zeker dat u dit wilt verwijderen?", "IRREVERSIBLE_ACTION_WARNING": "Deze actie is onomkeerbaar. Alle gerelateerde gegevens gaan verloren.", "SHIFT_TIMING": "Werkrooster", diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index 0aac0db99..e5df9337c 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Budżet projektu" }, "common": { + "PENDING_TASKS": "Zadania oczekujące", + "SHOW_MORE": "Pokaż więcej", + "TASKS": "Zadania", + "MEN_HOURS": "Roboczogodziny", + "MEMBERS_WORKED": "Członkowie pracowali", + "COLLAPSE_COLUMN": "Zwiń kolumnę", + "EDIT_STATUS": "Edytuj status", + "ADD_TIME_ENTRY": "Dodaj wpis czasu", + "OPT_IN_UPDATES": "Zgódź się na otrzymywanie aktualizacji i wiadomości o Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Zapisz się na nasz newsletter", + "VIEW_PROJECT": "Wyświetl projekt", + "SHARE_PROJECT": "Udostępnij projekt", + "DELETE_PROJECT": "Usuń projekt", "DELETE_CONFIRMATION": "Czy na pewno chcesz to usunąć?", "IRREVERSIBLE_ACTION_WARNING": "Ta akcja jest nieodwracalna. Wszystkie powiązane dane zostaną utracone.", "SHIFT_TIMING": "Czas zmiany", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index 4db3c7e9d..a17e5f55d 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Orçamento do Projeto" }, "common": { + "PENDING_TASKS": "Tarefas pendentes", + "SHOW_MORE": "Mostrar mais", + "TASKS": "Tarefas", + "MEN_HOURS": "Horas Homem", + "MEMBERS_WORKED": "Membros Trabalharam", + "COLLAPSE_COLUMN": "Colapsar Coluna", + "EDIT_STATUS": "Editar Status", + "ADD_TIME_ENTRY": "Adicionar Entrada de Tempo", + "OPT_IN_UPDATES": "Concorde em receber atualizações e notícias sobre Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Inscreva-se na nossa newsletter", + "VIEW_PROJECT": "Ver Projeto", + "SHARE_PROJECT": "Compartilhar Projeto", + "DELETE_PROJECT": "Excluir Projeto", "DELETE_CONFIRMATION": "Tem certeza de que deseja excluir isso?", "IRREVERSIBLE_ACTION_WARNING": "Esta ação é irreversível. Todos os dados relacionados serão perdidos.", "SHIFT_TIMING": "Horário de turno", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index 65b25b194..5f406cfb5 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "Бюджет проекта" }, "common": { + "PENDING_TASKS": "Ожидающие задачи", + "SHOW_MORE": "Показать больше", + "TASKS": "Задачи", + "MEN_HOURS": "Часы работы", + "MEMBERS_WORKED": "Члены, которые работали", + "COLLAPSE_COLUMN": "Свернуть столбец", + "EDIT_STATUS": "Редактировать статус", + "ADD_TIME_ENTRY": "Добавить запись времени", + "OPT_IN_UPDATES": "Согласитесь получать обновления и новости о Ever Teams.", + "SUBSCRIBE_NEWSLETTER": "Подпишитесь на нашу рассылку", + "VIEW_PROJECT": "Просмотр проекта", + "SHARE_PROJECT": "Поделиться проектом", + "DELETE_PROJECT": "Удалить проект", "DELETE_CONFIRMATION": "Вы уверены, что хотите удалить это?", "IRREVERSIBLE_ACTION_WARNING": "Это действие необратимо. Все связанные данные будут потеряны.", "SHIFT_TIMING": "График смен", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index 19482afe0..f5c69dbf5 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -19,6 +19,19 @@ "PROJECT_BUDGET": "项目预算" }, "common": { + "PENDING_TASKS": "待处理任务", + "SHOW_MORE": "显示更多", + "TASKS": "任务", + "MEN_HOURS": "工时", + "MEMBERS_WORKED": "参与成员", + "COLLAPSE_COLUMN": "折叠列", + "EDIT_STATUS": "编辑状态", + "ADD_TIME_ENTRY": "添加时间条目", + "OPT_IN_UPDATES": "选择接收关于 Ever Teams 的更新和新闻。", + "SUBSCRIBE_NEWSLETTER": "订阅我们的新闻通讯", + "VIEW_PROJECT": "查看项目", + "SHARE_PROJECT": "分享项目", + "DELETE_PROJECT": "删除项目", "DELETE_CONFIRMATION": "您确定要删除此项吗?", "IRREVERSIBLE_ACTION_WARNING": "此操作不可逆。所有相关数据将丢失。", "SHIFT_TIMING": "班次时间", From eb4a693f9dc2ca4575a6ea31ce2ba270a68b439d Mon Sep 17 00:00:00 2001 From: AKILIMAILI CIZUNGU Innocent <51681130+Innocent-Akim@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:17:42 +0200 Subject: [PATCH 4/5] [Feat]: Add Timesheet Creation Functionality (#3417) * feat: add timesheet creation functionality * fix: coderabbitai * fix: coderabbitai --- .../[memberId]/components/AddTaskModal.tsx | 191 ++++++++++++------ .../components/CompactTimesheetComponent.tsx | 2 +- .../[memberId]/components/EditTaskModal.tsx | 6 +- .../components/TimeSheetFilterPopover.tsx | 58 +++--- apps/web/app/hooks/features/useTimesheet.ts | 2 +- apps/web/app/interfaces/timer/ITimerLog.ts | 12 +- apps/web/components/ui/sidebar.tsx | 2 +- .../lib/features/multiple-select/index.tsx | 9 +- 8 files changed, 180 insertions(+), 102 deletions(-) diff --git a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx index 514305b6c..5aa025acb 100644 --- a/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx +++ b/apps/web/app/[locale]/timesheet/[memberId]/components/AddTaskModal.tsx @@ -1,47 +1,68 @@ import React from 'react' -import { useOrganizationProjects, 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 { organizationProjects } = useOrganizationProjects(); + const { activeTeam } = useOrganizationTeams(); + const { createTimesheet, loadingCreateTimesheet } = useTimesheet({}); - const timeOptions = generateTimeOptions(15); + const timeOptions = generateTimeOptions(5); const t = useTranslations(); - 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: organizationProjects ?? [], + 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 }; @@ -56,6 +77,38 @@ 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} /> + />
@@ -129,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')} />
@@ -144,14 +206,14 @@ export function AddTaskModal({ closeModal, isOpen }: IAddTaskModalProps) {
{t('common.NOTES')}