diff --git a/.env.compose b/.env.compose index 19e8d6e0c65..cd854fa0e8b 100644 --- a/.env.compose +++ b/.env.compose @@ -1,3 +1,7 @@ +# Docker Compose sample .env file for Production + +NODE_ENV=production + # The name of the application. APP_NAME="Gauzy" @@ -32,7 +36,7 @@ WEB_HOST=webapp WEB_PORT=4200 # set true if running as a Demo -DEMO=true +DEMO=false # DO (DIGITALOCEAN), AWS, AZURE, CIVO, CW (COREWEAVE), HEROKU, LINODE, LOCAL, OVH, SCALEWAY, VULTR, etc CLOUD_PROVIDER= diff --git a/.env.demo.compose b/.env.demo.compose new file mode 100644 index 00000000000..21b23563694 --- /dev/null +++ b/.env.demo.compose @@ -0,0 +1,436 @@ +# Docker Compose sample .env file for Demo + +# The environment to use for the application. +NODE_ENV=production + +# The name of the application. +APP_NAME="Gauzy" + +# The URL for the application logo. +APP_LOGO="http://localhost:4200/assets/images/logos/logo_Gauzy.png" + +# The signature or tagline for the application. +APP_SIGNATURE="Gauzy" + +# The link to the application. +APP_LINK="http://localhost:4200" + +# The URL for email confirmation in the application. +APP_EMAIL_CONFIRMATION_URL="http://localhost:4200/#/auth/confirm-email" + +# The URL for magic sign-in in the application. +APP_MAGIC_SIGN_URL="http://localhost:4200/#/auth/magic-sign-in" + +# set true if running inside Docker container +IS_DOCKER=true + +# API Host +API_HOST=api + +# API Port +API_PORT=3000 + +# WEB UI Host +WEB_HOST=webapp + +# WEB UI Port +WEB_PORT=4200 + +# set true if running as a Demo +DEMO=true + +# DO (DIGITALOCEAN), AWS, AZURE, CIVO, CW (COREWEAVE), HEROKU, LINODE, LOCAL, OVH, SCALEWAY, VULTR, etc +CLOUD_PROVIDER= + +ALLOW_SUPER_ADMIN_ROLE=true + +# set to Gauzy API base URL +API_BASE_URL=http://localhost:3000 + +# set to Gauzy UI base URL +CLIENT_BASE_URL=http://localhost:4200 + +#set to Website Platform +PLATFORM_WEBSITE_URL=https://gauzy.co +PLATFORM_WEBSITE_DOWNLOAD_URL=https://gauzy.co/downloads + +# DB_TYPE: sqlite | postgres | better-sqlite3 +DB_TYPE=postgres +DB_SYNCHRONIZE=false + +# PostgreSQL Connection Parameters +DB_HOST=db +DB_PORT=5432 +DB_NAME=gauzy +DB_USER=postgres +DB_PASS=gauzy_password + +EXPRESS_SESSION_SECRET=gauzy + +# JWT Refresh Token Configuration +JWT_REFRESH_TOKEN_SECRET=refreshSecretKey +JWT_REFRESH_TOKEN_EXPIRATION_TIME=86400 + +# Email Verification Config +JWT_VERIFICATION_TOKEN_SECRET=verificationSecretKey +JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=86400 + +# Email Verification Config +JWT_VERIFICATION_TOKEN_SECRET=verificationSecretKey +JWT_VERIFICATION_TOKEN_EXPIRATION_TIME=86400 + +# Password Less Authentication Configuration +MAGIC_CODE_EXPIRATION_TIME=600 + +# Join Request Organization Team Configuration +TEAM_JOIN_REQUEST_EXPIRATION_TIME=86400 + +# Rate Limiting +THROTTLE_TTL=60 +THROTTLE_LIMIT=300 + +# Twitter OAuth Configuration +TWITTER_CLIENT_ID=XXXXXXX +TWITTER_CLIENT_SECRET=XXXXXXX +TWITTER_CALLBACK_URL=http://localhost:3000/api/auth/twitter/callback + +# Google OAuth Configuration +GOOGLE_CLIENT_ID=XXXXXXX +GOOGLE_CLIENT_SECRET=XXXXXXX +GOOGLE_CALLBACK_URL=http://localhost:3000/api/auth/google/callback + +# Facebook OAuth Configuration +FACEBOOK_CLIENT_ID=XXXXXXX +FACEBOOK_CLIENT_SECRET=XXXXXXX +FACEBOOK_CALLBACK_URL=http://localhost:3000/api/auth/facebook/callback +FACEBOOK_GRAPH_VERSION=v3.0 + +# Github OAuth App Integration +GAUZY_GITHUB_OAUTH_CLIENT_ID=XXXXXXX +GAUZY_GITHUB_OAUTH_CLIENT_SECRET=XXXXXXX +GAUZY_GITHUB_OAUTH_CALLBACK_URL="http://localhost:3000/api/auth/github/callback" + +# LinkedIn OAuth Configuration +LINKEDIN_CLIENT_ID=XXXXXXX +LINKEDIN_CLIENT_SECRET=XXXXXXX +LINKEDIN_CALLBACK_URL=http://localhost:3000/api/auth/linkedin/callback + +# Microsoft OAuth Configuration +MICROSOFT_GRAPH_API_URL=https://graph.microsoft.com/v1.0 +MICROSOFT_AUTHORIZATION_URL=https://login.microsoftonline.com/common/oauth2/v2.0/authorize +MICROSOFT_TOKEN_URL=https://login.microsoftonline.com/common/oauth2/v2.0/token +MICROSOFT_CLIENT_ID=XXXXXXX +MICROSOFT_CLIENT_SECRET=XXXXXXX +MICROSOFT_CALLBACK_URL=http://localhost:3000/api/auth/microsoft/callback + +# Github Apps Integration +GAUZY_GITHUB_CLIENT_ID=XXXXXXX +GAUZY_GITHUB_CLIENT_SECRET=XXXXXXX + +# Github App Install Integration +GAUZY_GITHUB_APP_NAME= +GAUZY_GITHUB_APP_ID=XXXXXXX +GAUZY_GITHUB_APP_PRIVATE_KEY= + +# Github Webhook Configuration +GAUZY_GITHUB_WEBHOOK_URL=http://localhost:3000/api/auth/github/webhook +GAUZY_GITHUB_WEBHOOK_SECRET=XXXXXXX + +# Github Redirect URL +GAUZY_GITHUB_REDIRECT_URL=http://localhost:3000/api/integration/github/callback +GAUZY_GITHUB_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/github/setup/installation" +GAUZY_GITHUB_API_VERSION="2022-11-28" + +FIVERR_CLIENT_ID=XXXXXXX +FIVERR_CLIENT_SECRET=XXXXXXX + +AUTH0_CLIENT_ID=XXXXXXX +AUTH0_CLIENT_SECRET=XXXXXXX +AUTH0_DOMAIN=XXXXXXX + +KEYCLOAK_REALM=XXXXXXX +KEYCLOAK_CLIENT_ID=XXXXXXX +KEYCLOAK_SECRET=XXXXXXX +KEYCLOAK_AUTH_SERVER_URL=XXXXXXX +KEYCLOAK_COOKIE_KEY=XXXXXXX + +INTEGRATED_HUBSTAFF_USER_PASS=hubstaffPassword + +# Upwork Integration Config +UPWORK_API_KEY=XXXXXXX +UPWORK_API_SECRET=XXXXXXX +UPWORK_REDIRECT_URL="http://localhost:3000/api/integrations/upwork/callback" +UPWORK_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/upwork" + +# Hubstaff Integration Configuration +HUBSTAFF_CLIENT_ID=XXXXXXX +HUBSTAFF_CLIENT_SECRET=XXXXXXX +HUBSTAFF_REDIRECT_URL="http://localhost:3000/api/integration/hubstaff/callback" +HUBSTAFF_POST_INSTALL_URL="http://localhost:4200/#/pages/integrations/hubstaff" + +# File System: LOCAL | S3 | WASABI | CLOUDINARY +FILE_PROVIDER=LOCAL + +# AWS Config (optional) +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 +AWS_S3_BUCKET=gauzy + +# WASABI Config (optional) +WASABI_ACCESS_KEY_ID= +WASABI_SECRET_ACCESS_KEY= +WASABI_REGION=us-east-1 +WASABI_SERVICE_URL=https://s3.wasabisys.com +WASABI_S3_BUCKET=gauzy + +# Cloudinary Config (optional) +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= +CLOUDINARY_API_SECURE=true +CLOUDINARY_CDN_URL=https://res.cloudinary.com + +# Gauzy AI Endpoints (optional, do not set unless you subscribed to Gauzy AI) +GAUZY_AI_GRAPHQL_ENDPOINT=http://localhost:3005/graphql +GAUZY_AI_REST_ENDPOINT=http://localhost:3005/api + +# Gauzy AI Key/Secret pair authentication +GAUZY_AI_API_KEY= +GAUZY_AI_API_SECRET= + +# Gauzy Cloud +GAUZY_CLOUD_ENDPOINT=https://api.gauzy.co +GAUZY_CLOUD_APP=https://app.gauzy.co + +# SMTP Mail Config +MAIL_FROM_ADDRESS=gauzy@ever.co +MAIL_HOST=smtp.gmail.com +MAIL_PORT=465 +MAIL_USERNAME= +MAIL_PASSWORD= + +# Sentry Client Key +SENTRY_DSN=https://7cd381188b6f446ca0e69185227b9031@o51327.ingest.sentry.io/4397292 +SENTRY_HTTP_TRACING_ENABLED=false +SENTRY_POSTGRES_TRACKING_ENABLED=false +SENTRY_TRACES_SAMPLE_RATE=0.1 + +# Default Currency +DEFAULT_CURRENCY=USD + +# Default Country +DEFAULT_COUNTRY=US + +# Google Maps API Key +GOOGLE_MAPS_API_KEY= + +# Chatwoot SDK Token +CHATWOOT_SDK_TOKEN= + +# Restrict Access to Google Place Autocomplete +GOOGLE_PLACE_AUTOCOMPLETE=false + +# Nebular CHAT API key for a map message type (which is required by Google Maps) +CHAT_MESSAGE_GOOGLE_MAP= + +# Default Latitude and Longitude +DEFAULT_LATITUDE= +DEFAULT_LONGITUDE= + +# Keymetrics settings (optional) +PM2_SECRET_KEY= +PM2_PUBLIC_KEY= +PM2_MACHINE_NAME= +PM2_APP_NAME=Gauzy +PM2_API_NAME=GauzyApi +WEB_CONCURRENCY=1 +WEB_MEMORY=4096 + +# Unleash Configuration for Features management (optional) + +UNLEASH_APP_NAME=Gauzy +UNLEASH_API_URL= +UNLEASH_INSTANCE_ID= +UNLEASH_REFRESH_INTERVAL=15000 +UNLEASH_METRICS_INTERVAL=60000 +UNLEASH_API_KEY= + +# Defines feature flags and settings related to user authentication methods. +FEATURE_EMAIL_PASSWORD_LOGIN=true +FEATURE_MAGIC_LOGIN=true +FEATURE_GITHUB_LOGIN=true +FEATURE_FACEBOOK_LOGIN=true +FEATURE_GOOGLE_LOGIN=true +FEATURE_TWITTER_LOGIN=true +FEATURE_MICROSOFT_LOGIN=true +FEATURE_LINKEDIN_LOGIN=true + +# Features Toggles + +FEATURE_DASHBOARD=true +FEATURE_TIME_TRACKING=true + +FEATURE_ESTIMATE=true +FEATURE_ESTIMATE_RECEIVED=true +FEATURE_INVOICE=true +FEATURE_INVOICE_RECURRING=true +FEATURE_INVOICE_RECEIVED=true +FEATURE_INCOME=true +FEATURE_EXPENSE=true +FEATURE_PAYMENT=true + +FEATURE_PROPOSAL=true +FEATURE_PROPOSAL_TEMPLATE=true + +FEATURE_PIPELINE=true +FEATURE_PIPELINE_DEAL=true + +FEATURE_DASHBOARD_TASK=true +FEATURE_TEAM_TASK=true +FEATURE_MY_TASK=true + +FEATURE_JOB=true + +FEATURE_EMPLOYEES=true +FEATURE_EMPLOYEE_TIME_ACTIVITY=true +FEATURE_EMPLOYEE_TIMESHEETS=true +FEATURE_EMPLOYEE_APPOINTMENT=true +FEATURE_EMPLOYEE_APPROVAL=true +FEATURE_EMPLOYEE_APPROVAL_POLICY=true +FEATURE_EMPLOYEE_LEVEL=true +FEATURE_EMPLOYEE_POSITION=true +FEATURE_EMPLOYEE_TIMEOFF=true +FEATURE_EMPLOYEE_RECURRING_EXPENSE=true +FEATURE_EMPLOYEE_CANDIDATE=true +FEATURE_MANAGE_INTERVIEW=true +FEATURE_MANAGE_INVITE=true + +FEATURE_ORGANIZATION=true +FEATURE_ORGANIZATION_EQUIPMENT=true +FEATURE_ORGANIZATION_INVENTORY=true +FEATURE_ORGANIZATION_TAG=true +FEATURE_ORGANIZATION_VENDOR=true +FEATURE_ORGANIZATION_PROJECT=true +FEATURE_ORGANIZATION_DEPARTMENT=true +FEATURE_ORGANIZATION_TEAM=true +FEATURE_ORGANIZATION_DOCUMENT=true +FEATURE_ORGANIZATION_EMPLOYMENT_TYPE=true +FEATURE_ORGANIZATION_RECURRING_EXPENSE=true +FEATURE_ORGANIZATION_HELP_CENTER=true + +FEATURE_CONTACT=true + +FEATURE_GOAL=true +FEATURE_GOAL_REPORT=true +FEATURE_GOAL_SETTING=true + +FEATURE_REPORT=true + +FEATURE_USER=true +FEATURE_ORGANIZATIONS=true +FEATURE_APP_INTEGRATION=true + +FEATURE_SETTING=true +FEATURE_EMAIL_HISTORY=true +FEATURE_EMAIL_TEMPLATE=true +FEATURE_IMPORT_EXPORT=true +FEATURE_FILE_STORAGE=true +FEATURE_PAYMENT_GATEWAY=true +FEATURE_SMS_GATEWAY=true +FEATURE_SMTP=true +FEATURE_ROLES_PERMISSION=true + +# Email Verification +FEATURE_EMAIL_VERIFICATION=false + +# GitHub App Integration +GITHUB_INTEGRATION_APP_ID= +GITHUB_INTEGRATION_CLIENT_ID= +GITHUB_INTEGRATION_CLIENT_SECRET= +GITHUB_INTEGRATION_PRIVATE_KEY= +GITHUB_INTEGRATION_WEBHOOK_SECRET= + +# HubStaff Integration +HUBSTAFF_CLIENT_ID= +HUBSTAFF_CLIENT_SECRET= +HUBSTAFF_PERSONAL_ACCESS_TOKEN= + +# Jitsu Browser Configuration +JITSU_BROWSER_URL= +JITSU_BROWSER_WRITE_KEY= + +# Jitsu Server Configuration +JITSU_SERVER_URL= +JITSU_SERVER_WRITE_KEY= +JITSU_SERVER_DEBUG= +JITSU_SERVER_ECHO_EVENTS= + +# Signoz Configuration +OTEL_EXPORTER_OTLP_HEADERS= +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT= +OTEL_ENABLED=false + +# Platform Logo resource URL (SVG is Recommended) +PLATFORM_LOGO='assets/images/logos/logo_Gauzy.svg' + +# Desktop App 512x512 icon +GAUZY_DESKTOP_LOGO_512X512='assets/icons/icon_512x512.png' + +# Platform Privacy URL +PLATFORM_PRIVACY_URL='https://gauzy.co/privacy' + +# Platform terms of Services URL +PLATFORM_TOS_URL='https://gauzy.co/tos' + +# Platform no internet logo +NO_INTERNET_LOGO='assets/images/logos/logo_Gauzy.svg' + +# Company Information +COMPANY_NAME='Ever Co. LTD' +COMPANY_SITE='Gauzy' +COMPANY_LINK='https://ever.co' +COMPANY_SITE_LINK='https://gauzy.co' +COMPANY_GITHUB_LINK='https://github.com/ever-co' +COMPANY_GITLAB_LINK='https://gitlab.com/ever-co' +COMPANY_FACEBOOK_LINK='https://www.facebook.com/gauzyplatform' +COMPANY_TWITTER_LINK='https://twitter.com/gauzyplatform' +COMPANY_LINKEDIN_LINK='https://www.linkedin.com/company/ever-co' + +# Desktop download links +DESKTOP_APP_DOWNLOAD_LINK_APPLE='https://gauzy.co/downloads#desktop/apple' +DESKTOP_APP_DOWNLOAD_LINK_WINDOWS='https://gauzy.co/downloads#desktop/windows' +DESKTOP_APP_DOWNLOAD_LINK_LINUX='https://gauzy.co/downloads#desktop/linux' +MOBILE_APP_DOWNLOAD_LINK='https://gauzy.co/downloads#mobile' +EXTENSION_DOWNLOAD_LINK='https://gauzy.co/downloads#extensions' + +# Desktop Timer Application Configuration +PROJECT_REPO='https://github.com/ever-co/ever-gauzy.git' +DESKTOP_TIMER_APP_NAME='gauzy-desktop-timer' +DESKTOP_TIMER_APP_DESCRIPTION='Gauzy Desktop Timer' +DESKTOP_TIMER_APP_ID='com.ever.gauzydesktoptimer' +DESKTOP_TIMER_APP_REPO_NAME='ever-gauzy-desktop-timer' +DESKTOP_TIMER_APP_REPO_OWNER='ever-co' +DESKTOP_TIMER_APP_WELCOME_TITLE= +DESKTOP_TIMER_APP_WELCOME_CONTENT= + +# Desktop Application Configuration +DESKTOP_APP_NAME='gauzy-desktop' +DESKTOP_APP_DESCRIPTION='Gauzy Desktop' +DESKTOP_APP_ID='com.ever.gauzydesktop' +DESKTOP_APP_REPO_NAME='ever-gauzy-desktop' +DESKTOP_APP_REPO_OWNER='ever-co' +DESKTOP_APP_WELCOME_TITLE= +DESKTOP_APP_WELCOME_CONTENT= + +# Desktop Server Application Configuration +DESKTOP_SERVER_APP_NAME='gauzy-server' +DESKTOP_SERVER_APP_DESCRIPTION='Gauzy Server' +DESKTOP_SERVER_APP_ID='com.ever.gauzyserver' +DESKTOP_SERVER_APP_REPO_NAME='ever-gauzy-server' +DESKTOP_SERVER_APP_REPO_OWNER='ever-co' +DESKTOP_SERVER_APP_WELCOME_TITLE= +DESKTOP_SERVER_APP_WELCOME_CONTENT= + +# I18N Translation Files URL +I18N_FILES_URL= diff --git a/README.md b/README.md index 4741b192c85..72a06e3a81a 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ It's built with a React / ReactNative (Expo) stack and connects to headless [Eve [Ever® Gauzy™][uri_gauzy] - **Open Business Management Platform** for Collaborative, On-Demand and Sharing Economies. -- **Enterprise Resource Planning** (ERP) software. -- **Customer Relationship Management** (CRM) software. -- **Human Resource Management** (HRM) software with employee **Time and Activity Tracking** functionality. -- **Work and Project Management** software. +- **Enterprise Resource Planning** (ERP) software. +- **Customer Relationship Management** (CRM) software. +- **Human Resource Management** (HRM) software with employee **Time and Activity Tracking** functionality. +- **Work and Project Management** software. ![overview](https://docs.gauzy.co/docs/assets/overview.png) @@ -31,48 +31,48 @@ Ever® Gauzy™ Platform is a part of our larger Open Platform for **Collaborati Main features: -- Human Resources Management (HRM) with Time Management / Tracking and Employees Performance Monitoring -- Customer Relationship Management (CRM) -- Enterprise Resource Planning (ERP) -- Projects / Tasks Management -- Sales Management -- Financial and Cost Management (including _Accounting_, _Invoicing_, etc) -- Inventory, Supply Chain Management, and Production Management +- Human Resources Management (HRM) with Time Management / Tracking and Employees Performance Monitoring +- Customer Relationship Management (CRM) +- Enterprise Resource Planning (ERP) +- Projects / Tasks Management +- Sales Management +- Financial and Cost Management (including _Accounting_, _Invoicing_, etc) +- Inventory, Supply Chain Management, and Production Management A more detailed list of the features available in the platform: -- [Headless APIs](https://api.gauzy.co/swg) -- Dashboard (provides an overview of different metrics, such as company income/expenses, employee bonuses, etc.) -- Time Management / Time Tracking / Activity Tracking / Timesheets -- Employees Management (register of company employees/contractors, rates of employees, etc.) -- Employee Onboarding / Candidates Interviews -- Contacts Management (Clients / Customers / Leads / etc.) -- Schedules / Appointments / Events -- Project Management / Tasks -- Goals / KPI / Objectives / Key Results -- Sales Pipelines -- Proposals -- Accounting / Invoicing / Estimates -- Billing -- Payments -- Income / Expenses Management -- Time Off Management / Holidays / Approvals -- Inventory -- Equipment / Sharing -- Multiple Organizations Management -- Organization Departments and Teams -- Organization Clients and Vendors -- Help Center / Knowledge Base -- Tags / Labels -- Reports / Insights / Analytics -- Organization and Employee Public Pages -- Integrations (Upwork, HubStaff, etc.) -- Email History / Email Templates -- Data Import / Export -- Roles / Permissions -- Multi-currency -- Multi-lingual -- Dark / Light / Corporate / Material and other Themes +- [Headless APIs](https://api.gauzy.co/swg) +- Dashboard (provides an overview of different metrics, such as company income/expenses, employee bonuses, etc.) +- Time Management / Time Tracking / Activity Tracking / Timesheets +- Employees Management (register of company employees/contractors, rates of employees, etc.) +- Employee Onboarding / Candidates Interviews +- Contacts Management (Clients / Customers / Leads / etc.) +- Schedules / Appointments / Events +- Project Management / Tasks +- Goals / KPI / Objectives / Key Results +- Sales Pipelines +- Proposals +- Accounting / Invoicing / Estimates +- Billing +- Payments +- Income / Expenses Management +- Time Off Management / Holidays / Approvals +- Inventory +- Equipment / Sharing +- Multiple Organizations Management +- Organization Departments and Teams +- Organization Clients and Vendors +- Help Center / Knowledge Base +- Tags / Labels +- Reports / Insights / Analytics +- Organization and Employee Public Pages +- Integrations (Upwork, HubStaff, etc.) +- Email History / Email Templates +- Data Import / Export +- Roles / Permissions +- Multi-currency +- Multi-lingual +- Dark / Light / Corporate / Material and other Themes Read more [about Gauzy](https://github.com/ever-co/ever-gauzy/wiki/About-Gauzy) and [how to use it](https://github.com/ever-co/ever-gauzy/wiki/How-to-use-Gauzy) at your company, on-demand business, freelance business, agency, studio or in-house teams. @@ -97,12 +97,12 @@ Read more [about Gauzy](https://github.com/ever-co/ever-gauzy/wiki/About-Gauzy) ## 🔗 Links -- **** - check more information about the platform at the official website. -- **** - get more information about our company products. +- **** - check more information about the platform at the official website. +- **** - get more information about our company products. ## 📊 Activity -![Alt](https://repobeats.axiom.co/api/embed/7c6f6c3bf56fd91647549cf4ae70af49ed5ee106.svg "Repobeats analytics image") +![Alt](https://repobeats.axiom.co/api/embed/7c6f6c3bf56fd91647549cf4ae70af49ed5ee106.svg 'Repobeats analytics image') ## 💻 Demo, Downloads, Testing and Production @@ -112,9 +112,9 @@ Ever Gauzy Platform Demo at . Notes: -- Default super-admin user login is `admin@ever.co` and the password is `admin` -- Content of demo DB resets on each deployment to the demo environment (usually daily) -- Demo environment deployed using CI/CD from the `develop` branch +- Default super-admin user login is `admin@ever.co` and the password is `admin` +- Content of demo DB resets on each deployment to the demo environment (usually daily) +- Demo environment deployed using CI/CD from the `develop` branch ### Downloads @@ -122,10 +122,10 @@ You can download Gauzy Platform, Gauzy Server, or Desktop Apps (Windows/Mac/Linu In addition, all downloads are also available from the following pages: -- [Platform Releases](https://github.com/ever-co/ever-gauzy/releases) -- [Server Releases](https://github.com/ever-co/ever-gauzy-server/releases) -- [Desktop App Releases](https://github.com/ever-co/ever-gauzy-desktop/releases) -- [Desktop Timer App Releases](https://github.com/ever-co/ever-gauzy-desktop-timer/releases) +- [Platform Releases](https://github.com/ever-co/ever-gauzy/releases) +- [Server Releases](https://github.com/ever-co/ever-gauzy-server/releases) +- [Desktop App Releases](https://github.com/ever-co/ever-gauzy-desktop/releases) +- [Desktop Timer App Releases](https://github.com/ever-co/ever-gauzy-desktop-timer/releases) ### Production (SaaS) @@ -135,44 +135,44 @@ Note: it's currently in Alpha version / in testing mode, please use it with caut ### Staging -- Gauzy Platform Staging builds (using CI/CD, from the `stage` branch) are available at -- We are using the Staging environment to test releases before they are deployed to the production environment -- Our pre-releases of desktop/server apps are built from this environment and can be configured manually (in settings) to connect to Stage API: +- Gauzy Platform Staging builds (using CI/CD, from the `stage` branch) are available at +- We are using the Staging environment to test releases before they are deployed to the production environment +- Our pre-releases of desktop/server apps are built from this environment and can be configured manually (in settings) to connect to Stage API: ### Server & Desktop Apps We have Gauzy Server and two Desktop Apps (for Windows/Mac/Linux): -- Ever® Gauzy™ Server - includes Gauzy API, SQLite DB (or connects to external PostgreSQL) and serves Guazy frontend. It allows to quickly run Gauzy Server for multiple clients (browser-based or Desktop based). It's recommended option if you want to setup the Ever Gauzy Platform in small to medium organizations. +- Ever® Gauzy™ Server - includes Gauzy API, SQLite DB (or connects to external PostgreSQL) and serves Guazy frontend. It allows to quickly run Gauzy Server for multiple clients (browser-based or Desktop based). It's recommended option if you want to setup the Ever Gauzy Platform in small to medium organizations. -- Ever® Gauzy™ Desktop App - includes Gauzy frontend (UI), Gauzy API, SQLite DB, etc., all-in-one! It allows to quickly run the whole Gauzy solution locally, both UI and Timer (for time tracking, optionally of course). In addition, it allows you to connect to the external database (e.g. PostgreSQL) or external API (if you have Gauzy Server with API / DB installed on a different computer or if you want to connect to our live API). It's recommended option if you want to try Gauzy quickly / for personal use or if you want to connect to Gauzy Server in the "client-server" configuration (and use Desktop App instead of web browser). +- Ever® Gauzy™ Desktop App - includes Gauzy frontend (UI), Gauzy API, SQLite DB, etc., all-in-one! It allows to quickly run the whole Gauzy solution locally, both UI and Timer (for time tracking, optionally of course). In addition, it allows you to connect to the external database (e.g. PostgreSQL) or external API (if you have Gauzy Server with API / DB installed on a different computer or if you want to connect to our live API). It's recommended option if you want to try Gauzy quickly / for personal use or if you want to connect to Gauzy Server in the "client-server" configuration (and use Desktop App instead of web browser). -- Ever® Gauzy™ Desktop Timer App - allows running Time & Activity Tracking for employees/contractors with screenshots and activity monitoring. Recommended to setup by organization employees as long as they are not interested in other Gauzy Platform features (e.g. accounting) and only need to track work time. +- Ever® Gauzy™ Desktop Timer App - allows running Time & Activity Tracking for employees/contractors with screenshots and activity monitoring. Recommended to setup by organization employees as long as they are not interested in other Gauzy Platform features (e.g. accounting) and only need to track work time. More information about our Server & Desktop Apps: -- Download for your OS from the official [Downloads](https://web.gauzy.co/downloads) page or see the section "Download" above for other links to our releases pages. -- Setup Gauzy Server with default choices in Setup Wizard and run it. -- You can also setup Gauzy Desktop App (can run independently or connect to Gauzy Server) or Gauzy Desktop Timer App (should be connected to Gauzy Server) -- You can login with `admin@ever.co` and password `admin` to check Admin functionality if you installed Gauzy Server or Gauzy Desktop App. Note: such an Admin user is not an employee, so you will not be able to track time. -- You can login with `employee@ever.co` and password `123456` to check Employee related functionality in Gauzy UI or to run Desktop Timer from an "Employee" perspective (such a user is an Employee and can track time). -- If you install Gauzy Server, it is possible to connect to it using a browser (by default on ) or using Gauzy Desktop Apps (make sure to configure Desktop apps to connect to Gauzy API on because it's where Gauzy Server API runs by default). -- You can read more information about our Desktop Apps on the [Desktop Apps Wiki Page](https://github.com/ever-co/ever-gauzy/wiki/Gauzy-Desktop-Apps) and about our Server on the [Server Wiki Page](https://github.com/ever-co/ever-gauzy/wiki/Gauzy-Server). +- Download for your OS from the official [Downloads](https://web.gauzy.co/downloads) page or see the section "Download" above for other links to our releases pages. +- Setup Gauzy Server with default choices in Setup Wizard and run it. +- You can also setup Gauzy Desktop App (can run independently or connect to Gauzy Server) or Gauzy Desktop Timer App (should be connected to Gauzy Server) +- You can login with `admin@ever.co` and password `admin` to check Admin functionality if you installed Gauzy Server or Gauzy Desktop App. Note: such an Admin user is not an employee, so you will not be able to track time. +- You can login with `employee@ever.co` and password `123456` to check Employee related functionality in Gauzy UI or to run Desktop Timer from an "Employee" perspective (such a user is an Employee and can track time). +- If you install Gauzy Server, it is possible to connect to it using a browser (by default on ) or using Gauzy Desktop Apps (make sure to configure Desktop apps to connect to Gauzy API on because it's where Gauzy Server API runs by default). +- You can read more information about our Desktop Apps on the [Desktop Apps Wiki Page](https://github.com/ever-co/ever-gauzy/wiki/Gauzy-Desktop-Apps) and about our Server on the [Server Wiki Page](https://github.com/ever-co/ever-gauzy/wiki/Gauzy-Server). ## 🧱 Technology Stack and Requirements -- [TypeScript](https://www.typescriptlang.org) language -- [NodeJs](https://nodejs.org) / [NestJs](https://github.com/nestjs/nest) -- [Nx](https://nx.dev) -- [Angular](https://angular.io) -- [RxJS](http://reactivex.io/rxjs) -- [TypeORM](https://github.com/typeorm/typeorm) -- [Ngx-admin](https://github.com/akveo/ngx-admin) +- [TypeScript](https://www.typescriptlang.org) language +- [NodeJs](https://nodejs.org) / [NestJs](https://github.com/nestjs/nest) +- [Nx](https://nx.dev) +- [Angular](https://angular.io) +- [RxJS](http://reactivex.io/rxjs) +- [TypeORM](https://github.com/typeorm/typeorm) +- [Ngx-admin](https://github.com/akveo/ngx-admin) For Production, we recommend: -- [PostgreSQL](https://www.postgresql.org) -- [PM2](https://github.com/Unitech/pm2) +- [PostgreSQL](https://www.postgresql.org) +- [PM2](https://github.com/Unitech/pm2) Note: thanks to TypeORM, Gauzy will support lots of DBs: SQLite (default, for demos), PostgreSQL (development/production), MySql, MariaDb, CockroachDb, MS SQL, Oracle, MongoDb, and others, with minimal changes. @@ -186,78 +186,78 @@ Please refer to our official [Platform Documentation](https://docs.gauzy.co) and ### With Docker Compose -- Clone repo. -- Make sure you have Docker Compose [installed locally](https://docs.docker.com/compose/install). -- Copy `.env.compose` file into `.env` file in the root of mono-repo (the file contains default env variables definitions). Important: the file `.env.compose` is different from `.env.sample` in some settings, please make sure you use the correct one! -- Run `docker-compose -f docker-compose.demo.yml up`, if you want to run the platform using our prebuild Docker images. _(Note: it uses latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ -- Run `docker-compose up`, if you want to build everything (code and Docker images) locally. _(Note: this is extremely long process, option above is much faster.)_ -- :coffee: time... It might take some time for our API to seed fake data in the DB during the first Docker Compose run, even if you used prebuild Docker images. -- Open in your browser. -- Login with email `admin@ever.co` and password: `admin` for Super Admin user. -- Login with email `employee@ever.co` and password: `123456` for Employee user. -- Enjoy! +- Clone repo. +- Make sure you have Docker Compose [installed locally](https://docs.docker.com/compose/install). +- Run `docker-compose up`, if you want to run the platform in production configuration using our prebuild Docker images. Check `.env.compose` file for different settings (optionally), e.g. DB type. _(Note: docker compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ +- Run `docker-compose -f docker-compose.demo.yml up`, if you want to run the platform in basic configuration (e.g. for Demo / explore functionality / quick run) using our prebuild Docker images. Check `.env.demo.compose` file for different settings (optionally), e.g. DB type. _(Note: docker compose will use latest images pre-build automatically from head of `master` branch using GitHub CI/CD.)_ +- Run `docker-compose -f docker-compose.build.yml up`, if you want to build everything (code and Docker images) locally. Check `.env.compose` file for different settings (optionally), e.g. DB type. _(Note: this is extremely long process because it builds whole platform locally. Other options above are much faster!)_ +- :coffee: time... It might take some time for our API to seed fake data in the DB during the first Docker Compose run, even if you used prebuild Docker images. +- Open in your browser. +- Login with email `admin@ever.co` and password: `admin` for Super Admin user. +- Login with email `employee@ever.co` and password: `123456` for Employee user. +- Enjoy! Together with Gauzy, Docker Compose will run the following: -- [PostgreSQL](https://www.postgresql.org) - Primary Database. -- [Pgweb](https://github.com/sosedoff/pgweb) - Cross-platform client for PostgreSQL DBs, available on . -- [ElasticSearch](https://github.com/elastic/elasticsearch) - Search Engine. -- [Dejavu](https://github.com/appbaseio/dejavu) - Web UI for ElasticSearch, available on . -- [MinIO](https://github.com/minio/minio) - Multi-Cloud ☁️ Object Storage (AWS S3 compatible). -- [Jitsu](https://github.com/jitsucom/jitsu) - Jitsu is an open-source Segment alternative. Fully-scriptable data ingestion engine for modern data teams. -- [Redis](https://github.com/redis/redis) - In-memory data store/caching (also used by Jitsu) -- [Cube](https://github.com/cube-js/cube) - "Semantic Layer" used for Reports, Dashboards, Analytics, and other BI-related features, with UI available on . +- [PostgreSQL](https://www.postgresql.org) - Primary Database. +- [Pgweb](https://github.com/sosedoff/pgweb) - Cross-platform client for PostgreSQL DBs, available on . +- [ElasticSearch](https://github.com/elastic/elasticsearch) - Search Engine. +- [Dejavu](https://github.com/appbaseio/dejavu) - Web UI for ElasticSearch, available on . +- [MinIO](https://github.com/minio/minio) - Multi-Cloud ☁️ Object Storage (AWS S3 compatible). +- [Jitsu](https://github.com/jitsucom/jitsu) - Jitsu is an open-source Segment alternative. Fully-scriptable data ingestion engine for modern data teams. +- [Redis](https://github.com/redis/redis) - In-memory data store/caching (also used by Jitsu) +- [Cube](https://github.com/cube-js/cube) - "Semantic Layer" used for Reports, Dashboards, Analytics, and other BI-related features, with UI available on . ### Manually #### Required -- Install [NodeJs](https://nodejs.org/en/download) LTS version or later, e.g. 18.x. -- Install [Yarn](https://github.com/yarnpkg/yarn) (if you don't have it) with `npm i -g yarn`. -- Install NPM packages and bootstrap solution using the command `yarn bootstrap`. -- If you will need to make code changes (and push to Git repo), please run `yarn prepare:husky`. -- Adjust settings in the [`.env.local`](https://github.com/ever-co/ever-gauzy/blob/develop/.env.local) which is used in local runs. -- Alternatively, you can copy [`.env.sample`](https://github.com/ever-co/ever-gauzy/blob/develop/.env.sample) to `.env` and change default settings there, e.g. database type, name, user, password, etc. -- Run both API and UI with a single command: `yarn start`. -- Open Gauzy UI on in your browser (API runs on ). -- Login with email `admin@ever.co` and password: `admin` for Super Admin user. -- Login with email `employee@ever.co` and password: `123456` for Employee user. -- Enjoy! +- Install [NodeJs](https://nodejs.org/en/download) LTS version or later, e.g. 18.x. +- Install [Yarn](https://github.com/yarnpkg/yarn) (if you don't have it) with `npm i -g yarn`. +- Install NPM packages and bootstrap solution using the command `yarn bootstrap`. +- If you will need to make code changes (and push to Git repo), please run `yarn prepare:husky`. +- Adjust settings in the [`.env.local`](https://github.com/ever-co/ever-gauzy/blob/develop/.env.local) which is used in local runs. +- Alternatively, you can copy [`.env.sample`](https://github.com/ever-co/ever-gauzy/blob/develop/.env.sample) to `.env` and change default settings there, e.g. database type, name, user, password, etc. +- Run both API and UI with a single command: `yarn start`. +- Open Gauzy UI on in your browser (API runs on ). +- Login with email `admin@ever.co` and password: `admin` for Super Admin user. +- Login with email `employee@ever.co` and password: `123456` for Employee user. +- Enjoy! Notes: -- during the first API start, DB will be automatically seeded with a minimum set of initial data if no users are found. -- you can run seed any moment manually (e.g. if you changed entities schemas) with `yarn seed` command to re-initialize DB (warning: unsafe for production!). -- it is possible to run generation of extremely large amounts of fake data for demo purposes/testing with `yarn seed:all` (warning: takes ~10 min to complete) +- during the first API start, DB will be automatically seeded with a minimum set of initial data if no users are found. +- you can run seed any moment manually (e.g. if you changed entities schemas) with `yarn seed` command to re-initialize DB (warning: unsafe for production!). +- it is possible to run generation of extremely large amounts of fake data for demo purposes/testing with `yarn seed:all` (warning: takes ~10 min to complete) #### Optional / Recommended for Production -- Optionally (recommended for production) install and run [PostgreSQL](https://www.postgresql.org) version 14 or later. Note: other DB can be configured manually in TypeORM. The default DB is set to SQLite (recommended for testing/demo purposes only). -- Optionally (recommended for production) install and run [Redis](https://github.com/redis/redis). Notes: the platform will work without Redis using an in-memory caching strategy instead of distributed one (recommended for testing/demo purposes only). Please note however that Redis is required for Jitsu. -- Optionally (recommended for production) install and run [ElasticSearch](https://github.com/elastic/elasticsearch). Note: the platform will work without ElasticSearch using DB build-in search capabilities (recommended for testing/demo purposes only). -- Optionally install and run [MinIO](https://github.com/minio/minio) or [LocalStack](https://github.com/localstack/localstack). Note: the platform will work without MinIO / LocalStack or other S3-compatible storage using local filesystem-based storage (recommended for testing/demo purposes only). For production, we recommend using Wasabi or AWS S3 storage or another S3-compatible cloud storage. -- Optionally (recommended for production) install and run [Jitsu](https://github.com/jitsucom/jitsu). Note: the platform will work without Jitsu, however, data ingestion will be disabled for additional analyses / real-time pipelines. -- Optionally (recommended for production) install and run [Cube](https://github.com/cube-js/cube). Note: the platform will work without Cube, however some advanced (dynamic) reporting and data processing capabilities will be disabled. +- Optionally (recommended for production) install and run [PostgreSQL](https://www.postgresql.org) version 14 or later. Note: other DB can be configured manually in TypeORM. The default DB is set to SQLite (recommended for testing/demo purposes only). +- Optionally (recommended for production) install and run [Redis](https://github.com/redis/redis). Notes: the platform will work without Redis using an in-memory caching strategy instead of distributed one (recommended for testing/demo purposes only). Please note however that Redis is required for Jitsu. +- Optionally (recommended for production) install and run [ElasticSearch](https://github.com/elastic/elasticsearch). Note: the platform will work without ElasticSearch using DB build-in search capabilities (recommended for testing/demo purposes only). +- Optionally install and run [MinIO](https://github.com/minio/minio) or [LocalStack](https://github.com/localstack/localstack). Note: the platform will work without MinIO / LocalStack or other S3-compatible storage using local filesystem-based storage (recommended for testing/demo purposes only). For production, we recommend using Wasabi or AWS S3 storage or another S3-compatible cloud storage. +- Optionally (recommended for production) install and run [Jitsu](https://github.com/jitsucom/jitsu). Note: the platform will work without Jitsu, however, data ingestion will be disabled for additional analyses / real-time pipelines. +- Optionally (recommended for production) install and run [Cube](https://github.com/cube-js/cube). Note: the platform will work without Cube, however some advanced (dynamic) reporting and data processing capabilities will be disabled. ### Production -- See [Setup Gauzy for Client Server](https://github.com/ever-co/ever-gauzy/wiki/Setup-Gauzy-for-Client-Server) for more information about production setup on your servers. -- We recommend deploying to Kubernetes (k8s), either manually (see below) or with our [Ever Helm Charts](https://github.com/ever-co/ever-charts). -- For simple deployment scenarios (e.g. for yourself or your small organization), check our [Kubernetes configurations](https://github.com/ever-co/ever-gauzy/tree/develop/.deploy/k8s), which we are using to deploy Gauzy into [DigitalOcean k8s cluster](https://www.digitalocean.com/products/kubernetes). -- In addition, check [Gauzy Pulumi](https://github.com/ever-co/ever-gauzy-pulumi) project (WIP), it makes complex Clouds deployments possible with a single command (`pulumi up`). Note: it currently supports AWS EKS (Kubernetes) for development and production with Application Load Balancers and AWS RDS Serverless PostgreSQL DB deployments. We also implemented deployments to ECS EC2 and Fargate Clusters in the same Pulumi project. +- See [Setup Gauzy for Client Server](https://github.com/ever-co/ever-gauzy/wiki/Setup-Gauzy-for-Client-Server) for more information about production setup on your servers. +- We recommend deploying to Kubernetes (k8s), either manually (see below) or with our [Ever Helm Charts](https://github.com/ever-co/ever-charts). +- For simple deployment scenarios (e.g. for yourself or your small organization), check our [Kubernetes configurations](https://github.com/ever-co/ever-gauzy/tree/develop/.deploy/k8s), which we are using to deploy Gauzy into [DigitalOcean k8s cluster](https://www.digitalocean.com/products/kubernetes). +- In addition, check [Gauzy Pulumi](https://github.com/ever-co/ever-gauzy-pulumi) project (WIP), it makes complex Clouds deployments possible with a single command (`pulumi up`). Note: it currently supports AWS EKS (Kubernetes) for development and production with Application Load Balancers and AWS RDS Serverless PostgreSQL DB deployments. We also implemented deployments to ECS EC2 and Fargate Clusters in the same Pulumi project. ## 💌 Contact Us -- [Ever.co Website Contact Us page](https://ever.co/contacts) -- [Slack Community](https://join.slack.com/t/gauzy/shared_invite/enQtNzc5MTA5MDUwODg2LTI0MGEwYTlmNWFlNzQzMzBlOWExNTk0NzAyY2IwYWYwMzZjMTliYjMwNDI3NTJmYmM4MDQ4NDliMDNiNDY1NWU) -- [Discord Chat](https://discord.gg/hKQfn4j) -- [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/gauzy) -- [![Gitter](https://badges.gitter.im/JoinChat.svg)](https://gitter.im/ever-co/ever-gauzy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -- [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/evereq?utm_source=github&utm_medium=button&utm_term=evereq&utm_campaign=github) -- For business inquiries: -- Please report security vulnerabilities to -- [Gauzy Platform @ Twitter](https://twitter.com/gauzyplatform) -- [Gauzy Platform @ Facebook](https://www.facebook.com/gauzyplatform) +- [Ever.co Website Contact Us page](https://ever.co/contacts) +- [Slack Community](https://join.slack.com/t/gauzy/shared_invite/enQtNzc5MTA5MDUwODg2LTI0MGEwYTlmNWFlNzQzMzBlOWExNTk0NzAyY2IwYWYwMzZjMTliYjMwNDI3NTJmYmM4MDQ4NDliMDNiNDY1NWU) +- [Discord Chat](https://discord.gg/hKQfn4j) +- [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/gauzy) +- [![Gitter](https://badges.gitter.im/JoinChat.svg)](https://gitter.im/ever-co/ever-gauzy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +- [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/evereq?utm_source=github&utm_medium=button&utm_term=evereq&utm_campaign=github) +- For business inquiries: +- Please report security vulnerabilities to +- [Gauzy Platform @ Twitter](https://twitter.com/gauzyplatform) +- [Gauzy Platform @ Facebook](https://www.facebook.com/gauzyplatform) ## 🔐 Security @@ -267,7 +267,7 @@ See more details in the [LICENSE](LICENSE.md). In a production setup, all client-side to server-side (backend, APIs) communications should be encrypted using HTTPS/WSS/SSL (REST APIs, GraphQL endpoint, Socket.io WebSockets, etc.). -If you discover any issue regarding security, please disclose the information responsibly by sending an email to or on [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) and not by creating a GitHub issue. +If you discover any issue regarding security, please disclose the information responsibly by sending an email to or on [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) and not by creating a GitHub issue. ## 🛡️ License @@ -275,9 +275,9 @@ We support the open-source community. If you're building awesome non-profit/open This software is available under the following licenses: -- [Ever® Gauzy™ Platform Community Edition](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-community-edition-license) -- [Ever® Gauzy™ Platform Small Business](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-small-business-license) -- [Ever® Gauzy™ Platform Enterprise](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-enterprise-license) +- [Ever® Gauzy™ Platform Community Edition](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-community-edition-license) +- [Ever® Gauzy™ Platform Small Business](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-small-business-license) +- [Ever® Gauzy™ Platform Enterprise](https://github.com/ever-co/ever-gauzy/blob/master/LICENSE.md#gauzy-platform-enterprise-license) #### The default Ever® Gauzy™ Platform license, without a valid Ever® Gauzy™ Platform Enterprise or Ever® Gauzy™ Platform Small Business License agreement, is the Ever® Gauzy™ Platform Community Edition License @@ -288,7 +288,7 @@ This software is available under the following licenses: ## ™️ Trademarks **Ever**® is a registered trademark of [Ever Co. LTD](https://ever.co). -**Ever® Demand™**, **Ever® Gauzy™** and **Ever® OpenSaaS™** are all trademarks of [Ever Co. LTD](https://ever.co). +**Ever® Demand™**, **Ever® Gauzy™** and **Ever® OpenSaaS™** are all trademarks of [Ever Co. LTD](https://ever.co). The trademarks may only be used with the written permission of Ever Co. LTD. and may not be used to promote or otherwise market competitive products or services. @@ -296,9 +296,9 @@ All other brand and product names are trademarks, registered trademarks or servi ## 🍺 Contribute -- Please give us :star: on Github, it **helps**! -- You are more than welcome to submit feature requests in the [separate repo](https://github.com/ever-co/feature-requests/issues) -- Pull requests are always welcome! Please base pull requests against the _develop_ branch and follow the [contributing guide](.github/CONTRIBUTING.md). +- Please give us :star: on Github, it **helps**! +- You are more than welcome to submit feature requests in the [separate repo](https://github.com/ever-co/feature-requests/issues) +- Pull requests are always welcome! Please base pull requests against the _develop_ branch and follow the [contributing guide](.github/CONTRIBUTING.md). ## 💪 Thanks to our Contributors @@ -326,7 +326,7 @@ You can also view a full list of our [contributors tracked by Github](https://gi [![huntr](https://cdn.huntr.dev/huntr_security_badge_mono.svg)](https://huntr.dev) [![Circle CI](https://circleci.com/gh/ever-co/ever-gauzy.svg?style=svg)](https://circleci.com/gh/ever-co/ever-gauzy) [![codecov](https://codecov.io/gh/ever-co/ever-gauzy/branch/master/graph/badge.svg)](https://codecov.io/gh/ever-co/ever-gauzy) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/8c46f9eb9df64aa9859dea4d572059ac)](https://www.codacy.com/gh/ever-co/ever-gauzy/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ever-co/ever-gauzy&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/8c46f9eb9df64aa9859dea4d572059ac)](https://www.codacy.com/gh/ever-co/ever-gauzy/dashboard?utm_source=github.com&utm_medium=referral&utm_content=ever-co/ever-gauzy&utm_campaign=Badge_Grade) [![DeepScan grade](https://deepscan.io/api/teams/3293/projects/16703/branches/363423/badge/grade.svg)](https://deepscan.io/dashboard#view=project&tid=3293&pid=16703&bid=363423) [![Known Vulnerabilities](https://snyk.io/test/github/ever-co/ever-gauzy/badge.svg)](https://snyk.io/test/github/ever-co/ever-gauzy) [![Total alerts](https://img.shields.io/lgtm/alerts/g/ever-co/ever-gauzy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ever-co/ever-gauzy/alerts/) @@ -336,5 +336,5 @@ You can also view a full list of our [contributors tracked by Github](https://gi ## 🔥 P.S -- If you are interested in running an on-demand (delivery) or digital marketplace business, check open-source [Ever Demand Platform](https://github.com/ever-co/ever-demand) -- [We are Hiring: remote TypeScript / NestJS / Angular developers](https://github.com/ever-co/jobs#available-positions) +- If you are interested in running an on-demand (delivery) or digital marketplace business, check open-source [Ever Demand Platform](https://github.com/ever-co/ever-demand) +- [We are Hiring: remote TypeScript / NestJS / Angular developers](https://github.com/ever-co/jobs#available-positions) diff --git a/apps/api/src/plugin-config.ts b/apps/api/src/plugin-config.ts index e8e41d6c54b..cb87dc482cb 100644 --- a/apps/api/src/plugin-config.ts +++ b/apps/api/src/plugin-config.ts @@ -6,7 +6,6 @@ import { DEFAULT_API_HOST, DEFAULT_API_BASE_URL } from '@gauzy/common'; -import { environment } from '@gauzy/config'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; import { DataSourceOptions } from 'typeorm'; import * as path from 'path'; @@ -26,22 +25,16 @@ if (__dirname.startsWith('/srv/gauzy')) { assetPath = '/srv/gauzy/apps/api/src/assets'; assetPublicPath = '/srv/gauzy/apps/api/public'; } else { - assetPath = path.join( - path.resolve( - __dirname, - '../../../', - ...['apps', 'api', 'src', 'assets'] - ) - ); - - assetPublicPath = path.join( - path.resolve(__dirname, '../../../', ...['apps', 'api', 'public']) - ); + assetPath = path.join(path.resolve(__dirname, '../../../', ...['apps', 'api', 'src', 'assets'])); + + assetPublicPath = path.join(path.resolve(__dirname, '../../../', ...['apps', 'api', 'public'])); } console.log('Plugin Config -> assetPath: ' + assetPath); console.log('Plugin Config -> assetPublicPath: ' + assetPublicPath); +console.log('DB Synchronize: ' + process.env.DB_SYNCHRONIZE); + export const pluginConfig: IPluginConfig = { apiConfigOptions: { host: process.env.API_HOST || DEFAULT_API_HOST, @@ -57,8 +50,8 @@ export const pluginConfig: IPluginConfig = { }, dbConnectionOptions: { migrationsTransactionMode: 'each', // Run migrations automatically in each transaction. i.e."all" | "none" | "each" - migrationsRun: !environment.production, // Run migrations automatically, you can disable this if you prefer running migration manually. - ...getDbConfig(), + migrationsRun: process.env.DB_SYNCHRONIZE === 'true' ? false : true, // Run migrations automatically if we don't do DB_SYNCHRONIZE + ...getDbConfig() }, assetOptions: { assetPath: assetPath, @@ -68,17 +61,16 @@ export const pluginConfig: IPluginConfig = { }; function getDbConfig(): DataSourceOptions { - let dbType:string; + let dbType: string; - if (process.env.DB_TYPE) - dbType = process.env.DB_TYPE; - else - dbType = 'sqlite'; + if (process.env.DB_TYPE) dbType = process.env.DB_TYPE; + else dbType = 'better-sqlite3'; - switch (dbType) { + console.log('DB Type: ' + dbType); + switch (dbType) { case 'mongodb': - throw "MongoDB not supported yet"; + throw 'MongoDB not supported yet'; case 'postgres': const ssl = process.env.DB_SSL_MODE === 'true' ? true : undefined; @@ -93,15 +85,13 @@ function getDbConfig(): DataSourceOptions { sslParams = { rejectUnauthorized: true, ca: sslCert - } + }; } const postgresConnectionOptions: PostgresConnectionOptions = { type: dbType, host: process.env.DB_HOST || 'localhost', - port: process.env.DB_PORT - ? parseInt(process.env.DB_PORT, 10) - : 5432, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 5432, database: process.env.DB_NAME || 'postgres', username: process.env.DB_USER || 'postgres', password: process.env.DB_PASS || 'root', @@ -110,39 +100,32 @@ function getDbConfig(): DataSourceOptions { logger: 'file', // Removes console logging, instead logs all queries in a file ormlogs.log synchronize: process.env.DB_SYNCHRONIZE === 'true' ? true : false, // We are using migrations, synchronize should be set to false. uuidExtension: 'pgcrypto' - } + }; return postgresConnectionOptions; case 'sqlite': const sqlitePath = - process.env.DB_PATH || - path.join( - path.resolve('.', ...['apps', 'api', 'data']), - 'gauzy.sqlite3' - ); + process.env.DB_PATH || path.join(path.resolve('.', ...['apps', 'api', 'data']), 'gauzy.sqlite3'); return { type: dbType, database: sqlitePath, logging: 'all', logger: 'file', // Removes console logging, instead logs all queries in a file ormlogs.log - synchronize: process.env.DB_SYNCHRONIZE === 'true' ? true : false, // We are using migrations, synchronize should be set to false. + synchronize: process.env.DB_SYNCHRONIZE === 'true' ? true : false // We are using migrations, synchronize should be set to false. }; + case 'better-sqlite3': const betterSqlitePath = - process.env.DB_PATH || - path.join( - path.resolve('.', ...['apps', 'api', 'data']), - 'gauzy.sqlite3' - ); + process.env.DB_PATH || path.join(path.resolve('.', ...['apps', 'api', 'data']), 'gauzy.sqlite3'); return { type: dbType, database: betterSqlitePath, logging: 'all', logger: 'file', // Removes console logging, instead logs all queries in a file ormlogs.log - synchronize: process.env.DB_SYNCHRONIZE === 'true', // We are using migrations, synchronize should be set to false. + synchronize: process.env.DB_SYNCHRONIZE === 'true' ? true : false, // We are using migrations, synchronize should be set to false. prepareDatabase: (db) => { if (!process.env.IS_ELECTRON) { // Enhance performance diff --git a/apps/gauzy/src/app/@shared/report/amounts-owed-grid/amounts-owed-grid.component.html b/apps/gauzy/src/app/@shared/report/amounts-owed-grid/amounts-owed-grid.component.html index 0d5920ffd2e..6992e01911f 100644 --- a/apps/gauzy/src/app/@shared/report/amounts-owed-grid/amounts-owed-grid.component.html +++ b/apps/gauzy/src/app/@shared/report/amounts-owed-grid/amounts-owed-grid.component.html @@ -33,11 +33,7 @@
{{ 'REPORT_PAGE.EMPLOYEE' | translate }}
- - +
@@ -45,10 +41,7 @@ {{ 'REPORT_PAGE.CURRENT_RATE' | translate }}
- {{ employeeRow?.employee?.billRateValue - | currency: employeeRow?.employee?.billRateCurrency - | position: organization?.currencyPosition - }} + {{ (employeeRow?.employee?.billRateValue || 0) | currency: employeeRow?.employee?.billRateCurrency | position: organization?.currencyPosition }}
@@ -64,10 +57,7 @@ {{ 'REPORT_PAGE.AMOUNT' | translate }}
- {{ employeeRow?.amount - | currency: employeeRow?.employee?.billRateCurrency - | position: organization?.currencyPosition - }} + {{ (employeeRow?.amount || 0) | currency: employeeRow?.employee?.billRateCurrency | position: organization?.currencyPosition }}
@@ -100,11 +90,11 @@
{{ 'REPORT_PAGE.NO_EMPLOYEE' | translate }} diff --git a/apps/gauzy/src/app/@shared/timesheet/edit-time-log-modal/edit-time-log-modal.component.ts b/apps/gauzy/src/app/@shared/timesheet/edit-time-log-modal/edit-time-log-modal.component.ts index 68a62d25938..3bb82a64afb 100644 --- a/apps/gauzy/src/app/@shared/timesheet/edit-time-log-modal/edit-time-log-modal.component.ts +++ b/apps/gauzy/src/app/@shared/timesheet/edit-time-log-modal/edit-time-log-modal.component.ts @@ -151,6 +151,9 @@ export class EditTimeLogModalComponent implements OnInit, AfterViewInit, OnDestr const { start, end } = selectedRange; const startMoment = moment(start); const endMoment = moment(end); + if (!startMoment.isValid() || !endMoment.isValid()) { + return this.timeDiff = null; + } this.timeDiff = new Date( endMoment.diff(startMoment, 'seconds') ); diff --git a/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.html b/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.html index f20e00264ea..fd206bb59e0 100644 --- a/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.html +++ b/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.html @@ -74,17 +74,17 @@
{{ 'TIMESHEET.SCREENSHOTS.SCREENSHOTS' | translate }}
- +
{{ 'TIMESHEET.APPS' | translate }}
-
+
{{ app }}
- +
{{ 'TIMESHEET.SCREENSHOTS.TIME_LOG' | translate }}
diff --git a/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.ts b/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.ts index ea36b0eece7..ba91adbb5a8 100644 --- a/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.ts +++ b/apps/gauzy/src/app/@shared/timesheet/screenshots/view-screenshots-modal/view-screenshots-modal.component.ts @@ -131,7 +131,7 @@ export class ViewScreenshotsModalComponent implements OnInit { this.timeLogs = this.timeSlot.timeLogs; // Retrieve and set unique apps from the screenshots of the time slot - this.apps = this.getScreenshotUniqueApps(); + this.apps = this.getScreenshotUniqueApps() || []; } catch (error) { // Handle errors by logging and displaying a toastr message console.error('Error while retrieving TimeSlot:', error); diff --git a/apps/gauzy/src/app/app.component.ts b/apps/gauzy/src/app/app.component.ts index cd9c276fae0..8081526c0dc 100644 --- a/apps/gauzy/src/app/app.component.ts +++ b/apps/gauzy/src/app/app.component.ts @@ -25,7 +25,6 @@ import { } from './@core/services'; import { environment } from '../environments/environment'; import { JitsuService } from './@core/services/analytics/jitsu.service'; -import { moment } from './@core/moment-extend'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -110,9 +109,6 @@ export class AppComponent implements OnInit, AfterViewInit { * It also sets the loading flag to false after language change. */ this.translate.onLangChange.subscribe((langChangeEvent: LangChangeEvent) => { - // Translate date when the language changes - moment.locale(langChangeEvent.lang); - // Set the loading flag to false after the language change this.loading = false; }); diff --git a/apps/gauzy/src/app/auth/accept-invite/accept-invite-form/accept-invite-form.component.html b/apps/gauzy/src/app/auth/accept-invite/accept-invite-form/accept-invite-form.component.html index eec0201bad6..ae8da3868fb 100644 --- a/apps/gauzy/src/app/auth/accept-invite/accept-invite-form/accept-invite-form.component.html +++ b/apps/gauzy/src/app/auth/accept-invite/accept-invite-form/accept-invite-form.component.html @@ -25,6 +25,7 @@ - -
- {{ 'INTEGRATIONS.GAUZY_AI_PAGE.CONSUMER_KEYS' | translate }} -
-
-
-
-
- {{ getTitleForSetting(setting) }} - + + + +
+
+
+
+
+ + + {{ 'FORM.PLACEHOLDERS.ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS' | translate }} + + +
+
+
+
+
+
+ + + {{ 'FORM.PLACEHOLDERS.ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS' | translate }} + + +
+
+
-
- {{ 'INTEGRATIONS.GAUZY_AI_PAGE.GENERATED' | translate }} {{ setting.createdAt | dateFormat }} + + + + + + + +
+ {{ 'INTEGRATIONS.GAUZY_AI_PAGE.CONSUMER_KEYS' | translate }} +
+
+
+
+
+ {{ getTitleForSetting(setting) }} + +
+
+ {{ 'INTEGRATIONS.GAUZY_AI_PAGE.GENERATED' | translate }} {{ setting.createdAt | dateFormat }} +
+
+
+ +
+
-
- -
-
-
-
- - - + + diff --git a/apps/gauzy/src/app/pages/integrations/gauzy-ai/gauzy-ai.module.ts b/apps/gauzy/src/app/pages/integrations/gauzy-ai/gauzy-ai.module.ts index 1869d3b45dc..c2ee8247c18 100644 --- a/apps/gauzy/src/app/pages/integrations/gauzy-ai/gauzy-ai.module.ts +++ b/apps/gauzy/src/app/pages/integrations/gauzy-ai/gauzy-ai.module.ts @@ -7,6 +7,7 @@ import { NbIconModule, NbInputModule, NbTabsetModule, + NbToggleModule, NbTooltipModule } from '@nebular/theme'; import { TranslateModule } from './../../../@shared/translate/translate.module'; @@ -34,6 +35,8 @@ import { GauzyAIViewComponent } from './components/view/view.component'; NbIconModule, NbInputModule, NbTabsetModule, + NbToggleModule, + NbToggleModule, NbTooltipModule, GauzyAIRoutingModule, TranslateModule, diff --git a/apps/gauzy/src/assets/i18n/bg.json b/apps/gauzy/src/assets/i18n/bg.json index 7088bb51351..aa7accc8f16 100644 --- a/apps/gauzy/src/assets/i18n/bg.json +++ b/apps/gauzy/src/assets/i18n/bg.json @@ -455,7 +455,14 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto-sync tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Is tasks Auto-sync based on Label?", + "AUTO_SYNC_TAG": "Label", + "GITHUB_REPOSITORY": "GitHub Repository", + "PROJECT": "Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Искате ли да активирате търсенето на работа и анализът на съответствието?", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Искате ли да активирате Анализ на ефективността на служителите?" }, "PLACEHOLDERS": { "NAME": "Име", @@ -640,7 +647,13 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto Sync Tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Auto-sync Tasks On Label", + "AUTO_SYNC_TAG": "Select Auto-sync Label", + "SELECT_PROJECT": "Select Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Активиране на търсене на работа и анализ на съответствие", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Активиране на анализ на ефективността на служителите" }, "RATES": { "DEFAULT_RATE": "Норма по подразбиране", @@ -1212,9 +1225,11 @@ "SETTINGS": "Settings" }, "TOOLTIP": { - "API_KEY": "The API Key serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "API_SECRET": "The API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "OPEN_AI_API_SECRET_KEY": "The OpenAI API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible." + "API_KEY": "API ключът служи като идентификатор на вашето приложение за API заявки, оставащ постоянно скрит, с някои видими символи.", + "API_SECRET": "API тайният ключ служи като идентификатор на вашето приложение за API заявки, оставащ постоянно скрит, с някои видими символи.", + "OPEN_AI_API_SECRET_KEY": "Тайният API ключ на OpenAI служи като идентификатор на вашето приложение за API заявки, оставащ постоянно скрит, с някои видими символи.", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Активира разширен анализ на търсенето на работа и съвпадение за по-голяма точност. При изключване скрива пунктовете 'Преглед' и 'Съвпадение' за по-лесен интерфейс.", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Управлява предаването на метрики за работата на служителите, включително клавишни въвеждания, движения на мишката и снимки на екрана, за анализ от Gauzy AI. Включването позволява изчерпателен анализ на производителността. При изключване Gauzy AI няма да получава и анализира снимки или други подробни данни, за да гарантира строга конфиденциалност и контрол върху споделянето на данни." } }, "GITHUB_PAGE": { diff --git a/apps/gauzy/src/assets/i18n/en.json b/apps/gauzy/src/assets/i18n/en.json index 243225237c5..94e844910ca 100644 --- a/apps/gauzy/src/assets/i18n/en.json +++ b/apps/gauzy/src/assets/i18n/en.json @@ -482,7 +482,9 @@ "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Is tasks Auto-sync based on Label?", "AUTO_SYNC_TAG": "Label", "GITHUB_REPOSITORY": "GitHub Repository", - "PROJECT": "Project" + "PROJECT": "Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Do you want to enable Jobs Search & Matching Analysis?", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Do you want to enable Employee Performance Analysis?" }, "PLACEHOLDERS": { "NAME": "Name", @@ -675,7 +677,9 @@ "AUTO_SYNC_TASKS": "Auto Sync Tasks", "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Auto-sync Tasks On Label", "AUTO_SYNC_TAG": "Select Auto-sync Label", - "SELECT_PROJECT": "Select Project" + "SELECT_PROJECT": "Select Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Enable Jobs Search & Matching Analysis", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Enable Employee Performance Analysis" }, "RATES": { "DEFAULT_RATE": "Default Rate", @@ -1259,7 +1263,9 @@ "TOOLTIP": { "API_KEY": "The API Key serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", "API_SECRET": "The API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "OPEN_AI_API_SECRET_KEY": "The OpenAI API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible." + "OPEN_AI_API_SECRET_KEY": "The OpenAI API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Enables advanced job search and matching analyses for enhanced accuracy. Disabling hides 'Browse' and 'Matching' menu items for a streamlined interface.", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Manages the transmission of employee work metrics, including keyboard inputs, mouse movements, and screenshots, for analysis by Gauzy AI. Enable for comprehensive performance analysis; disable for strict user privacy and data control." } }, "GITHUB_PAGE": { diff --git a/apps/gauzy/src/assets/i18n/he.json b/apps/gauzy/src/assets/i18n/he.json index 36092c8c6ef..f532cf14ca7 100644 --- a/apps/gauzy/src/assets/i18n/he.json +++ b/apps/gauzy/src/assets/i18n/he.json @@ -455,7 +455,14 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto-sync tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Is tasks Auto-sync based on Label?", + "AUTO_SYNC_TAG": "Label", + "GITHUB_REPOSITORY": "GitHub Repository", + "PROJECT": "Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "האם ברצונך להפעיל חיפוש משרות וניתוח התאמה?", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "האם ברצונך להפעיל ניתוח ביצועי עובדים?" }, "PLACEHOLDERS": { "NAME": "שם", @@ -640,7 +647,13 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto Sync Tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Auto-sync Tasks On Label", + "AUTO_SYNC_TAG": "Select Auto-sync Label", + "SELECT_PROJECT": "Select Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "אפשר חיפוש משרות וניתוח התאמהs", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "אפשר ניתוח ביצועי עובדים" }, "RATES": { "DEFAULT_RATE": "Default Rate", @@ -1212,9 +1225,11 @@ "SETTINGS": "Settings" }, "TOOLTIP": { - "API_KEY": "The API Key serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "API_SECRET": "The API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "OPEN_AI_API_SECRET_KEY": "The OpenAI API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible." + "API_KEY": "ה-API Key משמש כזהות האפליקציה שלך לבקשות API, נשמרת מסתירה לצמיתות, עם חלק מהתווים נראים.", + "API_SECRET": "ה-API Secret משמש כזהות האפליקציה שלך לבקשות API, נשמרת מסתירה לצמיתות, עם חלק מהתווים נראים.", + "OPEN_AI_API_SECRET_KEY": "ה-OpenAI API Secret משמש כזהות האפליקציה שלך לבקשות API, נשמרת מסתירה לצמיתות, עם חלק מהתווים נראים.", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "מאפשר ניתוח מתקדם של חיפוש והתאמה למשתמש לשיפור הדיוק. בהשבתה, פריטי התפריט 'גלישה' ו-'התאמה' בתפריט המשרות ייחבאו כדי להבטיח ממשק משתמש קל ופשוט.", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "ניהול העברת מדדי עבודה של עובד, כולל קלטי מקלדת, תנועות עכבר וצילומי מסך, לניתוח על ידי Gauzy AI. הפעלה מאפשרת ניתוח ביצועים מקיף. בכיבוי, Gauzy AI לא יקבל ולא ינתח צילומי מסך או כל קלט פרטני נוסף, מבטיח פרטיות מחמירה ושליטה מוחלטת על שיתוף המידע." } }, "GITHUB_PAGE": { diff --git a/apps/gauzy/src/assets/i18n/ru.json b/apps/gauzy/src/assets/i18n/ru.json index c5617949d8f..32e4f2d260b 100644 --- a/apps/gauzy/src/assets/i18n/ru.json +++ b/apps/gauzy/src/assets/i18n/ru.json @@ -455,7 +455,14 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto-sync tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Is tasks Auto-sync based on Label?", + "AUTO_SYNC_TAG": "Label", + "GITHUB_REPOSITORY": "GitHub Repository", + "PROJECT": "Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Хотите включить поиск вакансий и анализ соответствия?", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Хотите включить анализ эффективности сотрудников?" }, "PLACEHOLDERS": { "NAME": "Имя", @@ -640,7 +647,13 @@ "UPWORK_ORGANIZATION_ID": "Upwork Organization ID", "UPWORK_ORGANIZATION_NAME": "Upwork Organization Name", "UPWORK_ID": "Upwork ID", - "LINKEDIN_ID": "LinkedIn ID" + "LINKEDIN_ID": "LinkedIn ID", + "AUTO_SYNC_TASKS": "Auto Sync Tasks", + "AUTO_SYNC_TASKS_BASED_ON_LABEL": "Auto-sync Tasks On Label", + "AUTO_SYNC_TAG": "Select Auto-sync Label", + "SELECT_PROJECT": "Select Project", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Включить поиск вакансий и анализ соответствия", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Включить анализ производительности сотрудников" }, "RATES": { "DEFAULT_RATE": "Стандартная ставка", @@ -1212,9 +1225,11 @@ "SETTINGS": "Settings" }, "TOOLTIP": { - "API_KEY": "The API Key serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "API_SECRET": "The API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible.", - "OPEN_AI_API_SECRET_KEY": "The OpenAI API Secret serves as your App identifier for API requests, remaining permanently hidden, with some characters visible." + "API_KEY": "Ключ API служит идентификатором вашего приложения для запросов API, оставаясь постоянно скрытым, с видимыми некоторыми символами.", + "API_SECRET": "Секретный ключ API служит идентификатором вашего приложения для запросов API, оставаясь постоянно скрытым, с видимыми некоторыми символами.", + "OPEN_AI_API_SECRET_KEY": "Секретный ключ API OpenAI служит идентификатором вашего приложения для запросов API, оставаясь постоянно скрытым, с видимыми некоторыми символами.", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Включает расширенные анализы поиска работы и сопоставления для повышения точности. При отключении скрывает пункты меню 'Просмотр' и 'Сопоставление' для удобного интерфейса.", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Управляет передачей метрик работы сотрудника, включая нажатия клавиш, движения мыши и снимки экрана, для анализа Gauzy AI. Включите для полного анализа производительности; отключите для строгой конфиденциальности и контроля за данными пользователя." } }, "GITHUB_PAGE": { diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 00000000000..91ebe8382d8 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,388 @@ +version: '3.8' + +services: + db: + image: postgres:15-alpine + container_name: db + restart: always + environment: + POSTGRES_DB: ${DB_NAME:-gauzy} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} + healthcheck: + test: + [ + 'CMD-SHELL', + 'psql postgres://$${POSTGRES_USER}:$${POSTGRES_PASSWORD}@localhost:5432/$${POSTGRES_DB} || exit 1' + ] + volumes: + - postgres_data:/var/lib/postgresql/data/ + - ./.deploy/db/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh + ports: + - '5432:5432' + networks: + - overlay + + cube: + image: cubejs/cube:latest + container_name: cube + ports: + - '4000:4000' # Cube Playground + - '5430:5430' # Port for Cube SQL + environment: + CUBEJS_DEV_MODE: 'true' + CUBEJS_DB_TYPE: postgres + CUBEJS_DB_HOST: db + CUBEJS_DB_PORT: 5432 + CUBEJS_DB_NAME: ${DB_NAME:-gauzy} + CUBEJS_DB_USER: ${DB_USER:-postgres} + CUBEJS_DB_PASS: ${DB_PASS:-gauzy_password} + # Credentials to connect to Cube SQL APIs + CUBEJS_PG_SQL_PORT: 5430 + CUBEJS_SQL_USER: ${CUBE_USER:-cube_user} + CUBEJS_SQL_PASSWORD: ${CUBE_PASS:-cube_pass} + volumes: + - 'cube_data:/cube/conf' + links: + - db + networks: + - overlay + + jitsu: + container_name: jitsu + image: jitsucom/jitsu:latest + extra_hosts: + - 'host.docker.internal:host-gateway' + environment: + - REDIS_URL=redis://redis:6379 + # Retroactive users recognition can affect RAM significant. + # Read more about the solution https://jitsu.com/docs/other-features/retroactive-user-recognition + - USER_RECOGNITION_ENABLED=true + - USER_RECOGNITION_REDIS_URL=redis://jitsu_redis_users_recognition:6380 + - TERM=xterm-256color + depends_on: + redis: + condition: service_healthy + jitsu_redis_users_recognition: + condition: service_healthy + volumes: + - ./.deploy/jitsu/configurator/data/logs:/home/configurator/data/logs + - ./.deploy/jitsu/server/data/logs:/home/eventnative/data/logs + - ./.deploy/jitsu/server/data/logs/events:/home/eventnative/data/logs/events + - /var/run/docker.sock:/var/run/docker.sock + - jitsu_workspace:/home/eventnative/data/airbyte + restart: always + ports: + - '8000:8000' + networks: + - overlay + + elasticsearch: + image: 'elasticsearch:7.17.7' + container_name: elasticsearch + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + environment: + ES_JAVA_OPTS: -Xms512m -Xmx1024m + discovery.type: single-node + http.port: 9200 + http.cors.enabled: 'true' + http.cors.allow-origin: http://localhost:3000,http://127.0.0.1:3000,http://localhost:1358,http://127.0.0.1:1358 + http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization + http.cors.allow-credentials: 'true' + bootstrap.memory_lock: 'true' + xpack.security.enabled: 'false' + ports: + - '9200' + - '9300' + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cat/health'] + interval: 5s + timeout: 5s + retries: 10 + start_period: 20s + networks: + - overlay + + # Elasticsearch Management UI + dejavu: + image: appbaseio/dejavu:3.6.0 + container_name: dejavu + ports: + - '1358:1358' + links: + - elasticsearch + networks: + - overlay + + # TODO: For now used in Jitsu, but we will need to create another one dedicated for Jitsu later + redis: + image: 'redis:7.0.2-alpine' + container_name: redis + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 PING'] + interval: 1s + timeout: 30s + ports: + - '6379' + volumes: + - ./.deploy/redis/data:/data + networks: + - overlay + + jitsu_redis_users_recognition: + image: 'redis:7.0.2-alpine' + container_name: jitsu_redis_users_recognition + command: redis-server /usr/local/etc/redis/redis.conf + restart: unless-stopped + healthcheck: + test: ['CMD-SHELL', 'redis-cli -h localhost -p 6380 PING'] + interval: 1s + timeout: 30s + ports: + - '6380' + volumes: + - ./.deploy/redis/jitsu_users_recognition/data:/data + - ./.deploy/redis/jitsu_users_recognition/redis.conf:/usr/local/etc/redis/redis.conf + networks: + - overlay + + minio: + restart: unless-stopped + image: quay.io/minio/minio:latest + container_name: minio + volumes: + - minio_data:/data + environment: + MINIO_ROOT_USER: ever-gauzy-access-key + MINIO_ROOT_PASSWORD: ever-gauzy-secret-key + command: server /data --address :9000 --console-address ":9001" + ports: + - 9000:9000 + - 9001:9001 + networks: + - overlay + + minio_create_buckets: + image: minio/mc + environment: + MINIO_ROOT_USER: ever-gauzy-access-key + MINIO_ROOT_PASSWORD: ever-gauzy-secret-key + entrypoint: + - '/bin/sh' + - '-c' + command: + - "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do + echo 'Waiting to start minio...' && sleep 1; + done; + /usr/bin/mc mb minio/ever-gauzy --region=eu-north-1; + exit 0;" + depends_on: + - minio + networks: + - overlay + + pgweb: + image: sosedoff/pgweb + container_name: pgweb + restart: always + depends_on: + - db + links: + - db:${DB_HOST:-db} + environment: + POSTGRES_DB: ${DB_NAME:-gauzy} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} + PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable + ports: + - '8081:8081' + networks: + - overlay + + api: + container_name: api + image: gauzy-api:latest + build: + context: . + dockerfile: .deploy/api/Dockerfile + args: + NODE_ENV: ${NODE_ENV:-development} + API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} + CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} + environment: + API_HOST: ${API_HOST:-api} + API_PORT: ${API_PORT:-3000} + NODE_ENV: ${NODE_ENV:-development} + DB_HOST: db + API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} + CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} + CLOUD_PROVIDER: ${CLOUD_PROVIDER:-} + SENTRY_DSN: ${SENTRY_DSN:-} + SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-} + SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-} + JITSU_SERVER_URL: ${JITSU_SERVER_URL:-} + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-} + OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-} + OTEL_ENABLED: ${OTEL_ENABLED:-} + JITSU_SERVER_WRITE_KEY: ${JITSU_SERVER_WRITE_KEY:-} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_CLIENT_SECRET: ${GAUZY_GITHUB_CLIENT_SECRET:-} + GAUZY_GITHUB_WEBHOOK_URL: ${GAUZY_GITHUB_WEBHOOK_URL:-} + GAUZY_GITHUB_WEBHOOK_SECRET: ${GAUZY_GITHUB_WEBHOOK_SECRET:-} + GAUZY_GITHUB_APP_PRIVATE_KEY: ${GAUZY_GITHUB_APP_PRIVATE_KEY:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_OAUTH_CLIENT_ID: ${GAUZY_GITHUB_OAUTH_CLIENT_ID:-} + GAUZY_GITHUB_OAUTH_CLIENT_SECRET: ${GAUZY_GITHUB_OAUTH_CLIENT_SECRET:-} + GAUZY_GITHUB_OAUTH_CALLBACK_URL: ${GAUZY_GITHUB_OAUTH_CALLBACK_URL:-} + MAGIC_CODE_EXPIRATION_TIME: ${MAGIC_CODE_EXPIRATION_TIME:-} + APP_NAME: ${APP_NAME:-} + APP_LOGO: ${APP_LOGO:-} + APP_SIGNATURE: ${APP_SIGNATURE:-} + APP_LINK: ${APP_LINK:-} + APP_EMAIL_CONFIRMATION_URL: ${APP_EMAIL_CONFIRMATION_URL:-} + APP_MAGIC_SIGN_URL: ${APP_MAGIC_SIGN_URL:-} + COMPANY_LINK: ${COMPANY_LINK:-} + COMPANY_NAME: ${COMPANY_NAME:-} + + env_file: + - .env.compose + entrypoint: './entrypoint.compose.sh' + command: ['node', 'main.js'] + restart: on-failure + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + minio_create_buckets: + condition: service_started + elasticsearch: + condition: service_healthy + cube: + condition: service_started + links: + - db:${DB_HOST:-db} + - cube:${CUBE_HOST:-cube} + - redis:${REDIS_HOST:-redis} + - minio:${MINIO_HOST:-minio} + - elasticsearch:${ES_HOST:-elasticsearch} + # volumes: + # - webapp_node_modules:/srv/gauzy/node_modules + # - api_node_modules:/srv/gauzy/apps/api/node_modules + ports: + - '3000:${API_PORT:-3000}' + networks: + - overlay + + webapp: + container_name: webapp + image: gauzy-webapp:latest + build: + context: . + dockerfile: .deploy/webapp/Dockerfile + args: + NODE_ENV: ${NODE_ENV:-development} + API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} + CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} + SENTRY_DSN: ${SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0.1} + CHATWOOT_SDK_TOKEN: ${CHATWOOT_SDK_TOKEN:-} + CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-} + CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY:-} + GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY:-} + GOOGLE_PLACE_AUTOCOMPLETE: ${GOOGLE_PLACE_AUTOCOMPLETE:-false} + DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} + DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} + DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} + JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} + DEMO: 'true' + API_HOST: ${API_HOST:-api} + API_PORT: ${API_PORT:-3000} + environment: + WEB_HOST: ${WEB_HOST:-webapp} + WEB_PORT: ${WEB_PORT:-4200} + NODE_ENV: ${NODE_ENV:-development} + API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} + CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} + SENTRY_DSN: ${SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0.1} + CHATWOOT_SDK_TOKEN: ${CHATWOOT_SDK_TOKEN:-} + CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-} + CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY:-} + GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY:-} + GOOGLE_PLACE_AUTOCOMPLETE: ${GOOGLE_PLACE_AUTOCOMPLETE:-false} + DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} + DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} + DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} + JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} + DEMO: 'true' + API_HOST: ${API_HOST:-api} + API_PORT: ${API_PORT:-3000} + entrypoint: './entrypoint.compose.sh' + command: ['nginx', '-g', 'daemon off;'] + env_file: + - .env.compose + restart: on-failure + links: + - db:${DB_HOST:-db} + - api:${API_HOST:-api} + - cube:${CUBE_HOST:-cube} + - redis:${REDIS_HOST:-redis} + - minio:${MINIO_HOST:-minio} + - elasticsearch:${ES_HOST:-elasticsearch} + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + minio_create_buckets: + condition: service_started + elasticsearch: + condition: service_healthy + api: + condition: service_started + # volumes: + # - webapp_node_modules:/srv/gauzy/node_modules + ports: + - '4200:${UI_PORT:-4200}' + networks: + - overlay + +volumes: + # webapp_node_modules: + # api_node_modules: + redis_data: {} + postgres_data: {} + elasticsearch_data: {} + minio_data: {} + cube_data: {} + certificates: {} + jitsu_workspace: {} + +networks: + overlay: + driver: bridge diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index 69faab4efa5..3a1bd643395 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -23,188 +23,6 @@ services: networks: - overlay - cube: - image: cubejs/cube:latest - container_name: cube - ports: - - '4000:4000' # Cube Playground - - '5430:5430' # Port for Cube SQL - environment: - CUBEJS_DEV_MODE: 'true' - CUBEJS_DB_TYPE: postgres - CUBEJS_DB_HOST: db - CUBEJS_DB_PORT: 5432 - CUBEJS_DB_NAME: ${DB_NAME:-gauzy} - CUBEJS_DB_USER: ${DB_USER:-postgres} - CUBEJS_DB_PASS: ${DB_PASS:-gauzy_password} - # Credentials to connect to Cube SQL APIs - CUBEJS_PG_SQL_PORT: 5430 - CUBEJS_SQL_USER: ${CUBE_USER:-cube_user} - CUBEJS_SQL_PASSWORD: ${CUBE_PASS:-cube_pass} - volumes: - - 'cube_data:/cube/conf' - links: - - db - networks: - - overlay - - jitsu: - container_name: jitsu - image: jitsucom/jitsu:latest - extra_hosts: - - 'host.docker.internal:host-gateway' - environment: - - REDIS_URL=redis://redis:6379 - # Retroactive users recognition can affect RAM significant. - # Read more about the solution https://jitsu.com/docs/other-features/retroactive-user-recognition - - USER_RECOGNITION_ENABLED=true - - USER_RECOGNITION_REDIS_URL=redis://jitsu_redis_users_recognition:6380 - - TERM=xterm-256color - depends_on: - redis: - condition: service_healthy - jitsu_redis_users_recognition: - condition: service_healthy - volumes: - - ./.deploy/jitsu/configurator/data/logs:/home/configurator/data/logs - - ./.deploy/jitsu/server/data/logs:/home/eventnative/data/logs - - ./.deploy/jitsu/server/data/logs/events:/home/eventnative/data/logs/events - - /var/run/docker.sock:/var/run/docker.sock - - jitsu_workspace:/home/eventnative/data/airbyte - restart: always - ports: - - '8000:8000' - networks: - - overlay - - elasticsearch: - image: 'elasticsearch:7.17.7' - container_name: elasticsearch - volumes: - - elasticsearch_data:/usr/share/elasticsearch/data - environment: - ES_JAVA_OPTS: -Xms512m -Xmx1024m - discovery.type: single-node - http.port: 9200 - http.cors.enabled: 'true' - http.cors.allow-origin: http://localhost:3000,http://127.0.0.1:3000,http://localhost:1358,http://127.0.0.1:1358 - http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization - http.cors.allow-credentials: 'true' - bootstrap.memory_lock: 'true' - xpack.security.enabled: 'false' - ports: - - '9200' - - '9300' - ulimits: - memlock: - soft: -1 - hard: -1 - healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:9200/_cat/health'] - interval: 5s - timeout: 5s - retries: 10 - start_period: 20s - networks: - - overlay - - # Elasticsearch Management UI - dejavu: - image: appbaseio/dejavu:3.6.0 - container_name: dejavu - ports: - - '1358:1358' - links: - - elasticsearch - networks: - - overlay - - # TODO: For now used in Jitsu, but we will need to create another one dedicated for Jitsu later - redis: - image: 'redis:7.0.2-alpine' - container_name: redis - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6379 PING'] - interval: 1s - timeout: 30s - ports: - - '6379' - volumes: - - ./.deploy/redis/data:/data - networks: - - overlay - - jitsu_redis_users_recognition: - image: 'redis:7.0.2-alpine' - container_name: jitsu_redis_users_recognition - command: redis-server /usr/local/etc/redis/redis.conf - restart: unless-stopped - healthcheck: - test: ['CMD-SHELL', 'redis-cli -h localhost -p 6380 PING'] - interval: 1s - timeout: 30s - ports: - - '6380' - volumes: - - ./.deploy/redis/jitsu_users_recognition/data:/data - - ./.deploy/redis/jitsu_users_recognition/redis.conf:/usr/local/etc/redis/redis.conf - networks: - - overlay - - minio: - restart: unless-stopped - image: quay.io/minio/minio:latest - container_name: minio - volumes: - - minio_data:/data - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - command: server /data --address :9000 --console-address ":9001" - ports: - - 9000:9000 - - 9001:9001 - networks: - - overlay - - minio_create_buckets: - image: minio/mc - environment: - MINIO_ROOT_USER: ever-gauzy-access-key - MINIO_ROOT_PASSWORD: ever-gauzy-secret-key - entrypoint: - - '/bin/sh' - - '-c' - command: - - "until (/usr/bin/mc alias set minio http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD) do - echo 'Waiting to start minio...' && sleep 1; - done; - /usr/bin/mc mb minio/ever-gauzy --region=eu-north-1; - exit 0;" - depends_on: - - minio - networks: - - overlay - - pgweb: - image: sosedoff/pgweb - container_name: pgweb - restart: always - depends_on: - - db - links: - - db:${DB_HOST:-db} - environment: - POSTGRES_DB: ${DB_NAME:-gauzy} - POSTGRES_USER: ${DB_USER:-postgres} - POSTGRES_PASSWORD: ${DB_PASS:-gauzy_password} - PGWEB_DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASS:-gauzy_password}@${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-gauzy}?sslmode=disable - ports: - - '8081:8081' - networks: - - overlay - api: container_name: api image: ghcr.io/ever-co/gauzy-api:latest @@ -246,29 +64,15 @@ services: COMPANY_NAME: ${COMPANY_NAME:-} env_file: - - .env.compose + - .env.demo.compose entrypoint: './entrypoint.compose.sh' command: ['node', 'main.js'] restart: on-failure depends_on: db: condition: service_healthy - redis: - condition: service_started - minio: - condition: service_started - minio_create_buckets: - condition: service_started - elasticsearch: - condition: service_healthy - cube: - condition: service_started links: - db:${DB_HOST:-db} - - cube:${CUBE_HOST:-cube} - - redis:${REDIS_HOST:-redis} - - minio:${MINIO_HOST:-minio} - - elasticsearch:${ES_HOST:-elasticsearch} # volumes: # - webapp_node_modules:/srv/gauzy/node_modules # - api_node_modules:/srv/gauzy/apps/api/node_modules @@ -309,26 +113,14 @@ services: entrypoint: './entrypoint.compose.sh' command: ['nginx', '-g', 'daemon off;'] env_file: - - .env.compose + - .env.demo.compose restart: on-failure links: - db:${DB_HOST:-db} - api:${API_HOST:-api} - - cube:${CUBE_HOST:-cube} - - redis:${REDIS_HOST:-redis} - - minio:${MINIO_HOST:-minio} - - elasticsearch:${ES_HOST:-elasticsearch} depends_on: db: condition: service_healthy - redis: - condition: service_started - minio: - condition: service_started - minio_create_buckets: - condition: service_started - elasticsearch: - condition: service_healthy api: condition: service_started # volumes: @@ -341,13 +133,7 @@ services: volumes: # webapp_node_modules: # api_node_modules: - redis_data: {} postgres_data: {} - elasticsearch_data: {} - minio_data: {} - cube_data: {} - certificates: {} - jitsu_workspace: {} networks: overlay: diff --git a/docker-compose.yml b/docker-compose.yml index 91ebe8382d8..58b5d08d990 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,14 +207,7 @@ services: api: container_name: api - image: gauzy-api:latest - build: - context: . - dockerfile: .deploy/api/Dockerfile - args: - NODE_ENV: ${NODE_ENV:-development} - API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} - CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} + image: ghcr.io/ever-co/gauzy-api:latest environment: API_HOST: ${API_HOST:-api} API_PORT: ${API_PORT:-3000} @@ -286,34 +279,7 @@ services: webapp: container_name: webapp - image: gauzy-webapp:latest - build: - context: . - dockerfile: .deploy/webapp/Dockerfile - args: - NODE_ENV: ${NODE_ENV:-development} - API_BASE_URL: ${API_BASE_URL:-http://localhost:3000} - CLIENT_BASE_URL: ${CLIENT_BASE_URL:-http://localhost:4200} - SENTRY_DSN: ${SENTRY_DSN:-} - SENTRY_TRACES_SAMPLE_RATE: ${SENTRY_TRACES_SAMPLE_RATE:-0.1} - CHATWOOT_SDK_TOKEN: ${CHATWOOT_SDK_TOKEN:-} - CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-} - CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY:-} - GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY:-} - GOOGLE_PLACE_AUTOCOMPLETE: ${GOOGLE_PLACE_AUTOCOMPLETE:-false} - DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} - DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} - DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} - GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} - GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} - GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} - GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} - GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} - JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} - JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} - DEMO: 'true' - API_HOST: ${API_HOST:-api} - API_PORT: ${API_PORT:-3000} + image: ghcr.io/ever-co/gauzy-webapp:latest environment: WEB_HOST: ${WEB_HOST:-webapp} WEB_PORT: ${WEB_PORT:-4200} diff --git a/packages/contracts/src/tag.model.ts b/packages/contracts/src/tag.model.ts index 890208b248d..b2c8069190d 100644 --- a/packages/contracts/src/tag.model.ts +++ b/packages/contracts/src/tag.model.ts @@ -4,6 +4,7 @@ import { IBasePerTenantAndOrganizationEntityModel } from './base-entity.model'; export interface ITag extends IBasePerTenantAndOrganizationEntityModel, IRelationalOrganizationTeam { name: string; color: string; + textColor?: string; icon?: string; description?: string; isSystem?: boolean; @@ -12,11 +13,12 @@ export interface ITag extends IBasePerTenantAndOrganizationEntityModel, IRelatio export interface ITagFindInput extends IBasePerTenantAndOrganizationEntityModel, Pick { name?: string; color?: string; + textColor?: string; description?: string; isSystem?: boolean; } -export interface ITagCreateInput extends ITag {} +export interface ITagCreateInput extends ITag { } export interface ITagUpdateInput extends Partial { id?: string; diff --git a/packages/core/src/database/migrations/1702452723642-AlterTagEntityTable.ts b/packages/core/src/database/migrations/1702452723642-AlterTagEntityTable.ts new file mode 100644 index 00000000000..7d7ebf4c151 --- /dev/null +++ b/packages/core/src/database/migrations/1702452723642-AlterTagEntityTable.ts @@ -0,0 +1,94 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AlterTagEntityTable1702452723642 implements MigrationInterface { + + name = 'AlterTagEntityTable1702452723642'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + if (['sqlite', 'better-sqlite3'].includes(queryRunner.connection.options.type)) { + await this.sqliteUpQueryRunner(queryRunner); + } else { + await this.postgresUpQueryRunner(queryRunner); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + if (['sqlite', 'better-sqlite3'].includes(queryRunner.connection.options.type)) { + await this.sqliteDownQueryRunner(queryRunner); + } else { + await this.postgresDownQueryRunner(queryRunner); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag" ADD "textColor" character varying`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag" DROP COLUMN "textColor"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_49746602acc4e5e8721062b69e"`); + await queryRunner.query(`DROP INDEX "IDX_b08dd29fb6a8acdf83c83d8988"`); + await queryRunner.query(`DROP INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9"`); + await queryRunner.query(`DROP INDEX "IDX_1f22c73374bcca1ea84a4dca59"`); + await queryRunner.query(`DROP INDEX "IDX_58876ee26a90170551027459bf"`); + await queryRunner.query(`CREATE TABLE "temporary_tag" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "description" varchar, "color" varchar NOT NULL, "isSystem" boolean NOT NULL DEFAULT (0), "icon" varchar, "organizationTeamId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, "textColor" varchar, CONSTRAINT "FK_49746602acc4e5e8721062b69ec" FOREIGN KEY ("organizationTeamId") REFERENCES "organization_team" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b08dd29fb6a8acdf83c83d8988f" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c2f6bec0b39eaa3a6d90903ae99" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "temporary_tag"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt" FROM "tag"`); + await queryRunner.query(`DROP TABLE "tag"`); + await queryRunner.query(`ALTER TABLE "temporary_tag" RENAME TO "tag"`); + await queryRunner.query(`CREATE INDEX "IDX_49746602acc4e5e8721062b69e" ON "tag" ("organizationTeamId") `); + await queryRunner.query(`CREATE INDEX "IDX_b08dd29fb6a8acdf83c83d8988" ON "tag" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9" ON "tag" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_1f22c73374bcca1ea84a4dca59" ON "tag" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_58876ee26a90170551027459bf" ON "tag" ("isArchived") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_58876ee26a90170551027459bf"`); + await queryRunner.query(`DROP INDEX "IDX_1f22c73374bcca1ea84a4dca59"`); + await queryRunner.query(`DROP INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9"`); + await queryRunner.query(`DROP INDEX "IDX_b08dd29fb6a8acdf83c83d8988"`); + await queryRunner.query(`DROP INDEX "IDX_49746602acc4e5e8721062b69e"`); + await queryRunner.query(`ALTER TABLE "tag" RENAME TO "temporary_tag"`); + await queryRunner.query(`CREATE TABLE "tag" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "name" varchar NOT NULL, "description" varchar, "color" varchar NOT NULL, "isSystem" boolean NOT NULL DEFAULT (0), "icon" varchar, "organizationTeamId" varchar, "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "deletedAt" datetime, CONSTRAINT "FK_49746602acc4e5e8721062b69ec" FOREIGN KEY ("organizationTeamId") REFERENCES "organization_team" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_b08dd29fb6a8acdf83c83d8988f" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c2f6bec0b39eaa3a6d90903ae99" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE)`); + await queryRunner.query(`INSERT INTO "tag"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "name", "description", "color", "isSystem", "icon", "organizationTeamId", "isActive", "isArchived", "deletedAt" FROM "temporary_tag"`); + await queryRunner.query(`DROP TABLE "temporary_tag"`); + await queryRunner.query(`CREATE INDEX "IDX_58876ee26a90170551027459bf" ON "tag" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_1f22c73374bcca1ea84a4dca59" ON "tag" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_c2f6bec0b39eaa3a6d90903ae9" ON "tag" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_b08dd29fb6a8acdf83c83d8988" ON "tag" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_49746602acc4e5e8721062b69e" ON "tag" ("organizationTeamId") `); + } +} diff --git a/packages/core/src/dev-config.ts b/packages/core/src/dev-config.ts index 5805afb5a07..bf573a437e7 100644 --- a/packages/core/src/dev-config.ts +++ b/packages/core/src/dev-config.ts @@ -1,11 +1,11 @@ import { - DEFAULT_API_BASE_URL, - DEFAULT_API_HOST, - DEFAULT_API_PORT, - DEFAULT_GRAPHQL_API_PATH, - IPluginConfig -} from "@gauzy/common"; -import { dbConnectionConfig, environment } from "@gauzy/config"; + DEFAULT_API_BASE_URL, + DEFAULT_API_HOST, + DEFAULT_API_PORT, + DEFAULT_GRAPHQL_API_PATH, + IPluginConfig +} from '@gauzy/common'; +import { dbConnectionConfig } from '@gauzy/config'; export const devConfig: IPluginConfig = { apiConfigOptions: { @@ -22,8 +22,8 @@ export const devConfig: IPluginConfig = { }, dbConnectionOptions: { migrationsTransactionMode: 'each', // Run migrations automatically in each transaction. i.e."all" | "none" | "each" - migrationsRun: !environment.production, // Run migrations automatically, you can disable this if you prefer running migration manually. + migrationsRun: process.env.DB_SYNCHRONIZE === 'true' ? false : true, // Run migrations automatically if we don't do DB_SYNCHRONIZE ...dbConnectionConfig }, plugins: [] -}; \ No newline at end of file +}; diff --git a/packages/core/src/employee/employee.subscriber.ts b/packages/core/src/employee/employee.subscriber.ts index f130df54258..abe1f749943 100644 --- a/packages/core/src/employee/employee.subscriber.ts +++ b/packages/core/src/employee/employee.subscriber.ts @@ -23,21 +23,30 @@ export class EmployeeSubscriber implements EntitySubscriberInterface { } /** - * Called after entity is loaded from the database. + * Called after an Employee entity is loaded from the database. * - * @param entity - * @param event + * @param entity - The loaded Employee entity. + * @param event - The LoadEvent associated with the entity loading. */ afterLoad(entity: Employee, event?: LoadEvent): void | Promise { try { + // Set fullName based on user name if available if (entity.user) { entity.fullName = entity.user.name; } + + // Set isDeleted based on the presence of deletedAt property if ('deletedAt' in entity) { entity.isDeleted = !!entity.deletedAt; } + + // Ensure billRateValue is initialized to 0 if not set + if ('billRateValue' in entity) { + entity.billRateValue = entity.billRateValue || 0; + } } catch (error) { - console.log(error); + // Handle or log the error as needed + console.log("Error in afterLoad:", error.message); } } @@ -134,22 +143,22 @@ export class EmployeeSubscriber implements EntitySubscriberInterface { */ createSlug(entity: Employee) { try { - if (!entity || !entity.user) { - console.error("Entity or User object is not defined."); - return; - } + if (!entity || !entity.user) { + console.error("Entity or User object is not defined."); + return; + } - const { user } = entity; + const { user } = entity; - if (user.firstName || user.lastName) { // Use first &/or last name to create slug - entity.profile_link = sluggable(`${user.firstName || ''} ${user.lastName || ''}`.trim()); - } else if (user.username) { // Use username to create slug if first & last name not found - entity.profile_link = sluggable(user.username); + if (user.firstName || user.lastName) { // Use first &/or last name to create slug + entity.profile_link = sluggable(`${user.firstName || ''} ${user.lastName || ''}`.trim()); + } else if (user.username) { // Use username to create slug if first & last name not found + entity.profile_link = sluggable(user.username); } else { // Use email to create slug if nothing found - entity.profile_link = sluggable(retrieveNameFromEmail(user.email)); + entity.profile_link = sluggable(retrieveNameFromEmail(user.email)); } } catch (error) { - console.error(`Error creating slug for entity with id ${entity.id}: `, error); + console.error(`Error creating slug for entity with id ${entity.id}: `, error); } } diff --git a/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts b/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts index 2e95178c426..36b83cb530d 100644 --- a/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts +++ b/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts @@ -18,14 +18,10 @@ export class JitsuAnalyticsService { // Initialize the Jitsu Analytics instance this.jitsu = createJitsu(this.config); } else { - this.logger.error( - `Jitsu Analytics initialization failed: Missing host or writeKey.` - ); + this.logger.warn(`Jitsu Analytics initialization failed: Missing host or writeKey.`); } } catch (error) { - this.logger.error( - `Jitsu Analytics initialization failed: ${error.message}` - ); + this.logger.error(`Jitsu Analytics initialization failed: ${error.message}`); } } @@ -35,10 +31,7 @@ export class JitsuAnalyticsService { * @param properties Additional event properties (optional). * @returns A promise that resolves when the event is tracked. */ - async trackEvent( - event: string, - properties?: Record | null - ): Promise { + async trackEvent(event: string, properties?: Record | null): Promise { // Check if this.jitsu is defined and both host and writeKey are defined if (this.jitsu && this.config.host && this.config.writeKey) { return await this.jitsu.track(event, properties); @@ -53,10 +46,7 @@ export class JitsuAnalyticsService { * @param traits User traits or properties to associate with the user. * @returns A Promise that resolves when the user is identified. */ - async identify( - id: string | object, - traits?: Record | null - ): Promise { + async identify(id: string | object, traits?: Record | null): Promise { // Check if this.jitsu is defined and both host and writeKey are defined if (this.jitsu && this.config.host && this.config.writeKey) { return await this.jitsu.identify(id, traits); @@ -69,10 +59,7 @@ export class JitsuAnalyticsService { * @param traits Additional data or traits associated with the group. * @returns A Promise that resolves when the users are grouped. */ - async group( - id: string | object, - traits?: Record | null - ): Promise { + async group(id: string | object, traits?: Record | null): Promise { // Check if this.jitsu is defined and both host and writeKey are defined if (this.jitsu && this.config.host && this.config.writeKey) { return await this.jitsu.group(id, traits); diff --git a/packages/core/src/tags/default-tags.ts b/packages/core/src/tags/default-tags.ts index 17f78d8a314..f56d7e18fec 100644 --- a/packages/core/src/tags/default-tags.ts +++ b/packages/core/src/tags/default-tags.ts @@ -38,56 +38,64 @@ export const DEFAULT_TAGS: ITag[] = [ { name: TagEnum.MOBILE, icon: 'task-labels/mobile.svg', - color: '#4E4AE8', + color: '#4e4ae8', + textColor: '#67a946', description: null, isSystem: true }, { name: TagEnum.FRONTEND, icon: 'task-labels/frontend.svg', - color: '#41AB6B', + color: '#41ab6b', + textColor: '#42576c', description: null, isSystem: true }, { name: TagEnum.BACKEND, icon: 'task-labels/backend.svg', - color: '#E84A5D', + color: '#e84a5d', + textColor: '#c5da3e', description: null, isSystem: true }, { name: TagEnum.WEB, icon: 'task-labels/web.svg', - color: '#4192AB', + color: '#4192ab', + textColor: '#5c64cf', description: null, isSystem: true }, { name: TagEnum.UI_UX, icon: 'task-labels/ui-ux.svg', - color: '#9641AB', + color: '#9641ab', + textColor: '#276ea9', description: null, isSystem: true }, { name: TagEnum.FULL_STACK, icon: 'task-labels/fullstack.svg', - color: '#AB9A41', + color: '#ab9a41', + textColor: '#404fac', description: null, isSystem: true }, { name: TagEnum.TABLET, icon: 'task-labels/tablet.svg', - color: '#5CAB41', + color: '#5cab41', + textColor: '#f15894', description: null, isSystem: true }, { name: TagEnum.BUG, icon: 'task-labels/bug.svg', - color: '#E78F5E', + color: '#e78f5e', + textColor: '#9c00de', description: null, isSystem: true } diff --git a/packages/core/src/tags/dto/create-tag.dto.ts b/packages/core/src/tags/dto/create-tag.dto.ts index 95d6bd73bc3..663db93d794 100644 --- a/packages/core/src/tags/dto/create-tag.dto.ts +++ b/packages/core/src/tags/dto/create-tag.dto.ts @@ -5,5 +5,5 @@ import { Tag } from './../tag.entity'; export class CreateTagDTO extends IntersectionType( PartialType(TenantOrganizationBaseDTO), - PickType(Tag, ['name', 'description', 'color', 'icon', 'organizationTeamId']) + PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId']) ) implements ITagCreateInput { } diff --git a/packages/core/src/tags/dto/update-tag.dto.ts b/packages/core/src/tags/dto/update-tag.dto.ts index fb07e3954ed..c1433adf805 100644 --- a/packages/core/src/tags/dto/update-tag.dto.ts +++ b/packages/core/src/tags/dto/update-tag.dto.ts @@ -5,5 +5,5 @@ import { Tag } from './../tag.entity'; export class UpdateTagDTO extends IntersectionType( PartialType(TenantOrganizationBaseDTO), - PartialType(PickType(Tag, ['name', 'description', 'color', 'icon', 'organizationTeamId'])), + PartialType(PickType(Tag, ['name', 'description', 'color', 'textColor', 'icon', 'organizationTeamId'])), ) implements ITagUpdateInput { } diff --git a/packages/core/src/tags/tag.entity.ts b/packages/core/src/tags/tag.entity.ts index d58ddb17e6d..b16ef85c397 100644 --- a/packages/core/src/tags/tag.entity.ts +++ b/packages/core/src/tags/tag.entity.ts @@ -75,18 +75,24 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { @Column() color: string; - @ApiPropertyOptional({ type: () => String, required: false }) + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsString() + @Column({ nullable: true }) + textColor?: string; + + @ApiPropertyOptional({ type: () => String }) @IsOptional() @IsString() @Column({ nullable: true }) description?: string; - @ApiPropertyOptional({ type: () => String, required: false }) + @ApiPropertyOptional({ type: () => String }) @IsOptional() @Column({ nullable: true }) icon?: string; - @ApiPropertyOptional({ type: () => Boolean, default: false, required: false }) + @ApiPropertyOptional({ type: () => Boolean, default: false }) @Column({ default: false }) isSystem?: boolean; @@ -101,7 +107,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Organization Team */ - @ManyToOne(() => OrganizationTeam, (team) => team.labels, { + @ManyToOne(() => OrganizationTeam, (it) => it.labels, { + /** Database cascade action on delete. */ onDelete: 'SET NULL', }) organizationTeam?: IOrganizationTeam; @@ -123,8 +130,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Candidate */ - @ApiProperty({ type: () => Candidate, isArray: true }) - @ManyToMany(() => Candidate, (candidate) => candidate.tags, { + @ManyToMany(() => Candidate, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) candidates?: ICandidate[]; @@ -132,8 +139,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Employee */ - @ApiProperty({ type: () => Employee, isArray: true }) - @ManyToMany(() => Employee, (employee) => employee.tags, { + @ManyToMany(() => Employee, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) employees?: IEmployee[]; @@ -141,8 +148,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Equipment */ - @ApiProperty({ type: () => Equipment, isArray: true }) - @ManyToMany(() => Equipment, (equipment) => equipment.tags, { + @ManyToMany(() => Equipment, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) equipments?: IEquipment[]; @@ -150,8 +157,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * EventType */ - @ApiProperty({ type: () => EventType, isArray: true }) - @ManyToMany(() => EventType, (eventType) => eventType.tags, { + @ManyToMany(() => EventType, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) eventTypes?: IEventType[]; @@ -159,8 +166,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Income */ - @ApiProperty({ type: () => Income, isArray: true }) - @ManyToMany(() => Income, (income) => income.tags, { + @ManyToMany(() => Income, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) incomes?: IIncome[]; @@ -168,8 +175,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Expense */ - @ApiProperty({ type: () => Expense, isArray: true }) - @ManyToMany(() => Expense, (expense) => expense.tags, { + @ManyToMany(() => Expense, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) expenses?: IExpense[]; @@ -177,8 +184,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Invoice */ - @ApiProperty({ type: () => Invoice, isArray: true }) - @ManyToMany(() => Invoice, (invoice) => invoice.tags, { + @ManyToMany(() => Invoice, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) invoices?: IInvoice[]; @@ -186,8 +193,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Income */ - @ApiProperty({ type: () => Task, isArray: true }) - @ManyToMany(() => Task, (task) => task.tags, { + @ManyToMany(() => Task, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) tasks?: ITask[]; @@ -195,8 +202,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Proposal */ - @ApiProperty({ type: () => Proposal, isArray: true }) - @ManyToMany(() => Proposal, (proposal) => proposal.tags, { + @ManyToMany(() => Proposal, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) proposals?: IProposal[]; @@ -204,8 +211,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationVendor */ - @ApiProperty({ type: () => OrganizationVendor, isArray: true }) - @ManyToMany(() => OrganizationVendor, (organizationVendor) => organizationVendor.tags, { + @ManyToMany(() => OrganizationVendor, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationVendors?: IOrganizationVendor[]; @@ -213,8 +220,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationTeam */ - @ApiProperty({ type: () => OrganizationTeam, isArray: true }) - @ManyToMany(() => OrganizationTeam, (organizationTeam) => organizationTeam.tags, { + @ManyToMany(() => OrganizationTeam, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationTeams?: IOrganizationTeam[]; @@ -222,7 +229,6 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationProject */ - @ApiProperty({ type: () => OrganizationProject, isArray: true }) @ManyToMany(() => OrganizationProject, (it) => it.tags, { /** Defines the database cascade action on delete. */ onDelete: 'CASCADE', @@ -232,8 +238,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationPosition */ - @ApiProperty({ type: () => OrganizationPosition, isArray: true }) - @ManyToMany(() => OrganizationPosition, (organizationPosition) => organizationPosition.tags, { + @ManyToMany(() => OrganizationPosition, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationPositions?: IOrganizationPosition[]; @@ -241,8 +247,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * ExpenseCategory */ - @ApiProperty({ type: () => ExpenseCategory, isArray: true }) - @ManyToMany(() => ExpenseCategory, (expenseCategory) => expenseCategory.tags, { + @ManyToMany(() => ExpenseCategory, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) expenseCategories?: IExpenseCategory[]; @@ -250,8 +256,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationEmploymentType */ - @ApiProperty({ type: () => OrganizationEmploymentType, isArray: true }) - @ManyToMany(() => OrganizationEmploymentType, (organizationEmploymentType) => organizationEmploymentType.tags, { + @ManyToMany(() => OrganizationEmploymentType, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationEmploymentTypes?: IOrganizationEmploymentType[]; @@ -259,8 +265,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * EmployeeLevel */ - @ApiProperty({ type: () => EmployeeLevel, isArray: true }) - @ManyToMany(() => EmployeeLevel, (employeeLevel) => employeeLevel.tags, { + @ManyToMany(() => EmployeeLevel, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) employeeLevels?: IEmployeeLevel[]; @@ -268,8 +274,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationDepartment */ - @ApiProperty({ type: () => OrganizationDepartment, isArray: true }) - @ManyToMany(() => OrganizationDepartment, (organizationDepartment) => organizationDepartment.tags, { + @ManyToMany(() => OrganizationDepartment, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationDepartments?: IOrganizationDepartment[]; @@ -277,8 +283,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * OrganizationContact */ - @ApiProperty({ type: () => OrganizationContact, isArray: true }) - @ManyToMany(() => OrganizationContact, (organizationContact) => organizationContact.tags, { + @ManyToMany(() => OrganizationContact, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizationContacts?: IOrganizationContact[]; @@ -286,8 +292,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Product */ - @ApiProperty({ type: () => Product, isArray: true }) - @ManyToMany(() => Product, (product) => product.tags, { + @ManyToMany(() => Product, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) products?: IProduct[]; @@ -295,8 +301,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Payment */ - @ApiProperty({ type: () => Payment, isArray: true }) - @ManyToMany(() => Payment, (payment) => payment.tags, { + @ManyToMany(() => Payment, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) payments?: IPayment[]; @@ -304,8 +310,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * RequestApproval */ - @ApiProperty({ type: () => RequestApproval, isArray: true }) - @ManyToMany(() => RequestApproval, (requestApproval) => requestApproval.tags, { + @ManyToMany(() => RequestApproval, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) requestApprovals?: IRequestApproval[]; @@ -313,8 +319,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * User */ - @ApiProperty({ type: () => User, isArray: true }) - @ManyToMany(() => User, (user) => user.tags, { + @ManyToMany(() => User, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) users?: IUser[]; @@ -322,8 +328,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Integration */ - @ApiProperty({ type: () => Integration, isArray: true }) - @ManyToMany(() => Integration, (integration) => integration.tags, { + @ManyToMany(() => Integration, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) integrations?: IIntegration[]; @@ -331,8 +337,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Merchant */ - @ApiProperty({ type: () => Merchant, isArray: true }) - @ManyToMany(() => Merchant, (merchant) => merchant.tags, { + @ManyToMany(() => Merchant, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) merchants?: IMerchant[]; @@ -340,8 +346,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Warehouse */ - @ApiProperty({ type: () => Warehouse, isArray: true }) - @ManyToMany(() => Warehouse, (warehouse) => warehouse.tags, { + @ManyToMany(() => Warehouse, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) warehouses?: IWarehouse[]; @@ -349,8 +355,8 @@ export class Tag extends TenantOrganizationBaseEntity implements ITag { /** * Organization */ - @ApiProperty({ type: () => Organization, isArray: true }) - @ManyToMany(() => Organization, (organization) => organization.tags, { + @ManyToMany(() => Organization, (it) => it.tags, { + /** Defines the database cascade action on delete. */ onDelete: 'CASCADE' }) organizations?: IOrganization[]; diff --git a/packages/core/src/time-tracking/screenshot/screenshot.service.ts b/packages/core/src/time-tracking/screenshot/screenshot.service.ts index fcaad6087c9..2a2d2ddf82a 100644 --- a/packages/core/src/time-tracking/screenshot/screenshot.service.ts +++ b/packages/core/src/time-tracking/screenshot/screenshot.service.ts @@ -83,7 +83,7 @@ export class ScreenshotService extends TenantAwareCrudService { name: IntegrationEnum.GAUZY_AI }); - console.log('AI Integraiton Tenant: %s', integration); + console.log('AI Integration Tenant: %s', integration); // Check if integration exists if (!!integration) { @@ -101,7 +101,7 @@ export class ScreenshotService extends TenantAwareCrudService { } } catch (error) { // If needed, consider throwing or handling the error appropriately. - console.log('Failed to get AI integraiton for provided options: %s', error?.message); + console.log('Failed to get AI Integration for provided options: %s', error?.message); } } } diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-client.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-client.handler.ts index 7f369377457..85f86328764 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-client.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-client.handler.ts @@ -1,161 +1,102 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; -import { chain, pluck, reduce } from 'underscore'; +import { chain, pluck } from 'underscore'; import * as moment from 'moment'; import { IOrganizationContact, IReportDayGroupByClient, - ITimeLog, - ITimeSlot + ITimeLog } from '@gauzy/contracts'; import { GetTimeLogGroupByClientCommand } from '../get-time-log-group-by-client.command'; -import { ArraySum } from '@gauzy/common'; +import { calculateAverage, calculateAverageActivity } from './../../time-log.utils'; @CommandHandler(GetTimeLogGroupByClientCommand) -export class GetTimeLogGroupByClientHandler - implements ICommandHandler -{ - constructor() { } +export class GetTimeLogGroupByClientHandler implements ICommandHandler { + /** + * Executes the command to generate a time log report grouped by client. + * @param command The command containing time logs and other parameters. + * @returns A Promise that resolves to the generated report grouped by client. + */ public async execute( command: GetTimeLogGroupByClientCommand ): Promise { const { timeLogs } = command; + // Group timeLogs by organizationContactId const dailyLogs: any = chain(timeLogs) - .groupBy((log: ITimeLog) => - log.organizationContact ? log.organizationContactId : null - ) - .map((byClientLogs: ITimeLog[]) => { - /** - * calculate average duration for specific client. - */ - const avgDuration = reduce( - pluck(byClientLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity for specific client. - */ - const slots: ITimeSlot[] = chain(byClientLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - const avgActivity = - (reduce(pluck(slots, 'overall'), ArraySum, 0) * 100) / - reduce(pluck(slots, 'duration'), ArraySum, 0) || 0; - - const log = byClientLogs.length > 0 ? byClientLogs[0] : null; - let client: IOrganizationContact = null; - if (log && log.organizationContact) { - client = log.organizationContact; - } else if ( - log && - log.project && - log.project.organizationContact - ) { - client = log.project.organizationContact; - } - - const byClient = chain(byClientLogs) - .groupBy((log) => log.projectId) - .map((byProjectLogs: ITimeLog[]) => { - const project = - byProjectLogs.length > 0 - ? byProjectLogs[0].project - : null; - - const byDate = chain(byProjectLogs) - .groupBy((log) => - moment(log.startedAt).format('YYYY-MM-DD') - ) - .map((byDateLogs: ITimeLog[], date) => { - const byEmployee = chain(byDateLogs) - .groupBy('employeeId') - .map((byEmployeeLogs: ITimeLog[]) => { - /** - * calculate average duration of the employee for specific date range. - */ - const sum = reduce( - pluck(byEmployeeLogs, 'duration'), - ArraySum, - 0 - ); - - /** - * calculate average activity of the employee for specific date range. - */ - const slots: ITimeSlot[] = chain( - byEmployeeLogs - ) - .pluck('timeSlots') - .flatten(true) - .value(); - - /** - * Calculate Average activity of the employee - */ - const avgActivity = - (reduce( - pluck(slots, 'overall'), - ArraySum, - 0 - ) * - 100) / - reduce( - pluck(slots, 'duration'), - ArraySum, - 0 - ) || 0; - - const employee = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].employee - : null; - - const task = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].task - : null; - - const description = byEmployeeLogs.length > 0 ? byEmployeeLogs[0].description : null; - - return { - description, - task, - employee, - sum, - activity: parseFloat( - parseFloat( - avgActivity + '' - ).toFixed(2) - ) - }; - }) - .value(); - - return { - date, - projectLogs: byEmployee - }; - }) + .groupBy((log: ITimeLog) => log.organizationContactId) + .map((logs: ITimeLog[]) => { + // Calculate average duration for specific client. + const avgDuration = calculateAverage(pluck(logs, 'duration')); + + // Calculate average activity for specific client. + const avgActivity = calculateAverageActivity(chain(logs).pluck('timeSlots').flatten(true).value()); + + // Retrieve the first log for further details + const log = logs.length > 0 ? logs[0] : null; + + // Extract client information using optional chaining + const client: IOrganizationContact | null = log?.organizationContact ?? (log?.project?.organizationContact ?? null); + + // Group logs by projectId + const byClient = chain(logs).groupBy((log: ITimeLog) => log.projectId) + .map((projectLogs: ITimeLog[]) => { + // Retrieve the first log for further details + const project = projectLogs.length > 0 ? projectLogs[0].project : null; + + // Group projectLogs by date + const byDate = chain(projectLogs) + .groupBy((log) => moment(log.startedAt).format('YYYY-MM-DD')) + .map((dateLogs: ITimeLog[], date) => ({ + date, + projectLogs: this.getGroupByEmployee(dateLogs) // Group dateLogs by employeeId + })) .value(); - return { project, logs: byDate }; - }) - .value(); + return { + project, + logs: byDate + }; + }).value(); return { client, logs: byClient, sum: avgDuration || null, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) }; - }) - .value(); + }).value(); return dailyLogs; } + + /** + * Groups time logs by employee and calculates average duration and activity for each employee. + * @param logs An array of time logs. + * @returns An array containing logs grouped by employee with calculated averages. + */ + getGroupByEmployee(logs: ITimeLog[]) { + const byEmployee = chain(logs).groupBy('employeeId').map((timeLogs: ITimeLog[]) => { + // Calculate average duration for specific employee. + const sum = calculateAverage(pluck(timeLogs, 'duration')); + + // Calculate average activity for specific employee. + const avgActivity = calculateAverageActivity(chain(timeLogs).pluck('timeSlots').flatten(true).value()); + + // Retrieve employee details + const employee = timeLogs.length > 0 ? timeLogs[0].employee : null; + const task = timeLogs.length > 0 ? timeLogs[0].task : null; + const description = timeLogs.length > 0 ? timeLogs[0].description : null; + + return { + description, + task, + employee, + sum, + activity: parseFloat(avgActivity.toFixed(2)) + }; + }).value(); + + return byEmployee; + } } diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-date.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-date.handler.ts index bace8a38e77..8cb7204bd47 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-date.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-date.handler.ts @@ -1,139 +1,86 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; -import { chain, pluck, reduce } from 'underscore'; +import { chain, pluck } from 'underscore'; import * as moment from 'moment'; -import { ArraySum } from '@gauzy/common'; -import { IReportDayGroupByDate, ITimeLog, ITimeSlot } from '@gauzy/contracts'; +import { IReportDayGroupByDate, ITimeLog } from '@gauzy/contracts'; import { GetTimeLogGroupByDateCommand } from '../get-time-log-group-by-date.command'; +import { calculateAverage, calculateAverageActivity } from './../../time-log.utils'; @CommandHandler(GetTimeLogGroupByDateCommand) -export class GetTimeLogGroupByDateHandler - implements ICommandHandler -{ - constructor() { } +export class GetTimeLogGroupByDateHandler implements ICommandHandler { + /** + * Executes the command to generate a time log report grouped by date. + * @param command The command containing time logs and other parameters. + * @returns A Promise that resolves to the generated report grouped by date. + */ public async execute( command: GetTimeLogGroupByDateCommand ): Promise { const { timeLogs } = command; const dailyLogs: any = chain(timeLogs) - .groupBy((log) => moment(log.startedAt).format('YYYY-MM-DD')) + .groupBy((log: ITimeLog) => moment(log.startedAt).format('YYYY-MM-DD')) .map((byDateLogs: ITimeLog[], date: string) => { - /** - * calculate average duration for specific date range. - */ - const avgDuration = reduce( - pluck(byDateLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity for specific date range. - */ - const slots: ITimeSlot[] = chain(byDateLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - const avgActivity = - (reduce(pluck(slots, 'overall'), ArraySum, 0) * 100) / - reduce(pluck(slots, 'duration'), ArraySum, 0) || 0; + // Calculate average duration for specific date range. + const avgDuration = calculateAverage(pluck(byDateLogs, 'duration')); + + // Calculate average activity for specific date range. + const avgActivity = calculateAverageActivity(chain(byDateLogs).pluck('timeSlots').flatten(true).value()); const byProject = chain(byDateLogs) .groupBy('projectId') .map((byProjectLogs: ITimeLog[]) => { - const project = - byProjectLogs.length > 0 - ? byProjectLogs[0].project - : null; - - const client = - byProjectLogs.length > 0 - ? byProjectLogs[0].organizationContact - : project - ? project.organizationContact - : null; - - const byEmployee = chain(byProjectLogs) - .groupBy('employeeId') - .map((byEmployeeLogs: ITimeLog[]) => { - /** - * calculate average duration of the employee for specific date range. - */ - const sum = reduce( - pluck(byEmployeeLogs, 'duration'), - ArraySum, - 0 - ); - - /** - * calculate average activity of the employee for specific date range. - */ - const slots: ITimeSlot[] = chain(byEmployeeLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - - /** - * Calculate Average activity of the employee - */ - const avgActivity = - (reduce( - pluck(slots, 'overall'), - ArraySum, - 0 - ) * - 100) / - reduce( - pluck(slots, 'duration'), - ArraySum, - 0 - ) || 0; - - const employee = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].employee - : null; - - const task = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].task - : null; + // Extract project information + const project = byProjectLogs.length > 0 ? byProjectLogs[0].project : null; - const description = byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].description - : null; - - return { - description, - employee, - sum: sum, - task, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) - }; - }) - .value(); + // Extract client information using optional chaining + const client = byProjectLogs.length > 0 ? byProjectLogs[0].organizationContact : project ? project.organizationContact : null; return { project, client, - employeeLogs: byEmployee + employeeLogs: this.getGroupByEmployee(byProjectLogs) }; - }) - .value(); + }).value(); return { date, logs: byProject, sum: avgDuration || null, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) }; - }) - .value(); + }).value(); return dailyLogs; } + + /** + * Groups time logs by employee and calculates average duration and activity for each employee. + * @param logs An array of time logs. + * @returns An array containing logs grouped by employee with calculated averages. + */ + getGroupByEmployee(logs: ITimeLog[]) { + const byEmployee = chain(logs).groupBy('employeeId').map((timeLogs: ITimeLog[]) => { + // Calculate average duration of the employee for specific date range. + const sum = calculateAverage(pluck(timeLogs, 'duration')); + + // Calculate Average activity of the employee + const avgActivity = calculateAverageActivity(chain(timeLogs).pluck('timeSlots').flatten(true).value()); + + // Retrieve employee details + const employee = timeLogs.length > 0 ? timeLogs[0].employee : null; + const task = timeLogs.length > 0 ? timeLogs[0].task : null; + const description = timeLogs.length > 0 ? timeLogs[0].description : null; + + return { + description, + employee, + sum, + task, + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) + }; + }).value(); + + return byEmployee; + } } diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-employee.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-employee.handler.ts index 4ec08d2d11d..147e6d5732a 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-employee.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-employee.handler.ts @@ -1,141 +1,86 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; -import { chain, pluck, reduce } from 'underscore'; +import { chain, pluck } from 'underscore'; import * as moment from 'moment'; import { IReportDayGroupByEmployee, - ITimeLog, - ITimeSlot + ITimeLog } from '@gauzy/contracts'; -import { ArraySum } from '@gauzy/common'; import { GetTimeLogGroupByEmployeeCommand } from '../get-time-log-group-by-employee.command'; +import { calculateAverage, calculateAverageActivity } from './../../time-log.utils'; @CommandHandler(GetTimeLogGroupByEmployeeCommand) -export class GetTimeLogGroupByEmployeeHandler - implements ICommandHandler -{ - constructor() { } +export class GetTimeLogGroupByEmployeeHandler implements ICommandHandler { + /** + * Executes the command to generate a time log report grouped by employee. + * @param command The command containing time logs and other parameters. + * @returns A Promise that resolves to the generated report grouped by employee. + */ public async execute( command: GetTimeLogGroupByEmployeeCommand ): Promise { const { timeLogs } = command; const dailyLogs: any = chain(timeLogs) - .groupBy((log) => log.employeeId) + .groupBy((log: ITimeLog) => log.employeeId) .map((byEmployeeLogs: ITimeLog[]) => { - /** - * calculate average duration for specific employee. - */ - const avgDuration = reduce( - pluck(byEmployeeLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity for specific date range. - */ - const slots: ITimeSlot[] = chain(byEmployeeLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - const avgActivity = - (reduce(pluck(slots, 'overall'), ArraySum, 0) * 100) / - reduce(pluck(slots, 'duration'), ArraySum, 0) || 0; - - const employee = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].employee - : null; - - const byDate = chain(byEmployeeLogs) - .groupBy((log) => - moment(log.startedAt).format('YYYY-MM-DD') - ) - .map((byDateLogs: ITimeLog[], date) => { - const byProject = chain(byDateLogs) - .groupBy('projectId') - .map((byProjectLogs: ITimeLog[]) => { - /** - * calculate average duration of the employee - */ - const sum = reduce( - pluck(byProjectLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity of the employee - */ - const slots: ITimeSlot[] = chain(byProjectLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - /** - * Calculate average activity of the employee - */ - const avgActivity = - (reduce( - pluck(slots, 'overall'), - ArraySum, - 0 - ) * - 100) / - reduce( - pluck(slots, 'duration'), - ArraySum, - 0 - ) || 0; - - const project = - byProjectLogs.length > 0 - ? byProjectLogs[0].project - : null; - - const task = - byProjectLogs.length > 0 - ? byProjectLogs[0].task - : null; - - const client = - byProjectLogs.length > 0 - ? byProjectLogs[0].organizationContact - : project - ? project.organizationContact - : null; + // Calculate average duration for specific date range. + const avgDuration = calculateAverage(pluck(byEmployeeLogs, 'duration')); - const description = byProjectLogs.length > 0 ? byProjectLogs[0].description : null; + // Calculate average activity for specific date range. + const avgActivity = calculateAverageActivity(chain(byEmployeeLogs).pluck('timeSlots').flatten(true).value()); - return { - description, - task, - project, - client, - sum, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) - }; - }) - .value(); + // Extract employee information + const employee = byEmployeeLogs.length > 0 ? byEmployeeLogs[0].employee : null; - return { - date, - projectLogs: byProject - }; - }) + const byDate = chain(byEmployeeLogs) + .groupBy((log: ITimeLog) => moment(log.startedAt).format('YYYY-MM-DD')) + .map((byDateLogs: ITimeLog[], date) => ({ + date, + projectLogs: this.getGroupByProject(byDateLogs) + })) .value(); return { employee, logs: byDate, sum: avgDuration || null, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) }; - }) - .value(); + }).value(); return dailyLogs; } + + /** + * Groups time logs by employee and calculates average duration and activity for each project. + * @param logs An array of time logs. + * @returns An array containing logs grouped by employee with calculated averages. + */ + getGroupByProject(logs: ITimeLog[]) { + const byProject = chain(logs).groupBy('projectId').map((timeLogs: ITimeLog[]) => { + // Calculate average duration of the employee for specific project. + const sum = calculateAverage(pluck(timeLogs, 'duration')); + + // Calculate Average activity of the employee + const avgActivity = calculateAverageActivity(chain(timeLogs).pluck('timeSlots').flatten(true).value()); + + // Retrieve employee details + const project = timeLogs.length > 0 ? timeLogs[0].project : null; + const task = timeLogs.length > 0 ? timeLogs[0].task : null; + const client = timeLogs.length > 0 ? timeLogs[0].organizationContact : project ? project.organizationContact : null; + const description = timeLogs.length > 0 ? timeLogs[0].description : null; + + return { + description, + task, + project, + client, + sum, + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) + }; + }).value(); + + return byProject; + } } diff --git a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-project.handler.ts b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-project.handler.ts index 5d3e47f74be..8a16f01b16c 100644 --- a/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-project.handler.ts +++ b/packages/core/src/time-tracking/time-log/commands/handlers/get-time-log-group-by-project.handler.ts @@ -1,141 +1,92 @@ import { ICommandHandler, CommandHandler } from '@nestjs/cqrs'; -import { chain, pluck, reduce } from 'underscore'; +import { chain, pluck } from 'underscore'; import * as moment from 'moment'; import { IReportDayGroupByProject, - ITimeLog, - ITimeSlot + ITimeLog } from '@gauzy/contracts'; import { GetTimeLogGroupByProjectCommand } from '../get-time-log-group-by-project.command'; -import { ArraySum } from '@gauzy/common'; +import { calculateAverage, calculateAverageActivity } from './../../time-log.utils'; @CommandHandler(GetTimeLogGroupByProjectCommand) -export class GetTimeLogGroupByProjectHandler - implements ICommandHandler -{ - constructor() { } +export class GetTimeLogGroupByProjectHandler implements ICommandHandler { + /** + * Executes the command to generate a time log report grouped by project. + * @param command The command containing time logs and other parameters. + * @returns A Promise that resolves to the generated report grouped by project. + */ public async execute( command: GetTimeLogGroupByProjectCommand ): Promise { const { timeLogs } = command; + // Group timeLogs by projectId const dailyLogs: any = chain(timeLogs) - .groupBy((log) => log.projectId) + .groupBy((log: ITimeLog) => log.projectId) .map((byProjectLogs: ITimeLog[]) => { - /** - * calculate average duration for specific project. - */ - const avgDuration = reduce( - pluck(byProjectLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity for specific project. - */ - const slots: ITimeSlot[] = chain(byProjectLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - const avgActivity = - (reduce(pluck(slots, 'overall'), ArraySum, 0) * 100) / - reduce(pluck(slots, 'duration'), ArraySum, 0) || 0; - - const project = - byProjectLogs.length > 0 ? byProjectLogs[0].project : null; - - const client = - byProjectLogs.length > 0 - ? byProjectLogs[0].organizationContact - : project - ? project.organizationContact - : null; - - const byDate = chain(byProjectLogs) - .groupBy((log) => - moment(log.startedAt).format('YYYY-MM-DD') - ) - .map((byDateLogs: ITimeLog[], date) => { - const byEmployee = chain(byDateLogs) - .groupBy('employeeId') - .map((byEmployeeLogs: ITimeLog[]) => { - /** - * calculate average duration of the employee - */ - const sum = reduce( - pluck(byEmployeeLogs, 'duration'), - ArraySum, - 0 - ); - /** - * calculate average activity of the employee - */ - const slots: ITimeSlot[] = chain(byEmployeeLogs) - .pluck('timeSlots') - .flatten(true) - .value(); - /** - * Calculate average activity of the employee - */ - const avgActivity = - (reduce( - pluck(slots, 'overall'), - ArraySum, - 0 - ) * - 100) / - reduce( - pluck(slots, 'duration'), - ArraySum, - 0 - ) || 0; - - const task = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].task - : null; + // Calculate average duration for specific project. + const avgDuration = calculateAverage(pluck(byProjectLogs, 'duration')); - const employee = - byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].employee - : null; + // Calculate average activity for specific project. + const avgActivity = calculateAverageActivity(chain(byProjectLogs).pluck('timeSlots').flatten(true).value()); - const description = byEmployeeLogs.length > 0 - ? byEmployeeLogs[0].description - : null; + // Extract project information + const project = byProjectLogs.length > 0 ? byProjectLogs[0].project : null; + // Extract client information using optional chaining + const client = byProjectLogs.length > 0 ? byProjectLogs[0].organizationContact : project ? project.organizationContact : null; - return { - description, - task, - employee, - sum, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) - }; - }) - .value(); - - return { - date, - employeeLogs: byEmployee - }; - }) + // Group projectLogs by date + const byDate = chain(byProjectLogs) + .groupBy((log: ITimeLog) => moment(log.startedAt).format('YYYY-MM-DD')) + .map((byDateLogs: ITimeLog[], date) => ({ + date, + employeeLogs: this.getGroupByEmployee(byDateLogs) + })) .value(); + return { project, client, logs: byDate, sum: avgDuration || null, - activity: parseFloat( - parseFloat(avgActivity + '').toFixed(2) - ) + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) }; }) .value(); return dailyLogs; } + + /** + * Groups time logs by employee and calculates average duration and activity for each employee. + * @param logs An array of time logs. + * @returns An array containing logs grouped by employee with calculated averages. + */ + getGroupByEmployee(logs: ITimeLog[]) { + const byEmployee = chain(logs).groupBy('employeeId').map((timeLogs: ITimeLog[]) => { + + // Calculate average duration of the employee for specific employee. + const sum = calculateAverage(pluck(timeLogs, 'duration')); + + // Calculate Average activity of the employee + const avgActivity = calculateAverageActivity(chain(timeLogs).pluck('timeSlots').flatten(true).value()); + + // Retrieve employee details + const task = timeLogs.length > 0 ? timeLogs[0].task : null; + const employee = timeLogs.length > 0 ? timeLogs[0].employee : null; + const description = timeLogs.length > 0 ? timeLogs[0].description : null; + + return { + description, + task, + employee, + sum, + activity: parseFloat(parseFloat(avgActivity + '').toFixed(2)) + }; + }).value(); + + return byEmployee; + } } diff --git a/packages/core/src/time-tracking/time-log/time-log.service.ts b/packages/core/src/time-tracking/time-log/time-log.service.ts index d50d450bbf9..b3a35d70842 100644 --- a/packages/core/src/time-tracking/time-log/time-log.service.ts +++ b/packages/core/src/time-tracking/time-log/time-log.service.ts @@ -358,6 +358,9 @@ export class TimeLogService extends TenantAwareCrudService { startedAt: true, stoppedAt: true, description: true, + projectId: true, + taskId: true, + organizationContactId: true, project: { id: true, name: true, @@ -978,120 +981,106 @@ export class TimeLogService extends TenantAwareCrudService { ); } - getFilterTimeLogQuery( - query: SelectQueryBuilder, - request: IGetTimeLogInput - ) { + /** + * Modifies the provided query to filter TimeLogs based on the given criteria. + * @param query - The query to be modified. + * @param request - The criteria for filtering TimeLogs. + * @returns The modified query. + */ + getFilterTimeLogQuery(query: SelectQueryBuilder, request: IGetTimeLogInput) { const { organizationId, projectIds = [] } = request; const tenantId = RequestContext.currentTenantId(); + const user = RequestContext.currentUser(); + + // Check if the current user has the permission to change the selected employee + const hasChangeSelectedEmployeePermission: boolean = RequestContext.hasPermission( + PermissionsEnum.CHANGE_SELECTED_EMPLOYEE + ); + + // Set employeeIds based on permissions and request + const employeeIds: string[] = hasChangeSelectedEmployeePermission && isNotEmpty(request.employeeIds) ? request.employeeIds : [user.employeeId]; - let employeeIds: string[]; - if ( - RequestContext.hasPermission( - PermissionsEnum.CHANGE_SELECTED_EMPLOYEE - ) - ) { - if (isNotEmpty(request.employeeIds)) { - employeeIds = request.employeeIds; - } - } else { - const user = RequestContext.currentUser(); - employeeIds = [user.employeeId]; - } if (isNotEmpty(request.timesheetId)) { - const { timesheetId } = request; query.andWhere(`"${query.alias}"."timesheetId" = :timesheetId`, { - timesheetId + timesheetId: request.timesheetId }); } + + // if (isNotEmpty(request.startDate) && isNotEmpty(request.endDate)) { const { start: startDate, end: endDate } = getDateRangeFormat( moment.utc(request.startDate), moment.utc(request.endDate) ); - query.andWhere( - `"${query.alias}"."startedAt" >= :startDate AND "${query.alias}"."startedAt" < :endDate`, - { - startDate, - endDate - } - ); + query.andWhere(`"${query.alias}"."startedAt" >= :startDate AND "${query.alias}"."startedAt" < :endDate`, { + startDate, + endDate + }); } + + // if (isNotEmpty(employeeIds)) { - query.andWhere( - `"${query.alias}"."employeeId" IN (:...employeeIds)`, - { - employeeIds - } - ); + query.andWhere(`"${query.alias}"."employeeId" IN (:...employeeIds)`, { + employeeIds + }); } + + // if (isNotEmpty(projectIds)) { query.andWhere(`"${query.alias}"."projectId" IN (:...projectIds)`, { projectIds }); } + if (isNotEmpty(request.activityLevel)) { /** * Activity Level should be 0-100% - * So, we have convert it into 10 minutes timeslot by multiply by 6 + * Convert it into a 10-minute time slot by multiplying by 6 */ const { activityLevel } = request; - const start = activityLevel.start * 6; - const end = activityLevel.end * 6; query.andWhere(`"time_slot"."overall" BETWEEN :start AND :end`, { - start, - end + start: activityLevel.start * 6, + end: activityLevel.end * 6 }); } + + // if (isNotEmpty(request.source)) { const { source } = request; - if (source instanceof Array) { - query.andWhere(`"${query.alias}"."source" IN (:...source)`, { - source - }); - } else { - query.andWhere(`"${query.alias}"."source" = :source`, { - source - }); - } + const condition = source instanceof Array ? `"${query.alias}"."source" IN (:...source)` : `"${query.alias}"."source" = :source`; + + query.andWhere(condition, { source }); } + if (isNotEmpty(request.logType)) { const { logType } = request; - if (logType instanceof Array) { - query.andWhere(`"${query.alias}"."logType" IN (:...logType)`, { - logType - }); - } else { - query.andWhere(`"${query.alias}"."logType" = :logType`, { - logType - }); - } + const condition = logType instanceof Array ? `"${query.alias}"."logType" IN (:...logType)` : `"${query.alias}"."logType" = :logType`; + + query.andWhere(condition, { logType }); } + if (isNotEmpty(request.teamId)) { const { teamId } = request; - /** - * If used organization team filter - */ - query.andWhere( - `"organization_teams"."organizationTeamId" = :teamId`, - { - teamId - } - ); + + // Filter by organization team ID if used in the request + query.andWhere(`"organization_teams"."organizationTeamId" = :teamId`, { + teamId + }); } + + // query.andWhere( new Brackets((qb: WhereExpressionBuilder) => { qb.andWhere(`"${query.alias}"."tenantId" = :tenantId`, { tenantId }); - qb.andWhere( - `"${query.alias}"."organizationId" = :organizationId`, - { organizationId } - ); - qb.andWhere(`"${query.alias}"."deletedAt" IS NULL`); + qb.andWhere(`"${query.alias}"."organizationId" = :organizationId`, { + organizationId + }); }) ); + return query; } diff --git a/packages/core/src/time-tracking/time-log/time-log.utils.ts b/packages/core/src/time-tracking/time-log/time-log.utils.ts new file mode 100644 index 00000000000..e44301ee15c --- /dev/null +++ b/packages/core/src/time-tracking/time-log/time-log.utils.ts @@ -0,0 +1,23 @@ +import { pluck, reduce } from "underscore"; +import { ArraySum } from "@gauzy/common"; +import { ITimeSlot } from "@gauzy/contracts"; + +/** + * Calculates the average of an array of numbers. + * @param values An array of numbers. + * @returns The calculated average. + */ +export const calculateAverage = (values: number[]): number => { + return reduce(values, ArraySum, 0); +}; + +/** + * Calculates the average activity based on overall and duration values of an array of time slots. + * @param slots An array of time slots. + * @returns The calculated average activity. + */ +export const calculateAverageActivity = (slots: ITimeSlot[]): number => { + const overallSum = calculateAverage(pluck(slots, 'overall')); + const durationSum = calculateAverage(pluck(slots, 'duration')); + return (overallSum * 100) / durationSum || 0; +}; diff --git a/packages/desktop-ui-lib/src/lib/services/index.ts b/packages/desktop-ui-lib/src/lib/services/index.ts index 2976387a32e..64d94285696 100644 --- a/packages/desktop-ui-lib/src/lib/services/index.ts +++ b/packages/desktop-ui-lib/src/lib/services/index.ts @@ -26,3 +26,4 @@ export * from './teams-cache.service'; export * from './status-icon-service'; export * from './task-priority-cache.service'; export * from './task-size-cache.service'; +export * from './tag.service'; diff --git a/packages/desktop-ui-lib/src/lib/services/store.service.ts b/packages/desktop-ui-lib/src/lib/services/store.service.ts index d827565974c..31333bf85ad 100644 --- a/packages/desktop-ui-lib/src/lib/services/store.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/store.service.ts @@ -402,6 +402,20 @@ export class Store { ); } + hasPermissions(...permissions: PermissionsEnum[]) { + const { userRolePermissions } = this.appQuery.getValue(); + + // Check if userRolePermissions is defined and not an empty array + if (!userRolePermissions || userRolePermissions.length === 0) { + return false; + } + + // Use some for a more concise check + return userRolePermissions.some( + (p) => p.enabled && permissions.includes(p.permission as PermissionsEnum) // Check if the permission is in the provided list + ); + } + getDateFromOrganizationSettings() { const dateObj = this.selectedDate; switch ( diff --git a/packages/desktop-ui-lib/src/lib/services/tag-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/tag-cache.service.ts index 9fca40dd56d..d8211202887 100644 --- a/packages/desktop-ui-lib/src/lib/services/tag-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/tag-cache.service.ts @@ -7,15 +7,9 @@ import { Store } from '../services'; @Injectable({ providedIn: 'root', }) -export class TagCacheService extends AbstractCacheService<{ - items: ITag[]; - total: number; -}> { +export class TagCacheService extends AbstractCacheService { constructor( - protected _storageService: StorageService<{ - items: ITag[]; - total: number; - }>, + protected _storageService: StorageService, protected _store: Store ) { super(_storageService, _store); diff --git a/packages/desktop-ui-lib/src/lib/services/tag.service.ts b/packages/desktop-ui-lib/src/lib/services/tag.service.ts new file mode 100644 index 00000000000..fda8f902567 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/services/tag.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { concatMap, firstValueFrom, map, shareReplay } from 'rxjs'; +import { API_PREFIX, Store, TagCacheService } from '@gauzy/desktop-ui-lib'; +import { IPagination, ITag } from '@gauzy/contracts'; +import { toParams } from '@gauzy/common-angular'; + +@Injectable({ + providedIn: 'root' +}) +export class TagService { + constructor( + private readonly _http: HttpClient, + private readonly _tagCacheService: TagCacheService, + private readonly _store: Store + ) {} + + public create(tag: Partial): Promise { + return firstValueFrom( + this._http.post(`${API_PREFIX}/tags`, tag).pipe( + concatMap(() => { + this._tagCacheService.clear(); + return this.getTags(); + }) + ) + ); + } + + public getTags(): Promise { + const params = { + organizationId: this._store.organizationId, + tenantId: this._store.tenantId + }; + let tags$ = this._tagCacheService.getValue(params); + if (!tags$) { + tags$ = this._http + .get(`${API_PREFIX}/tags/level`, { + params: toParams(params) + }) + .pipe( + map((response: IPagination) => response.items), + shareReplay(1) + ); + this._tagCacheService.setValue(tags$, params); + } + return firstValueFrom(tags$); + } +} diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html index c04e0622525..01467686c54 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.html @@ -217,10 +217,12 @@
diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts index 5aa0069daa1..d5dbff7ae34 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts @@ -7,20 +7,22 @@ import { IOrganizationProject, IOrganizationTeam, ITag, + ITagCreateInput, ITaskPriority, ITaskSize, ITaskStatus, IUserOrganization, - TaskStatusEnum, + PermissionsEnum, + TaskStatusEnum } from '@gauzy/contracts'; import { NbDialogRef, NbToastrService } from '@nebular/theme'; import * as moment from 'moment'; import { TranslateService } from '@ngx-translate/core'; import { CkEditorConfig, ColorAdapter } from '../utils'; -import { Store } from '../services'; +import { Store, TagService } from '../services'; import { GAUZY_ENV } from '../constants'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { tap, from } from 'rxjs'; +import { tap, from, Observable, map } from 'rxjs'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -44,6 +46,7 @@ export class TasksComponent implements OnInit { }> = new EventEmitter(); public isSaving: boolean; public editorConfig = CkEditorConfig.minimal(); + public hasAddTagPermission$: Observable; form: FormGroup; projects: IOrganizationProject[] = []; @@ -80,6 +83,7 @@ export class TasksComponent implements OnInit { name: this._formatStatus(TaskStatusEnum.COMPLETED), }, ]; + public isLoading = false; constructor( private timeTrackerService: TimeTrackerService, @@ -88,7 +92,8 @@ export class TasksComponent implements OnInit { @Inject(GAUZY_ENV) private readonly _environment: any, private store: Store, - private _dialogRef: NbDialogRef + private _dialogRef: NbDialogRef, + private _tagService: TagService ) { this.isSaving = false; } @@ -108,10 +113,9 @@ export class TasksComponent implements OnInit { } } - private async _tags(user: IUserOrganization): Promise { + private async _tags(): Promise { try { - const tagsRes = await this.timeTrackerService.getTags(user); - this.tags = tagsRes.items; + this.tags = await this._tagService.getTags(); } catch (error) { console.error('[error]', 'while get tags::' + error.message); } @@ -174,7 +178,7 @@ export class TasksComponent implements OnInit { from( Promise.allSettled([ this._projects(this.userData), - this._tags(this.userData), + this._tags(), this._employees(this.userData), this._clients(this.userData), this._teams(), @@ -220,6 +224,9 @@ export class TasksComponent implements OnInit { organizationContactId: new FormControl(this.selected.contactId), organizationTeamId: new FormControl(this.selected.teamId), }); + this.hasAddTagPermission$ = this.store.userRolePermissions$.pipe( + map(() => this.store.hasPermissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TAGS_ADD)) + ); } public close(res?: any): void { @@ -317,4 +324,33 @@ export class TasksComponent implements OnInit { public backgroundContrast(bgColor: string) { return ColorAdapter.contrast(bgColor); } + + /** + * Create new tag + * + * @param name + * @returns + */ + public createTag = async (name: ITagCreateInput['name']): Promise => { + if (!name) { + return; + } + this.isLoading = true; + + const { organizationId, tenantId } = this.store; + + try { + this.tags = await this._tagService.create({ + name, + color: ColorAdapter.randomColor(), + description: '', + tenantId, + organizationId + }); + } catch (error) { + console.log('Error while creating tags', error); + } finally { + this.isLoading = false; + } + }; } diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts index 9049428dc8e..8a0f72ae662 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.module.ts @@ -18,7 +18,7 @@ import { NbToastrService, NbAccordionModule, NbDatepickerModule, - NbBadgeModule, + NbBadgeModule } from '@nebular/theme'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NgSelectModule } from '@ng-select/ng-select'; @@ -27,6 +27,7 @@ import { DesktopDirectiveModule } from '../directives/desktop-directive.module'; import { TranslateModule } from '@ngx-translate/core'; import { CKEditorModule } from 'ckeditor4-angular'; import { TaskRenderModule } from '../time-tracker/task-render'; +import { TagService } from '../services'; @NgModule({ declarations: [TasksComponent], @@ -54,9 +55,9 @@ import { TaskRenderModule } from '../time-tracker/task-render'; DesktopDirectiveModule, TranslateModule, CKEditorModule, - TaskRenderModule, + TaskRenderModule ], - providers: [NbToastrService, TimeTrackerService], - exports: [TasksComponent], + providers: [NbToastrService, TimeTrackerService, TagService], + exports: [TasksComponent] }) export class TasksModule {} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html index 288df879ca7..5b8e29c107a 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-render/task-render-cell/task-render-cell.component.html @@ -1,11 +1,5 @@
-
{{ number }}
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index e356dc5bb9e..91dcceb04ea 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -423,7 +423,7 @@
{ if (tasks.length > 0) { - const idx = tasks.findIndex( - (row) => row.id === this.taskSelect - ); - if (idx > -1) { - tasks[idx].isSelected = true; - } + tasks = tasks.map((row) => ({ ...row, isSelected: row.id === this.taskSelect })); } else { tasks = []; this.taskSelect = null; @@ -1872,14 +1867,14 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { public setTask(item: string): void { this.taskSelect = item; this.electronService.ipcRenderer.send('update_project_on', { - taskId: this.taskSelect, + taskId: this.taskSelect }); if (item) this.errors.task = false; } public descriptionChange(e): void { if (e) this.errors.note = false; - this.setTask(null); + this.clearSelectedTaskAndRefresh(); this._clearItem(); this.electronService.ipcRenderer.send('update_project_on', { note: this.note, @@ -2015,6 +2010,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { if (res.employee && res.employee.organization) { this.userData = res; if (res.role && res.role.rolePermissions) { + this._store.userRolePermissions = res.role.rolePermissions; this.userPermission = res.role.rolePermissions .map((permission) => permission.enabled ? permission.permission : null @@ -2142,26 +2138,31 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.electronService.ipcRenderer.send('expand', !this.isExpand); } - public rowSelect(value): void { - if (!value?.source?.data?.length) { - this.taskSelect = null; - return; + public handleRowSelection(selectionEvent): void { + if (this.isNoRowSelected(selectionEvent)) { + this.clearSelectedTaskAndRefresh(); + } else { + const selectedRow = selectionEvent.selected[0]; + this.handleSelectedTaskChange(selectedRow.id); } - this.taskSelect = value.data.id; - value.data.isSelected = true; - const selectedLast = value.source.data.findIndex( - (row) => row.isSelected && row.id !== value.data.id - ); - if (selectedLast > -1) { - value.source.data[selectedLast].isSelected = false; + } + + private isNoRowSelected(selectionEvent): boolean { + return !selectionEvent.selected.length; + } + + private clearSelectedTaskAndRefresh(): void { + this.setTask(null); + } + + private handleSelectedTaskChange(selectedTaskId): void { + if (this.isDifferentTask(selectedTaskId)) { + this.setTask(selectedTaskId); } - const idx = value.source.data.findIndex( - (row) => row.id === value.data.id - ); - value.source.data.splice(idx, 1); - value.source.data.unshift(value.data); - value.source.data[idx].isSelected = true; - this.setTask(value.data.id); + } + + private isDifferentTask(selectedTaskId): boolean { + return this.taskSelect !== selectedTaskId; } public onSearch(query: string = ''): void { diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts index 7b5283e4e2d..004610e8c3c 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.service.ts @@ -27,7 +27,6 @@ import { EmployeeCacheService, ProjectCacheService, Store, - TagCacheService, TaskCacheService, TaskPriorityCacheService, TaskSizeCacheService, @@ -56,7 +55,6 @@ export class TimeTrackerService { private readonly _projectCacheService: ProjectCacheService, private readonly _timeSlotCacheService: TimeSlotCacheService, private readonly _employeeCacheService: EmployeeCacheService, - private readonly _tagCacheService: TagCacheService, private readonly _userOrganizationService: UserOrganizationService, private readonly _timeLogService: TimeLogCacheService, private readonly _loggerService: LoggerService, @@ -185,28 +183,6 @@ export class TimeTrackerService { return firstValueFrom(employee$); } - async getTags(values) { - const params = values.organizationId - ? { - organizationId: values.organizationId, - tenantId: values.tenantId, - } - : {}; - let tags$ = this._tagCacheService.getValue(params); - if (!tags$) { - tags$ = this.http - .get(`${API_PREFIX}/tags/level`, { - params: toParams(params), - }) - .pipe( - map((response: any) => response), - shareReplay(1) - ); - this._tagCacheService.setValue(tags$, params); - } - return firstValueFrom(tags$); - } - async getProjects(values) { const params = { organizationId: values.organizationId, diff --git a/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts b/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts index 6abcfa9ffae..0fe7ec15dea 100644 --- a/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts +++ b/packages/desktop-ui-lib/src/lib/utils/color-adapter.ts @@ -7,7 +7,7 @@ export class ColorAdapter { r: parseInt(hex.slice(1, 3), 16), g: parseInt(hex.slice(3, 5), 16), b: parseInt(hex.slice(5, 7), 16), - a: 1, + a: 1 }); } @@ -26,9 +26,7 @@ export class ColorAdapter { color = color.valid ? color : new Color(this.hex2Rgb(bgColor)); const MIN_THRESHOLD = 128; const MAX_THRESHOLD = 186; - const contrast = color.rgb - ? color.rgb.r * 0.299 + color.rgb.g * 0.587 + color.rgb.b * 0.114 - : null; + const contrast = color.rgb ? color.rgb.r * 0.299 + color.rgb.g * 0.587 + color.rgb.b * 0.114 : null; if (contrast < MIN_THRESHOLD) { return '#ffffff'; } else if (contrast > MAX_THRESHOLD) { @@ -46,4 +44,14 @@ export class ColorAdapter { color = color.valid ? color : new Color(this.hex2Rgb(hexColor)); return color.hslString(); } + + public static randomColor(): string { + const color = new Color({ + r: Math.floor(Math.random() * 256), + g: Math.floor(Math.random() * 256), + b: Math.floor(Math.random() * 256), + a: 1 + }); + return color.hexString().toString(); + } }