Compare commits
4 Commits
feature/te
...
feature/gu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa00f6fe21 | ||
|
|
95aa2ef8ee | ||
|
|
e3443eedfb | ||
|
|
03bd3659e0 |
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(npm run type-check:*)",
|
||||
"Bash(npm run:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
237
.cursor/rules/antd-components.mdc
Normal file
237
.cursor/rules/antd-components.mdc
Normal file
@@ -0,0 +1,237 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# Ant Design Import Rules for Worklenz
|
||||
|
||||
## 🚨 CRITICAL: Always Use Centralized Imports
|
||||
|
||||
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
|
||||
|
||||
### ✅ Correct Import Pattern
|
||||
```typescript
|
||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
|
||||
// or
|
||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
|
||||
```
|
||||
|
||||
### ❌ Forbidden Import Patterns
|
||||
```typescript
|
||||
// NEVER do this:
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
```
|
||||
|
||||
## Why This Rule Exists
|
||||
|
||||
### Benefits of Centralized Imports:
|
||||
- **Better Tree-Shaking**: Optimized bundle size through centralized management
|
||||
- **Consistent React Context**: Proper context sharing across components
|
||||
- **Type Safety**: Centralized TypeScript definitions
|
||||
- **Maintainability**: Single source of truth for all Ant Design imports
|
||||
- **Performance**: Reduced bundle size and improved loading times
|
||||
|
||||
## What's Available in `@antd-imports`
|
||||
|
||||
### Core Components
|
||||
- **Layout**: Layout, Row, Col, Flex, Divider, Space
|
||||
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
|
||||
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
|
||||
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
|
||||
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
|
||||
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
|
||||
|
||||
### Icons
|
||||
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
|
||||
|
||||
### Utilities
|
||||
- **appMessage**: Centralized message utility
|
||||
- **appNotification**: Centralized notification utility
|
||||
- **antdConfig**: Default Ant Design configuration
|
||||
- **taskManagementAntdConfig**: Task-specific configuration
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### When Creating New Components:
|
||||
1. **Always** import from `@/shared/antd-imports`
|
||||
2. Use `appMessage` and `appNotification` for user feedback
|
||||
3. Apply `antdConfig` for consistent styling
|
||||
4. Use `taskManagementAntdConfig` for task-related components
|
||||
|
||||
### When Refactoring Existing Code:
|
||||
1. Replace direct 'antd' imports with `@/shared/antd-imports`
|
||||
2. Replace direct '@ant-design/icons' imports with `@/shared/antd-imports`
|
||||
3. Update any custom message/notification calls to use the utilities
|
||||
|
||||
### File Location
|
||||
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
||||
|
||||
## Examples
|
||||
|
||||
### Component Creation
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
|
||||
|
||||
const MyComponent = () => {
|
||||
const handleClick = () => {
|
||||
appMessage.success('Operation completed!');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button icon={<EditOutlined />} onClick={handleClick}>
|
||||
Edit Item
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Form Implementation
|
||||
```typescript
|
||||
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
|
||||
|
||||
const MyForm = () => {
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Name" name="name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" name="type">
|
||||
<Select options={options} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Date" name="date">
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
This rule is **MANDATORY** and applies to:
|
||||
- All new component development
|
||||
- All code refactoring
|
||||
- All bug fixes
|
||||
- All feature implementations
|
||||
|
||||
**Violations will result in code review rejection.**
|
||||
|
||||
### File Path:
|
||||
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
||||
# Ant Design Import Rules for Worklenz
|
||||
|
||||
## 🚨 CRITICAL: Always Use Centralized Imports
|
||||
|
||||
**NEVER import Ant Design components directly from 'antd' or '@ant-design/icons'**
|
||||
|
||||
### ✅ Correct Import Pattern
|
||||
```typescript
|
||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@antd-imports';
|
||||
// or
|
||||
import { Button, Input, Select, EditOutlined, PlusOutlined } from '@/shared/antd-imports';
|
||||
```
|
||||
|
||||
### ❌ Forbidden Import Patterns
|
||||
```typescript
|
||||
// NEVER do this:
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import { EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
```
|
||||
|
||||
## Why This Rule Exists
|
||||
|
||||
### Benefits of Centralized Imports:
|
||||
- **Better Tree-Shaking**: Optimized bundle size through centralized management
|
||||
- **Consistent React Context**: Proper context sharing across components
|
||||
- **Type Safety**: Centralized TypeScript definitions
|
||||
- **Maintainability**: Single source of truth for all Ant Design imports
|
||||
- **Performance**: Reduced bundle size and improved loading times
|
||||
|
||||
## What's Available in `@antd-imports`
|
||||
|
||||
### Core Components
|
||||
- **Layout**: Layout, Row, Col, Flex, Divider, Space
|
||||
- **Navigation**: Menu, Tabs, Breadcrumb, Pagination
|
||||
- **Data Entry**: Input, Select, DatePicker, TimePicker, Form, Checkbox, InputNumber
|
||||
- **Data Display**: Table, List, Card, Tag, Avatar, Badge, Progress, Statistic
|
||||
- **Feedback**: Modal, Drawer, Alert, Message, Notification, Spin, Skeleton, Result
|
||||
- **Other**: Button, Typography, Tooltip, Popconfirm, Dropdown, ConfigProvider
|
||||
|
||||
### Icons
|
||||
Common icons including: EditOutlined, DeleteOutlined, PlusOutlined, MoreOutlined, CheckOutlined, CloseOutlined, CalendarOutlined, UserOutlined, TeamOutlined, and many more.
|
||||
|
||||
### Utilities
|
||||
- **appMessage**: Centralized message utility
|
||||
- **appNotification**: Centralized notification utility
|
||||
- **antdConfig**: Default Ant Design configuration
|
||||
- **taskManagementAntdConfig**: Task-specific configuration
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### When Creating New Components:
|
||||
1. **Always** import from `@antd-imports` or `@/shared/antd-imports`
|
||||
2. Use `appMessage` and `appNotification` for user feedback
|
||||
3. Apply `antdConfig` for consistent styling
|
||||
4. Use `taskManagementAntdConfig` for task-related components
|
||||
|
||||
### When Refactoring Existing Code:
|
||||
1. Replace direct 'antd' imports with `@antd-imports`
|
||||
2. Replace direct '@ant-design/icons' imports with `@antd-imports`
|
||||
3. Update any custom message/notification calls to use the utilities
|
||||
|
||||
### File Location
|
||||
The centralized import file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
||||
|
||||
## Examples
|
||||
|
||||
### Component Creation
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { Button, Input, Modal, EditOutlined, appMessage } from '@antd-imports';
|
||||
|
||||
const MyComponent = () => {
|
||||
const handleClick = () => {
|
||||
appMessage.success('Operation completed!');
|
||||
};
|
||||
|
||||
return (
|
||||
<Button icon={<EditOutlined />} onClick={handleClick}>
|
||||
Edit Item
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Form Implementation
|
||||
```typescript
|
||||
import { Form, Input, Select, Button, DatePicker } from '@antd-imports';
|
||||
|
||||
const MyForm = () => {
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="Name" name="name">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" name="type">
|
||||
<Select options={options} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Date" name="date">
|
||||
<DatePicker />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
This rule is **MANDATORY** and applies to:
|
||||
- All new component development
|
||||
- All code refactoring
|
||||
- All bug fixes
|
||||
- All feature implementations
|
||||
|
||||
**Violations will result in code review rejection.**
|
||||
|
||||
### File Path:
|
||||
The centralized file is located at: `worklenz-frontend/src/shared/antd-imports.ts`
|
||||
131
.cursor/rules/general-coding-guidelines.md
Normal file
131
.cursor/rules/general-coding-guidelines.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# General Coding Guidelines
|
||||
|
||||
## Rule Summary
|
||||
Follow these rules when you write code:
|
||||
|
||||
1. **Use Early Returns**
|
||||
- Prefer early returns and guard clauses to reduce nesting and improve readability, especially for error handling.
|
||||
|
||||
2. **Tailwind for Styling**
|
||||
- Always use Tailwind CSS utility classes for styling HTML elements.
|
||||
- Avoid writing custom CSS or using inline `style` tags.
|
||||
|
||||
3. **Class Tag Syntax**
|
||||
- Use `class:` directive (e.g., `class:active={isActive}`) instead of the ternary operator in class tags whenever possible.
|
||||
|
||||
4. **Descriptive Naming**
|
||||
- Use clear, descriptive names for variables, functions, and constants.
|
||||
- Use auxiliary verbs for booleans and state (e.g., `isLoaded`, `hasError`, `shouldRender`).
|
||||
- Event handler functions should be prefixed with `handle`, e.g., `handleClick` for `onClick`, `handleKeyDown` for `onKeyDown`.
|
||||
|
||||
5. **Naming Conventions**
|
||||
- **Directories:** Use lowercase with dashes (e.g., `components/auth-wizard`).
|
||||
- **Variables & Functions:** Use `camelCase` (e.g., `userList`, `fetchData`).
|
||||
- **Types & Interfaces:** Use `PascalCase` (e.g., `User`, `ButtonProps`).
|
||||
- **Exports:** Favor named exports for components.
|
||||
- **No Unused Variables:** Remove unused variables and imports.
|
||||
|
||||
6. **File Layout**
|
||||
- Order: exported component → subcomponents → hooks/helpers → static content.
|
||||
|
||||
7. **Props & Types**
|
||||
- Define props with TypeScript `interface` or `type`, not `prop-types`.
|
||||
- Example:
|
||||
```ts
|
||||
interface ButtonProps {
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function Button({ label, onClick }: ButtonProps) {
|
||||
return <button onClick={onClick}>{label}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
8. **Component Declaration**
|
||||
- Use the `function` keyword for components, not arrow functions.
|
||||
|
||||
9. **Hooks Usage**
|
||||
- Call hooks (e.g., `useState`, `useEffect`) only at the top level of components.
|
||||
- Extract reusable logic into custom hooks (e.g., `useAuth`, `useFormValidation`).
|
||||
|
||||
10. **Memoization & Performance**
|
||||
- Use `React.memo`, `useCallback`, and `useMemo` where appropriate.
|
||||
- Avoid inline functions in JSX—pull handlers out or wrap in `useCallback`.
|
||||
|
||||
11. **Composition**
|
||||
- Favor composition (render props, `children`) over inheritance.
|
||||
|
||||
12. **Code Splitting**
|
||||
- Use `React.lazy` + `Suspense` for code splitting.
|
||||
|
||||
13. **Refs**
|
||||
- Use refs only for direct DOM access.
|
||||
|
||||
14. **Forms**
|
||||
- Prefer controlled components for forms.
|
||||
|
||||
15. **Error Boundaries**
|
||||
- Implement an error boundary component for catching render errors.
|
||||
|
||||
16. **Effect Cleanup**
|
||||
- Clean up effects in `useEffect` to prevent memory leaks.
|
||||
|
||||
17. **Accessibility**
|
||||
- Apply appropriate ARIA attributes to interactive elements.
|
||||
- For example, an `<a>` tag should have `tabindex="0"`, `aria-label`, `onClick`, and `onKeyDown` attributes as appropriate.
|
||||
|
||||
## Examples
|
||||
|
||||
### ✅ Correct
|
||||
```tsx
|
||||
// File: components/user-profile.tsx
|
||||
|
||||
interface UserProfileProps {
|
||||
user: User;
|
||||
isLoaded: boolean;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export function UserProfile({ user, isLoaded, hasError }: UserProfileProps) {
|
||||
if (!isLoaded) return <div>Loading...</div>;
|
||||
if (hasError) return <div role="alert">Error loading user.</div>;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
// ...
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className="bg-blue-500 text-white"
|
||||
aria-label="View user profile"
|
||||
tabIndex={0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{user.name}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Incorrect
|
||||
```tsx
|
||||
// File: components/UserProfile.jsx
|
||||
function userprofile(props) {
|
||||
if (props.isLoaded) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button style={{ color: 'white' }} onClick={() => doSomething()}>
|
||||
View
|
||||
</button>
|
||||
);
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
- All new code must follow these guidelines.
|
||||
- Code reviews should reject code that does not comply with these rules.
|
||||
- Refactor existing code to follow these guidelines when making changes.
|
||||
38
.cursor/rules/localization.md
Normal file
38
.cursor/rules/localization.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Localization Rule: No Hard-Coded User-Facing Text
|
||||
|
||||
## Rule
|
||||
- All user-facing text **must** be added to the localization system at `@/locales`.
|
||||
- **Never** hard-code user-facing strings directly in components, pages, or business logic.
|
||||
- Use the appropriate i18n or localization utility to fetch and display all text.
|
||||
- **Always** provide a `defaultValue` when using the `t()` function for translations, e.g., `{t('emailPlaceholder', {defaultValue: 'Enter your email'})}`.
|
||||
|
||||
## Rationale
|
||||
- Ensures the application is fully translatable and accessible to all supported languages.
|
||||
- Prevents missed strings during translation updates.
|
||||
- Promotes consistency and maintainability.
|
||||
- Providing a `defaultValue` ensures a fallback is shown if the translation key is missing.
|
||||
|
||||
## Examples
|
||||
|
||||
### ✅ Correct
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <input placeholder={t('emailPlaceholder', { defaultValue: 'Enter your email' })} />;
|
||||
```
|
||||
|
||||
### ❌ Incorrect
|
||||
```tsx
|
||||
return <input placeholder={t('emailPlaceholder')} />;
|
||||
|
||||
// or
|
||||
return <input placeholder="Enter your email" />;
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
- All new user-facing text **must** be added to the appropriate file in `@/locales`.
|
||||
- Every use of `t()` **must** include a `defaultValue` for fallback.
|
||||
- Code reviews should reject any hard-coded user-facing strings or missing `defaultValue` in translations.
|
||||
- Refactor existing hard-coded text to use the localization system and add `defaultValue` when modifying related code.
|
||||
39
.cursor/rules/react-component-naming.md
Normal file
39
.cursor/rules/react-component-naming.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# React Component Naming Rule: PascalCase
|
||||
|
||||
## Rule
|
||||
- All React component names **must** use PascalCase.
|
||||
- This applies to:
|
||||
- Component file names (e.g., `MyComponent.tsx`, `UserProfile.jsx`)
|
||||
- Exported component identifiers (e.g., `export const MyComponent = ...` or `function UserProfile() { ... }`)
|
||||
|
||||
## Rationale
|
||||
- PascalCase is the community standard for React components.
|
||||
- Ensures consistency and readability across the codebase.
|
||||
- Prevents confusion between components and regular functions/variables.
|
||||
|
||||
## Examples
|
||||
|
||||
### ✅ Correct
|
||||
```tsx
|
||||
// File: UserProfile.tsx
|
||||
export function UserProfile() { ... }
|
||||
|
||||
// File: TaskList.tsx
|
||||
const TaskList = () => { ... }
|
||||
export default TaskList;
|
||||
```
|
||||
|
||||
### ❌ Incorrect
|
||||
```tsx
|
||||
// File: userprofile.tsx
|
||||
export function userprofile() { ... }
|
||||
|
||||
// File: task-list.jsx
|
||||
const task_list = () => { ... }
|
||||
export default task_list;
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
- All new React components **must** follow this rule.
|
||||
- Refactor existing components to PascalCase when modifying or moving them.
|
||||
- Code reviews should reject non-PascalCase component names.
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -36,8 +36,6 @@ lerna-debug.log*
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
.cursor/
|
||||
.claude/
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
|
||||
41
test_sort_fix.sql
Normal file
41
test_sort_fix.sql
Normal file
@@ -0,0 +1,41 @@
|
||||
-- Test script to verify the sort order constraint fix
|
||||
|
||||
-- Test the helper function
|
||||
SELECT get_sort_column_name('status'); -- Should return 'status_sort_order'
|
||||
SELECT get_sort_column_name('priority'); -- Should return 'priority_sort_order'
|
||||
SELECT get_sort_column_name('phase'); -- Should return 'phase_sort_order'
|
||||
SELECT get_sort_column_name('members'); -- Should return 'member_sort_order'
|
||||
SELECT get_sort_column_name('unknown'); -- Should return 'status_sort_order' (default)
|
||||
|
||||
-- Test bulk update function (example - would need real project_id and task_ids)
|
||||
/*
|
||||
SELECT update_task_sort_orders_bulk(
|
||||
'[
|
||||
{"task_id": "example-uuid", "sort_order": 1, "status_id": "status-uuid"},
|
||||
{"task_id": "example-uuid-2", "sort_order": 2, "status_id": "status-uuid"}
|
||||
]'::json,
|
||||
'status'
|
||||
);
|
||||
*/
|
||||
|
||||
-- Verify that sort_order constraint still exists and works
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.table_name,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE tc.constraint_name = 'tasks_sort_order_unique';
|
||||
|
||||
-- Check that new sort order columns don't have unique constraints (which is correct)
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.table_name,
|
||||
kcu.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
WHERE kcu.table_name = 'tasks'
|
||||
AND kcu.column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
|
||||
AND tc.constraint_type = 'UNIQUE';
|
||||
30
test_sort_orders.sql
Normal file
30
test_sort_orders.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Test script to validate the separate sort order implementation
|
||||
|
||||
-- Check if new columns exist
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'tasks'
|
||||
AND column_name IN ('status_sort_order', 'priority_sort_order', 'phase_sort_order', 'member_sort_order')
|
||||
ORDER BY column_name;
|
||||
|
||||
-- Check if helper function exists
|
||||
SELECT routine_name, routine_type
|
||||
FROM information_schema.routines
|
||||
WHERE routine_name IN ('get_sort_column_name', 'update_task_sort_orders_bulk', 'handle_task_list_sort_order_change');
|
||||
|
||||
-- Sample test data to verify different sort orders work
|
||||
-- (This would be run after the migrations)
|
||||
/*
|
||||
-- Test: Tasks should have different orders for different groupings
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
sort_order,
|
||||
status_sort_order,
|
||||
priority_sort_order,
|
||||
phase_sort_order,
|
||||
member_sort_order
|
||||
FROM tasks
|
||||
WHERE project_id = '<test-project-id>'
|
||||
ORDER BY status_sort_order;
|
||||
*/
|
||||
@@ -1,85 +0,0 @@
|
||||
-- Create holiday types table
|
||||
CREATE TABLE IF NOT EXISTS holiday_types (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
color_code WL_HEX_COLOR NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE holiday_types
|
||||
ADD CONSTRAINT holiday_types_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
-- Insert default holiday types
|
||||
INSERT INTO holiday_types (name, description, color_code) VALUES
|
||||
('Public Holiday', 'Official public holidays', '#f37070'),
|
||||
('Company Holiday', 'Company-specific holidays', '#70a6f3'),
|
||||
('Personal Holiday', 'Personal or optional holidays', '#75c997'),
|
||||
('Religious Holiday', 'Religious observances', '#fbc84c')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Create organization holidays table
|
||||
CREATE TABLE IF NOT EXISTS organization_holidays (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
organization_id UUID NOT NULL,
|
||||
holiday_type_id UUID NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
date DATE NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_organization_id_fk
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_holiday_type_id_fk
|
||||
FOREIGN KEY (holiday_type_id) REFERENCES holiday_types
|
||||
ON DELETE RESTRICT;
|
||||
|
||||
-- Add unique constraint to prevent duplicate holidays on the same date for an organization
|
||||
ALTER TABLE organization_holidays
|
||||
ADD CONSTRAINT organization_holidays_organization_date_unique
|
||||
UNIQUE (organization_id, date);
|
||||
|
||||
-- Create country holidays table for predefined holidays
|
||||
CREATE TABLE IF NOT EXISTS country_holidays (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
country_code CHAR(2) NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
date DATE NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_country_code_fk
|
||||
FOREIGN KEY (country_code) REFERENCES countries(code)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Add unique constraint to prevent duplicate holidays for the same country, name, and date
|
||||
ALTER TABLE country_holidays
|
||||
ADD CONSTRAINT country_holidays_country_name_date_unique
|
||||
UNIQUE (country_code, name, date);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_organization_holidays_organization_id ON organization_holidays(organization_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_organization_holidays_date ON organization_holidays(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_country_holidays_country_code ON country_holidays(country_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_country_holidays_date ON country_holidays(date);
|
||||
@@ -1,60 +0,0 @@
|
||||
-- ================================================================
|
||||
-- Sri Lankan Holidays Migration
|
||||
-- ================================================================
|
||||
-- This migration populates Sri Lankan holidays from verified sources
|
||||
--
|
||||
-- SOURCES & VERIFICATION:
|
||||
-- - 2025 data: Verified from official government sources
|
||||
-- - Fixed holidays: Independence Day, May Day, Christmas (all years)
|
||||
-- - Variable holidays: Added only when officially verified
|
||||
--
|
||||
-- MAINTENANCE:
|
||||
-- - Use scripts/update-sri-lankan-holidays.js for updates
|
||||
-- - See docs/sri-lankan-holiday-update-process.md for process
|
||||
-- ================================================================
|
||||
|
||||
-- Insert fixed holidays for multiple years (these never change dates)
|
||||
DO $$
|
||||
DECLARE
|
||||
current_year INT;
|
||||
BEGIN
|
||||
FOR current_year IN 2020..2050 LOOP
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES
|
||||
('LK', 'Independence Day', 'Commemorates the independence of Sri Lanka from British rule in 1948',
|
||||
make_date(current_year, 2, 4), true),
|
||||
('LK', 'May Day', 'International Workers'' Day',
|
||||
make_date(current_year, 5, 1), true),
|
||||
('LK', 'Christmas Day', 'Christian celebration of the birth of Jesus Christ',
|
||||
make_date(current_year, 12, 25), true)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- Insert specific holidays for years 2025-2028 (from our JSON data)
|
||||
|
||||
-- 2025 holidays
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES
|
||||
('LK', 'Duruthu Full Moon Poya Day', 'Commemorates the first visit of Buddha to Sri Lanka', '2025-01-13', false),
|
||||
('LK', 'Navam Full Moon Poya Day', 'Commemorates the appointment of Sariputta and Moggallana as Buddha''s chief disciples', '2025-02-12', false),
|
||||
('LK', 'Medin Full Moon Poya Day', 'Commemorates Buddha''s first visit to his father''s palace after enlightenment', '2025-03-14', false),
|
||||
('LK', 'Eid al-Fitr', 'Festival marking the end of Ramadan', '2025-03-31', false),
|
||||
('LK', 'Bak Full Moon Poya Day', 'Commemorates Buddha''s second visit to Sri Lanka', '2025-04-12', false),
|
||||
('LK', 'Good Friday', 'Christian commemoration of the crucifixion of Jesus Christ', '2025-04-18', false),
|
||||
('LK', 'Vesak Full Moon Poya Day', 'Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha', '2025-05-12', false),
|
||||
('LK', 'Day after Vesak Full Moon Poya Day', 'Additional day for Vesak celebrations', '2025-05-13', false),
|
||||
('LK', 'Eid al-Adha', 'Islamic festival of sacrifice', '2025-06-07', false),
|
||||
('LK', 'Poson Full Moon Poya Day', 'Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda', '2025-06-11', false),
|
||||
('LK', 'Esala Full Moon Poya Day', 'Commemorates Buddha''s first sermon and the arrival of the Sacred Tooth Relic', '2025-07-10', false),
|
||||
('LK', 'Nikini Full Moon Poya Day', 'Commemorates the first Buddhist council', '2025-08-09', false),
|
||||
('LK', 'Binara Full Moon Poya Day', 'Commemorates Buddha''s visit to heaven to preach to his mother', '2025-09-07', false),
|
||||
('LK', 'Vap Full Moon Poya Day', 'Marks the end of Buddhist Lent and Buddha''s return from heaven', '2025-10-07', false),
|
||||
('LK', 'Deepavali', 'Hindu Festival of Lights', '2025-10-20', false),
|
||||
('LK', 'Il Full Moon Poya Day', 'Commemorates Buddha''s ordination of sixty disciples', '2025-11-05', false),
|
||||
('LK', 'Unduvap Full Moon Poya Day', 'Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling', '2025-12-04', false)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING;
|
||||
|
||||
-- NOTE: Data for 2026+ should be added only after verification from official sources
|
||||
-- Use the holiday management script to generate templates for new years:
|
||||
-- node update-sri-lankan-holidays.js --poya-template YYYY
|
||||
@@ -1,63 +0,0 @@
|
||||
-- Create organization holiday settings table
|
||||
CREATE TABLE IF NOT EXISTS organization_holiday_settings (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
organization_id UUID NOT NULL,
|
||||
country_code CHAR(2),
|
||||
state_code TEXT,
|
||||
auto_sync_holidays BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE organization_holiday_settings
|
||||
ADD CONSTRAINT organization_holiday_settings_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE organization_holiday_settings
|
||||
ADD CONSTRAINT organization_holiday_settings_organization_id_fk
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations
|
||||
ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE organization_holiday_settings
|
||||
ADD CONSTRAINT organization_holiday_settings_country_code_fk
|
||||
FOREIGN KEY (country_code) REFERENCES countries(code)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Ensure one settings record per organization
|
||||
ALTER TABLE organization_holiday_settings
|
||||
ADD CONSTRAINT organization_holiday_settings_organization_unique
|
||||
UNIQUE (organization_id);
|
||||
|
||||
-- Create index for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_organization_holiday_settings_organization_id ON organization_holiday_settings(organization_id);
|
||||
|
||||
-- Add state holidays table for more granular holiday data
|
||||
CREATE TABLE IF NOT EXISTS state_holidays (
|
||||
id UUID DEFAULT uuid_generate_v4() NOT NULL,
|
||||
country_code CHAR(2) NOT NULL,
|
||||
state_code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
date DATE NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE state_holidays
|
||||
ADD CONSTRAINT state_holidays_pk
|
||||
PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE state_holidays
|
||||
ADD CONSTRAINT state_holidays_country_code_fk
|
||||
FOREIGN KEY (country_code) REFERENCES countries(code)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Add unique constraint to prevent duplicate holidays for the same state, name, and date
|
||||
ALTER TABLE state_holidays
|
||||
ADD CONSTRAINT state_holidays_state_name_date_unique
|
||||
UNIQUE (country_code, state_code, name, date);
|
||||
|
||||
-- Create indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_state_holidays_country_state ON state_holidays(country_code, state_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_state_holidays_date ON state_holidays(date);
|
||||
@@ -1,93 +0,0 @@
|
||||
-- Migration: Add survey tables for account setup questionnaire
|
||||
-- Date: 2025-07-24
|
||||
-- Description: Creates tables to store survey questions and user responses for account setup flow
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Create surveys table to define different types of surveys
|
||||
CREATE TABLE IF NOT EXISTS surveys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
survey_type VARCHAR(50) DEFAULT 'account_setup' NOT NULL, -- 'account_setup', 'onboarding', 'feedback'
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Create survey_questions table to store individual questions
|
||||
CREATE TABLE IF NOT EXISTS survey_questions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||
question_key VARCHAR(100) NOT NULL, -- Used for localization keys
|
||||
question_type VARCHAR(50) NOT NULL, -- 'single_choice', 'multiple_choice', 'text'
|
||||
is_required BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0 NOT NULL,
|
||||
options JSONB, -- For choice questions, store options as JSON array
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Create survey_responses table to track user responses to surveys
|
||||
CREATE TABLE IF NOT EXISTS survey_responses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
started_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Create survey_answers table to store individual question answers
|
||||
CREATE TABLE IF NOT EXISTS survey_answers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
response_id UUID REFERENCES survey_responses(id) ON DELETE CASCADE NOT NULL,
|
||||
question_id UUID REFERENCES survey_questions(id) ON DELETE CASCADE NOT NULL,
|
||||
answer_text TEXT,
|
||||
answer_json JSONB, -- For multiple choice answers stored as array
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Add performance indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_surveys_type_active ON surveys(survey_type, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_order ON survey_questions(survey_id, sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_responses_user_survey ON survey_responses(user_id, survey_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_responses_completed ON survey_responses(survey_id, is_completed);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_answers_response ON survey_answers(response_id);
|
||||
|
||||
-- Add constraints
|
||||
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_sort_order_check CHECK (sort_order >= 0);
|
||||
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_type_check CHECK (question_type IN ('single_choice', 'multiple_choice', 'text'));
|
||||
|
||||
-- Add unique constraint to prevent duplicate responses per user per survey
|
||||
ALTER TABLE survey_responses ADD CONSTRAINT unique_user_survey_response UNIQUE (user_id, survey_id);
|
||||
|
||||
-- Add unique constraint to prevent duplicate answers per question per response
|
||||
ALTER TABLE survey_answers ADD CONSTRAINT unique_response_question_answer UNIQUE (response_id, question_id);
|
||||
|
||||
-- Insert the default account setup survey
|
||||
INSERT INTO surveys (name, description, survey_type, is_active) VALUES
|
||||
('Account Setup Survey', 'Initial questionnaire during account setup to understand user needs', 'account_setup', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Get the survey ID for inserting questions
|
||||
DO $$
|
||||
DECLARE
|
||||
survey_uuid UUID;
|
||||
BEGIN
|
||||
SELECT id INTO survey_uuid FROM surveys WHERE survey_type = 'account_setup' AND name = 'Account Setup Survey' LIMIT 1;
|
||||
|
||||
-- Insert survey questions
|
||||
INSERT INTO survey_questions (survey_id, question_key, question_type, is_required, sort_order, options) VALUES
|
||||
(survey_uuid, 'organization_type', 'single_choice', true, 1, '["freelancer", "startup", "small_medium_business", "agency", "enterprise", "other"]'),
|
||||
(survey_uuid, 'user_role', 'single_choice', true, 2, '["founder_ceo", "project_manager", "software_developer", "designer", "operations", "other"]'),
|
||||
(survey_uuid, 'main_use_cases', 'multiple_choice', true, 3, '["task_management", "team_collaboration", "resource_planning", "client_communication", "time_tracking", "other"]'),
|
||||
(survey_uuid, 'previous_tools', 'text', false, 4, null),
|
||||
(survey_uuid, 'how_heard_about', 'single_choice', false, 5, '["google_search", "twitter", "linkedin", "friend_colleague", "blog_article", "other"]')
|
||||
ON CONFLICT DO NOTHING;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -2297,60 +2297,3 @@ ALTER TABLE organization_working_days
|
||||
ALTER TABLE organization_working_days
|
||||
ADD CONSTRAINT org_organization_id_fk
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations;
|
||||
|
||||
-- Survey tables for account setup questionnaire
|
||||
CREATE TABLE IF NOT EXISTS surveys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
survey_type VARCHAR(50) DEFAULT 'account_setup' NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS survey_questions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||
question_key VARCHAR(100) NOT NULL,
|
||||
question_type VARCHAR(50) NOT NULL,
|
||||
is_required BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
sort_order INTEGER DEFAULT 0 NOT NULL,
|
||||
options JSONB,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS survey_responses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
survey_id UUID REFERENCES surveys(id) ON DELETE CASCADE NOT NULL,
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||
is_completed BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
started_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
completed_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS survey_answers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
response_id UUID REFERENCES survey_responses(id) ON DELETE CASCADE NOT NULL,
|
||||
question_id UUID REFERENCES survey_questions(id) ON DELETE CASCADE NOT NULL,
|
||||
answer_text TEXT,
|
||||
answer_json JSONB,
|
||||
created_at TIMESTAMP DEFAULT now() NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
-- Survey table indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_surveys_type_active ON surveys(survey_type, is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_questions_survey_order ON survey_questions(survey_id, sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_responses_user_survey ON survey_responses(user_id, survey_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_responses_completed ON survey_responses(survey_id, is_completed);
|
||||
CREATE INDEX IF NOT EXISTS idx_survey_answers_response ON survey_answers(response_id);
|
||||
|
||||
-- Survey table constraints
|
||||
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_sort_order_check CHECK (sort_order >= 0);
|
||||
ALTER TABLE survey_questions ADD CONSTRAINT survey_questions_type_check CHECK (question_type IN ('single_choice', 'multiple_choice', 'text'));
|
||||
ALTER TABLE survey_responses ADD CONSTRAINT unique_user_survey_response UNIQUE (user_id, survey_id);
|
||||
ALTER TABLE survey_answers ADD CONSTRAINT unique_response_question_answer UNIQUE (response_id, question_id);
|
||||
|
||||
@@ -142,25 +142,3 @@ DROP FUNCTION sys_insert_license_types();
|
||||
INSERT INTO timezones (name, abbrev, utc_offset)
|
||||
SELECT name, abbrev, utc_offset
|
||||
FROM pg_timezone_names;
|
||||
|
||||
-- Insert default account setup survey
|
||||
INSERT INTO surveys (name, description, survey_type, is_active) VALUES
|
||||
('Account Setup Survey', 'Initial questionnaire during account setup to understand user needs', 'account_setup', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert survey questions for account setup survey
|
||||
DO $$
|
||||
DECLARE
|
||||
survey_uuid UUID;
|
||||
BEGIN
|
||||
SELECT id INTO survey_uuid FROM surveys WHERE survey_type = 'account_setup' AND name = 'Account Setup Survey' LIMIT 1;
|
||||
|
||||
-- Insert survey questions
|
||||
INSERT INTO survey_questions (survey_id, question_key, question_type, is_required, sort_order, options) VALUES
|
||||
(survey_uuid, 'organization_type', 'single_choice', true, 1, '["freelancer", "startup", "small_medium_business", "agency", "enterprise", "other"]'),
|
||||
(survey_uuid, 'user_role', 'single_choice', true, 2, '["founder_ceo", "project_manager", "software_developer", "designer", "operations", "other"]'),
|
||||
(survey_uuid, 'main_use_cases', 'multiple_choice', true, 3, '["task_management", "team_collaboration", "resource_planning", "client_communication", "time_tracking", "other"]'),
|
||||
(survey_uuid, 'previous_tools', 'text', false, 4, null),
|
||||
(survey_uuid, 'how_heard_about', 'single_choice', false, 5, '["google_search", "twitter", "linkedin", "friend_colleague", "blog_article", "other"]')
|
||||
ON CONFLICT DO NOTHING;
|
||||
END $$;
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# 🌍 Holiday Calendar System
|
||||
|
||||
The Worklenz Holiday Calendar System provides comprehensive holiday management for organizations operating globally.
|
||||
|
||||
## 📋 Features
|
||||
|
||||
- **200+ Countries Supported** - Comprehensive holiday data for countries worldwide
|
||||
- **Multiple Holiday Types** - Public, Company, Personal, and Religious holidays
|
||||
- **Import Country Holidays** - Bulk import official holidays from any supported country
|
||||
- **Manual Holiday Management** - Add, edit, and delete custom holidays
|
||||
- **Recurring Holidays** - Support for annual recurring holidays
|
||||
- **Visual Calendar** - Interactive calendar with color-coded holiday display
|
||||
- **Dark/Light Mode** - Full theme support
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Database Setup
|
||||
|
||||
Run the migration to create the holiday tables:
|
||||
|
||||
```bash
|
||||
# Run the migration
|
||||
psql -d your_database -f database/migrations/20250130000000-add-holiday-calendar.sql
|
||||
```
|
||||
|
||||
### 2. Populate Country Holidays
|
||||
|
||||
Use the npm package to populate holidays for 200+ countries:
|
||||
|
||||
```bash
|
||||
# Run the holiday population script
|
||||
./scripts/run-holiday-population.sh
|
||||
```
|
||||
|
||||
This will populate holidays for years 2020-2030 for all supported countries.
|
||||
|
||||
### 3. Access the Holiday Calendar
|
||||
|
||||
Navigate to **Admin Center → Overview** to access the holiday calendar.
|
||||
|
||||
## 🌐 Supported Countries
|
||||
|
||||
The system includes **200+ countries** across all continents:
|
||||
|
||||
### North America
|
||||
- United States 🇺🇸
|
||||
- Canada 🇨🇦
|
||||
- Mexico 🇲🇽
|
||||
|
||||
### Europe
|
||||
- United Kingdom 🇬🇧
|
||||
- Germany 🇩🇪
|
||||
- France 🇫🇷
|
||||
- Italy 🇮🇹
|
||||
- Spain 🇪🇸
|
||||
- Netherlands 🇳🇱
|
||||
- Belgium 🇧🇪
|
||||
- Switzerland 🇨🇭
|
||||
- Austria 🇦🇹
|
||||
- Sweden 🇸🇪
|
||||
- Norway 🇳🇴
|
||||
- Denmark 🇩🇰
|
||||
- Finland 🇫🇮
|
||||
- Poland 🇵🇱
|
||||
- Czech Republic 🇨🇿
|
||||
- Hungary 🇭🇺
|
||||
- Romania 🇷🇴
|
||||
- Bulgaria 🇧🇬
|
||||
- Croatia 🇭🇷
|
||||
- Slovenia 🇸🇮
|
||||
- Slovakia 🇸🇰
|
||||
- Lithuania 🇱🇹
|
||||
- Latvia 🇱🇻
|
||||
- Estonia 🇪🇪
|
||||
- Ireland 🇮🇪
|
||||
- Portugal 🇵🇹
|
||||
- Greece 🇬🇷
|
||||
- Cyprus 🇨🇾
|
||||
- Malta 🇲🇹
|
||||
- Luxembourg 🇱🇺
|
||||
- Iceland 🇮🇸
|
||||
|
||||
### Asia
|
||||
- China 🇨🇳
|
||||
- Japan 🇯🇵
|
||||
- South Korea 🇰🇷
|
||||
- India 🇮🇳
|
||||
- Pakistan 🇵🇰
|
||||
- Bangladesh 🇧🇩
|
||||
- Sri Lanka 🇱🇰
|
||||
- Nepal 🇳🇵
|
||||
- Thailand 🇹🇭
|
||||
- Vietnam 🇻🇳
|
||||
- Malaysia 🇲🇾
|
||||
- Singapore 🇸🇬
|
||||
- Indonesia 🇮🇩
|
||||
- Philippines 🇵🇭
|
||||
- Myanmar 🇲🇲
|
||||
- Cambodia 🇰🇭
|
||||
- Laos 🇱🇦
|
||||
- Brunei 🇧🇳
|
||||
- Timor-Leste 🇹🇱
|
||||
- Mongolia 🇲🇳
|
||||
- Kazakhstan 🇰🇿
|
||||
- Uzbekistan 🇺🇿
|
||||
- Kyrgyzstan 🇰🇬
|
||||
- Tajikistan 🇹🇯
|
||||
- Turkmenistan 🇹🇲
|
||||
- Afghanistan 🇦🇫
|
||||
- Iran 🇮🇷
|
||||
- Iraq 🇮🇶
|
||||
- Saudi Arabia 🇸🇦
|
||||
- UAE 🇦🇪
|
||||
- Qatar 🇶🇦
|
||||
- Kuwait 🇰🇼
|
||||
- Bahrain 🇧🇭
|
||||
- Oman 🇴🇲
|
||||
- Yemen 🇾🇪
|
||||
- Jordan 🇯🇴
|
||||
- Lebanon 🇱🇧
|
||||
- Syria 🇸🇾
|
||||
- Israel 🇮🇱
|
||||
- Palestine 🇵🇸
|
||||
- Turkey 🇹🇷
|
||||
- Georgia 🇬🇪
|
||||
- Armenia 🇦🇲
|
||||
- Azerbaijan 🇦🇿
|
||||
|
||||
### Oceania
|
||||
- Australia 🇦🇺
|
||||
- New Zealand 🇳🇿
|
||||
- Fiji 🇫🇯
|
||||
- Papua New Guinea 🇵🇬
|
||||
- Solomon Islands 🇸🇧
|
||||
- Vanuatu 🇻🇺
|
||||
- New Caledonia 🇳🇨
|
||||
- French Polynesia 🇵🇫
|
||||
- Tonga 🇹🇴
|
||||
- Samoa 🇼🇸
|
||||
- Kiribati 🇰🇮
|
||||
- Tuvalu 🇹🇻
|
||||
- Nauru 🇳🇷
|
||||
- Palau 🇵🇼
|
||||
- Marshall Islands 🇲🇭
|
||||
- Micronesia 🇫🇲
|
||||
|
||||
### Africa
|
||||
- South Africa 🇿🇦
|
||||
- Egypt 🇪🇬
|
||||
- Nigeria 🇳🇬
|
||||
- Kenya 🇰🇪
|
||||
- Ethiopia 🇪🇹
|
||||
- Tanzania 🇹🇿
|
||||
- Uganda 🇺🇬
|
||||
- Ghana 🇬🇭
|
||||
- Ivory Coast 🇨🇮
|
||||
- Senegal 🇸🇳
|
||||
- Mali 🇲🇱
|
||||
- Burkina Faso 🇧🇫
|
||||
- Niger 🇳🇪
|
||||
- Chad 🇹🇩
|
||||
- Cameroon 🇨🇲
|
||||
- Central African Republic 🇨🇫
|
||||
- Republic of the Congo 🇨🇬
|
||||
- Democratic Republic of the Congo 🇨🇩
|
||||
- Gabon 🇬🇦
|
||||
- Equatorial Guinea 🇬🇶
|
||||
- São Tomé and Príncipe 🇸🇹
|
||||
- Angola 🇦🇴
|
||||
- Zambia 🇿🇲
|
||||
- Zimbabwe 🇿🇼
|
||||
- Botswana 🇧🇼
|
||||
- Namibia 🇳🇦
|
||||
- Lesotho 🇱🇸
|
||||
- Eswatini 🇸🇿
|
||||
- Madagascar 🇲🇬
|
||||
- Mauritius 🇲🇺
|
||||
- Seychelles 🇸🇨
|
||||
- Comoros 🇰🇲
|
||||
- Djibouti 🇩🇯
|
||||
- Somalia 🇸🇴
|
||||
- Eritrea 🇪🇷
|
||||
- Sudan 🇸🇩
|
||||
- South Sudan 🇸🇸
|
||||
- Libya 🇱🇾
|
||||
- Tunisia 🇹🇳
|
||||
- Algeria 🇩🇿
|
||||
- Morocco 🇲🇦
|
||||
- Western Sahara 🇪🇭
|
||||
- Mauritania 🇲🇷
|
||||
- Gambia 🇬🇲
|
||||
- Guinea-Bissau 🇬🇼
|
||||
- Guinea 🇬🇳
|
||||
- Sierra Leone 🇸🇱
|
||||
- Liberia 🇱🇷
|
||||
- Togo 🇹🇬
|
||||
- Benin 🇧🇯
|
||||
|
||||
### South America
|
||||
- Brazil 🇧🇷
|
||||
- Argentina 🇦🇷
|
||||
- Chile 🇨🇱
|
||||
- Colombia 🇨🇴
|
||||
- Peru 🇵🇪
|
||||
- Venezuela 🇻🇪
|
||||
- Ecuador 🇪🇨
|
||||
- Bolivia 🇧🇴
|
||||
- Paraguay 🇵🇾
|
||||
- Uruguay 🇺🇾
|
||||
- Guyana 🇬🇾
|
||||
- Suriname 🇸🇷
|
||||
- Falkland Islands 🇫🇰
|
||||
- French Guiana 🇬🇫
|
||||
|
||||
### Central America & Caribbean
|
||||
- Mexico 🇲🇽
|
||||
- Guatemala 🇬🇹
|
||||
- Belize 🇧🇿
|
||||
- El Salvador 🇸🇻
|
||||
- Honduras 🇭🇳
|
||||
- Nicaragua 🇳🇮
|
||||
- Costa Rica 🇨🇷
|
||||
- Panama 🇵🇦
|
||||
- Cuba 🇨🇺
|
||||
- Jamaica 🇯🇲
|
||||
- Haiti 🇭🇹
|
||||
- Dominican Republic 🇩🇴
|
||||
- Puerto Rico 🇵🇷
|
||||
- Trinidad and Tobago 🇹🇹
|
||||
- Barbados 🇧🇧
|
||||
- Grenada 🇬🇩
|
||||
- Saint Lucia 🇱🇨
|
||||
- Saint Vincent and the Grenadines 🇻🇨
|
||||
- Antigua and Barbuda 🇦🇬
|
||||
- Saint Kitts and Nevis 🇰🇳
|
||||
- Dominica 🇩🇲
|
||||
- Bahamas 🇧🇸
|
||||
- Turks and Caicos Islands 🇹🇨
|
||||
- Cayman Islands 🇰🇾
|
||||
- Bermuda 🇧🇲
|
||||
- Anguilla 🇦🇮
|
||||
- British Virgin Islands 🇻🇬
|
||||
- U.S. Virgin Islands 🇻🇮
|
||||
- Aruba 🇦🇼
|
||||
- Curaçao 🇨🇼
|
||||
- Sint Maarten 🇸🇽
|
||||
- Saint Martin 🇲🇫
|
||||
- Saint Barthélemy 🇧🇱
|
||||
- Guadeloupe 🇬🇵
|
||||
- Martinique 🇲🇶
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
### Holiday Types
|
||||
```http
|
||||
GET /api/holidays/types
|
||||
```
|
||||
|
||||
### Organization Holidays
|
||||
```http
|
||||
GET /api/holidays/organization?year=2024
|
||||
POST /api/holidays/organization
|
||||
PUT /api/holidays/organization/:id
|
||||
DELETE /api/holidays/organization/:id
|
||||
```
|
||||
|
||||
### Country Holidays
|
||||
```http
|
||||
GET /api/holidays/countries
|
||||
GET /api/holidays/countries/:country_code?year=2024
|
||||
POST /api/holidays/import
|
||||
```
|
||||
|
||||
### Calendar View
|
||||
```http
|
||||
GET /api/holidays/calendar?year=2024&month=1
|
||||
```
|
||||
|
||||
## 📊 Holiday Types
|
||||
|
||||
The system supports four types of holidays:
|
||||
|
||||
1. **Public Holiday** - Official government holidays (Red)
|
||||
2. **Company Holiday** - Organization-specific holidays (Blue)
|
||||
3. **Personal Holiday** - Personal or optional holidays (Green)
|
||||
4. **Religious Holiday** - Religious observances (Yellow)
|
||||
|
||||
## 🎯 Usage Examples
|
||||
|
||||
### Import US Holidays
|
||||
```javascript
|
||||
const result = await holidayApiService.importCountryHolidays({
|
||||
country_code: 'US',
|
||||
year: 2024
|
||||
});
|
||||
```
|
||||
|
||||
### Add Custom Holiday
|
||||
```javascript
|
||||
const holiday = await holidayApiService.createOrganizationHoliday({
|
||||
name: 'Company Retreat',
|
||||
description: 'Annual team building event',
|
||||
date: '2024-06-15',
|
||||
holiday_type_id: 'company-holiday-id',
|
||||
is_recurring: true
|
||||
});
|
||||
```
|
||||
|
||||
### Get Calendar View
|
||||
```javascript
|
||||
const calendar = await holidayApiService.getHolidayCalendar(2024, 1);
|
||||
```
|
||||
|
||||
## 🔄 Data Sources
|
||||
|
||||
The holiday data is sourced from the `date-holidays` npm package, which provides:
|
||||
|
||||
- **Official government holidays** for 200+ countries
|
||||
- **Religious holidays** (Christian, Islamic, Jewish, Hindu, Buddhist)
|
||||
- **Cultural and traditional holidays**
|
||||
- **Historical and commemorative days**
|
||||
|
||||
## 🛠️ Maintenance
|
||||
|
||||
### Adding New Countries
|
||||
|
||||
1. Add the country to the `countries` table
|
||||
2. Update the `populate-holidays.js` script
|
||||
3. Run the population script
|
||||
|
||||
### Updating Holiday Data
|
||||
|
||||
```bash
|
||||
# Re-run the holiday population script
|
||||
./scripts/run-holiday-population.sh
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Holidays are stored for years 2020-2030 by default
|
||||
- The system prevents duplicate holidays on the same date
|
||||
- Imported holidays are automatically classified as "Public Holiday" type
|
||||
- All holidays support recurring annual patterns
|
||||
- The calendar view combines organization and country holidays
|
||||
|
||||
## 🎉 Benefits
|
||||
|
||||
- **Global Compliance** - Ensure compliance with local holiday regulations
|
||||
- **Resource Planning** - Better project scheduling and resource allocation
|
||||
- **Team Coordination** - Improved team communication and planning
|
||||
- **Cost Management** - Accurate billing and time tracking
|
||||
- **Cultural Awareness** - Respect for diverse cultural and religious practices
|
||||
555
worklenz-backend/package-lock.json
generated
555
worklenz-backend/package-lock.json
generated
@@ -26,7 +26,6 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"csrf-sync": "^4.2.1",
|
||||
"csurf": "^1.11.0",
|
||||
"date-holidays": "^3.24.4",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.3.0",
|
||||
@@ -34,6 +33,7 @@
|
||||
"express-rate-limit": "^6.8.0",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^6.15.0",
|
||||
"grunt-cli": "^1.5.0",
|
||||
"helmet": "^6.2.0",
|
||||
"hpp": "^0.2.3",
|
||||
"http-errors": "^2.0.0",
|
||||
@@ -126,7 +126,7 @@
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"node": ">=16.13.0",
|
||||
"npm": ">=8.11.0",
|
||||
"yarn": "WARNING: Please use npm package manager instead of yarn"
|
||||
}
|
||||
@@ -6452,14 +6452,33 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-each": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
|
||||
"integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-slice": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
|
||||
"integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
@@ -6482,15 +6501,6 @@
|
||||
"integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/astronomia": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/astronomia/-/astronomia-4.1.1.tgz",
|
||||
"integrity": "sha512-TcJD9lUC5eAo0/Ji7rnQauX/yQbi0yZWM+JsNr77W3OA5fsrgvuFgubLMFwfw4VlZ29cu9dG/yfJbfvuTSftjg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
|
||||
@@ -6941,7 +6951,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -7088,18 +7097,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/caldate": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/caldate/-/caldate-2.0.5.tgz",
|
||||
"integrity": "sha512-JndhrUuDuE975KUhFqJaVR1OQkCHZqpOrJur/CFXEIEhWhBMjxO85cRSK8q4FW+B+yyPq6GYua2u4KvNzTcq0w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"moment-timezone": "^0.5.43"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -7942,73 +7939,6 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/date-bengali-revised": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/date-bengali-revised/-/date-bengali-revised-2.0.2.tgz",
|
||||
"integrity": "sha512-q9iDru4+TSA9k4zfm0CFHJj6nBsxP7AYgWC/qodK/i7oOIlj5K2z5IcQDtESfs/Qwqt/xJYaP86tkazd/vRptg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-chinese": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/date-chinese/-/date-chinese-2.1.4.tgz",
|
||||
"integrity": "sha512-WY+6+Qw92ZGWFvGtStmNQHEYpNa87b8IAQ5T8VKt4wqrn24lBXyyBnWI5jAIyy7h/KVwJZ06bD8l/b7yss82Ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-easter": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/date-easter/-/date-easter-1.0.3.tgz",
|
||||
"integrity": "sha512-aOViyIgpM4W0OWUiLqivznwTtuMlD/rdUWhc5IatYnplhPiWrLv75cnifaKYhmQwUBLAMWLNG4/9mlLIbXoGBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays": {
|
||||
"version": "3.24.4",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays/-/date-holidays-3.24.4.tgz",
|
||||
"integrity": "sha512-IZsFU6KJvmomA+bzk1uvDJ8P0/9nEOGZ8YMPQGpipNDUY+pL219AmnwWypYrz36nyWYJ2/fSkGNHaWOfFwpiAg==",
|
||||
"license": "(ISC AND CC-BY-3.0)",
|
||||
"dependencies": {
|
||||
"date-holidays-parser": "^3.4.7",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prepin": "^1.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"holidays2json": "scripts/holidays2json.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-holidays-parser": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/date-holidays-parser/-/date-holidays-parser-3.4.7.tgz",
|
||||
"integrity": "sha512-h09ZEtM6u5cYM6m1bX+1Ny9f+nLO9KVZUKNPEnH7lhbXYTfqZogaGTnhONswGeIJFF91UImIftS3CdM9HLW5oQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"astronomia": "^4.1.1",
|
||||
"caldate": "^2.0.5",
|
||||
"date-bengali-revised": "^2.0.2",
|
||||
"date-chinese": "^2.1.4",
|
||||
"date-easter": "^1.0.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
"jalaali-js": "^1.2.7",
|
||||
"moment-timezone": "^0.5.47"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
@@ -8126,6 +8056,15 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-file": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
|
||||
"integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
@@ -8985,6 +8924,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-tilde": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
|
||||
"integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"homedir-polyfill": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz",
|
||||
@@ -9137,6 +9088,12 @@
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-csv": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
|
||||
@@ -9265,7 +9222,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -9331,6 +9287,46 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/findup-sync": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
|
||||
"integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-file": "^1.0.0",
|
||||
"is-glob": "^4.0.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"resolve-dir": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fined": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
|
||||
"integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.2",
|
||||
"is-plain-object": "^2.0.3",
|
||||
"object.defaults": "^1.1.0",
|
||||
"object.pick": "^1.2.0",
|
||||
"parse-filepath": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/flagged-respawn": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
|
||||
"integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||
@@ -9431,6 +9427,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
"integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/for-own": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
|
||||
"integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"for-in": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -9828,6 +9845,48 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/global-modules": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
|
||||
"integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"global-prefix": "^1.0.1",
|
||||
"is-windows": "^1.0.1",
|
||||
"resolve-dir": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
|
||||
"integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.2",
|
||||
"homedir-polyfill": "^1.0.1",
|
||||
"ini": "^1.3.4",
|
||||
"is-windows": "^1.0.1",
|
||||
"which": "^1.2.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/global-prefix/node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
@@ -9884,6 +9943,34 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/grunt-cli": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.5.0.tgz",
|
||||
"integrity": "sha512-rILKAFoU0dzlf22SUfDtq2R1fosChXXlJM5j7wI6uoW8gwmXDXzbUvirlKZSYCdXl3LXFbR+8xyS+WFo+b6vlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"grunt-known-options": "~2.0.0",
|
||||
"interpret": "~1.1.0",
|
||||
"liftup": "~3.0.1",
|
||||
"nopt": "~5.0.0",
|
||||
"v8flags": "^4.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"grunt": "bin/grunt"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/grunt-known-options": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
|
||||
"integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -9955,6 +10042,18 @@
|
||||
"dev": true,
|
||||
"license": "https://www.highcharts.com/license"
|
||||
},
|
||||
"node_modules/homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
"integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parse-passwd": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hpp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz",
|
||||
@@ -10164,6 +10263,12 @@
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/interpret": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
|
||||
"integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -10173,6 +10278,19 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-absolute": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
|
||||
"integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-relative": "^1.0.0",
|
||||
"is-windows": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
@@ -10234,7 +10352,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10263,7 +10380,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -10276,7 +10392,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -10292,6 +10407,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isobject": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
@@ -10316,6 +10443,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-relative": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
|
||||
"integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-unc-path": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -10328,6 +10467,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-unc-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
|
||||
"integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"unc-path-regex": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-windows": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
|
||||
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -10338,9 +10498,17 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -10470,12 +10638,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jalaali-js": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/javascript-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz",
|
||||
@@ -11162,6 +11324,7 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -11363,6 +11526,15 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||
@@ -11454,6 +11626,25 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/liftup": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
|
||||
"integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"findup-sync": "^4.0.0",
|
||||
"fined": "^1.2.0",
|
||||
"flagged-respawn": "^1.0.1",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"object.map": "^1.0.1",
|
||||
"rechoir": "^0.7.0",
|
||||
"resolve": "^1.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lines-and-columns": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||
@@ -11692,6 +11883,18 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/make-iterator": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
|
||||
"integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"kind-of": "^6.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
|
||||
@@ -11702,6 +11905,15 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/map-cache": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
|
||||
"integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -11759,7 +11971,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -12207,6 +12418,46 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object.defaults": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
|
||||
"integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-each": "^1.0.1",
|
||||
"array-slice": "^1.0.0",
|
||||
"for-own": "^1.0.0",
|
||||
"isobject": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object.map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
|
||||
"integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"for-own": "^1.0.0",
|
||||
"make-iterator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object.pick": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
|
||||
"integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isobject": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -12369,6 +12620,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-filepath": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
|
||||
"integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-absolute": "^1.0.0",
|
||||
"map-cache": "^0.2.0",
|
||||
"path-root": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
@@ -12388,6 +12653,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-passwd": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
|
||||
"integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-srcset": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||
@@ -12526,6 +12800,27 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-root": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
|
||||
"integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-root-regex": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-root-regex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
|
||||
"integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
@@ -12673,7 +12968,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -12882,15 +13176,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prepin": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/prepin/-/prepin-1.0.3.tgz",
|
||||
"integrity": "sha512-0XL2hreherEEvUy0fiaGEfN/ioXFV+JpImqIzQjxk6iBg4jQ2ARKqvC4+BmRD8w/pnpD+lbxvh0Ub+z7yBEjvA==",
|
||||
"license": "Unlicense",
|
||||
"bin": {
|
||||
"prepin": "bin/prepin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
|
||||
@@ -13278,6 +13563,18 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rechoir": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
|
||||
"integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
|
||||
@@ -13429,6 +13726,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-dir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
|
||||
"integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expand-tilde": "^2.0.0",
|
||||
"global-modules": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -14664,7 +14974,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -15185,6 +15494,15 @@
|
||||
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unc-path-regex": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
|
||||
"integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
@@ -15414,6 +15732,15 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8flags": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz",
|
||||
"integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"crypto-js": "^4.1.1",
|
||||
"csrf-sync": "^4.2.1",
|
||||
"csurf": "^1.11.0",
|
||||
"date-holidays": "^3.24.4",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"exceljs": "^4.3.0",
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
const Holidays = require("date-holidays");
|
||||
const { Pool } = require("pg");
|
||||
const config = require("../build/config/db-config").default;
|
||||
|
||||
// Database connection
|
||||
const pool = new Pool(config);
|
||||
|
||||
// Countries to populate with holidays
|
||||
const countries = [
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "GB", name: "United Kingdom" },
|
||||
{ code: "CA", name: "Canada" },
|
||||
{ code: "AU", name: "Australia" },
|
||||
{ code: "DE", name: "Germany" },
|
||||
{ code: "FR", name: "France" },
|
||||
{ code: "IT", name: "Italy" },
|
||||
{ code: "ES", name: "Spain" },
|
||||
{ code: "NL", name: "Netherlands" },
|
||||
{ code: "BE", name: "Belgium" },
|
||||
{ code: "CH", name: "Switzerland" },
|
||||
{ code: "AT", name: "Austria" },
|
||||
{ code: "SE", name: "Sweden" },
|
||||
{ code: "NO", name: "Norway" },
|
||||
{ code: "DK", name: "Denmark" },
|
||||
{ code: "FI", name: "Finland" },
|
||||
{ code: "PL", name: "Poland" },
|
||||
{ code: "CZ", name: "Czech Republic" },
|
||||
{ code: "HU", name: "Hungary" },
|
||||
{ code: "RO", name: "Romania" },
|
||||
{ code: "BG", name: "Bulgaria" },
|
||||
{ code: "HR", name: "Croatia" },
|
||||
{ code: "SI", name: "Slovenia" },
|
||||
{ code: "SK", name: "Slovakia" },
|
||||
{ code: "LT", name: "Lithuania" },
|
||||
{ code: "LV", name: "Latvia" },
|
||||
{ code: "EE", name: "Estonia" },
|
||||
{ code: "IE", name: "Ireland" },
|
||||
{ code: "PT", name: "Portugal" },
|
||||
{ code: "GR", name: "Greece" },
|
||||
{ code: "CY", name: "Cyprus" },
|
||||
{ code: "MT", name: "Malta" },
|
||||
{ code: "LU", name: "Luxembourg" },
|
||||
{ code: "IS", name: "Iceland" },
|
||||
{ code: "CN", name: "China" },
|
||||
{ code: "JP", name: "Japan" },
|
||||
{ code: "KR", name: "South Korea" },
|
||||
{ code: "IN", name: "India" },
|
||||
{ code: "PK", name: "Pakistan" },
|
||||
{ code: "BD", name: "Bangladesh" },
|
||||
{ code: "LK", name: "Sri Lanka" },
|
||||
{ code: "NP", name: "Nepal" },
|
||||
{ code: "TH", name: "Thailand" },
|
||||
{ code: "VN", name: "Vietnam" },
|
||||
{ code: "MY", name: "Malaysia" },
|
||||
{ code: "SG", name: "Singapore" },
|
||||
{ code: "ID", name: "Indonesia" },
|
||||
{ code: "PH", name: "Philippines" },
|
||||
{ code: "MM", name: "Myanmar" },
|
||||
{ code: "KH", name: "Cambodia" },
|
||||
{ code: "LA", name: "Laos" },
|
||||
{ code: "BN", name: "Brunei" },
|
||||
{ code: "TL", name: "Timor-Leste" },
|
||||
{ code: "MN", name: "Mongolia" },
|
||||
{ code: "KZ", name: "Kazakhstan" },
|
||||
{ code: "UZ", name: "Uzbekistan" },
|
||||
{ code: "KG", name: "Kyrgyzstan" },
|
||||
{ code: "TJ", name: "Tajikistan" },
|
||||
{ code: "TM", name: "Turkmenistan" },
|
||||
{ code: "AF", name: "Afghanistan" },
|
||||
{ code: "IR", name: "Iran" },
|
||||
{ code: "IQ", name: "Iraq" },
|
||||
{ code: "SA", name: "Saudi Arabia" },
|
||||
{ code: "AE", name: "United Arab Emirates" },
|
||||
{ code: "QA", name: "Qatar" },
|
||||
{ code: "KW", name: "Kuwait" },
|
||||
{ code: "BH", name: "Bahrain" },
|
||||
{ code: "OM", name: "Oman" },
|
||||
{ code: "YE", name: "Yemen" },
|
||||
{ code: "JO", name: "Jordan" },
|
||||
{ code: "LB", name: "Lebanon" },
|
||||
{ code: "SY", name: "Syria" },
|
||||
{ code: "IL", name: "Israel" },
|
||||
{ code: "PS", name: "Palestine" },
|
||||
{ code: "TR", name: "Turkey" },
|
||||
{ code: "GE", name: "Georgia" },
|
||||
{ code: "AM", name: "Armenia" },
|
||||
{ code: "AZ", name: "Azerbaijan" },
|
||||
{ code: "NZ", name: "New Zealand" },
|
||||
{ code: "FJ", name: "Fiji" },
|
||||
{ code: "PG", name: "Papua New Guinea" },
|
||||
{ code: "SB", name: "Solomon Islands" },
|
||||
{ code: "VU", name: "Vanuatu" },
|
||||
{ code: "NC", name: "New Caledonia" },
|
||||
{ code: "PF", name: "French Polynesia" },
|
||||
{ code: "TO", name: "Tonga" },
|
||||
{ code: "WS", name: "Samoa" },
|
||||
{ code: "KI", name: "Kiribati" },
|
||||
{ code: "TV", name: "Tuvalu" },
|
||||
{ code: "NR", name: "Nauru" },
|
||||
{ code: "PW", name: "Palau" },
|
||||
{ code: "MH", name: "Marshall Islands" },
|
||||
{ code: "FM", name: "Micronesia" },
|
||||
{ code: "ZA", name: "South Africa" },
|
||||
{ code: "EG", name: "Egypt" },
|
||||
{ code: "NG", name: "Nigeria" },
|
||||
{ code: "KE", name: "Kenya" },
|
||||
{ code: "ET", name: "Ethiopia" },
|
||||
{ code: "TZ", name: "Tanzania" },
|
||||
{ code: "UG", name: "Uganda" },
|
||||
{ code: "GH", name: "Ghana" },
|
||||
{ code: "CI", name: "Ivory Coast" },
|
||||
{ code: "SN", name: "Senegal" },
|
||||
{ code: "ML", name: "Mali" },
|
||||
{ code: "BF", name: "Burkina Faso" },
|
||||
{ code: "NE", name: "Niger" },
|
||||
{ code: "TD", name: "Chad" },
|
||||
{ code: "CM", name: "Cameroon" },
|
||||
{ code: "CF", name: "Central African Republic" },
|
||||
{ code: "CG", name: "Republic of the Congo" },
|
||||
{ code: "CD", name: "Democratic Republic of the Congo" },
|
||||
{ code: "GA", name: "Gabon" },
|
||||
{ code: "GQ", name: "Equatorial Guinea" },
|
||||
{ code: "ST", name: "São Tomé and Príncipe" },
|
||||
{ code: "AO", name: "Angola" },
|
||||
{ code: "ZM", name: "Zambia" },
|
||||
{ code: "ZW", name: "Zimbabwe" },
|
||||
{ code: "BW", name: "Botswana" },
|
||||
{ code: "NA", name: "Namibia" },
|
||||
{ code: "LS", name: "Lesotho" },
|
||||
{ code: "SZ", name: "Eswatini" },
|
||||
{ code: "MG", name: "Madagascar" },
|
||||
{ code: "MU", name: "Mauritius" },
|
||||
{ code: "SC", name: "Seychelles" },
|
||||
{ code: "KM", name: "Comoros" },
|
||||
{ code: "DJ", name: "Djibouti" },
|
||||
{ code: "SO", name: "Somalia" },
|
||||
{ code: "ER", name: "Eritrea" },
|
||||
{ code: "SD", name: "Sudan" },
|
||||
{ code: "SS", name: "South Sudan" },
|
||||
{ code: "LY", name: "Libya" },
|
||||
{ code: "TN", name: "Tunisia" },
|
||||
{ code: "DZ", name: "Algeria" },
|
||||
{ code: "MA", name: "Morocco" },
|
||||
{ code: "EH", name: "Western Sahara" },
|
||||
{ code: "MR", name: "Mauritania" },
|
||||
{ code: "GM", name: "Gambia" },
|
||||
{ code: "GW", name: "Guinea-Bissau" },
|
||||
{ code: "GN", name: "Guinea" },
|
||||
{ code: "SL", name: "Sierra Leone" },
|
||||
{ code: "LR", name: "Liberia" },
|
||||
{ code: "TG", name: "Togo" },
|
||||
{ code: "BJ", name: "Benin" },
|
||||
{ code: "BR", name: "Brazil" },
|
||||
{ code: "AR", name: "Argentina" },
|
||||
{ code: "CL", name: "Chile" },
|
||||
{ code: "CO", name: "Colombia" },
|
||||
{ code: "PE", name: "Peru" },
|
||||
{ code: "VE", name: "Venezuela" },
|
||||
{ code: "EC", name: "Ecuador" },
|
||||
{ code: "BO", name: "Bolivia" },
|
||||
{ code: "PY", name: "Paraguay" },
|
||||
{ code: "UY", name: "Uruguay" },
|
||||
{ code: "GY", name: "Guyana" },
|
||||
{ code: "SR", name: "Suriname" },
|
||||
{ code: "FK", name: "Falkland Islands" },
|
||||
{ code: "GF", name: "French Guiana" },
|
||||
{ code: "MX", name: "Mexico" },
|
||||
{ code: "GT", name: "Guatemala" },
|
||||
{ code: "BZ", name: "Belize" },
|
||||
{ code: "SV", name: "El Salvador" },
|
||||
{ code: "HN", name: "Honduras" },
|
||||
{ code: "NI", name: "Nicaragua" },
|
||||
{ code: "CR", name: "Costa Rica" },
|
||||
{ code: "PA", name: "Panama" },
|
||||
{ code: "CU", name: "Cuba" },
|
||||
{ code: "JM", name: "Jamaica" },
|
||||
{ code: "HT", name: "Haiti" },
|
||||
{ code: "DO", name: "Dominican Republic" },
|
||||
{ code: "PR", name: "Puerto Rico" },
|
||||
{ code: "TT", name: "Trinidad and Tobago" },
|
||||
{ code: "BB", name: "Barbados" },
|
||||
{ code: "GD", name: "Grenada" },
|
||||
{ code: "LC", name: "Saint Lucia" },
|
||||
{ code: "VC", name: "Saint Vincent and the Grenadines" },
|
||||
{ code: "AG", name: "Antigua and Barbuda" },
|
||||
{ code: "KN", name: "Saint Kitts and Nevis" },
|
||||
{ code: "DM", name: "Dominica" },
|
||||
{ code: "BS", name: "Bahamas" },
|
||||
{ code: "TC", name: "Turks and Caicos Islands" },
|
||||
{ code: "KY", name: "Cayman Islands" },
|
||||
{ code: "BM", name: "Bermuda" },
|
||||
{ code: "AI", name: "Anguilla" },
|
||||
{ code: "VG", name: "British Virgin Islands" },
|
||||
{ code: "VI", name: "U.S. Virgin Islands" },
|
||||
{ code: "AW", name: "Aruba" },
|
||||
{ code: "CW", name: "Curaçao" },
|
||||
{ code: "SX", name: "Sint Maarten" },
|
||||
{ code: "MF", name: "Saint Martin" },
|
||||
{ code: "BL", name: "Saint Barthélemy" },
|
||||
{ code: "GP", name: "Guadeloupe" },
|
||||
{ code: "MQ", name: "Martinique" }
|
||||
];
|
||||
|
||||
async function populateHolidays() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log("Starting holiday population...");
|
||||
|
||||
for (const country of countries) {
|
||||
console.log(`Processing ${country.name} (${country.code})...`);
|
||||
|
||||
try {
|
||||
const hd = new Holidays(country.code);
|
||||
|
||||
// Get holidays for multiple years (2020-2030)
|
||||
for (let year = 2020; year <= 2030; year++) {
|
||||
const holidays = hd.getHolidays(year);
|
||||
|
||||
for (const holiday of holidays) {
|
||||
// Skip if holiday is not a date object
|
||||
if (!holiday.date || typeof holiday.date !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dateStr = holiday.date.toISOString().split("T")[0];
|
||||
const name = holiday.name || "Unknown Holiday";
|
||||
const description = holiday.type || "Public Holiday";
|
||||
|
||||
// Insert holiday into database
|
||||
const query = `
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING
|
||||
`;
|
||||
|
||||
await client.query(query, [
|
||||
country.code,
|
||||
name,
|
||||
description,
|
||||
dateStr,
|
||||
true // Most holidays are recurring
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ Completed ${country.name}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`✗ Error processing ${country.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Holiday population completed!");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Database error:", error);
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
populateHolidays().catch(console.error);
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🌍 Starting Holiday Population Script..."
|
||||
echo "This will populate the database with holidays for 200+ countries using the date-holidays npm package."
|
||||
echo ""
|
||||
|
||||
# Check if Node.js is installed
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "❌ Node.js is not installed. Please install Node.js first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the script exists
|
||||
if [ ! -f "scripts/populate-holidays.js" ]; then
|
||||
echo "❌ Holiday population script not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the holiday population script
|
||||
echo "🚀 Running holiday population script..."
|
||||
node scripts/populate-holidays.js
|
||||
|
||||
echo ""
|
||||
echo "✅ Holiday population completed!"
|
||||
echo "You can now use the holiday import feature in the admin center."
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,416 +0,0 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import {
|
||||
ICreateHolidayRequest,
|
||||
IUpdateHolidayRequest,
|
||||
IImportCountryHolidaysRequest,
|
||||
} from "../interfaces/holiday.interface";
|
||||
|
||||
export default class HolidayController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getHolidayTypes(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT id, name, description, color_code, created_at, updated_at
|
||||
FROM holiday_types
|
||||
ORDER BY name;`;
|
||||
const result = await db.query(q);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getOrganizationHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { year } = req.query;
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
|
||||
const params = year ? [req.user?.owner_id, year] : [req.user?.owner_id];
|
||||
|
||||
const q = `SELECT oh.id, oh.organization_id, oh.holiday_type_id, oh.name, oh.description,
|
||||
oh.date, oh.is_recurring, oh.created_at, oh.updated_at,
|
||||
ht.name as holiday_type_name, ht.color_code
|
||||
FROM organization_holidays oh
|
||||
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
|
||||
WHERE oh.organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
) ${yearFilter}
|
||||
ORDER BY oh.date;`;
|
||||
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async createOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { name, description, date, holiday_type_id, is_recurring = false }: ICreateHolidayRequest = req.body;
|
||||
|
||||
const q = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
|
||||
VALUES (
|
||||
(SELECT id FROM organizations WHERE user_id = $1),
|
||||
$2, $3, $4, $5, $6
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, holiday_type_id, name, description, date, is_recurring]);
|
||||
return res.status(201).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async updateOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { name, description, date, holiday_type_id, is_recurring }: IUpdateHolidayRequest = req.body;
|
||||
|
||||
const updateFields = [];
|
||||
const values = [req.user?.owner_id, id];
|
||||
let paramIndex = 3;
|
||||
|
||||
if (name !== undefined) {
|
||||
updateFields.push(`name = $${paramIndex++}`);
|
||||
values.push(name);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updateFields.push(`description = $${paramIndex++}`);
|
||||
values.push(description);
|
||||
}
|
||||
if (date !== undefined) {
|
||||
updateFields.push(`date = $${paramIndex++}`);
|
||||
values.push(date);
|
||||
}
|
||||
if (holiday_type_id !== undefined) {
|
||||
updateFields.push(`holiday_type_id = $${paramIndex++}`);
|
||||
values.push(holiday_type_id);
|
||||
}
|
||||
if (is_recurring !== undefined) {
|
||||
updateFields.push(`is_recurring = $${paramIndex++}`);
|
||||
values.push(is_recurring.toString());
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).send(new ServerResponse(false, "No fields to update"));
|
||||
}
|
||||
|
||||
const q = `UPDATE organization_holidays
|
||||
SET ${updateFields.join(", ")}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2 AND organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, values);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, "Holiday not found"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteOrganizationHoliday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
|
||||
const q = `DELETE FROM organization_holidays
|
||||
WHERE id = $2 AND organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
RETURNING id;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, "Holiday not found"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { message: "Holiday deleted successfully" }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { country_code, year } = req.query;
|
||||
|
||||
if (!country_code) {
|
||||
return res.status(400).send(new ServerResponse(false, "Country code is required"));
|
||||
}
|
||||
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
|
||||
const params = year ? [country_code, year] : [country_code];
|
||||
|
||||
const q = `SELECT id, country_code, name, description, date, is_recurring, created_at, updated_at
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1 ${yearFilter}
|
||||
ORDER BY date;`;
|
||||
|
||||
const result = await db.query(q, params);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getAvailableCountries(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `SELECT DISTINCT c.code, c.name
|
||||
FROM countries c
|
||||
JOIN country_holidays ch ON c.code = ch.country_code
|
||||
ORDER BY c.name;`;
|
||||
|
||||
const result = await db.query(q);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async importCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { country_code, year }: IImportCountryHolidaysRequest = req.body;
|
||||
|
||||
if (!country_code) {
|
||||
return res.status(400).send(new ServerResponse(false, "Country code is required"));
|
||||
}
|
||||
|
||||
// Get organization ID
|
||||
const orgQ = `SELECT id FROM organizations WHERE user_id = $1`;
|
||||
const orgResult = await db.query(orgQ, [req.user?.owner_id]);
|
||||
const organizationId = orgResult.rows[0]?.id;
|
||||
|
||||
if (!organizationId) {
|
||||
return res.status(404).send(new ServerResponse(false, "Organization not found"));
|
||||
}
|
||||
|
||||
// Get default holiday type (Public Holiday)
|
||||
const typeQ = `SELECT id FROM holiday_types WHERE name = 'Public Holiday' LIMIT 1`;
|
||||
const typeResult = await db.query(typeQ);
|
||||
const holidayTypeId = typeResult.rows[0]?.id;
|
||||
|
||||
if (!holidayTypeId) {
|
||||
return res.status(404).send(new ServerResponse(false, "Default holiday type not found"));
|
||||
}
|
||||
|
||||
// Get country holidays for the specified year
|
||||
const yearFilter = year ? `AND EXTRACT(YEAR FROM date) = $2` : "";
|
||||
const params = year ? [country_code, year] : [country_code];
|
||||
|
||||
const holidaysQ = `SELECT name, description, date, is_recurring
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1 ${yearFilter}`;
|
||||
|
||||
const holidaysResult = await db.query(holidaysQ, params);
|
||||
|
||||
if (holidaysResult.rows.length === 0) {
|
||||
return res.status(404).send(new ServerResponse(false, "No holidays found for this country and year"));
|
||||
}
|
||||
|
||||
// Import holidays to organization
|
||||
const importQ = `INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (organization_id, date) DO NOTHING`;
|
||||
|
||||
let importedCount = 0;
|
||||
for (const holiday of holidaysResult.rows) {
|
||||
try {
|
||||
await db.query(importQ, [
|
||||
organizationId,
|
||||
holidayTypeId,
|
||||
holiday.name,
|
||||
holiday.description,
|
||||
holiday.date,
|
||||
holiday.is_recurring
|
||||
]);
|
||||
importedCount++;
|
||||
} catch (error) {
|
||||
// Skip duplicates
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
message: `Successfully imported ${importedCount} holidays`,
|
||||
imported_count: importedCount
|
||||
}));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getHolidayCalendar(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { year, month } = req.query;
|
||||
|
||||
if (!year || !month) {
|
||||
return res.status(400).send(new ServerResponse(false, "Year and month are required"));
|
||||
}
|
||||
|
||||
const q = `SELECT oh.id, oh.name, oh.description, oh.date, oh.is_recurring,
|
||||
ht.name as holiday_type_name, ht.color_code,
|
||||
'organization' as source
|
||||
FROM organization_holidays oh
|
||||
JOIN holiday_types ht ON oh.holiday_type_id = ht.id
|
||||
WHERE oh.organization_id = (
|
||||
SELECT id FROM organizations WHERE user_id = $1
|
||||
)
|
||||
AND EXTRACT(YEAR FROM oh.date) = $2
|
||||
AND EXTRACT(MONTH FROM oh.date) = $3
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT ch.id, ch.name, ch.description, ch.date, ch.is_recurring,
|
||||
'Public Holiday' as holiday_type_name, '#f37070' as color_code,
|
||||
'country' as source
|
||||
FROM country_holidays ch
|
||||
JOIN organizations o ON ch.country_code = (
|
||||
SELECT c.code FROM countries c WHERE c.id = o.country
|
||||
)
|
||||
WHERE o.user_id = $1
|
||||
AND EXTRACT(YEAR FROM ch.date) = $2
|
||||
AND EXTRACT(MONTH FROM ch.date) = $3
|
||||
|
||||
ORDER BY date;`;
|
||||
|
||||
const result = await db.query(q, [req.user?.owner_id, year, month]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async populateCountryHolidays(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
// Check if this organization has recently populated holidays (within last hour)
|
||||
const recentPopulationCheck = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM organization_holidays
|
||||
WHERE organization_id = (SELECT id FROM organizations WHERE user_id = $1)
|
||||
AND created_at > NOW() - INTERVAL '1 hour'
|
||||
`;
|
||||
|
||||
const recentResult = await db.query(recentPopulationCheck, [req.user?.owner_id]);
|
||||
const recentCount = parseInt(recentResult.rows[0]?.count || '0');
|
||||
|
||||
// If there are recent holidays added, skip population
|
||||
if (recentCount > 10) {
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
success: true,
|
||||
message: "Holidays were recently populated, skipping to avoid duplicates",
|
||||
total_populated: 0,
|
||||
recently_populated: true
|
||||
}));
|
||||
}
|
||||
|
||||
const Holidays = require("date-holidays");
|
||||
|
||||
const countries = [
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "GB", name: "United Kingdom" },
|
||||
{ code: "CA", name: "Canada" },
|
||||
{ code: "AU", name: "Australia" },
|
||||
{ code: "DE", name: "Germany" },
|
||||
{ code: "FR", name: "France" },
|
||||
{ code: "IT", name: "Italy" },
|
||||
{ code: "ES", name: "Spain" },
|
||||
{ code: "NL", name: "Netherlands" },
|
||||
{ code: "BE", name: "Belgium" },
|
||||
{ code: "CH", name: "Switzerland" },
|
||||
{ code: "AT", name: "Austria" },
|
||||
{ code: "SE", name: "Sweden" },
|
||||
{ code: "NO", name: "Norway" },
|
||||
{ code: "DK", name: "Denmark" },
|
||||
{ code: "FI", name: "Finland" },
|
||||
{ code: "PL", name: "Poland" },
|
||||
{ code: "CZ", name: "Czech Republic" },
|
||||
{ code: "HU", name: "Hungary" },
|
||||
{ code: "RO", name: "Romania" },
|
||||
{ code: "BG", name: "Bulgaria" },
|
||||
{ code: "HR", name: "Croatia" },
|
||||
{ code: "SI", name: "Slovenia" },
|
||||
{ code: "SK", name: "Slovakia" },
|
||||
{ code: "LT", name: "Lithuania" },
|
||||
{ code: "LV", name: "Latvia" },
|
||||
{ code: "EE", name: "Estonia" },
|
||||
{ code: "IE", name: "Ireland" },
|
||||
{ code: "PT", name: "Portugal" },
|
||||
{ code: "GR", name: "Greece" },
|
||||
{ code: "CY", name: "Cyprus" },
|
||||
{ code: "MT", name: "Malta" },
|
||||
{ code: "LU", name: "Luxembourg" },
|
||||
{ code: "IS", name: "Iceland" },
|
||||
{ code: "CN", name: "China" },
|
||||
{ code: "JP", name: "Japan" },
|
||||
{ code: "KR", name: "South Korea" },
|
||||
{ code: "IN", name: "India" },
|
||||
{ code: "BR", name: "Brazil" },
|
||||
{ code: "AR", name: "Argentina" },
|
||||
{ code: "MX", name: "Mexico" },
|
||||
{ code: "ZA", name: "South Africa" },
|
||||
{ code: "NZ", name: "New Zealand" },
|
||||
{ code: "LK", name: "Sri Lanka" }
|
||||
];
|
||||
|
||||
let totalPopulated = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const country of countries) {
|
||||
try {
|
||||
// Special handling for Sri Lanka
|
||||
if (country.code === 'LK') {
|
||||
// Import the holiday data provider
|
||||
const { HolidayDataProvider } = require("../services/holiday-data-provider");
|
||||
|
||||
for (let year = 2020; year <= 2050; year++) {
|
||||
const sriLankanHolidays = await HolidayDataProvider.getSriLankanHolidays(year);
|
||||
|
||||
for (const holiday of sriLankanHolidays) {
|
||||
const query = `
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING
|
||||
`;
|
||||
|
||||
await db.query(query, [
|
||||
'LK',
|
||||
holiday.name,
|
||||
holiday.description,
|
||||
holiday.date,
|
||||
holiday.is_recurring
|
||||
]);
|
||||
|
||||
totalPopulated++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use date-holidays for other countries
|
||||
const hd = new Holidays(country.code);
|
||||
|
||||
for (let year = 2020; year <= 2050; year++) {
|
||||
const holidays = hd.getHolidays(year);
|
||||
|
||||
for (const holiday of holidays) {
|
||||
if (!holiday.date || typeof holiday.date !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dateStr = holiday.date.toISOString().split("T")[0];
|
||||
const name = holiday.name || "Unknown Holiday";
|
||||
const description = holiday.type || "Public Holiday";
|
||||
|
||||
const query = `
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING
|
||||
`;
|
||||
|
||||
await db.query(query, [
|
||||
country.code,
|
||||
name,
|
||||
description,
|
||||
dateStr,
|
||||
true
|
||||
]);
|
||||
|
||||
totalPopulated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
errors.push(`${country.name}: ${error?.message || "Unknown error"}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
message: `Successfully populated ${totalPopulated} holidays`,
|
||||
total_populated: totalPopulated,
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, response));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import {getColor} from "../shared/utils";
|
||||
import TeamMembersController from "./team-members-controller";
|
||||
import {checkTeamSubscriptionStatus} from "../shared/paddle-utils";
|
||||
import {updateUsers} from "../shared/paddle-requests";
|
||||
import {statusExclude, TRIAL_MEMBER_LIMIT} from "../shared/constants";
|
||||
import {statusExclude} from "../shared/constants";
|
||||
import {NotificationsService} from "../services/notifications/notifications.service";
|
||||
|
||||
export default class ProjectMembersController extends WorklenzControllerBase {
|
||||
@@ -118,17 +118,6 @@ export default class ProjectMembersController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Maximum number of life time users reached."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks trial user team member limit
|
||||
*/
|
||||
if (subscriptionData.subscription_status === "trialing") {
|
||||
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
|
||||
|
||||
if (currentTrialMembers + 1 > TRIAL_MEMBER_LIMIT) {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
|
||||
}
|
||||
}
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
if (!userExists && !subscriptionData.is_credit && !subscriptionData.is_custom && subscriptionData.subscription_status !== "trialing") {
|
||||
// if (subscriptionData.subscription_status === "active") {
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
import db from "../config/db";
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
|
||||
export default class ProjectRateCardController extends WorklenzControllerBase {
|
||||
|
||||
// Insert a single role for a project
|
||||
@HandleExceptions()
|
||||
public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id, job_title_id, rate, man_day_rate } = req.body;
|
||||
if (!project_id || !job_title_id || typeof rate !== "number") {
|
||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||
}
|
||||
|
||||
// Handle both rate and man_day_rate fields
|
||||
const columns = ["project_id", "job_title_id", "rate"];
|
||||
const values = [project_id, job_title_id, rate];
|
||||
|
||||
if (typeof man_day_rate !== "undefined") {
|
||||
columns.push("man_day_rate");
|
||||
values.push(man_day_rate);
|
||||
}
|
||||
|
||||
const q = `
|
||||
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
|
||||
VALUES (${values.map((_, i) => `$${i + 1}`).join(", ")})
|
||||
ON CONFLICT (project_id, job_title_id) DO UPDATE SET
|
||||
rate = EXCLUDED.rate${typeof man_day_rate !== "undefined" ? ", man_day_rate = EXCLUDED.man_day_rate" : ""}
|
||||
RETURNING *,
|
||||
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle;
|
||||
`;
|
||||
const result = await db.query(q, values);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
// Insert multiple roles for a project
|
||||
@HandleExceptions()
|
||||
public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id, roles } = req.body;
|
||||
if (!Array.isArray(roles) || !project_id) {
|
||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||
}
|
||||
|
||||
// Handle both rate and man_day_rate fields for each role
|
||||
const columns = ["project_id", "job_title_id", "rate", "man_day_rate"];
|
||||
const values = roles.map((role: any) => [
|
||||
project_id,
|
||||
role.job_title_id,
|
||||
typeof role.rate !== "undefined" ? role.rate : 0,
|
||||
typeof role.man_day_rate !== "undefined" ? role.man_day_rate : 0
|
||||
]);
|
||||
|
||||
const q = `
|
||||
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
|
||||
VALUES ${values.map((_, i) => `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`).join(",")}
|
||||
ON CONFLICT (project_id, job_title_id) DO UPDATE SET
|
||||
rate = EXCLUDED.rate,
|
||||
man_day_rate = EXCLUDED.man_day_rate
|
||||
RETURNING *,
|
||||
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle;
|
||||
`;
|
||||
const flatValues = values.flat();
|
||||
const result = await db.query(q, flatValues);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
// Get all roles for a project
|
||||
@HandleExceptions()
|
||||
public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id } = req.params;
|
||||
const q = `
|
||||
SELECT
|
||||
fprr.*,
|
||||
jt.name as jobtitle,
|
||||
(
|
||||
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
||||
FROM project_members pm
|
||||
WHERE pm.project_rate_card_role_id = fprr.id
|
||||
) AS members
|
||||
FROM finance_project_rate_card_roles fprr
|
||||
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
||||
WHERE fprr.project_id = $1
|
||||
ORDER BY fprr.created_at;
|
||||
`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
// Get a single role by id
|
||||
@HandleExceptions()
|
||||
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const q = `
|
||||
SELECT
|
||||
fprr.*,
|
||||
jt.name as jobtitle,
|
||||
(
|
||||
SELECT COALESCE(json_agg(pm.id), '[]'::json)
|
||||
FROM project_members pm
|
||||
WHERE pm.project_rate_card_role_id = fprr.id
|
||||
) AS members
|
||||
FROM finance_project_rate_card_roles fprr
|
||||
LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id
|
||||
WHERE fprr.id = $1;
|
||||
`;
|
||||
const result = await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
// Update a single role by id
|
||||
@HandleExceptions()
|
||||
public static async updateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const { job_title_id, rate, man_day_rate } = req.body;
|
||||
let setClause = "job_title_id = $1, updated_at = NOW()";
|
||||
const values = [job_title_id];
|
||||
if (typeof man_day_rate !== "undefined") {
|
||||
setClause += ", man_day_rate = $2";
|
||||
values.push(man_day_rate);
|
||||
} else {
|
||||
setClause += ", rate = $2";
|
||||
values.push(rate);
|
||||
}
|
||||
values.push(id);
|
||||
const q = `
|
||||
WITH updated AS (
|
||||
UPDATE finance_project_rate_card_roles
|
||||
SET ${setClause}
|
||||
WHERE id = $3
|
||||
RETURNING *
|
||||
),
|
||||
jobtitles AS (
|
||||
SELECT u.*, jt.name AS jobtitle
|
||||
FROM updated u
|
||||
JOIN job_titles jt ON jt.id = u.job_title_id
|
||||
),
|
||||
members AS (
|
||||
SELECT json_agg(pm.id) AS members, pm.project_rate_card_role_id
|
||||
FROM project_members pm
|
||||
WHERE pm.project_rate_card_role_id IN (SELECT id FROM jobtitles)
|
||||
GROUP BY pm.project_rate_card_role_id
|
||||
)
|
||||
SELECT jt.*, m.members
|
||||
FROM jobtitles jt
|
||||
LEFT JOIN members m ON m.project_rate_card_role_id = jt.id;
|
||||
`;
|
||||
const result = await db.query(q, values);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
// update project member rate for a project with members
|
||||
@HandleExceptions()
|
||||
public static async updateProjectMemberByProjectIdAndMemberId(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { project_id, id } = req.params;
|
||||
const { project_rate_card_role_id } = req.body;
|
||||
|
||||
if (!project_id || !id || !project_rate_card_role_id) {
|
||||
return res.status(400).send(new ServerResponse(false, null, "Missing values"));
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Check current role assignment
|
||||
const checkQuery = `
|
||||
SELECT project_rate_card_role_id
|
||||
FROM project_members
|
||||
WHERE id = $1 AND project_id = $2;
|
||||
`;
|
||||
const { rows: checkRows } = await db.query(checkQuery, [id, project_id]);
|
||||
|
||||
const currentRoleId = checkRows[0]?.project_rate_card_role_id;
|
||||
|
||||
if (currentRoleId !== null && currentRoleId !== project_rate_card_role_id) {
|
||||
// Step 2: Fetch members with the requested role
|
||||
const membersQuery = `
|
||||
SELECT COALESCE(json_agg(id), '[]'::json) AS members
|
||||
FROM project_members
|
||||
WHERE project_id = $1 AND project_rate_card_role_id = $2;
|
||||
`;
|
||||
const { rows: memberRows } = await db.query(membersQuery, [project_id, project_rate_card_role_id]);
|
||||
|
||||
return res.status(200).send(
|
||||
new ServerResponse(false, memberRows[0], "Already Assigned !")
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Perform the update
|
||||
const updateQuery = `
|
||||
UPDATE project_members
|
||||
SET project_rate_card_role_id = CASE
|
||||
WHEN project_rate_card_role_id = $1 THEN NULL
|
||||
ELSE $1
|
||||
END
|
||||
WHERE id = $2
|
||||
AND project_id = $3
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM finance_project_rate_card_roles
|
||||
WHERE id = $1 AND project_id = $3
|
||||
)
|
||||
RETURNING project_rate_card_role_id;
|
||||
`;
|
||||
const { rows: updateRows } = await db.query(updateQuery, [project_rate_card_role_id, id, project_id]);
|
||||
|
||||
if (updateRows.length === 0) {
|
||||
return res.status(200).send(new ServerResponse(true, [], "Project member not found or invalid project_rate_card_role_id"));
|
||||
}
|
||||
|
||||
const updatedRoleId = updateRows[0].project_rate_card_role_id || project_rate_card_role_id;
|
||||
|
||||
// Step 4: Fetch updated members list
|
||||
const membersQuery = `
|
||||
SELECT COALESCE(json_agg(id), '[]'::json) AS members
|
||||
FROM project_members
|
||||
WHERE project_id = $1 AND project_rate_card_role_id = $2;
|
||||
`;
|
||||
const { rows: finalMembers } = await db.query(membersQuery, [project_id, updatedRoleId]);
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, finalMembers[0]));
|
||||
} catch (error) {
|
||||
return res.status(500).send(new ServerResponse(false, null, "Internal server error"));
|
||||
}
|
||||
}
|
||||
// Update all roles for a project (delete then insert)
|
||||
@HandleExceptions()
|
||||
public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id, roles } = req.body;
|
||||
if (!Array.isArray(roles) || !project_id) {
|
||||
return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
|
||||
}
|
||||
if (roles.length === 0) {
|
||||
// If no roles provided, do nothing and return empty array
|
||||
return res.status(200).send(new ServerResponse(true, []));
|
||||
}
|
||||
// Build upsert query for all roles
|
||||
const columns = ["project_id", "job_title_id", "rate", "man_day_rate"];
|
||||
const values = roles.map((role: any) => [
|
||||
project_id,
|
||||
role.job_title_id,
|
||||
typeof role.rate !== "undefined" ? role.rate : null,
|
||||
typeof role.man_day_rate !== "undefined" ? role.man_day_rate : null
|
||||
]);
|
||||
const q = `
|
||||
WITH upserted AS (
|
||||
INSERT INTO finance_project_rate_card_roles (${columns.join(", ")})
|
||||
VALUES ${values.map((_, i) => `($${i * 4 + 1}, $${i * 4 + 2}, $${i * 4 + 3}, $${i * 4 + 4})`).join(",")}
|
||||
ON CONFLICT (project_id, job_title_id)
|
||||
DO UPDATE SET rate = EXCLUDED.rate, man_day_rate = EXCLUDED.man_day_rate, updated_at = NOW()
|
||||
RETURNING *
|
||||
),
|
||||
jobtitles AS (
|
||||
SELECT upr.*, jt.name AS jobtitle
|
||||
FROM upserted upr
|
||||
JOIN job_titles jt ON jt.id = upr.job_title_id
|
||||
),
|
||||
members AS (
|
||||
SELECT json_agg(pm.id) AS members, pm.project_rate_card_role_id
|
||||
FROM project_members pm
|
||||
WHERE pm.project_rate_card_role_id IN (SELECT id FROM jobtitles)
|
||||
GROUP BY pm.project_rate_card_role_id
|
||||
)
|
||||
SELECT jt.*, m.members
|
||||
FROM jobtitles jt
|
||||
LEFT JOIN members m ON m.project_rate_card_role_id = jt.id;
|
||||
`;
|
||||
const flatValues = values.flat();
|
||||
const result = await db.query(q, flatValues);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
// Delete a single role by id
|
||||
@HandleExceptions()
|
||||
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { id } = req.params;
|
||||
const q = `DELETE FROM finance_project_rate_card_roles WHERE id = $1 RETURNING *;`;
|
||||
const result = await db.query(q, [id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||
}
|
||||
|
||||
// Delete all roles for a project
|
||||
@HandleExceptions()
|
||||
public static async deleteByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { project_id } = req.params;
|
||||
const q = `DELETE FROM finance_project_rate_card_roles WHERE project_id = $1 RETURNING *;`;
|
||||
const result = await db.query(q, [project_id]);
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot have more than ${projectsLimit} projects.`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const q = `SELECT create_project($1) AS project`;
|
||||
|
||||
req.body.team_id = req.user?.team_id || null;
|
||||
@@ -317,58 +317,65 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getMembersByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const {sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
|
||||
const search = (req.query.search || "").toString().trim();
|
||||
|
||||
let searchFilter = "";
|
||||
const params = [req.params.id, req.user?.team_id ?? null, size, offset];
|
||||
if (search) {
|
||||
searchFilter = `
|
||||
AND (
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) ILIKE '%' || $5 || '%'
|
||||
OR (SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) ILIKE '%' || $5 || '%'
|
||||
)
|
||||
`;
|
||||
params.push(search);
|
||||
}
|
||||
|
||||
const q = `
|
||||
WITH filtered_members AS (
|
||||
SELECT project_members.id,
|
||||
team_member_id,
|
||||
(SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS name,
|
||||
(SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS email,
|
||||
u.avatar_url,
|
||||
(SELECT COUNT(*) FROM tasks WHERE archived IS FALSE AND project_id = project_members.project_id AND id IN (SELECT task_id FROM tasks_assignees WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
|
||||
(SELECT COUNT(*) FROM tasks WHERE archived IS FALSE AND project_id = project_members.project_id AND id IN (SELECT task_id FROM tasks_assignees WHERE tasks_assignees.project_member_id = project_members.id) AND status_id IN (SELECT id FROM task_statuses WHERE category_id = (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
EXISTS(SELECT email FROM email_invitations WHERE team_member_id = project_members.team_member_id AND email_invitations.team_id = $2) AS pending_invitation,
|
||||
(SELECT project_access_levels.name FROM project_access_levels WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
|
||||
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1
|
||||
${search ? searchFilter : ""}
|
||||
)
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM filtered_members) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (
|
||||
SELECT * FROM filtered_members
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $3 OFFSET $4
|
||||
) t
|
||||
) AS data
|
||||
SELECT ROW_TO_JSON(rec) AS members
|
||||
FROM (SELECT COUNT(*) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (SELECT project_members.id,
|
||||
team_member_id,
|
||||
(SELECT name
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id),
|
||||
(SELECT email
|
||||
FROM team_member_info_view
|
||||
WHERE team_member_info_view.team_member_id = tm.id) AS email,
|
||||
u.avatar_url,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = project_members.project_id
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE archived IS FALSE
|
||||
AND project_id = project_members.project_id
|
||||
AND id IN (SELECT task_id
|
||||
FROM tasks_assignees
|
||||
WHERE tasks_assignees.project_member_id = project_members.id)
|
||||
AND status_id IN (SELECT id
|
||||
FROM task_statuses
|
||||
WHERE category_id = (SELECT id
|
||||
FROM sys_task_status_categories
|
||||
WHERE is_done IS TRUE))) AS completed_tasks_count,
|
||||
EXISTS(SELECT email
|
||||
FROM email_invitations
|
||||
WHERE team_member_id = project_members.team_member_id
|
||||
AND email_invitations.team_id = $2) AS pending_invitation,
|
||||
(SELECT project_access_levels.name
|
||||
FROM project_access_levels
|
||||
WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
|
||||
(SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
|
||||
FROM project_members
|
||||
INNER JOIN team_members tm ON project_members.team_member_id = tm.id
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
WHERE project_id = $1
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $3 OFFSET $4) t) AS data
|
||||
FROM project_members
|
||||
WHERE project_id = $1) rec;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, params);
|
||||
const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
for (const member of data?.data || []) {
|
||||
for (const member of data?.members.data || []) {
|
||||
member.progress = member.all_tasks_count > 0
|
||||
? ((member.completed_tasks_count / member.all_tasks_count) * 100).toFixed(0) : 0;
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data || this.paginatedDatasetDefaultStruct));
|
||||
return res.status(200).send(new ServerResponse(true, data?.members || this.paginatedDatasetDefaultStruct));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -388,8 +395,6 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
projects.folder_id,
|
||||
projects.phase_label,
|
||||
projects.category_id,
|
||||
projects.currency,
|
||||
projects.budget,
|
||||
(projects.estimated_man_days) AS man_days,
|
||||
(projects.estimated_working_days) AS working_days,
|
||||
(projects.hours_per_day) AS hours_per_day,
|
||||
@@ -774,7 +779,7 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
let groupJoin = "";
|
||||
let groupByFields = "";
|
||||
let groupOrderBy = "";
|
||||
|
||||
|
||||
switch (groupBy) {
|
||||
case "client":
|
||||
groupField = "COALESCE(projects.client_id::text, 'no-client')";
|
||||
@@ -883,13 +888,13 @@ export default class ProjectsController extends WorklenzControllerBase {
|
||||
ELSE p2.updated_at END) AS updated_at
|
||||
FROM projects p2
|
||||
${groupJoin.replace("projects.", "p2.")}
|
||||
WHERE p2.team_id = $1
|
||||
WHERE p2.team_id = $1
|
||||
AND ${groupField.replace("projects.", "p2.")} = ${groupField}
|
||||
${categories.replace("projects.", "p2.")}
|
||||
${statuses.replace("projects.", "p2.")}
|
||||
${isArchived.replace("projects.", "p2.")}
|
||||
${isFavorites.replace("projects.", "p2.")}
|
||||
${filterByMember.replace("projects.", "p2.")}
|
||||
${categories.replace("projects.", "p2.")}
|
||||
${statuses.replace("projects.", "p2.")}
|
||||
${isArchived.replace("projects.", "p2.")}
|
||||
${isFavorites.replace("projects.", "p2.")}
|
||||
${filterByMember.replace("projects.", "p2.")}
|
||||
${searchQuery.replace("projects.", "p2.")}
|
||||
ORDER BY ${innerSortField} ${sortOrder}
|
||||
) project_data
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import db from "../config/db";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
|
||||
export default class RateCardController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async create(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
INSERT INTO finance_rate_cards (team_id, name)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, name, team_id, created_at, updated_at;
|
||||
`;
|
||||
const result = await db.query(q, [
|
||||
req.user?.team_id || null,
|
||||
req.body.name,
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async get(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const { searchQuery, sortField, sortOrder, size, offset } =
|
||||
this.toPaginationOptions(req.query, "name");
|
||||
|
||||
const q = `
|
||||
SELECT ROW_TO_JSON(rec) AS rate_cards
|
||||
FROM (
|
||||
SELECT COUNT(*) AS total,
|
||||
(
|
||||
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
FROM (
|
||||
SELECT id, name, team_id, currency, created_at, updated_at
|
||||
FROM finance_rate_cards
|
||||
WHERE team_id = $1 ${searchQuery}
|
||||
ORDER BY ${sortField} ${sortOrder}
|
||||
LIMIT $2 OFFSET $3
|
||||
) t
|
||||
) AS data
|
||||
FROM finance_rate_cards
|
||||
WHERE team_id = $1 ${searchQuery}
|
||||
) rec;
|
||||
`;
|
||||
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
|
||||
const [data] = result.rows;
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.send(
|
||||
new ServerResponse(
|
||||
true,
|
||||
data.rate_cards || this.paginatedDatasetDefaultStruct
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getById(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
// 1. Fetch the rate card
|
||||
const q = `
|
||||
SELECT id, name, team_id, currency, created_at, updated_at
|
||||
FROM finance_rate_cards
|
||||
WHERE id = $1 AND team_id = $2;
|
||||
`;
|
||||
const result = await db.query(q, [
|
||||
req.params.id,
|
||||
req.user?.team_id || null,
|
||||
]);
|
||||
const [data] = result.rows;
|
||||
|
||||
if (!data) {
|
||||
return res
|
||||
.status(404)
|
||||
.send(new ServerResponse(false, null, "Rate card not found"));
|
||||
}
|
||||
|
||||
// 2. Fetch job roles with job title names
|
||||
const jobRolesQ = `
|
||||
SELECT
|
||||
rcr.job_title_id,
|
||||
jt.name AS jobTitle,
|
||||
rcr.rate,
|
||||
rcr.man_day_rate,
|
||||
rcr.rate_card_id
|
||||
FROM finance_rate_card_roles rcr
|
||||
LEFT JOIN job_titles jt ON rcr.job_title_id = jt.id
|
||||
WHERE rcr.rate_card_id = $1
|
||||
`;
|
||||
const jobRolesResult = await db.query(jobRolesQ, [req.params.id]);
|
||||
const jobRolesList = jobRolesResult.rows;
|
||||
|
||||
// 3. Return the rate card with jobRolesList
|
||||
return res.status(200).send(
|
||||
new ServerResponse(true, {
|
||||
...data,
|
||||
jobRolesList,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async update(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
// 1. Update the rate card
|
||||
const updateRateCardQ = `
|
||||
UPDATE finance_rate_cards
|
||||
SET name = $3, currency = $4, updated_at = NOW()
|
||||
WHERE id = $1 AND team_id = $2
|
||||
RETURNING id, name, team_id, currency, created_at, updated_at;
|
||||
`;
|
||||
const result = await db.query(updateRateCardQ, [
|
||||
req.params.id,
|
||||
req.user?.team_id || null,
|
||||
req.body.name,
|
||||
req.body.currency,
|
||||
]);
|
||||
const [rateCardData] = result.rows;
|
||||
|
||||
// 2. Update job roles (delete old, insert new)
|
||||
if (Array.isArray(req.body.jobRolesList)) {
|
||||
// Delete existing roles for this rate card
|
||||
await db.query(
|
||||
`DELETE FROM finance_rate_card_roles WHERE rate_card_id = $1;`,
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
// Insert new roles
|
||||
for (const role of req.body.jobRolesList) {
|
||||
if (role.job_title_id) {
|
||||
await db.query(
|
||||
`INSERT INTO finance_rate_card_roles (rate_card_id, job_title_id, rate, man_day_rate)
|
||||
VALUES ($1, $2, $3, $4);`,
|
||||
[
|
||||
req.params.id,
|
||||
role.job_title_id,
|
||||
role.rate ?? 0,
|
||||
role.man_day_rate ?? 0,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Get jobRolesList with job title names
|
||||
const jobRolesQ = `
|
||||
SELECT
|
||||
rcr.job_title_id,
|
||||
jt.name AS jobTitle,
|
||||
rcr.rate
|
||||
FROM finance_rate_card_roles rcr
|
||||
LEFT JOIN job_titles jt ON rcr.job_title_id = jt.id
|
||||
WHERE rcr.rate_card_id = $1
|
||||
`;
|
||||
const jobRolesResult = await db.query(jobRolesQ, [req.params.id]);
|
||||
const jobRolesList = jobRolesResult.rows;
|
||||
|
||||
// 4. Return the updated rate card with jobRolesList
|
||||
return res.status(200).send(
|
||||
new ServerResponse(true, {
|
||||
...rateCardData,
|
||||
jobRolesList,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async deleteById(
|
||||
req: IWorkLenzRequest,
|
||||
res: IWorkLenzResponse
|
||||
): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
DELETE FROM finance_rate_cards
|
||||
WHERE id = $1 AND team_id = $2
|
||||
RETURNING id;
|
||||
`;
|
||||
const result = await db.query(q, [
|
||||
req.params.id,
|
||||
req.user?.team_id || null,
|
||||
]);
|
||||
return res
|
||||
.status(200)
|
||||
.send(new ServerResponse(true, result.rows.length > 0));
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
// Example of updated getMemberTimeSheets method with timezone support
|
||||
// This shows the key changes needed to handle timezones properly
|
||||
|
||||
import moment from "moment-timezone";
|
||||
import db from "../../config/db";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { DATE_RANGES } from "../../shared/constants";
|
||||
|
||||
export async function getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const archived = req.query.archived === "true";
|
||||
const teams = (req.body.teams || []) as string[];
|
||||
const teamIds = teams.map(id => `'${id}'`).join(",");
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
const {billable} = req.body;
|
||||
|
||||
// Get user timezone from request or database
|
||||
const userTimezone = req.body.timezone || await getUserTimezone(req.user?.id || "");
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const { duration, date_range } = req.body;
|
||||
|
||||
// Calculate date range with timezone support
|
||||
let startDate: moment.Moment;
|
||||
let endDate: moment.Moment;
|
||||
|
||||
if (date_range && date_range.length === 2) {
|
||||
// Convert user's local dates to their timezone's start/end of day
|
||||
startDate = moment.tz(date_range[0], userTimezone).startOf("day");
|
||||
endDate = moment.tz(date_range[1], userTimezone).endOf("day");
|
||||
} else if (duration === DATE_RANGES.ALL_TIME) {
|
||||
const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`;
|
||||
const minDateResult = await db.query(minDateQuery, []);
|
||||
const minDate = minDateResult.rows[0]?.min_date;
|
||||
startDate = minDate ? moment.tz(minDate, userTimezone) : moment.tz("2000-01-01", userTimezone);
|
||||
endDate = moment.tz(userTimezone);
|
||||
} else {
|
||||
// Calculate ranges based on user's timezone
|
||||
const now = moment.tz(userTimezone);
|
||||
|
||||
switch (duration) {
|
||||
case DATE_RANGES.YESTERDAY:
|
||||
startDate = now.clone().subtract(1, "day").startOf("day");
|
||||
endDate = now.clone().subtract(1, "day").endOf("day");
|
||||
break;
|
||||
case DATE_RANGES.LAST_WEEK:
|
||||
startDate = now.clone().subtract(1, "week").startOf("isoWeek");
|
||||
endDate = now.clone().subtract(1, "week").endOf("isoWeek");
|
||||
break;
|
||||
case DATE_RANGES.LAST_MONTH:
|
||||
startDate = now.clone().subtract(1, "month").startOf("month");
|
||||
endDate = now.clone().subtract(1, "month").endOf("month");
|
||||
break;
|
||||
case DATE_RANGES.LAST_QUARTER:
|
||||
startDate = now.clone().subtract(3, "months").startOf("day");
|
||||
endDate = now.clone().endOf("day");
|
||||
break;
|
||||
default:
|
||||
startDate = now.clone().startOf("day");
|
||||
endDate = now.clone().endOf("day");
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to UTC for database queries
|
||||
const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
// Calculate working days in user's timezone
|
||||
const totalDays = endDate.diff(startDate, "days") + 1;
|
||||
let workingDays = 0;
|
||||
|
||||
const current = startDate.clone();
|
||||
while (current.isSameOrBefore(endDate, "day")) {
|
||||
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
|
||||
workingDays++;
|
||||
}
|
||||
current.add(1, "day");
|
||||
}
|
||||
|
||||
// Updated SQL query with proper timezone handling
|
||||
const billableQuery = buildBillableQuery(billable);
|
||||
const archivedClause = archived ? "" : `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${req.user?.id}')`;
|
||||
|
||||
const q = `
|
||||
WITH project_hours AS (
|
||||
SELECT
|
||||
id,
|
||||
COALESCE(hours_per_day, 8) as hours_per_day
|
||||
FROM projects
|
||||
WHERE id IN (${projectIds})
|
||||
),
|
||||
total_working_hours AS (
|
||||
SELECT
|
||||
SUM(hours_per_day) * ${workingDays} as total_hours
|
||||
FROM project_hours
|
||||
)
|
||||
SELECT
|
||||
u.id,
|
||||
u.email,
|
||||
tm.name,
|
||||
tm.color_code,
|
||||
COALESCE(SUM(twl.time_spent), 0) as logged_time,
|
||||
COALESCE(SUM(twl.time_spent), 0) / 3600.0 as value,
|
||||
(SELECT total_hours FROM total_working_hours) as total_working_hours,
|
||||
CASE
|
||||
WHEN (SELECT total_hours FROM total_working_hours) > 0
|
||||
THEN ROUND((COALESCE(SUM(twl.time_spent), 0) / 3600.0) / (SELECT total_hours FROM total_working_hours) * 100, 2)
|
||||
ELSE 0
|
||||
END as utilization_percent,
|
||||
ROUND(COALESCE(SUM(twl.time_spent), 0) / 3600.0, 2) as utilized_hours,
|
||||
ROUND(COALESCE(SUM(twl.time_spent), 0) / 3600.0 - (SELECT total_hours FROM total_working_hours), 2) as over_under_utilized_hours,
|
||||
'${userTimezone}' as user_timezone,
|
||||
'${startDate.format("YYYY-MM-DD")}' as report_start_date,
|
||||
'${endDate.format("YYYY-MM-DD")}' as report_end_date
|
||||
FROM team_members tm
|
||||
LEFT JOIN users u ON tm.user_id = u.id
|
||||
LEFT JOIN task_work_log twl ON twl.user_id = u.id
|
||||
LEFT JOIN tasks t ON twl.task_id = t.id ${billableQuery}
|
||||
LEFT JOIN projects p ON t.project_id = p.id
|
||||
WHERE tm.team_id IN (${teamIds})
|
||||
AND (
|
||||
twl.id IS NULL
|
||||
OR (
|
||||
p.id IN (${projectIds})
|
||||
AND twl.created_at >= '${startUtc}'::TIMESTAMP
|
||||
AND twl.created_at <= '${endUtc}'::TIMESTAMP
|
||||
${archivedClause}
|
||||
)
|
||||
)
|
||||
GROUP BY u.id, u.email, tm.name, tm.color_code
|
||||
ORDER BY logged_time DESC`;
|
||||
|
||||
const result = await db.query(q, []);
|
||||
|
||||
// Add timezone context to response
|
||||
const response = {
|
||||
data: result.rows,
|
||||
timezone_info: {
|
||||
user_timezone: userTimezone,
|
||||
report_period: {
|
||||
start: startDate.format("YYYY-MM-DD HH:mm:ss z"),
|
||||
end: endDate.format("YYYY-MM-DD HH:mm:ss z"),
|
||||
working_days: workingDays,
|
||||
total_days: totalDays
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, response));
|
||||
}
|
||||
|
||||
async function getUserTimezone(userId: string): Promise<string> {
|
||||
const q = `SELECT tz.name as timezone
|
||||
FROM users u
|
||||
JOIN timezones tz ON u.timezone_id = tz.id
|
||||
WHERE u.id = $1`;
|
||||
const result = await db.query(q, [userId]);
|
||||
return result.rows[0]?.timezone || "UTC";
|
||||
}
|
||||
|
||||
function buildBillableQuery(billable: { billable: boolean; nonBillable: boolean }): string {
|
||||
if (!billable) return "";
|
||||
|
||||
const { billable: isBillable, nonBillable } = billable;
|
||||
|
||||
if (isBillable && nonBillable) {
|
||||
return "";
|
||||
} else if (isBillable) {
|
||||
return " AND tasks.billable IS TRUE";
|
||||
} else if (nonBillable) {
|
||||
return " AND tasks.billable IS FALSE";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -15,25 +15,6 @@ enum IToggleOptions {
|
||||
}
|
||||
|
||||
export default class ReportingAllocationController extends ReportingControllerBase {
|
||||
// Helper method to build billable query with custom table alias
|
||||
private static buildBillableQueryWithAlias(selectedStatuses: { billable: boolean; nonBillable: boolean }, tableAlias: string = 'tasks'): string {
|
||||
const { billable, nonBillable } = selectedStatuses;
|
||||
|
||||
if (billable && nonBillable) {
|
||||
// Both are enabled, no need to filter
|
||||
return "";
|
||||
} else if (billable && !nonBillable) {
|
||||
// Only billable is enabled - show only billable tasks
|
||||
return ` AND ${tableAlias}.billable IS TRUE`;
|
||||
} else if (!billable && nonBillable) {
|
||||
// Only non-billable is enabled - show only non-billable tasks
|
||||
return ` AND ${tableAlias}.billable IS FALSE`;
|
||||
} else {
|
||||
// Neither selected - this shouldn't happen in normal UI flow
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = "", billable: { billable: boolean; nonBillable: boolean }): Promise<any> {
|
||||
try {
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
@@ -96,8 +77,8 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
sps.icon AS status_icon,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
AND project_id = projects.id ${billableQuery}) AS all_tasks_count,
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END ${billableQuery}
|
||||
AND project_id = projects.id) AS all_tasks_count,
|
||||
(SELECT COUNT(*)
|
||||
FROM tasks
|
||||
WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
|
||||
@@ -113,11 +94,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
SELECT name,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
|
||||
WHERE user_id = users.id
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
|
||||
WHERE user_id = users.id ${billableQuery}
|
||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||
AND tasks.project_id = projects.id
|
||||
${billableQuery}
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
@@ -141,11 +121,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const q = `(SELECT id,
|
||||
(SELECT COALESCE(SUM(time_spent), 0)
|
||||
FROM task_work_log
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id
|
||||
LEFT JOIN tasks ON task_work_log.task_id = tasks.id ${billableQuery}
|
||||
WHERE user_id = users.id
|
||||
AND CASE WHEN ($1 IS TRUE) THEN tasks.project_id IS NOT NULL ELSE tasks.archived = FALSE END
|
||||
AND tasks.project_id IN (${projectIds})
|
||||
${billableQuery}
|
||||
${duration}) AS time_logged
|
||||
FROM users
|
||||
WHERE id IN (${userIds})
|
||||
@@ -367,8 +346,6 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
const categories = (req.body.categories || []) as string[];
|
||||
const noCategory = req.body.noCategory || false;
|
||||
const billable = req.body.billable;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
@@ -384,33 +361,6 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
// Prepare projects filter
|
||||
let projectsFilter = "";
|
||||
if (projectIds.length > 0) {
|
||||
projectsFilter = `AND p.id IN (${projectIds})`;
|
||||
} else {
|
||||
// If no projects are selected, don't show any data
|
||||
projectsFilter = `AND 1=0`; // This will match no rows
|
||||
}
|
||||
|
||||
// Prepare categories filter - updated logic
|
||||
let categoriesFilter = "";
|
||||
if (categories.length > 0 && noCategory) {
|
||||
// Both specific categories and "No Category" are selected
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||
} else if (categories.length === 0 && noCategory) {
|
||||
// Only "No Category" is selected
|
||||
categoriesFilter = `AND p.category_id IS NULL`;
|
||||
} else if (categories.length > 0 && !noCategory) {
|
||||
// Only specific categories are selected
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||
} else {
|
||||
// categories.length === 0 && !noCategory - no categories selected, show nothing
|
||||
categoriesFilter = `AND 1=0`; // This will match no rows
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
@@ -418,15 +368,13 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
SUM(total_minutes) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
|
||||
const utilization = (req.body.utilization || []) as string[];
|
||||
|
||||
const data = [];
|
||||
|
||||
for (const project of result.rows) {
|
||||
@@ -453,12 +401,10 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
const categories = (req.body.categories || []) as string[];
|
||||
const noCategory = req.body.noCategory || false;
|
||||
const billable = req.body.billable;
|
||||
|
||||
if (!teamIds)
|
||||
return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } }));
|
||||
if (!teamIds || !projectIds.length)
|
||||
return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
|
||||
|
||||
const { duration, date_range } = req.body;
|
||||
|
||||
@@ -470,9 +416,7 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
endDate = moment(date_range[1]);
|
||||
} else if (duration === DATE_RANGES.ALL_TIME) {
|
||||
// Fetch the earliest start_date (or created_at if null) from selected projects
|
||||
const minDateQuery = projectIds.length > 0
|
||||
? `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`
|
||||
: `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE team_id IN (${teamIds})`;
|
||||
const minDateQuery = `SELECT MIN(COALESCE(start_date, created_at)) as min_date FROM projects WHERE id IN (${projectIds})`;
|
||||
const minDateResult = await db.query(minDateQuery, []);
|
||||
const minDate = minDateResult.rows[0]?.min_date;
|
||||
startDate = minDate ? moment(minDate) : moment('2000-01-01');
|
||||
@@ -501,368 +445,59 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
}
|
||||
}
|
||||
|
||||
// Get organization working days
|
||||
const orgWorkingDaysQuery = `
|
||||
SELECT monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
||||
FROM organization_working_days
|
||||
WHERE organization_id IN (
|
||||
SELECT t.organization_id
|
||||
FROM teams t
|
||||
WHERE t.id IN (${teamIds})
|
||||
LIMIT 1
|
||||
);
|
||||
`;
|
||||
const orgWorkingDaysResult = await db.query(orgWorkingDaysQuery, []);
|
||||
const workingDaysConfig = orgWorkingDaysResult.rows[0] || {
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: false,
|
||||
sunday: false
|
||||
};
|
||||
|
||||
// Get organization ID for holiday queries
|
||||
const orgIdQuery = `SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1`;
|
||||
const orgIdResult = await db.query(orgIdQuery, []);
|
||||
const organizationId = orgIdResult.rows[0]?.organization_id;
|
||||
|
||||
// Fetch organization holidays within the date range
|
||||
const orgHolidaysQuery = `
|
||||
SELECT date
|
||||
FROM organization_holidays
|
||||
WHERE organization_id = $1
|
||||
AND date >= $2::date
|
||||
AND date <= $3::date
|
||||
`;
|
||||
const orgHolidaysResult = await db.query(orgHolidaysQuery, [
|
||||
organizationId,
|
||||
startDate.format('YYYY-MM-DD'),
|
||||
endDate.format('YYYY-MM-DD')
|
||||
]);
|
||||
|
||||
// Fetch country/state holidays if auto-sync is enabled
|
||||
let countryStateHolidays: any[] = [];
|
||||
const holidaySettingsQuery = `
|
||||
SELECT country_code, state_code, auto_sync_holidays
|
||||
FROM organization_holiday_settings
|
||||
WHERE organization_id = $1
|
||||
`;
|
||||
const holidaySettingsResult = await db.query(holidaySettingsQuery, [organizationId]);
|
||||
const holidaySettings = holidaySettingsResult.rows[0];
|
||||
|
||||
if (holidaySettings?.auto_sync_holidays && holidaySettings.country_code) {
|
||||
// Fetch country holidays
|
||||
const countryHolidaysQuery = `
|
||||
SELECT date
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1
|
||||
AND (
|
||||
(is_recurring = false AND date >= $2::date AND date <= $3::date) OR
|
||||
(is_recurring = true AND
|
||||
EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN (
|
||||
SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date)
|
||||
FROM generate_series($2::date, $3::date, '1 day'::interval) d
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
const countryHolidaysResult = await db.query(countryHolidaysQuery, [
|
||||
holidaySettings.country_code,
|
||||
startDate.format('YYYY-MM-DD'),
|
||||
endDate.format('YYYY-MM-DD')
|
||||
]);
|
||||
countryStateHolidays = countryStateHolidays.concat(countryHolidaysResult.rows);
|
||||
|
||||
// Fetch state holidays if state_code is set
|
||||
if (holidaySettings.state_code) {
|
||||
const stateHolidaysQuery = `
|
||||
SELECT date
|
||||
FROM state_holidays
|
||||
WHERE country_code = $1 AND state_code = $2
|
||||
AND (
|
||||
(is_recurring = false AND date >= $3::date AND date <= $4::date) OR
|
||||
(is_recurring = true AND
|
||||
EXTRACT(MONTH FROM date) || '-' || EXTRACT(DAY FROM date) IN (
|
||||
SELECT EXTRACT(MONTH FROM d::date) || '-' || EXTRACT(DAY FROM d::date)
|
||||
FROM generate_series($3::date, $4::date, '1 day'::interval) d
|
||||
)
|
||||
)
|
||||
)
|
||||
`;
|
||||
const stateHolidaysResult = await db.query(stateHolidaysQuery, [
|
||||
holidaySettings.country_code,
|
||||
holidaySettings.state_code,
|
||||
startDate.format('YYYY-MM-DD'),
|
||||
endDate.format('YYYY-MM-DD')
|
||||
]);
|
||||
countryStateHolidays = countryStateHolidays.concat(stateHolidaysResult.rows);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Set of holiday dates for efficient lookup
|
||||
const holidayDates = new Set<string>();
|
||||
|
||||
// Add organization holidays
|
||||
orgHolidaysResult.rows.forEach(row => {
|
||||
holidayDates.add(moment(row.date).format('YYYY-MM-DD'));
|
||||
});
|
||||
|
||||
// Add country/state holidays (handling recurring holidays)
|
||||
countryStateHolidays.forEach(row => {
|
||||
const holidayDate = moment(row.date);
|
||||
if (row.is_recurring) {
|
||||
// For recurring holidays, check each year in the date range
|
||||
let checkDate = startDate.clone().month(holidayDate.month()).date(holidayDate.date());
|
||||
if (checkDate.isBefore(startDate)) {
|
||||
checkDate.add(1, 'year');
|
||||
}
|
||||
while (checkDate.isSameOrBefore(endDate)) {
|
||||
if (checkDate.isSameOrAfter(startDate)) {
|
||||
holidayDates.add(checkDate.format('YYYY-MM-DD'));
|
||||
}
|
||||
checkDate.add(1, 'year');
|
||||
}
|
||||
} else {
|
||||
holidayDates.add(holidayDate.format('YYYY-MM-DD'));
|
||||
}
|
||||
});
|
||||
|
||||
// Count working days based on organization settings, excluding holidays
|
||||
// Count only weekdays (Mon-Fri) in the period
|
||||
let workingDays = 0;
|
||||
let current = startDate.clone();
|
||||
while (current.isSameOrBefore(endDate, 'day')) {
|
||||
const day = current.isoWeekday();
|
||||
const currentDateStr = current.format('YYYY-MM-DD');
|
||||
|
||||
// Check if it's a working day AND not a holiday
|
||||
if (
|
||||
!holidayDates.has(currentDateStr) && (
|
||||
(day === 1 && workingDaysConfig.monday) ||
|
||||
(day === 2 && workingDaysConfig.tuesday) ||
|
||||
(day === 3 && workingDaysConfig.wednesday) ||
|
||||
(day === 4 && workingDaysConfig.thursday) ||
|
||||
(day === 5 && workingDaysConfig.friday) ||
|
||||
(day === 6 && workingDaysConfig.saturday) ||
|
||||
(day === 7 && workingDaysConfig.sunday)
|
||||
)
|
||||
) {
|
||||
workingDays++;
|
||||
}
|
||||
if (day >= 1 && day <= 5) workingDays++;
|
||||
current.add(1, 'day');
|
||||
}
|
||||
|
||||
// Get organization working hours
|
||||
const orgWorkingHoursQuery = `SELECT hours_per_day FROM organizations WHERE id = (SELECT t.organization_id FROM teams t WHERE t.id IN (${teamIds}) LIMIT 1)`;
|
||||
const orgWorkingHoursResult = await db.query(orgWorkingHoursQuery, []);
|
||||
const orgWorkingHours = orgWorkingHoursResult.rows[0]?.hours_per_day || 8;
|
||||
|
||||
// Calculate total working hours with minimum baseline for non-working day scenarios
|
||||
let totalWorkingHours = workingDays * orgWorkingHours;
|
||||
let isNonWorkingPeriod = false;
|
||||
|
||||
// If no working days but there might be logged time, set minimum baseline
|
||||
// This ensures that time logged on non-working days is treated as over-utilization
|
||||
// Business Logic: If someone works on weekends/holidays when workingDays = 0,
|
||||
// we use a minimal baseline (1 hour) so any logged time results in >100% utilization
|
||||
if (totalWorkingHours === 0) {
|
||||
totalWorkingHours = 1; // Minimal baseline to ensure over-utilization
|
||||
isNonWorkingPeriod = true;
|
||||
// Get hours_per_day for all selected projects
|
||||
const projectHoursQuery = `SELECT id, hours_per_day FROM projects WHERE id IN (${projectIds})`;
|
||||
const projectHoursResult = await db.query(projectHoursQuery, []);
|
||||
const projectHoursMap: Record<string, number> = {};
|
||||
for (const row of projectHoursResult.rows) {
|
||||
projectHoursMap[row.id] = row.hours_per_day || 8;
|
||||
}
|
||||
// Sum total working hours for all selected projects
|
||||
let totalWorkingHours = 0;
|
||||
for (const pid of Object.keys(projectHoursMap)) {
|
||||
totalWorkingHours += workingDays * projectHoursMap[pid];
|
||||
}
|
||||
|
||||
const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
|
||||
const archivedClause = archived
|
||||
? ""
|
||||
: `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
|
||||
|
||||
const billableQuery = this.buildBillableQueryWithAlias(billable, 't');
|
||||
const members = (req.body.members || []) as string[];
|
||||
|
||||
// Prepare members filter
|
||||
let membersFilter = "";
|
||||
if (members.length > 0) {
|
||||
const memberIds = members.map(id => `'${id}'`).join(",");
|
||||
membersFilter = `AND tmiv.team_member_id IN (${memberIds})`;
|
||||
} else {
|
||||
// If no members are selected, we should not show any data
|
||||
// This is different from other filters where no selection means "show all"
|
||||
// For members, no selection should mean "show none" to respect the UI filter state
|
||||
membersFilter = `AND 1=0`; // This will match no rows
|
||||
}
|
||||
// Note: Members filter works differently - when no members are selected, show nothing
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
// Create custom duration clause for twl table alias
|
||||
let customDurationClause = "";
|
||||
if (date_range && date_range.length === 2) {
|
||||
const start = moment(date_range[0]).format("YYYY-MM-DD");
|
||||
const end = moment(date_range[1]).format("YYYY-MM-DD");
|
||||
if (start === end) {
|
||||
customDurationClause = `AND twl.created_at::DATE = '${start}'::DATE`;
|
||||
} else {
|
||||
customDurationClause = `AND twl.created_at::DATE >= '${start}'::DATE AND twl.created_at < '${end}'::DATE + INTERVAL '1 day'`;
|
||||
}
|
||||
} else {
|
||||
const key = duration || DATE_RANGES.LAST_WEEK;
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND twl.created_at < CURRENT_DATE::DATE";
|
||||
else if (key === DATE_RANGES.LAST_WEEK)
|
||||
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
else if (key === DATE_RANGES.LAST_MONTH)
|
||||
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
else if (key === DATE_RANGES.LAST_QUARTER)
|
||||
customDurationClause = "AND twl.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
|
||||
}
|
||||
|
||||
// Prepare conditional filters for the subquery - only apply if selections are made
|
||||
let conditionalProjectsFilter = "";
|
||||
let conditionalCategoriesFilter = "";
|
||||
|
||||
// Only apply project filter if projects are actually selected
|
||||
if (projectIds.length > 0) {
|
||||
conditionalProjectsFilter = `AND p.id IN (${projectIds})`;
|
||||
}
|
||||
|
||||
// Only apply category filter if categories are selected or noCategory is true
|
||||
if (categories.length > 0 && noCategory) {
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
conditionalCategoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||
} else if (categories.length === 0 && noCategory) {
|
||||
conditionalCategoriesFilter = `AND p.category_id IS NULL`;
|
||||
} else if (categories.length > 0 && !noCategory) {
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
conditionalCategoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||
}
|
||||
// If no categories and no noCategory, don't filter by category (show all)
|
||||
|
||||
// Check if all filters are unchecked (Clear All scenario) - return no data to avoid overwhelming UI
|
||||
const hasProjectFilter = projectIds.length > 0;
|
||||
const hasCategoryFilter = categories.length > 0 || noCategory;
|
||||
const hasMemberFilter = members.length > 0;
|
||||
// Note: We'll check utilization filter after the query since it's applied post-processing
|
||||
|
||||
if (!hasProjectFilter && !hasCategoryFilter && !hasMemberFilter) {
|
||||
// Still need to check utilization filter, but we'll do a quick check
|
||||
const utilization = (req.body.utilization || []) as string[];
|
||||
const hasUtilizationFilter = utilization.length > 0;
|
||||
|
||||
if (!hasUtilizationFilter) {
|
||||
return res.status(200).send(new ServerResponse(true, { filteredRows: [], totals: { total_time_logs: "0", total_estimated_hours: "0", total_utilization: "0" } }));
|
||||
}
|
||||
}
|
||||
|
||||
// Modified query to start from team members and calculate filtered time logs
|
||||
// This query ensures ALL active team members are included, even if they have no logged time
|
||||
const q = `
|
||||
SELECT
|
||||
tmiv.team_member_id,
|
||||
tmiv.email,
|
||||
tmiv.name,
|
||||
COALESCE(
|
||||
(SELECT SUM(twl.time_spent)
|
||||
FROM task_work_log twl
|
||||
LEFT JOIN tasks t ON t.id = twl.task_id
|
||||
LEFT JOIN projects p ON p.id = t.project_id
|
||||
WHERE twl.user_id = tmiv.user_id
|
||||
${customDurationClause}
|
||||
${conditionalProjectsFilter}
|
||||
${conditionalCategoriesFilter}
|
||||
${archivedClause}
|
||||
${billableQuery}
|
||||
AND p.team_id = tmiv.team_id
|
||||
), 0
|
||||
) AS logged_time
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.team_id IN (${teamIds})
|
||||
AND tmiv.active = TRUE
|
||||
${membersFilter}
|
||||
GROUP BY tmiv.email, tmiv.name, tmiv.team_member_id, tmiv.user_id, tmiv.team_id
|
||||
ORDER BY logged_time DESC;`;
|
||||
|
||||
SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
|
||||
FROM team_member_info_view tmiv
|
||||
LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
|
||||
LEFT JOIN tasks ON tasks.id = task_work_log.task_id ${billableQuery}
|
||||
LEFT JOIN projects p ON p.id = tasks.project_id AND p.team_id = tmiv.team_id
|
||||
WHERE p.id IN (${projectIds})
|
||||
${durationClause} ${archivedClause}
|
||||
GROUP BY tmiv.email, tmiv.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
const utilization = (req.body.utilization || []) as string[];
|
||||
|
||||
// Precompute totalWorkingHours * 3600 for efficiency
|
||||
const totalWorkingSeconds = totalWorkingHours * 3600;
|
||||
|
||||
// calculate utilization state
|
||||
for (let i = 0, len = result.rows.length; i < len; i++) {
|
||||
const member = result.rows[i];
|
||||
const loggedSeconds = member.logged_time ? parseFloat(member.logged_time) : 0;
|
||||
const utilizedHours = loggedSeconds / 3600;
|
||||
|
||||
// For individual members, use the same logic as total calculation
|
||||
let memberWorkingHours;
|
||||
let utilizationPercent;
|
||||
|
||||
if (isNonWorkingPeriod) {
|
||||
// Non-working period: each member's expected working hours is 0
|
||||
memberWorkingHours = 0;
|
||||
// Any time logged during non-working period is overtime
|
||||
utilizationPercent = loggedSeconds > 0 ? 100 : 0; // Show 100+ as numeric 100 for consistency
|
||||
} else {
|
||||
// Normal working period
|
||||
memberWorkingHours = totalWorkingHours;
|
||||
utilizationPercent = memberWorkingHours > 0 && loggedSeconds
|
||||
? ((loggedSeconds / (memberWorkingHours * 3600)) * 100)
|
||||
: 0;
|
||||
}
|
||||
const overUnder = utilizedHours - memberWorkingHours;
|
||||
|
||||
member.value = utilizedHours ? parseFloat(utilizedHours.toFixed(2)) : 0;
|
||||
for (const member of result.rows) {
|
||||
member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0;
|
||||
member.color_code = getColor(member.name);
|
||||
member.total_working_hours = memberWorkingHours;
|
||||
member.utilization_percent = utilizationPercent.toFixed(2);
|
||||
member.utilized_hours = utilizedHours.toFixed(2);
|
||||
member.total_working_hours = totalWorkingHours;
|
||||
member.utilization_percent = (totalWorkingHours > 0 && member.logged_time) ? ((parseFloat(member.logged_time) / (totalWorkingHours * 3600)) * 100).toFixed(2) : '0.00';
|
||||
member.utilized_hours = member.logged_time ? (parseFloat(member.logged_time) / 3600).toFixed(2) : '0.00';
|
||||
// Over/under utilized hours: utilized_hours - total_working_hours
|
||||
const overUnder = member.utilized_hours && member.total_working_hours ? (parseFloat(member.utilized_hours) - member.total_working_hours) : 0;
|
||||
member.over_under_utilized_hours = overUnder.toFixed(2);
|
||||
|
||||
if (utilizationPercent < 90) {
|
||||
member.utilization_state = 'under';
|
||||
} else if (utilizationPercent <= 110) {
|
||||
member.utilization_state = 'optimal';
|
||||
} else {
|
||||
member.utilization_state = 'over';
|
||||
}
|
||||
}
|
||||
|
||||
// Apply utilization filter
|
||||
let filteredRows;
|
||||
if (utilization.length > 0) {
|
||||
// Filter to only show selected utilization states
|
||||
filteredRows = result.rows.filter(member => utilization.includes(member.utilization_state));
|
||||
} else {
|
||||
// No utilization states selected
|
||||
// If we reached here, it means at least one other filter was applied
|
||||
// so we show all members (don't filter by utilization)
|
||||
filteredRows = result.rows;
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
const total_time_logs = filteredRows.reduce((sum, member) => sum + parseFloat(member.logged_time || '0'), 0);
|
||||
|
||||
let total_estimated_hours;
|
||||
let total_utilization;
|
||||
|
||||
if (isNonWorkingPeriod) {
|
||||
// Non-working period: expected capacity is 0
|
||||
total_estimated_hours = 0;
|
||||
// Special handling for utilization on non-working days
|
||||
total_utilization = total_time_logs > 0 ? "100+" : "0";
|
||||
} else {
|
||||
// Normal working period calculation
|
||||
total_estimated_hours = totalWorkingHours * filteredRows.length;
|
||||
total_utilization = total_time_logs > 0 && total_estimated_hours > 0
|
||||
? ((total_time_logs / (total_estimated_hours * 3600)) * 100).toFixed(1)
|
||||
: '0';
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, {
|
||||
filteredRows,
|
||||
totals: {
|
||||
total_time_logs: ((total_time_logs / 3600).toFixed(2)).toString(),
|
||||
total_estimated_hours: total_estimated_hours.toString(),
|
||||
total_utilization: total_utilization.toString(),
|
||||
},
|
||||
}));
|
||||
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -945,9 +580,6 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
|
||||
const projects = (req.body.projects || []) as string[];
|
||||
const projectIds = projects.map(p => `'${p}'`).join(",");
|
||||
|
||||
const categories = (req.body.categories || []) as string[];
|
||||
const noCategory = req.body.noCategory || false;
|
||||
const { type, billable } = req.body;
|
||||
|
||||
if (!teamIds || !projectIds.length)
|
||||
@@ -963,33 +595,6 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
|
||||
// Prepare projects filter
|
||||
let projectsFilter = "";
|
||||
if (projectIds.length > 0) {
|
||||
projectsFilter = `AND p.id IN (${projectIds})`;
|
||||
} else {
|
||||
// If no projects are selected, don't show any data
|
||||
projectsFilter = `AND 1=0`; // This will match no rows
|
||||
}
|
||||
|
||||
// Prepare categories filter - updated logic
|
||||
let categoriesFilter = "";
|
||||
if (categories.length > 0 && noCategory) {
|
||||
// Both specific categories and "No Category" are selected
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
categoriesFilter = `AND (p.category_id IS NULL OR p.category_id IN (${categoryIds}))`;
|
||||
} else if (categories.length === 0 && noCategory) {
|
||||
// Only "No Category" is selected
|
||||
categoriesFilter = `AND p.category_id IS NULL`;
|
||||
} else if (categories.length > 0 && !noCategory) {
|
||||
// Only specific categories are selected
|
||||
const categoryIds = categories.map(id => `'${id}'`).join(",");
|
||||
categoriesFilter = `AND p.category_id IN (${categoryIds})`;
|
||||
} else {
|
||||
// categories.length === 0 && !noCategory - no categories selected, show nothing
|
||||
categoriesFilter = `AND 1=0`; // This will match no rows
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
@@ -1003,9 +608,9 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
WHERE project_id = p.id) AS estimated,
|
||||
color_code
|
||||
FROM projects p
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id
|
||||
LEFT JOIN tasks ON tasks.project_id = p.id ${billableQuery}
|
||||
LEFT JOIN task_work_log ON task_work_log.task_id = tasks.id
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause} ${categoriesFilter} ${billableQuery}
|
||||
WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY logged_time DESC;`;
|
||||
const result = await db.query(q, []);
|
||||
@@ -1031,4 +636,4 @@ export default class ReportingAllocationController extends ReportingControllerBa
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import WorklenzControllerBase from "../worklenz-controller-base";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import db from "../../config/db";
|
||||
import moment from "moment-timezone";
|
||||
import { DATE_RANGES } from "../../shared/constants";
|
||||
|
||||
export default abstract class ReportingControllerBaseWithTimezone extends WorklenzControllerBase {
|
||||
|
||||
/**
|
||||
* Get the user's timezone from the database or request
|
||||
* @param userId - The user ID
|
||||
* @returns The user's timezone or 'UTC' as default
|
||||
*/
|
||||
protected static async getUserTimezone(userId: string): Promise<string> {
|
||||
const q = `SELECT tz.name as timezone
|
||||
FROM users u
|
||||
JOIN timezones tz ON u.timezone_id = tz.id
|
||||
WHERE u.id = $1`;
|
||||
const result = await db.query(q, [userId]);
|
||||
return result.rows[0]?.timezone || "UTC";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate date range clause with timezone support
|
||||
* @param key - Date range key (e.g., YESTERDAY, LAST_WEEK)
|
||||
* @param dateRange - Array of date strings
|
||||
* @param userTimezone - User's timezone (e.g., 'America/New_York')
|
||||
* @returns SQL clause for date filtering
|
||||
*/
|
||||
protected static getDateRangeClauseWithTimezone(key: string, dateRange: string[], userTimezone: string) {
|
||||
// For custom date ranges
|
||||
if (dateRange.length === 2) {
|
||||
try {
|
||||
// Handle different date formats that might come from frontend
|
||||
let startDate, endDate;
|
||||
|
||||
// Try to parse the date - it might be a full JS Date string or ISO string
|
||||
if (dateRange[0].includes("GMT") || dateRange[0].includes("(")) {
|
||||
// Parse JavaScript Date toString() format
|
||||
startDate = moment(new Date(dateRange[0]));
|
||||
endDate = moment(new Date(dateRange[1]));
|
||||
} else {
|
||||
// Parse ISO format or other formats
|
||||
startDate = moment(dateRange[0]);
|
||||
endDate = moment(dateRange[1]);
|
||||
}
|
||||
|
||||
// Convert to user's timezone and get start/end of day
|
||||
const start = startDate.tz(userTimezone).startOf("day");
|
||||
const end = endDate.tz(userTimezone).endOf("day");
|
||||
|
||||
// Convert to UTC for database comparison
|
||||
const startUtc = start.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const endUtc = end.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
if (start.isSame(end, "day")) {
|
||||
// Single day selection
|
||||
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
|
||||
}
|
||||
|
||||
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
|
||||
} catch (error) {
|
||||
console.error("Error parsing date range:", error, { dateRange, userTimezone });
|
||||
// Fallback to current date if parsing fails
|
||||
const now = moment.tz(userTimezone);
|
||||
const startUtc = now.clone().startOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const endUtc = now.clone().endOf("day").utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
|
||||
}
|
||||
}
|
||||
|
||||
// For predefined ranges, calculate based on user's timezone
|
||||
const now = moment.tz(userTimezone);
|
||||
let startDate, endDate;
|
||||
|
||||
switch (key) {
|
||||
case DATE_RANGES.YESTERDAY:
|
||||
startDate = now.clone().subtract(1, "day").startOf("day");
|
||||
endDate = now.clone().subtract(1, "day").endOf("day");
|
||||
break;
|
||||
case DATE_RANGES.LAST_WEEK:
|
||||
startDate = now.clone().subtract(1, "week").startOf("week");
|
||||
endDate = now.clone().subtract(1, "week").endOf("week");
|
||||
break;
|
||||
case DATE_RANGES.LAST_MONTH:
|
||||
startDate = now.clone().subtract(1, "month").startOf("month");
|
||||
endDate = now.clone().subtract(1, "month").endOf("month");
|
||||
break;
|
||||
case DATE_RANGES.LAST_QUARTER:
|
||||
startDate = now.clone().subtract(3, "months").startOf("day");
|
||||
endDate = now.clone().endOf("day");
|
||||
break;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
const startUtc = startDate.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const endUtc = endDate.utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
return `AND twl.created_at >= '${startUtc}'::TIMESTAMP AND twl.created_at <= '${endUtc}'::TIMESTAMP`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format dates for display in user's timezone
|
||||
* @param date - Date to format
|
||||
* @param userTimezone - User's timezone
|
||||
* @param format - Moment format string
|
||||
* @returns Formatted date string
|
||||
*/
|
||||
protected static formatDateInTimezone(date: string | Date, userTimezone: string, format = "YYYY-MM-DD HH:mm:ss") {
|
||||
return moment.tz(date, userTimezone).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get working days count between two dates in user's timezone
|
||||
* @param startDate - Start date
|
||||
* @param endDate - End date
|
||||
* @param userTimezone - User's timezone
|
||||
* @returns Number of working days
|
||||
*/
|
||||
protected static getWorkingDaysInTimezone(startDate: string, endDate: string, userTimezone: string): number {
|
||||
const start = moment.tz(startDate, userTimezone);
|
||||
const end = moment.tz(endDate, userTimezone);
|
||||
let workingDays = 0;
|
||||
|
||||
const current = start.clone();
|
||||
while (current.isSameOrBefore(end, "day")) {
|
||||
// Monday = 1, Friday = 5
|
||||
if (current.isoWeekday() >= 1 && current.isoWeekday() <= 5) {
|
||||
workingDays++;
|
||||
}
|
||||
current.add(1, "day");
|
||||
}
|
||||
|
||||
return workingDays;
|
||||
}
|
||||
}
|
||||
@@ -6,69 +6,10 @@ import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
|
||||
import { formatDuration, getColor, int } from "../../shared/utils";
|
||||
import ReportingControllerBaseWithTimezone from "./reporting-controller-base-with-timezone";
|
||||
import ReportingControllerBase from "./reporting-controller-base";
|
||||
import Excel from "exceljs";
|
||||
|
||||
export default class ReportingMembersController extends ReportingControllerBaseWithTimezone {
|
||||
|
||||
protected static getPercentage(n: number, total: number) {
|
||||
return +(n ? (n / total) * 100 : 0).toFixed();
|
||||
}
|
||||
|
||||
protected static getCurrentTeamId(req: IWorkLenzRequest): string | null {
|
||||
return req.user?.team_id ?? null;
|
||||
}
|
||||
|
||||
public static convertMinutesToHoursAndMinutes(totalMinutes: number) {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
public static convertSecondsToHoursAndMinutes(seconds: number) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
protected static formatEndDate(endDate: string) {
|
||||
const end = moment(endDate).format("YYYY-MM-DD");
|
||||
const fEndDate = moment(end);
|
||||
return fEndDate;
|
||||
}
|
||||
|
||||
protected static formatCurrentDate() {
|
||||
const current = moment().format("YYYY-MM-DD");
|
||||
const fCurrentDate = moment(current);
|
||||
return fCurrentDate;
|
||||
}
|
||||
|
||||
protected static getDaysLeft(endDate: string): number | null {
|
||||
if (!endDate) return null;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.diff(fCurrentDate, "days");
|
||||
}
|
||||
|
||||
protected static isOverdue(endDate: string): boolean {
|
||||
if (!endDate) return false;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.isBefore(fCurrentDate);
|
||||
}
|
||||
|
||||
protected static isToday(endDate: string): boolean {
|
||||
if (!endDate) return false;
|
||||
|
||||
const fCurrentDate = this.formatCurrentDate();
|
||||
const fEndDate = this.formatEndDate(endDate);
|
||||
|
||||
return fEndDate.isSame(fCurrentDate);
|
||||
}
|
||||
export default class ReportingMembersController extends ReportingControllerBase {
|
||||
|
||||
private static async getMembers(
|
||||
teamId: string, searchQuery = "",
|
||||
@@ -90,7 +31,6 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
const completedDurationClasue = this.completedDurationFilter(key, dateRange);
|
||||
const overdueActivityLogsClause = this.getActivityLogsOverdue(key, dateRange);
|
||||
const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
|
||||
const timeLogDateRangeClause = this.getTimeLogDateRangeClause(key, dateRange);
|
||||
|
||||
const q = `SELECT COUNT(DISTINCT email) AS total,
|
||||
(SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
|
||||
@@ -160,27 +100,12 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
FROM tasks t
|
||||
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||
WHERE team_member_id = tmiv.team_member_id
|
||||
AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs,
|
||||
|
||||
(SELECT COALESCE(SUM(twl.time_spent), 0)
|
||||
FROM task_work_log twl
|
||||
LEFT JOIN tasks t ON twl.task_id = t.id
|
||||
WHERE twl.user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
|
||||
AND t.billable IS TRUE
|
||||
AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
|
||||
${timeLogDateRangeClause}
|
||||
${includeArchived ? "" : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`}) AS billable_time,
|
||||
|
||||
(SELECT COALESCE(SUM(twl.time_spent), 0)
|
||||
FROM task_work_log twl
|
||||
LEFT JOIN tasks t ON twl.task_id = t.id
|
||||
WHERE twl.user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
|
||||
AND t.billable IS FALSE
|
||||
AND t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
|
||||
${timeLogDateRangeClause}
|
||||
${includeArchived ? "" : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`}) AS non_billable_time
|
||||
AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.team_id = $1 ${teamsClause}
|
||||
AND tmiv.team_member_id IN (SELECT team_member_id
|
||||
FROM project_members
|
||||
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
|
||||
${searchQuery}
|
||||
GROUP BY email, name, avatar_url, team_member_id, tmiv.team_id
|
||||
ORDER BY last_user_activity DESC NULLS LAST
|
||||
@@ -188,6 +113,9 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
${pagingClause}) t) AS members
|
||||
FROM team_member_info_view tmiv
|
||||
WHERE tmiv.team_id = $1 ${teamsClause}
|
||||
AND tmiv.team_member_id IN (SELECT team_member_id
|
||||
FROM project_members
|
||||
WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
|
||||
${searchQuery}`;
|
||||
const result = await db.query(q, [teamId]);
|
||||
const [data] = result.rows;
|
||||
@@ -383,30 +311,6 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
return "";
|
||||
}
|
||||
|
||||
protected static getTimeLogDateRangeClause(key: string, dateRange: string[]) {
|
||||
if (dateRange.length === 2) {
|
||||
const start = moment(dateRange[0]).format("YYYY-MM-DD");
|
||||
const end = moment(dateRange[1]).format("YYYY-MM-DD");
|
||||
|
||||
if (start === end) {
|
||||
return `AND twl.created_at::DATE = '${start}'::DATE`;
|
||||
}
|
||||
|
||||
return `AND twl.created_at::DATE >= '${start}'::DATE AND twl.created_at < '${end}'::DATE + INTERVAL '1 day'`;
|
||||
}
|
||||
|
||||
if (key === DATE_RANGES.YESTERDAY)
|
||||
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND twl.created_at < CURRENT_DATE::DATE`;
|
||||
if (key === DATE_RANGES.LAST_WEEK)
|
||||
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_MONTH)
|
||||
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
if (key === DATE_RANGES.LAST_QUARTER)
|
||||
return `AND twl.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND twl.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private static formatDuration(duration: moment.Duration) {
|
||||
const empty = "0h 0m";
|
||||
let format = "";
|
||||
@@ -519,8 +423,6 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
{ header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
|
||||
{ header: "Completed Tasks", key: "completed_tasks", width: 20 },
|
||||
{ header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
|
||||
{ header: "Billable Time (seconds)", key: "billable_time", width: 25 },
|
||||
{ header: "Non-Billable Time (seconds)", key: "non_billable_time", width: 25 },
|
||||
{ header: "Done Tasks(%)", key: "done_tasks", width: 20 },
|
||||
{ header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
|
||||
{ header: "Todo Tasks(%)", key: "todo_tasks", width: 20 }
|
||||
@@ -528,14 +430,14 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
|
||||
// set title
|
||||
sheet.getCell("A1").value = `Members from ${teamName}`;
|
||||
sheet.mergeCells("A1:M1");
|
||||
sheet.mergeCells("A1:K1");
|
||||
sheet.getCell("A1").alignment = { horizontal: "center" };
|
||||
sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
|
||||
sheet.getCell("A1").font = { size: 16 };
|
||||
|
||||
// set export date
|
||||
sheet.getCell("A2").value = `Exported on : ${exportDate}`;
|
||||
sheet.mergeCells("A2:M2");
|
||||
sheet.mergeCells("A2:K2");
|
||||
sheet.getCell("A2").alignment = { horizontal: "center" };
|
||||
sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
|
||||
sheet.getCell("A2").font = { size: 12 };
|
||||
@@ -545,7 +447,7 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
sheet.mergeCells("A3:D3");
|
||||
|
||||
// set table headers
|
||||
sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Billable Time (seconds)", "Non-Billable Time (seconds)", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
|
||||
sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
|
||||
sheet.getRow(5).font = { bold: true };
|
||||
|
||||
for (const member of result.members) {
|
||||
@@ -556,8 +458,6 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
overdue_tasks: member.overdue,
|
||||
completed_tasks: member.completed,
|
||||
ongoing_tasks: member.ongoing,
|
||||
billable_time: member.billable_time || 0,
|
||||
non_billable_time: member.non_billable_time || 0,
|
||||
done_tasks: member.completed,
|
||||
doing_tasks: member.ongoing_by_activity_logs,
|
||||
todo_tasks: member.todo_by_activity_logs
|
||||
@@ -587,9 +487,7 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
dateRange = date_range.split(",");
|
||||
}
|
||||
|
||||
// Get user timezone for proper date filtering
|
||||
const userTimezone = await this.getUserTimezone(req.user?.id as string);
|
||||
const durationClause = this.getDateRangeClauseWithTimezone(duration as string || DATE_RANGES.LAST_WEEK, dateRange, userTimezone);
|
||||
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "twl");
|
||||
const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log");
|
||||
const memberName = (req.query.member_name as string)?.trim() || null;
|
||||
|
||||
@@ -1140,9 +1038,7 @@ export default class ReportingMembersController extends ReportingControllerBaseW
|
||||
public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const { team_member_id, team_id, duration, date_range, archived, billable } = req.body;
|
||||
|
||||
// Get user timezone for proper date filtering
|
||||
const userTimezone = await this.getUserTimezone(req.user?.id as string);
|
||||
const durationClause = this.getDateRangeClauseWithTimezone(duration || DATE_RANGES.LAST_WEEK, date_range, userTimezone);
|
||||
const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl");
|
||||
const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
|
||||
|
||||
const billableQuery = this.buildBillableQuery(billable);
|
||||
@@ -1334,8 +1230,8 @@ public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLen
|
||||
row.actual_time = int(row.actual_time);
|
||||
row.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(row.estimated_time));
|
||||
row.actual_time_string = this.convertSecondsToHoursAndMinutes(int(row.actual_time));
|
||||
row.days_left = this.getDaysLeft(row.end_date);
|
||||
row.is_overdue = this.isOverdue(row.end_date);
|
||||
row.days_left = ReportingControllerBase.getDaysLeft(row.end_date);
|
||||
row.is_overdue = ReportingControllerBase.isOverdue(row.end_date);
|
||||
if (row.days_left && row.is_overdue) {
|
||||
row.days_left = row.days_left.toString().replace(/-/g, "");
|
||||
}
|
||||
@@ -1433,4 +1329,4 @@ public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLen
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase {
|
||||
const [workingDays] = workingDaysResults.rows;
|
||||
|
||||
// get organization working hours
|
||||
const getDataHoursq = `SELECT hours_per_day FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`;
|
||||
const getDataHoursq = `SELECT working_hours FROM organizations WHERE user_id = $1 GROUP BY id LIMIT 1;`;
|
||||
|
||||
const workingHoursResults = await db.query(getDataHoursq, [req.user?.owner_id]);
|
||||
|
||||
const [workingHours] = workingHoursResults.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.hours_per_day }));
|
||||
return res.status(200).send(new ServerResponse(true, { workingDays: workingDays?.working_days, workingHours: workingHours?.working_hours }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
@@ -74,13 +74,18 @@ export default class ScheduleControllerV2 extends WorklenzControllerBase {
|
||||
.map(day => `${day.toLowerCase()} = ${workingDays.includes(day)}`)
|
||||
.join(", ");
|
||||
|
||||
const updateQuery = `UPDATE public.organization_working_days
|
||||
const updateQuery = `
|
||||
UPDATE public.organization_working_days
|
||||
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE organization_id IN (SELECT id FROM organizations WHERE user_id = $1);`;
|
||||
WHERE organization_id IN (
|
||||
SELECT organization_id FROM organizations
|
||||
WHERE user_id = $1
|
||||
);
|
||||
`;
|
||||
|
||||
await db.query(updateQuery, [req.user?.owner_id]);
|
||||
|
||||
const getDataHoursq = `UPDATE organizations SET hours_per_day = $1 WHERE user_id = $2;`;
|
||||
const getDataHoursq = `UPDATE organizations SET working_hours = $1 WHERE user_id = $2;`;
|
||||
|
||||
await db.query(getDataHoursq, [workingHours, req.user?.owner_id]);
|
||||
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { ISurveySubmissionRequest } from "../interfaces/survey";
|
||||
import db from "../config/db";
|
||||
|
||||
export default class SurveyController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getAccountSetupSurvey(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const q = `
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.description,
|
||||
s.survey_type,
|
||||
s.is_active,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', sq.id,
|
||||
'survey_id', sq.survey_id,
|
||||
'question_key', sq.question_key,
|
||||
'question_type', sq.question_type,
|
||||
'is_required', sq.is_required,
|
||||
'sort_order', sq.sort_order,
|
||||
'options', sq.options
|
||||
) ORDER BY sq.sort_order
|
||||
) FILTER (WHERE sq.id IS NOT NULL),
|
||||
'[]'
|
||||
) AS questions
|
||||
FROM surveys s
|
||||
LEFT JOIN survey_questions sq ON s.id = sq.survey_id
|
||||
WHERE s.survey_type = 'account_setup' AND s.is_active = true
|
||||
GROUP BY s.id, s.name, s.description, s.survey_type, s.is_active
|
||||
LIMIT 1;
|
||||
`;
|
||||
|
||||
const result = await db.query(q);
|
||||
const [survey] = result.rows;
|
||||
|
||||
if (!survey) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Account setup survey not found"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, survey));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async submitSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id;
|
||||
const body = req.body as ISurveySubmissionRequest;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||
}
|
||||
|
||||
if (!body.survey_id || !body.answers || !Array.isArray(body.answers)) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Invalid survey submission data"));
|
||||
}
|
||||
|
||||
// Check if user has already submitted a response for this survey
|
||||
const existingResponseQuery = `
|
||||
SELECT id FROM survey_responses
|
||||
WHERE user_id = $1 AND survey_id = $2;
|
||||
`;
|
||||
const existingResult = await db.query(existingResponseQuery, [userId, body.survey_id]);
|
||||
|
||||
let responseId: string;
|
||||
|
||||
if (existingResult.rows.length > 0) {
|
||||
// Update existing response
|
||||
responseId = existingResult.rows[0].id;
|
||||
|
||||
const updateResponseQuery = `
|
||||
UPDATE survey_responses
|
||||
SET is_completed = true, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
`;
|
||||
await db.query(updateResponseQuery, [responseId]);
|
||||
|
||||
// Delete existing answers
|
||||
const deleteAnswersQuery = `DELETE FROM survey_answers WHERE response_id = $1;`;
|
||||
await db.query(deleteAnswersQuery, [responseId]);
|
||||
} else {
|
||||
// Create new response
|
||||
const createResponseQuery = `
|
||||
INSERT INTO survey_responses (survey_id, user_id, is_completed, completed_at)
|
||||
VALUES ($1, $2, true, NOW())
|
||||
RETURNING id;
|
||||
`;
|
||||
const responseResult = await db.query(createResponseQuery, [body.survey_id, userId]);
|
||||
responseId = responseResult.rows[0].id;
|
||||
}
|
||||
|
||||
// Insert new answers
|
||||
if (body.answers.length > 0) {
|
||||
const answerValues: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
body.answers.forEach((answer, index) => {
|
||||
const baseIndex = index * 4;
|
||||
answerValues.push(`($${baseIndex + 1}, $${baseIndex + 2}, $${baseIndex + 3}, $${baseIndex + 4})`);
|
||||
|
||||
params.push(
|
||||
responseId,
|
||||
answer.question_id,
|
||||
answer.answer_text || null,
|
||||
answer.answer_json ? JSON.stringify(answer.answer_json) : null
|
||||
);
|
||||
});
|
||||
|
||||
const insertAnswersQuery = `
|
||||
INSERT INTO survey_answers (response_id, question_id, answer_text, answer_json)
|
||||
VALUES ${answerValues.join(', ')};
|
||||
`;
|
||||
|
||||
await db.query(insertAnswersQuery, params);
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, { response_id: responseId }));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getUserSurveyResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id;
|
||||
const surveyId = req.params.survey_id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT
|
||||
sr.id,
|
||||
sr.survey_id,
|
||||
sr.user_id,
|
||||
sr.is_completed,
|
||||
sr.started_at,
|
||||
sr.completed_at,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'question_id', sa.question_id,
|
||||
'answer_text', sa.answer_text,
|
||||
'answer_json', sa.answer_json
|
||||
)
|
||||
) FILTER (WHERE sa.id IS NOT NULL),
|
||||
'[]'
|
||||
) AS answers
|
||||
FROM survey_responses sr
|
||||
LEFT JOIN survey_answers sa ON sr.id = sa.response_id
|
||||
WHERE sr.user_id = $1 AND sr.survey_id = $2
|
||||
GROUP BY sr.id, sr.survey_id, sr.user_id, sr.is_completed, sr.started_at, sr.completed_at;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [userId, surveyId]);
|
||||
const [response] = result.rows;
|
||||
|
||||
if (!response) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Survey response not found"));
|
||||
}
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, response));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async checkAccountSetupSurveyStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
const userId = req.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "User not authenticated"));
|
||||
}
|
||||
|
||||
const q = `
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM survey_responses sr
|
||||
INNER JOIN surveys s ON sr.survey_id = s.id
|
||||
WHERE sr.user_id = $1
|
||||
AND s.survey_type = 'account_setup'
|
||||
AND sr.is_completed = true
|
||||
) as is_completed,
|
||||
(
|
||||
SELECT sr.completed_at
|
||||
FROM survey_responses sr
|
||||
INNER JOIN surveys s ON sr.survey_id = s.id
|
||||
WHERE sr.user_id = $1
|
||||
AND s.survey_type = 'account_setup'
|
||||
AND sr.is_completed = true
|
||||
LIMIT 1
|
||||
) as completed_at;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [userId]);
|
||||
const status = result.rows[0] || { is_completed: false, completed_at: null };
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, status));
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { SocketEvents } from "../socket.io/events";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { formatDuration, getColor } from "../shared/utils";
|
||||
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA, TRIAL_MEMBER_LIMIT } from "../shared/constants";
|
||||
import { statusExclude, TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants";
|
||||
import { checkTeamSubscriptionStatus } from "../shared/paddle-utils";
|
||||
import { updateUsers } from "../shared/paddle-requests";
|
||||
import { NotificationsService } from "../services/notifications/notifications.service";
|
||||
@@ -141,17 +141,6 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Cannot exceed the maximum number of life time users."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks trial user team member limit
|
||||
*/
|
||||
if (subscriptionData.subscription_status === "trialing") {
|
||||
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
|
||||
|
||||
if (currentTrialMembers + incrementBy > TRIAL_MEMBER_LIMIT) {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks subscription details and updates the user count if applicable.
|
||||
* Sends a response if there is an issue with the subscription.
|
||||
@@ -1092,18 +1081,6 @@ export default class TeamMembersController extends WorklenzControllerBase {
|
||||
return res.status(200).send(new ServerResponse(false, "Please check your subscription status."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks trial user team member limit
|
||||
*/
|
||||
if (subscriptionData.subscription_status === "trialing") {
|
||||
const currentTrialMembers = parseInt(subscriptionData.current_count) || 0;
|
||||
const emailsToAdd = req.body.emails?.length || 1;
|
||||
|
||||
if (currentTrialMembers + emailsToAdd > TRIAL_MEMBER_LIMIT) {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Trial users cannot exceed ${TRIAL_MEMBER_LIMIT} team members. Please upgrade to add more members.`));
|
||||
}
|
||||
}
|
||||
|
||||
// if (subscriptionData.status === "trialing") break;
|
||||
if (!subscriptionData.is_credit && !subscriptionData.is_custom) {
|
||||
if (subscriptionData.subscription_status === "active") {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import moment from "moment";
|
||||
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||
|
||||
import db from "../config/db";
|
||||
|
||||
import { ServerResponse } from "../models/server-response";
|
||||
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||
import HandleExceptions from "../decorators/handle-exceptions";
|
||||
import { formatDuration, formatLogText, getColor } from "../shared/utils";
|
||||
|
||||
interface IUserRecentTask {
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
last_activity_at: string;
|
||||
activity_count: number;
|
||||
project_color?: string;
|
||||
task_status?: string;
|
||||
status_color?: string;
|
||||
}
|
||||
|
||||
interface IUserTimeLoggedTask {
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
total_time_logged: number;
|
||||
total_time_logged_string: string;
|
||||
last_logged_at: string;
|
||||
logged_by_timer: boolean;
|
||||
project_color?: string;
|
||||
task_status?: string;
|
||||
status_color?: string;
|
||||
log_entries_count?: number;
|
||||
estimated_time?: number;
|
||||
}
|
||||
|
||||
export default class UserActivityLogsController extends WorklenzControllerBase {
|
||||
@HandleExceptions()
|
||||
public static async getRecentTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (!req.user) {
|
||||
return res.status(401).send(new ServerResponse(false, null, "Unauthorized"));
|
||||
}
|
||||
|
||||
const { id: userId, team_id: teamId } = req.user;
|
||||
const { offset = 0, limit = 10 } = req.query;
|
||||
|
||||
// Optimized query with better performance and team filtering
|
||||
const q = `
|
||||
SELECT DISTINCT tal.task_id, t.name AS task_name, tal.project_id, p.name AS project_name,
|
||||
MAX(tal.created_at) AS last_activity_at,
|
||||
COUNT(DISTINCT tal.id) AS activity_count,
|
||||
p.color_code AS project_color,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color
|
||||
FROM task_activity_logs tal
|
||||
INNER JOIN tasks t ON tal.task_id = t.id AND t.archived = FALSE
|
||||
INNER JOIN projects p ON tal.project_id = p.id AND p.team_id = $1
|
||||
WHERE tal.user_id = $2
|
||||
AND tal.created_at >= NOW() - INTERVAL '30 days'
|
||||
GROUP BY tal.task_id, t.name, tal.project_id, p.name, p.color_code, t.status_id
|
||||
ORDER BY MAX(tal.created_at) DESC
|
||||
LIMIT $3 OFFSET $4;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [teamId, userId, limit, offset]);
|
||||
const tasks: IUserRecentTask[] = result.rows;
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, tasks));
|
||||
}
|
||||
|
||||
@HandleExceptions()
|
||||
public static async getTimeLoggedTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
|
||||
if (!req.user) {
|
||||
return res.status(401).send(new ServerResponse(false, null, "Unauthorized"));
|
||||
}
|
||||
|
||||
const { id: userId, team_id: teamId } = req.user;
|
||||
const { offset = 0, limit = 10 } = req.query;
|
||||
|
||||
// Optimized query with better performance, team filtering, and useful additional data
|
||||
const q = `
|
||||
SELECT twl.task_id, t.name AS task_name, t.project_id, p.name AS project_name,
|
||||
SUM(twl.time_spent) AS total_time_logged,
|
||||
MAX(twl.created_at) AS last_logged_at,
|
||||
MAX(twl.logged_by_timer::int)::boolean AS logged_by_timer,
|
||||
p.color_code AS project_color,
|
||||
(SELECT name FROM task_statuses WHERE id = t.status_id) AS task_status,
|
||||
(SELECT color_code
|
||||
FROM sys_task_status_categories
|
||||
WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
|
||||
COUNT(DISTINCT twl.id) AS log_entries_count,
|
||||
(t.total_minutes * 60) AS estimated_time
|
||||
FROM task_work_log twl
|
||||
INNER JOIN tasks t ON twl.task_id = t.id AND t.archived = FALSE
|
||||
INNER JOIN projects p ON t.project_id = p.id AND p.team_id = $1
|
||||
WHERE twl.user_id = $2
|
||||
AND twl.created_at >= NOW() - INTERVAL '90 days'
|
||||
GROUP BY twl.task_id, t.name, t.project_id, p.name, p.color_code, t.status_id, t.total_minutes
|
||||
HAVING SUM(twl.time_spent) > 0
|
||||
ORDER BY MAX(twl.created_at) DESC
|
||||
LIMIT $3 OFFSET $4;
|
||||
`;
|
||||
|
||||
const result = await db.query(q, [teamId, userId, limit, offset]);
|
||||
const tasks: IUserTimeLoggedTask[] = result.rows.map(task => ({
|
||||
...task,
|
||||
total_time_logged_string: formatDuration(moment.duration(task.total_time_logged, "seconds")),
|
||||
}));
|
||||
|
||||
return res.status(200).send(new ServerResponse(true, tasks));
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
{
|
||||
"_metadata": {
|
||||
"description": "Sri Lankan Public Holidays Data",
|
||||
"last_updated": "2025-01-31",
|
||||
"sources": {
|
||||
"2025": "Based on official government sources and existing verified data",
|
||||
"note": "All dates should be verified against official sources before use"
|
||||
},
|
||||
"official_sources": [
|
||||
"Central Bank of Sri Lanka - Holiday Circulars",
|
||||
"Department of Meteorology - Astrological calculations",
|
||||
"Ministry of Public Administration - Official gazette",
|
||||
"Buddhist and Pali University - Poya day calculations",
|
||||
"All Ceylon Jamiyyatul Ulama - Islamic calendar",
|
||||
"Hindu Cultural Centre - Hindu calendar"
|
||||
],
|
||||
"verification_process": "Each year should be verified against current official publications before adding to production systems"
|
||||
},
|
||||
"2025": [
|
||||
{
|
||||
"name": "Duruthu Full Moon Poya Day",
|
||||
"date": "2025-01-13",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the first visit of Buddha to Sri Lanka",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Navam Full Moon Poya Day",
|
||||
"date": "2025-02-12",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Independence Day",
|
||||
"date": "2025-02-04",
|
||||
"type": "Public",
|
||||
"description": "Commemorates the independence of Sri Lanka from British rule in 1948",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "Medin Full Moon Poya Day",
|
||||
"date": "2025-03-14",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates Buddha's first visit to his father's palace after enlightenment",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Eid al-Fitr",
|
||||
"date": "2025-03-31",
|
||||
"type": "Public",
|
||||
"description": "Festival marking the end of Ramadan",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Bak Full Moon Poya Day",
|
||||
"date": "2025-04-12",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates Buddha's second visit to Sri Lanka",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Sinhala and Tamil New Year Day",
|
||||
"date": "2025-04-13",
|
||||
"type": "Public",
|
||||
"description": "Traditional New Year celebrated by Sinhalese and Tamil communities",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "Day after Sinhala and Tamil New Year",
|
||||
"date": "2025-04-14",
|
||||
"type": "Public",
|
||||
"description": "Second day of traditional New Year celebrations",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "Good Friday",
|
||||
"date": "2025-04-18",
|
||||
"type": "Public",
|
||||
"description": "Christian commemoration of the crucifixion of Jesus Christ",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "May Day",
|
||||
"date": "2025-05-01",
|
||||
"type": "Public",
|
||||
"description": "International Workers' Day",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "Vesak Full Moon Poya Day",
|
||||
"date": "2025-05-12",
|
||||
"type": "Poya",
|
||||
"description": "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Day after Vesak Full Moon Poya Day",
|
||||
"date": "2025-05-13",
|
||||
"type": "Public",
|
||||
"description": "Additional day for Vesak celebrations",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Eid al-Adha",
|
||||
"date": "2025-06-07",
|
||||
"type": "Public",
|
||||
"description": "Islamic festival of sacrifice",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Poson Full Moon Poya Day",
|
||||
"date": "2025-06-11",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Esala Full Moon Poya Day",
|
||||
"date": "2025-07-10",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Nikini Full Moon Poya Day",
|
||||
"date": "2025-08-09",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the first Buddhist council",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Binara Full Moon Poya Day",
|
||||
"date": "2025-09-07",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates Buddha's visit to heaven to preach to his mother",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Vap Full Moon Poya Day",
|
||||
"date": "2025-10-07",
|
||||
"type": "Poya",
|
||||
"description": "Marks the end of Buddhist Lent and Buddha's return from heaven",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Deepavali",
|
||||
"date": "2025-10-20",
|
||||
"type": "Public",
|
||||
"description": "Hindu Festival of Lights",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Il Full Moon Poya Day",
|
||||
"date": "2025-11-05",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates Buddha's ordination of sixty disciples",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Unduvap Full Moon Poya Day",
|
||||
"date": "2025-12-04",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Christmas Day",
|
||||
"date": "2025-12-25",
|
||||
"type": "Public",
|
||||
"description": "Christian celebration of the birth of Jesus Christ",
|
||||
"is_recurring": true
|
||||
}
|
||||
],
|
||||
"fixed_holidays": [
|
||||
{
|
||||
"name": "Independence Day",
|
||||
"month": 2,
|
||||
"day": 4,
|
||||
"type": "Public",
|
||||
"description": "Commemorates the independence of Sri Lanka from British rule in 1948"
|
||||
},
|
||||
{
|
||||
"name": "May Day",
|
||||
"month": 5,
|
||||
"day": 1,
|
||||
"type": "Public",
|
||||
"description": "International Workers' Day"
|
||||
},
|
||||
{
|
||||
"name": "Christmas Day",
|
||||
"month": 12,
|
||||
"day": 25,
|
||||
"type": "Public",
|
||||
"description": "Christian celebration of the birth of Jesus Christ"
|
||||
}
|
||||
],
|
||||
"variable_holidays_info": {
|
||||
"sinhala_tamil_new_year": {
|
||||
"description": "Sinhala and Tamil New Year dates vary based on astrological calculations. Common patterns:",
|
||||
"common_dates": [
|
||||
{ "pattern": "April 12-13", "years": "Some years" },
|
||||
{ "pattern": "April 13-14", "years": "Most common" },
|
||||
{ "pattern": "April 14-15", "years": "Occasional" }
|
||||
],
|
||||
"note": "These dates should be verified annually from official sources like the Department of Meteorology or astrological authorities"
|
||||
},
|
||||
"poya_days": {
|
||||
"description": "Full moon Poya days follow the lunar calendar and change each year",
|
||||
"note": "Dates should be obtained from Buddhist calendar or astronomical calculations"
|
||||
},
|
||||
"religious_holidays": {
|
||||
"eid_fitr": "Based on Islamic lunar calendar - varies each year",
|
||||
"eid_adha": "Based on Islamic lunar calendar - varies each year",
|
||||
"good_friday": "Based on Easter calculation - varies each year",
|
||||
"deepavali": "Based on Hindu lunar calendar - varies each year"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
# Sri Lankan Holiday Annual Update Process
|
||||
|
||||
## Overview
|
||||
This document outlines the process for annually updating Sri Lankan holiday data to ensure accurate utilization calculations.
|
||||
|
||||
## Data Sources & Verification
|
||||
|
||||
### Official Government Sources
|
||||
1. **Central Bank of Sri Lanka**
|
||||
- Holiday circulars (usually published in December for the next year)
|
||||
- Website: [cbsl.gov.lk](https://www.cbsl.gov.lk)
|
||||
|
||||
2. **Department of Meteorology**
|
||||
- Astrological calculations for Sinhala & Tamil New Year
|
||||
- Website: [meteo.gov.lk](http://www.meteo.gov.lk)
|
||||
|
||||
3. **Ministry of Public Administration**
|
||||
- Official gazette notifications
|
||||
- Public holiday declarations
|
||||
|
||||
### Religious Authorities
|
||||
1. **Buddhist Calendar**
|
||||
- Buddhist and Pali University of Sri Lanka
|
||||
- Major temples (Malwatte, Asgiriya)
|
||||
|
||||
2. **Islamic Calendar**
|
||||
- All Ceylon Jamiyyatul Ulama (ACJU)
|
||||
- Colombo Grand Mosque
|
||||
|
||||
3. **Hindu Calendar**
|
||||
- Hindu Cultural Centre
|
||||
- Tamil cultural organizations
|
||||
|
||||
## Annual Update Workflow
|
||||
|
||||
### 1. Preparation (October - November)
|
||||
```bash
|
||||
# Check current data status
|
||||
node update-sri-lankan-holidays.js --list
|
||||
node update-sri-lankan-holidays.js --validate
|
||||
```
|
||||
|
||||
### 2. Research Phase (November - December)
|
||||
For the upcoming year (e.g., 2026):
|
||||
|
||||
1. **Fixed Holidays** ✅ Already handled
|
||||
- Independence Day (Feb 4)
|
||||
- May Day (May 1)
|
||||
- Christmas Day (Dec 25)
|
||||
|
||||
2. **Variable Holidays** ⚠️ Require verification
|
||||
- **Sinhala & Tamil New Year**: Check Department of Meteorology
|
||||
- **Poya Days**: Check Buddhist calendar/temples
|
||||
- **Good Friday**: Calculate from Easter
|
||||
- **Eid al-Fitr & Eid al-Adha**: Check Islamic calendar
|
||||
- **Deepavali**: Check Hindu calendar
|
||||
|
||||
### 3. Data Collection Template
|
||||
```bash
|
||||
# Generate template for the new year
|
||||
node update-sri-lankan-holidays.js --poya-template 2026
|
||||
```
|
||||
|
||||
This will output a template like:
|
||||
```json
|
||||
{
|
||||
"name": "Duruthu Full Moon Poya Day",
|
||||
"date": "2026-??-??",
|
||||
"type": "Poya",
|
||||
"description": "Commemorates the first visit of Buddha to Sri Lanka",
|
||||
"is_recurring": false
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Research Checklist
|
||||
|
||||
#### Sinhala & Tamil New Year
|
||||
- [ ] Check Department of Meteorology announcements
|
||||
- [ ] Verify with astrological authorities
|
||||
- [ ] Confirm if dates are April 12-13, 13-14, or 14-15
|
||||
|
||||
#### Poya Days (12 per year)
|
||||
- [ ] Get Buddhist calendar for the year
|
||||
- [ ] Verify with temples or Buddhist authorities
|
||||
- [ ] Double-check lunar calendar calculations
|
||||
|
||||
#### Religious Holidays
|
||||
- [ ] **Good Friday**: Calculate based on Easter
|
||||
- [ ] **Eid al-Fitr**: Check Islamic calendar/ACJU
|
||||
- [ ] **Eid al-Adha**: Check Islamic calendar/ACJU
|
||||
- [ ] **Deepavali**: Check Hindu calendar/cultural centers
|
||||
|
||||
### 5. Data Entry
|
||||
1. Edit `src/data/sri-lankan-holidays.json`
|
||||
2. Add new year section with verified dates
|
||||
3. Update metadata with sources used
|
||||
|
||||
### 6. Validation & Testing
|
||||
```bash
|
||||
# Validate the new data
|
||||
node update-sri-lankan-holidays.js --validate
|
||||
|
||||
# Generate SQL for database
|
||||
node update-sri-lankan-holidays.js --generate-sql 2026
|
||||
```
|
||||
|
||||
### 7. Database Update
|
||||
1. Create new migration file with the generated SQL
|
||||
2. Test in development environment
|
||||
3. Deploy to production
|
||||
|
||||
### 8. Documentation
|
||||
- Update metadata in JSON file
|
||||
- Document sources used
|
||||
- Note any special circumstances or date changes
|
||||
|
||||
## Emergency Updates
|
||||
|
||||
If holidays are announced late or changed:
|
||||
|
||||
1. **Quick JSON Update**:
|
||||
```bash
|
||||
# Edit the JSON file directly
|
||||
# Add the new/changed holiday
|
||||
```
|
||||
|
||||
2. **Database Hotfix**:
|
||||
```sql
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ('LK', 'Emergency Holiday', 'Description', 'YYYY-MM-DD', false)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING;
|
||||
```
|
||||
|
||||
3. **Notify Users**: Consider adding a notification system for holiday changes
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
### Pre-Release Checklist
|
||||
- [ ] All 12 Poya days included for the year
|
||||
- [ ] Sinhala & Tamil New Year dates verified
|
||||
- [ ] Religious holidays cross-checked with multiple sources
|
||||
- [ ] No duplicate dates
|
||||
- [ ] JSON format validation passes
|
||||
- [ ] Database migration tested
|
||||
|
||||
### Post-Release Monitoring
|
||||
- [ ] Monitor utilization calculations for anomalies
|
||||
- [ ] Check user feedback for missed holidays
|
||||
- [ ] Verify against actual government announcements
|
||||
|
||||
## Automation Opportunities
|
||||
|
||||
Future improvements could include:
|
||||
1. **API Integration**: Connect to reliable holiday APIs
|
||||
2. **Web Scraping**: Automated monitoring of official websites
|
||||
3. **Notification System**: Alert when new holidays are announced
|
||||
4. **Validation Service**: Cross-check against multiple sources
|
||||
|
||||
## Contact Information
|
||||
|
||||
For questions about the holiday update process:
|
||||
- Technical issues: Development team
|
||||
- Holiday verification: Sri Lankan team members
|
||||
- Religious holidays: Local community contacts
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0** (2025-01-31): Initial process documentation
|
||||
- **2025 Data**: Verified and included
|
||||
- **2026+ Data**: Pending official source verification
|
||||
@@ -1,54 +0,0 @@
|
||||
export interface IHolidayType {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color_code: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IOrganizationHoliday {
|
||||
id: string;
|
||||
organization_id: string;
|
||||
holiday_type_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
holiday_type?: IHolidayType;
|
||||
}
|
||||
|
||||
export interface ICountryHoliday {
|
||||
id: string;
|
||||
country_code: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
is_recurring: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ICreateHolidayRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
holiday_type_id: string;
|
||||
is_recurring?: boolean;
|
||||
}
|
||||
|
||||
export interface IUpdateHolidayRequest {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
holiday_type_id?: string;
|
||||
is_recurring?: boolean;
|
||||
}
|
||||
|
||||
export interface IImportCountryHolidaysRequest {
|
||||
country_code: string;
|
||||
year?: number;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
export interface ISurveyQuestion {
|
||||
id: string;
|
||||
survey_id: string;
|
||||
question_key: string;
|
||||
question_type: 'single_choice' | 'multiple_choice' | 'text';
|
||||
is_required: boolean;
|
||||
sort_order: number;
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
export interface ISurvey {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
survey_type: 'account_setup' | 'onboarding' | 'feedback';
|
||||
is_active: boolean;
|
||||
questions?: ISurveyQuestion[];
|
||||
}
|
||||
|
||||
export interface ISurveyAnswer {
|
||||
question_id: string;
|
||||
answer_text?: string;
|
||||
answer_json?: string[];
|
||||
}
|
||||
|
||||
export interface ISurveyResponse {
|
||||
id?: string;
|
||||
survey_id: string;
|
||||
user_id?: string;
|
||||
is_completed: boolean;
|
||||
answers: ISurveyAnswer[];
|
||||
}
|
||||
|
||||
export interface ISurveySubmissionRequest {
|
||||
survey_id: string;
|
||||
answers: ISurveyAnswer[];
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { NextFunction } from "express";
|
||||
import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
|
||||
import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
|
||||
import { ServerResponse } from "../../models/server-response";
|
||||
import { ISurveySubmissionRequest } from "../../interfaces/survey";
|
||||
|
||||
export default function surveySubmissionValidator(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void {
|
||||
const body = req.body as ISurveySubmissionRequest;
|
||||
|
||||
if (!body) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Request body is required"));
|
||||
}
|
||||
|
||||
if (!body.survey_id || typeof body.survey_id !== 'string') {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Survey ID is required and must be a string"));
|
||||
}
|
||||
|
||||
if (!body.answers || !Array.isArray(body.answers)) {
|
||||
return res.status(200).send(new ServerResponse(false, null, "Answers are required and must be an array"));
|
||||
}
|
||||
|
||||
// Validate each answer
|
||||
for (let i = 0; i < body.answers.length; i++) {
|
||||
const answer = body.answers[i];
|
||||
|
||||
if (!answer.question_id || typeof answer.question_id !== 'string') {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: Question ID is required and must be a string`));
|
||||
}
|
||||
|
||||
// answer_text and answer_json are both optional - users can submit empty answers
|
||||
|
||||
// Validate answer_text if provided
|
||||
if (answer.answer_text && typeof answer.answer_text !== 'string') {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_text must be a string`));
|
||||
}
|
||||
|
||||
// Validate answer_json if provided
|
||||
if (answer.answer_json && !Array.isArray(answer.answer_json)) {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json must be an array`));
|
||||
}
|
||||
|
||||
// Validate answer_json items are strings
|
||||
if (answer.answer_json) {
|
||||
for (let j = 0; j < answer.answer_json.length; j++) {
|
||||
if (typeof answer.answer_json[j] !== 'string') {
|
||||
return res.status(200).send(new ServerResponse(false, null, `Answer ${i + 1}: answer_json items must be strings`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
@@ -81,12 +81,5 @@
|
||||
"delete": "Fshi",
|
||||
"enterStatusName": "Shkruani emrin e statusit",
|
||||
"selectCategory": "Zgjidh kategorinë",
|
||||
"close": "Mbyll",
|
||||
"clearSort": "Pastro Renditjen",
|
||||
"sortAscending": "Rendit në Rritje",
|
||||
"sortDescending": "Rendit në Zbritje",
|
||||
"sortByField": "Rendit sipas {{field}}",
|
||||
"ascendingOrder": "Rritës",
|
||||
"descendingOrder": "Zbritës",
|
||||
"currentSort": "Renditja aktuale: {{field}} {{order}}"
|
||||
"close": "Mbyll"
|
||||
}
|
||||
|
||||
@@ -81,12 +81,5 @@
|
||||
"delete": "Löschen",
|
||||
"enterStatusName": "Statusnamen eingeben",
|
||||
"selectCategory": "Kategorie auswählen",
|
||||
"close": "Schließen",
|
||||
"clearSort": "Sortierung löschen",
|
||||
"sortAscending": "Aufsteigend sortieren",
|
||||
"sortDescending": "Absteigend sortieren",
|
||||
"sortByField": "Sortieren nach {{field}}",
|
||||
"ascendingOrder": "Aufsteigend",
|
||||
"descendingOrder": "Absteigend",
|
||||
"currentSort": "Aktuelle Sortierung: {{field}} {{order}}"
|
||||
"close": "Schließen"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"continue": "Continue",
|
||||
|
||||
"setupYourAccount": "Setup Your Account.",
|
||||
"setupYourAccount": "Setup Your Worklenz Account.",
|
||||
"organizationStepTitle": "Name Your Organization",
|
||||
"organizationStepLabel": "Pick a name for your Worklenz account.",
|
||||
|
||||
|
||||
@@ -81,12 +81,5 @@
|
||||
"delete": "Delete",
|
||||
"enterStatusName": "Enter status name",
|
||||
"selectCategory": "Select category",
|
||||
"close": "Close",
|
||||
"clearSort": "Clear Sort",
|
||||
"sortAscending": "Sort Ascending",
|
||||
"sortDescending": "Sort Descending",
|
||||
"sortByField": "Sort by {{field}}",
|
||||
"ascendingOrder": "Ascending",
|
||||
"descendingOrder": "Descending",
|
||||
"currentSort": "Current sort: {{field}} {{order}}"
|
||||
"close": "Close"
|
||||
}
|
||||
|
||||
@@ -77,12 +77,5 @@
|
||||
"delete": "Eliminar",
|
||||
"enterStatusName": "Introducir nombre del estado",
|
||||
"selectCategory": "Seleccionar categoría",
|
||||
"close": "Cerrar",
|
||||
"clearSort": "Limpiar Ordenamiento",
|
||||
"sortAscending": "Ordenar Ascendente",
|
||||
"sortDescending": "Ordenar Descendente",
|
||||
"sortByField": "Ordenar por {{field}}",
|
||||
"ascendingOrder": "Ascendente",
|
||||
"descendingOrder": "Descendente",
|
||||
"currentSort": "Ordenamiento actual: {{field}} {{order}}"
|
||||
"close": "Cerrar"
|
||||
}
|
||||
|
||||
@@ -78,12 +78,5 @@
|
||||
"delete": "Excluir",
|
||||
"enterStatusName": "Digite o nome do status",
|
||||
"selectCategory": "Selecionar categoria",
|
||||
"close": "Fechar",
|
||||
"clearSort": "Limpar Ordenação",
|
||||
"sortAscending": "Ordenar Crescente",
|
||||
"sortDescending": "Ordenar Decrescente",
|
||||
"sortByField": "Ordenar por {{field}}",
|
||||
"ascendingOrder": "Crescente",
|
||||
"descendingOrder": "Decrescente",
|
||||
"currentSort": "Ordenação atual: {{field}} {{order}}"
|
||||
"close": "Fechar"
|
||||
}
|
||||
|
||||
@@ -15,15 +15,5 @@
|
||||
"assignToMe": "分配给我",
|
||||
"archive": "归档",
|
||||
"newTaskNamePlaceholder": "写一个任务名称",
|
||||
"newSubtaskNamePlaceholder": "写一个子任务名称",
|
||||
"untitledSection": "无标题部分",
|
||||
"unmapped": "未映射",
|
||||
"clickToChangeDate": "点击更改日期",
|
||||
"noDueDate": "无截止日期",
|
||||
"save": "保存",
|
||||
"clear": "清除",
|
||||
"nextWeek": "下周",
|
||||
"noSubtasks": "无子任务",
|
||||
"showSubtasks": "显示子任务",
|
||||
"hideSubtasks": "隐藏子任务"
|
||||
"newSubtaskNamePlaceholder": "写一个子任务名称"
|
||||
}
|
||||
@@ -75,12 +75,5 @@
|
||||
"delete": "删除",
|
||||
"enterStatusName": "输入状态名称",
|
||||
"selectCategory": "选择类别",
|
||||
"close": "关闭",
|
||||
"clearSort": "清除排序",
|
||||
"sortAscending": "升序排列",
|
||||
"sortDescending": "降序排列",
|
||||
"sortByField": "按{{field}}排序",
|
||||
"ascendingOrder": "升序",
|
||||
"descendingOrder": "降序",
|
||||
"currentSort": "当前排序:{{field}} {{order}}"
|
||||
"close": "关闭"
|
||||
}
|
||||
@@ -18,10 +18,6 @@
|
||||
"changeCategory": "更改类别",
|
||||
"clickToEditGroupName": "点击编辑组名称",
|
||||
"enterGroupName": "输入组名称",
|
||||
"todo": "待办",
|
||||
"inProgress": "进行中",
|
||||
"done": "已完成",
|
||||
"defaultTaskName": "无标题任务",
|
||||
|
||||
"indicators": {
|
||||
"tooltips": {
|
||||
|
||||
@@ -29,37 +29,5 @@
|
||||
"noCategory": "无类别",
|
||||
"noProjects": "未找到项目",
|
||||
"noTeams": "未找到团队",
|
||||
"noData": "未找到数据",
|
||||
"groupBy": "分组方式",
|
||||
"groupByCategory": "类别",
|
||||
"groupByTeam": "团队",
|
||||
"groupByStatus": "状态",
|
||||
"groupByNone": "无",
|
||||
"clearSearch": "清除搜索",
|
||||
"selectedProjects": "已选项目",
|
||||
"projectsSelected": "个项目已选择",
|
||||
"showSelected": "仅显示已选择",
|
||||
"expandAll": "全部展开",
|
||||
"collapseAll": "全部折叠",
|
||||
"ungrouped": "未分组",
|
||||
"clearAll": "清除全部",
|
||||
"filterByBillableStatus": "按计费状态筛选",
|
||||
"searchByMember": "按成员搜索",
|
||||
"members": "成员",
|
||||
"utilization": "利用率",
|
||||
|
||||
"totalTimeLogged": "总记录时间",
|
||||
"acrossAllTeamMembers": "跨所有团队成员",
|
||||
"expectedCapacity": "预期容量",
|
||||
"basedOnWorkingSchedule": "基于工作时间表",
|
||||
"teamUtilization": "团队利用率",
|
||||
"targetRange": "目标范围",
|
||||
"variance": "差异",
|
||||
"overCapacity": "超出容量",
|
||||
"underCapacity": "容量不足",
|
||||
"considerWorkloadRedistribution": "考虑工作负载重新分配",
|
||||
"capacityAvailableForNewProjects": "可用于新项目的容量",
|
||||
"optimal": "最佳",
|
||||
"underUtilized": "利用率不足",
|
||||
"overUtilized": "过度利用"
|
||||
"noData": "未找到数据"
|
||||
}
|
||||
@@ -8,18 +8,11 @@ import teamOwnerOrAdminValidator from "../../middlewares/validators/team-owner-o
|
||||
const adminCenterApiRouter = express.Router();
|
||||
|
||||
// overview
|
||||
adminCenterApiRouter.get("/settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getAdminCenterSettings));
|
||||
adminCenterApiRouter.get("/organization", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationDetails));
|
||||
adminCenterApiRouter.get("/organization/admins", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationAdmins));
|
||||
adminCenterApiRouter.put("/organization", teamOwnerOrAdminValidator, organizationSettingsValidator, safeControllerFunction(AdminCenterController.updateOrganizationName));
|
||||
adminCenterApiRouter.put("/organization/calculation-method", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationCalculationMethod));
|
||||
adminCenterApiRouter.put("/organization/owner/contact-number", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOwnerContactNumber));
|
||||
|
||||
// holiday settings
|
||||
adminCenterApiRouter.get("/organization/holiday-settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationHolidaySettings));
|
||||
adminCenterApiRouter.put("/organization/holiday-settings", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.updateOrganizationHolidaySettings));
|
||||
adminCenterApiRouter.get("/countries-with-states", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getCountriesWithStates));
|
||||
|
||||
// users
|
||||
adminCenterApiRouter.get("/organization/users", teamOwnerOrAdminValidator, safeControllerFunction(AdminCenterController.getOrganizationUsers));
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import express from "express";
|
||||
import HolidayController from "../../controllers/holiday-controller";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
import teamOwnerOrAdminValidator from "../../middlewares/validators/team-owner-or-admin-validator";
|
||||
import idParamValidator from "../../middlewares/validators/id-param-validator";
|
||||
|
||||
const holidayApiRouter = express.Router();
|
||||
|
||||
// Holiday types
|
||||
holidayApiRouter.get("/types", safeControllerFunction(HolidayController.getHolidayTypes));
|
||||
|
||||
// Organization holidays
|
||||
holidayApiRouter.get("/organization", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getOrganizationHolidays));
|
||||
holidayApiRouter.post("/organization", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.createOrganizationHoliday));
|
||||
holidayApiRouter.put("/organization/:id", teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.updateOrganizationHoliday));
|
||||
holidayApiRouter.delete("/organization/:id", teamOwnerOrAdminValidator, idParamValidator, safeControllerFunction(HolidayController.deleteOrganizationHoliday));
|
||||
|
||||
// Country holidays
|
||||
holidayApiRouter.get("/countries", safeControllerFunction(HolidayController.getAvailableCountries));
|
||||
holidayApiRouter.get("/countries/:country_code", safeControllerFunction(HolidayController.getCountryHolidays));
|
||||
holidayApiRouter.post("/import", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.importCountryHolidays));
|
||||
|
||||
// Calendar view
|
||||
holidayApiRouter.get("/calendar", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.getHolidayCalendar));
|
||||
|
||||
// Populate holidays
|
||||
holidayApiRouter.post("/populate", teamOwnerOrAdminValidator, safeControllerFunction(HolidayController.populateCountryHolidays));
|
||||
|
||||
export default holidayApiRouter;
|
||||
@@ -1,137 +1,120 @@
|
||||
import express from "express";
|
||||
|
||||
import AccessControlsController from "../../controllers/access-controls-controller";
|
||||
import AuthController from "../../controllers/auth-controller";
|
||||
import LogsController from "../../controllers/logs-controller";
|
||||
import OverviewController from "../../controllers/overview-controller";
|
||||
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
|
||||
|
||||
import attachmentsApiRouter from "./attachments-api-router";
|
||||
import clientsApiRouter from "./clients-api-router";
|
||||
import jobTitlesApiRouter from "./job-titles-api-router";
|
||||
import notificationsApiRouter from "./notifications-api-router";
|
||||
import personalOverviewApiRouter from "./personal-overview-api-router";
|
||||
import projectMembersApiRouter from "./project-members-api-router";
|
||||
import projectsApiRouter from "./projects-api-router";
|
||||
import settingsApiRouter from "./settings-api-router";
|
||||
import statusesApiRouter from "./statuses-api-router";
|
||||
import subTasksApiRouter from "./sub-tasks-api-router";
|
||||
import taskCommentsApiRouter from "./task-comments-api-router";
|
||||
import taskWorkLogApiRouter from "./task-work-log-api-router";
|
||||
import tasksApiRouter from "./tasks-api-router";
|
||||
import teamMembersApiRouter from "./team-members-api-router";
|
||||
import teamsApiRouter from "./teams-api-router";
|
||||
import timezonesApiRouter from "./timezones-api-router";
|
||||
import todoListApiRouter from "./todo-list-api-router";
|
||||
import projectStatusesApiRouter from "./project-statuses-api-router";
|
||||
import labelsApiRouter from "./labels-api-router";
|
||||
import sharedProjectsApiRouter from "./shared-projects-api-router";
|
||||
import resourceAllocationApiRouter from "./resource-allocation-api-router";
|
||||
import taskTemplatesApiRouter from "./task-templates-api-router";
|
||||
import projectInsightsApiRouter from "./project-insights-api-router";
|
||||
import passwordValidator from "../../middlewares/validators/password-validator";
|
||||
import adminCenterApiRouter from "./admin-center-api-router";
|
||||
import reportingApiRouter from "./reporting-api-router";
|
||||
import activityLogsApiRouter from "./activity-logs-api-router";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
import projectFoldersApiRouter from "./project-folders-api-router";
|
||||
import taskPhasesApiRouter from "./task-phases-api-router";
|
||||
import projectCategoriesApiRouter from "./project-categories-api-router";
|
||||
import homePageApiRouter from "./home-page-api-router";
|
||||
import ganttApiRouter from "./gantt-api-router";
|
||||
import projectCommentsApiRouter from "./project-comments-api-router";
|
||||
import reportingExportApiRouter from "./reporting-export-api-router";
|
||||
import projectHealthsApiRouter from "./project-healths-api-router";
|
||||
import ptTasksApiRouter from "./pt-tasks-api-router";
|
||||
import projectTemplatesApiRouter from "./project-templates-api";
|
||||
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
|
||||
import ptStatusesApiRouter from "./pt-statuses-api-router";
|
||||
import workloadApiRouter from "./gannt-apis/workload-api-router";
|
||||
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
|
||||
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
|
||||
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
|
||||
import projectManagerApiRouter from "./project-managers-api-router";
|
||||
import surveyApiRouter from "./survey-api-router";
|
||||
|
||||
import billingApiRouter from "./billing-api-router";
|
||||
import taskDependenciesApiRouter from "./task-dependencies-api-router";
|
||||
|
||||
import taskRecurringApiRouter from "./task-recurring-api-router";
|
||||
|
||||
import customColumnsApiRouter from "./custom-columns-api-router";
|
||||
import projectFinanceApiRouter from "./project-finance-api-router";
|
||||
import projectRatecardApiRouter from "./project-ratecard-api-router";
|
||||
import ratecardApiRouter from "./ratecard-api-router";
|
||||
import holidayApiRouter from "./holiday-api-router";
|
||||
import userActivityLogsApiRouter from "./user-activity-logs-api-router";
|
||||
|
||||
const api = express.Router();
|
||||
|
||||
api.use("/projects", projectsApiRouter);
|
||||
api.use("/team-members", teamMembersApiRouter);
|
||||
api.use("/job-titles", jobTitlesApiRouter);
|
||||
api.use("/clients", clientsApiRouter);
|
||||
api.use("/teams", teamsApiRouter);
|
||||
api.use("/tasks", tasksApiRouter);
|
||||
api.use("/settings", settingsApiRouter);
|
||||
api.use("/personal-overview", personalOverviewApiRouter);
|
||||
api.use("/statuses", statusesApiRouter);
|
||||
api.use("/todo-list", todoListApiRouter);
|
||||
api.use("/notifications", notificationsApiRouter);
|
||||
api.use("/attachments", attachmentsApiRouter);
|
||||
api.use("/sub-tasks", subTasksApiRouter);
|
||||
api.use("/project-members", projectMembersApiRouter);
|
||||
api.use("/task-time-log", taskWorkLogApiRouter);
|
||||
api.use("/task-comments", taskCommentsApiRouter);
|
||||
api.use("/timezones", timezonesApiRouter);
|
||||
api.use("/project-statuses", projectStatusesApiRouter);
|
||||
api.use("/labels", labelsApiRouter);
|
||||
api.use("/resource-allocation", resourceAllocationApiRouter);
|
||||
api.use("/shared/projects", sharedProjectsApiRouter);
|
||||
api.use("/task-templates", taskTemplatesApiRouter);
|
||||
api.use("/project-insights", projectInsightsApiRouter);
|
||||
api.use("/admin-center", adminCenterApiRouter);
|
||||
api.use("/reporting", reportingApiRouter);
|
||||
api.use("/activity-logs", activityLogsApiRouter);
|
||||
api.use("/projects-folders", projectFoldersApiRouter);
|
||||
api.use("/task-phases", taskPhasesApiRouter);
|
||||
api.use("/project-categories", projectCategoriesApiRouter);
|
||||
api.use("/home", homePageApiRouter);
|
||||
api.use("/gantt", ganttApiRouter);
|
||||
api.use("/project-comments", projectCommentsApiRouter);
|
||||
api.use("/reporting-export", reportingExportApiRouter);
|
||||
api.use("/project-healths", projectHealthsApiRouter);
|
||||
api.use("/project-templates", projectTemplatesApiRouter);
|
||||
api.use("/pt-tasks", ptTasksApiRouter);
|
||||
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
|
||||
api.use("/pt-statuses", ptStatusesApiRouter);
|
||||
api.use("/workload-gannt", workloadApiRouter);
|
||||
api.use("/roadmap-gannt", roadmapApiRouter);
|
||||
api.use("/schedule-gannt", scheduleApiRouter);
|
||||
api.use("/schedule-gannt-v2", scheduleApiV2Router);
|
||||
api.use("/project-managers", projectManagerApiRouter);
|
||||
api.use("/surveys", surveyApiRouter);
|
||||
|
||||
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
|
||||
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
|
||||
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
|
||||
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
|
||||
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
|
||||
|
||||
api.use("/billing", billingApiRouter);
|
||||
api.use("/task-dependencies", taskDependenciesApiRouter);
|
||||
|
||||
api.use("/task-recurring", taskRecurringApiRouter);
|
||||
import express from "express";
|
||||
|
||||
import AccessControlsController from "../../controllers/access-controls-controller";
|
||||
import AuthController from "../../controllers/auth-controller";
|
||||
import LogsController from "../../controllers/logs-controller";
|
||||
import OverviewController from "../../controllers/overview-controller";
|
||||
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
|
||||
|
||||
import attachmentsApiRouter from "./attachments-api-router";
|
||||
import clientsApiRouter from "./clients-api-router";
|
||||
import jobTitlesApiRouter from "./job-titles-api-router";
|
||||
import notificationsApiRouter from "./notifications-api-router";
|
||||
import personalOverviewApiRouter from "./personal-overview-api-router";
|
||||
import projectMembersApiRouter from "./project-members-api-router";
|
||||
import projectsApiRouter from "./projects-api-router";
|
||||
import settingsApiRouter from "./settings-api-router";
|
||||
import statusesApiRouter from "./statuses-api-router";
|
||||
import subTasksApiRouter from "./sub-tasks-api-router";
|
||||
import taskCommentsApiRouter from "./task-comments-api-router";
|
||||
import taskWorkLogApiRouter from "./task-work-log-api-router";
|
||||
import tasksApiRouter from "./tasks-api-router";
|
||||
import teamMembersApiRouter from "./team-members-api-router";
|
||||
import teamsApiRouter from "./teams-api-router";
|
||||
import timezonesApiRouter from "./timezones-api-router";
|
||||
import todoListApiRouter from "./todo-list-api-router";
|
||||
import projectStatusesApiRouter from "./project-statuses-api-router";
|
||||
import labelsApiRouter from "./labels-api-router";
|
||||
import sharedProjectsApiRouter from "./shared-projects-api-router";
|
||||
import resourceAllocationApiRouter from "./resource-allocation-api-router";
|
||||
import taskTemplatesApiRouter from "./task-templates-api-router";
|
||||
import projectInsightsApiRouter from "./project-insights-api-router";
|
||||
import passwordValidator from "../../middlewares/validators/password-validator";
|
||||
import adminCenterApiRouter from "./admin-center-api-router";
|
||||
import reportingApiRouter from "./reporting-api-router";
|
||||
import activityLogsApiRouter from "./activity-logs-api-router";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
import projectFoldersApiRouter from "./project-folders-api-router";
|
||||
import taskPhasesApiRouter from "./task-phases-api-router";
|
||||
import projectCategoriesApiRouter from "./project-categories-api-router";
|
||||
import homePageApiRouter from "./home-page-api-router";
|
||||
import ganttApiRouter from "./gantt-api-router";
|
||||
import projectCommentsApiRouter from "./project-comments-api-router";
|
||||
import reportingExportApiRouter from "./reporting-export-api-router";
|
||||
import projectHealthsApiRouter from "./project-healths-api-router";
|
||||
import ptTasksApiRouter from "./pt-tasks-api-router";
|
||||
import projectTemplatesApiRouter from "./project-templates-api";
|
||||
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
|
||||
import ptStatusesApiRouter from "./pt-statuses-api-router";
|
||||
import workloadApiRouter from "./gannt-apis/workload-api-router";
|
||||
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
|
||||
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
|
||||
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
|
||||
import projectManagerApiRouter from "./project-managers-api-router";
|
||||
|
||||
import billingApiRouter from "./billing-api-router";
|
||||
import taskDependenciesApiRouter from "./task-dependencies-api-router";
|
||||
|
||||
import taskRecurringApiRouter from "./task-recurring-api-router";
|
||||
|
||||
import customColumnsApiRouter from "./custom-columns-api-router";
|
||||
|
||||
const api = express.Router();
|
||||
|
||||
api.use("/projects", projectsApiRouter);
|
||||
api.use("/team-members", teamMembersApiRouter);
|
||||
api.use("/job-titles", jobTitlesApiRouter);
|
||||
api.use("/clients", clientsApiRouter);
|
||||
api.use("/teams", teamsApiRouter);
|
||||
api.use("/tasks", tasksApiRouter);
|
||||
api.use("/settings", settingsApiRouter);
|
||||
api.use("/personal-overview", personalOverviewApiRouter);
|
||||
api.use("/statuses", statusesApiRouter);
|
||||
api.use("/todo-list", todoListApiRouter);
|
||||
api.use("/notifications", notificationsApiRouter);
|
||||
api.use("/attachments", attachmentsApiRouter);
|
||||
api.use("/sub-tasks", subTasksApiRouter);
|
||||
api.use("/project-members", projectMembersApiRouter);
|
||||
api.use("/task-time-log", taskWorkLogApiRouter);
|
||||
api.use("/task-comments", taskCommentsApiRouter);
|
||||
api.use("/timezones", timezonesApiRouter);
|
||||
api.use("/project-statuses", projectStatusesApiRouter);
|
||||
api.use("/labels", labelsApiRouter);
|
||||
api.use("/resource-allocation", resourceAllocationApiRouter);
|
||||
api.use("/shared/projects", sharedProjectsApiRouter);
|
||||
api.use("/task-templates", taskTemplatesApiRouter);
|
||||
api.use("/project-insights", projectInsightsApiRouter);
|
||||
api.use("/admin-center", adminCenterApiRouter);
|
||||
api.use("/reporting", reportingApiRouter);
|
||||
api.use("/activity-logs", activityLogsApiRouter);
|
||||
api.use("/projects-folders", projectFoldersApiRouter);
|
||||
api.use("/task-phases", taskPhasesApiRouter);
|
||||
api.use("/project-categories", projectCategoriesApiRouter);
|
||||
api.use("/home", homePageApiRouter);
|
||||
api.use("/gantt", ganttApiRouter);
|
||||
api.use("/project-comments", projectCommentsApiRouter);
|
||||
api.use("/reporting-export", reportingExportApiRouter);
|
||||
api.use("/project-healths", projectHealthsApiRouter);
|
||||
api.use("/project-templates", projectTemplatesApiRouter);
|
||||
api.use("/pt-tasks", ptTasksApiRouter);
|
||||
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
|
||||
api.use("/pt-statuses", ptStatusesApiRouter);
|
||||
api.use("/workload-gannt", workloadApiRouter);
|
||||
api.use("/roadmap-gannt", roadmapApiRouter);
|
||||
api.use("/schedule-gannt", scheduleApiRouter);
|
||||
api.use("/schedule-gannt-v2", scheduleApiV2Router);
|
||||
api.use("/project-managers", projectManagerApiRouter);
|
||||
|
||||
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
|
||||
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
|
||||
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
|
||||
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
|
||||
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
|
||||
|
||||
api.use("/billing", billingApiRouter);
|
||||
api.use("/task-dependencies", taskDependenciesApiRouter);
|
||||
|
||||
api.use("/task-recurring", taskRecurringApiRouter);
|
||||
|
||||
api.use("/custom-columns", customColumnsApiRouter);
|
||||
api.use("/project-finance", projectFinanceApiRouter);
|
||||
|
||||
api.use("/project-ratecard", projectRatecardApiRouter);
|
||||
|
||||
api.use("/ratecard", ratecardApiRouter);
|
||||
|
||||
api.use("/holidays", holidayApiRouter);
|
||||
|
||||
api.use("/logs", userActivityLogsApiRouter);
|
||||
export default api;
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import express from "express";
|
||||
|
||||
import ProjectfinanceController from "../../controllers/project-finance-controller";
|
||||
import idParamValidator from "../../middlewares/validators/id-param-validator";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
|
||||
const projectFinanceApiRouter = express.Router();
|
||||
|
||||
projectFinanceApiRouter.get(
|
||||
"/project/:project_id/tasks",
|
||||
safeControllerFunction(ProjectfinanceController.getTasks)
|
||||
);
|
||||
projectFinanceApiRouter.get(
|
||||
"/project/:project_id/tasks/:parent_task_id/subtasks",
|
||||
safeControllerFunction(ProjectfinanceController.getSubTasks)
|
||||
);
|
||||
projectFinanceApiRouter.get(
|
||||
"/task/:id/breakdown",
|
||||
idParamValidator,
|
||||
safeControllerFunction(ProjectfinanceController.getTaskBreakdown)
|
||||
);
|
||||
projectFinanceApiRouter.put(
|
||||
"/task/:task_id/fixed-cost",
|
||||
safeControllerFunction(ProjectfinanceController.updateTaskFixedCost)
|
||||
);
|
||||
|
||||
projectFinanceApiRouter.put(
|
||||
"/project/:project_id/currency",
|
||||
safeControllerFunction(ProjectfinanceController.updateProjectCurrency)
|
||||
);
|
||||
projectFinanceApiRouter.put(
|
||||
"/project/:project_id/budget",
|
||||
safeControllerFunction(ProjectfinanceController.updateProjectBudget)
|
||||
);
|
||||
projectFinanceApiRouter.put(
|
||||
"/project/:project_id/calculation-method",
|
||||
safeControllerFunction(
|
||||
ProjectfinanceController.updateProjectCalculationMethod
|
||||
)
|
||||
);
|
||||
projectFinanceApiRouter.put(
|
||||
"/rate-card-role/:rate_card_role_id/man-day-rate",
|
||||
safeControllerFunction(ProjectfinanceController.updateRateCardManDayRate)
|
||||
);
|
||||
projectFinanceApiRouter.get(
|
||||
"/project/:project_id/export",
|
||||
safeControllerFunction(ProjectfinanceController.exportFinanceData)
|
||||
);
|
||||
|
||||
export default projectFinanceApiRouter;
|
||||
@@ -1,19 +0,0 @@
|
||||
import express from "express";
|
||||
import ProjectRateCardController from "../../controllers/project-ratecard-controller";
|
||||
import idParamValidator from "../../middlewares/validators/id-param-validator";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
import projectManagerValidator from "../../middlewares/validators/project-manager-validator";
|
||||
|
||||
const projectRatecardApiRouter = express.Router();
|
||||
|
||||
projectRatecardApiRouter.post("/", projectManagerValidator, safeControllerFunction(ProjectRateCardController.createMany));
|
||||
projectRatecardApiRouter.post("/create-project-rate-card-role",projectManagerValidator,safeControllerFunction(ProjectRateCardController.createOne));
|
||||
projectRatecardApiRouter.get("/project/:project_id",safeControllerFunction(ProjectRateCardController.getByProjectId));
|
||||
projectRatecardApiRouter.get("/:id",idParamValidator,safeControllerFunction(ProjectRateCardController.getById));
|
||||
projectRatecardApiRouter.put("/:id",idParamValidator,safeControllerFunction(ProjectRateCardController.updateById));
|
||||
projectRatecardApiRouter.put("/project/:project_id",safeControllerFunction(ProjectRateCardController.updateByProjectId));
|
||||
projectRatecardApiRouter.put("/project/:project_id/members/:id/rate-card-role",idParamValidator,projectManagerValidator,safeControllerFunction( ProjectRateCardController.updateProjectMemberByProjectIdAndMemberId));
|
||||
projectRatecardApiRouter.delete("/:id",idParamValidator,safeControllerFunction(ProjectRateCardController.deleteById));
|
||||
projectRatecardApiRouter.delete("/project/:project_id",safeControllerFunction(ProjectRateCardController.deleteByProjectId));
|
||||
|
||||
export default projectRatecardApiRouter;
|
||||
@@ -1,13 +0,0 @@
|
||||
import express from "express";
|
||||
|
||||
import RatecardController from "../../controllers/ratecard-controller";
|
||||
|
||||
const ratecardApiRouter = express.Router();
|
||||
|
||||
ratecardApiRouter.post("/", RatecardController.create);
|
||||
ratecardApiRouter.get("/", RatecardController.get);
|
||||
ratecardApiRouter.get("/:id", RatecardController.getById);
|
||||
ratecardApiRouter.put("/:id", RatecardController.update);
|
||||
ratecardApiRouter.delete("/:id", RatecardController.deleteById);
|
||||
|
||||
export default ratecardApiRouter;
|
||||
@@ -1,20 +0,0 @@
|
||||
import express from "express";
|
||||
import SurveyController from "../../controllers/survey-controller";
|
||||
import surveySubmissionValidator from "../../middlewares/validators/survey-submission-validator";
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
|
||||
const surveyApiRouter = express.Router();
|
||||
|
||||
// Get account setup survey with questions
|
||||
surveyApiRouter.get("/account-setup", safeControllerFunction(SurveyController.getAccountSetupSurvey));
|
||||
|
||||
// Check if user has completed account setup survey
|
||||
surveyApiRouter.get("/account-setup/status", safeControllerFunction(SurveyController.checkAccountSetupSurveyStatus));
|
||||
|
||||
// Submit survey response
|
||||
surveyApiRouter.post("/responses", surveySubmissionValidator, safeControllerFunction(SurveyController.submitSurveyResponse));
|
||||
|
||||
// Get user's survey response for a specific survey
|
||||
surveyApiRouter.get("/responses/:survey_id", safeControllerFunction(SurveyController.getUserSurveyResponse));
|
||||
|
||||
export default surveyApiRouter;
|
||||
@@ -1,11 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import UserActivityLogsController from '../../controllers/user-activity-logs-controller';
|
||||
import safeControllerFunction from "../../shared/safe-controller-function";
|
||||
|
||||
const userActivityLogsApiRouter = express.Router();
|
||||
|
||||
userActivityLogsApiRouter.get('/user-recent-tasks', safeControllerFunction(UserActivityLogsController.getRecentTasks));
|
||||
userActivityLogsApiRouter.get('/user-time-logged-tasks', safeControllerFunction(UserActivityLogsController.getTimeLoggedTasks));
|
||||
|
||||
export default userActivityLogsApiRouter;
|
||||
@@ -1,346 +0,0 @@
|
||||
/**
|
||||
* Script to update Sri Lankan holidays JSON file
|
||||
*
|
||||
* This script can be used to:
|
||||
* 1. Add holidays for new years
|
||||
* 2. Update existing holiday data
|
||||
* 3. Generate SQL migration files
|
||||
*
|
||||
* Usage:
|
||||
* node update-sri-lankan-holidays.js --year 2029 --add-poya-days
|
||||
* node update-sri-lankan-holidays.js --generate-sql --year 2029
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
class SriLankanHolidayUpdater {
|
||||
constructor() {
|
||||
this.filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json");
|
||||
this.holidayData = this.loadHolidayData();
|
||||
}
|
||||
|
||||
loadHolidayData() {
|
||||
try {
|
||||
const content = fs.readFileSync(this.filePath, "utf8");
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.error("Error loading holiday data:", error);
|
||||
return { fixed_holidays: [] };
|
||||
}
|
||||
}
|
||||
|
||||
saveHolidayData() {
|
||||
try {
|
||||
fs.writeFileSync(this.filePath, JSON.stringify(this.holidayData, null, 2));
|
||||
console.log("Holiday data saved successfully");
|
||||
} catch (error) {
|
||||
console.error("Error saving holiday data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate fixed holidays for a year
|
||||
generateFixedHolidays(year) {
|
||||
return this.holidayData.fixed_holidays.map(holiday => ({
|
||||
name: holiday.name,
|
||||
date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`,
|
||||
type: holiday.type,
|
||||
description: holiday.description,
|
||||
is_recurring: true
|
||||
}));
|
||||
}
|
||||
|
||||
// Add a new year with basic holidays
|
||||
addYear(year) {
|
||||
if (this.holidayData[year.toString()]) {
|
||||
console.log(`Year ${year} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
const fixedHolidays = this.generateFixedHolidays(year);
|
||||
this.holidayData[year.toString()] = fixedHolidays;
|
||||
|
||||
console.log(`Added basic holidays for year ${year}`);
|
||||
console.log("Note: You need to manually add Poya days, Good Friday, Eid, and Deepavali dates");
|
||||
}
|
||||
|
||||
// Generate SQL for a specific year
|
||||
generateSQL(year) {
|
||||
const yearData = this.holidayData[year.toString()];
|
||||
if (!yearData) {
|
||||
console.log(`No data found for year ${year}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = yearData.map(holiday => {
|
||||
return `('LK', '${holiday.name.replace(/'/g, "''")}', '${holiday.description.replace(/'/g, "''")}', '${holiday.date}', ${holiday.is_recurring})`;
|
||||
}).join(",\n ");
|
||||
|
||||
const sql = `-- ${year} Sri Lankan holidays
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES
|
||||
${values}
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING;`;
|
||||
|
||||
console.log(sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
// List all available years
|
||||
listYears() {
|
||||
const years = Object.keys(this.holidayData)
|
||||
.filter(key => key !== "fixed_holidays" && key !== "_metadata" && key !== "variable_holidays_info")
|
||||
.sort();
|
||||
|
||||
console.log("📅 Available years:", years.join(", "));
|
||||
console.log("");
|
||||
|
||||
years.forEach(year => {
|
||||
const count = this.holidayData[year].length;
|
||||
const source = this.holidayData._metadata?.sources?.[year] || "Unknown source";
|
||||
console.log(` ${year}: ${count} holidays - ${source}`);
|
||||
});
|
||||
|
||||
console.log("");
|
||||
console.log("⚠️ IMPORTANT: Only 2025 data has been verified from official sources.");
|
||||
console.log(" Future years should be verified before production use.");
|
||||
console.log("");
|
||||
console.log("📖 See docs/sri-lankan-holiday-update-process.md for verification process");
|
||||
}
|
||||
|
||||
// Validate holiday data
|
||||
validate() {
|
||||
const issues = [];
|
||||
|
||||
Object.keys(this.holidayData).forEach(year => {
|
||||
if (year === "fixed_holidays") return;
|
||||
|
||||
const holidays = this.holidayData[year];
|
||||
holidays.forEach((holiday, index) => {
|
||||
// Check required fields
|
||||
if (!holiday.name) issues.push(`${year}[${index}]: Missing name`);
|
||||
if (!holiday.date) issues.push(`${year}[${index}]: Missing date`);
|
||||
if (!holiday.description) issues.push(`${year}[${index}]: Missing description`);
|
||||
|
||||
// Check date format
|
||||
if (holiday.date && !/^\d{4}-\d{2}-\d{2}$/.test(holiday.date)) {
|
||||
issues.push(`${year}[${index}]: Invalid date format: ${holiday.date}`);
|
||||
}
|
||||
|
||||
// Check if date matches the year
|
||||
if (holiday.date && !holiday.date.startsWith(year)) {
|
||||
issues.push(`${year}[${index}]: Date ${holiday.date} doesn't match year ${year}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
console.log("✅ All holiday data is valid");
|
||||
} else {
|
||||
console.log("❌ Found issues:");
|
||||
issues.forEach(issue => console.log(` ${issue}`));
|
||||
}
|
||||
|
||||
return issues.length === 0;
|
||||
}
|
||||
|
||||
// Template for adding Poya days (user needs to provide actual dates)
|
||||
getPoyaDayTemplate(year) {
|
||||
const poyaDays = [
|
||||
{ name: "Duruthu", description: "Commemorates the first visit of Buddha to Sri Lanka" },
|
||||
{ name: "Navam", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples" },
|
||||
{ name: "Medin", description: "Commemorates Buddha's first visit to his father's palace after enlightenment" },
|
||||
{ name: "Bak", description: "Commemorates Buddha's second visit to Sri Lanka" },
|
||||
{ name: "Vesak", description: "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha" },
|
||||
{ name: "Poson", description: "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda" },
|
||||
{ name: "Esala", description: "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic" },
|
||||
{ name: "Nikini", description: "Commemorates the first Buddhist council" },
|
||||
{ name: "Binara", description: "Commemorates Buddha's visit to heaven to preach to his mother" },
|
||||
{ name: "Vap", description: "Marks the end of Buddhist Lent and Buddha's return from heaven" },
|
||||
{ name: "Il", description: "Commemorates Buddha's ordination of sixty disciples" },
|
||||
{ name: "Unduvap", description: "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling" }
|
||||
];
|
||||
|
||||
console.log(`\n=== TEMPLATE FOR ${year} SRI LANKAN HOLIDAYS ===\n`);
|
||||
|
||||
console.log(`// Fixed holidays (same every year)`);
|
||||
console.log(`{
|
||||
"name": "Independence Day",
|
||||
"date": "${year}-02-04",
|
||||
"type": "Public",
|
||||
"description": "Commemorates the independence of Sri Lanka from British rule in 1948",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "May Day",
|
||||
"date": "${year}-05-01",
|
||||
"type": "Public",
|
||||
"description": "International Workers' Day",
|
||||
"is_recurring": true
|
||||
},
|
||||
{
|
||||
"name": "Christmas Day",
|
||||
"date": "${year}-12-25",
|
||||
"type": "Public",
|
||||
"description": "Christian celebration of the birth of Jesus Christ",
|
||||
"is_recurring": true
|
||||
},`);
|
||||
|
||||
console.log(`\n// Variable holidays (need to verify dates)`);
|
||||
console.log(`{
|
||||
"name": "Sinhala and Tamil New Year Day",
|
||||
"date": "${year}-04-??", // Usually April 13, but can be 12 or 14
|
||||
"type": "Public",
|
||||
"description": "Traditional New Year celebrated by Sinhalese and Tamil communities",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Day after Sinhala and Tamil New Year",
|
||||
"date": "${year}-04-??", // Day after New Year Day
|
||||
"type": "Public",
|
||||
"description": "Second day of traditional New Year celebrations",
|
||||
"is_recurring": false
|
||||
},`);
|
||||
|
||||
console.log(`\n// Poya Days (lunar calendar - need to find actual dates):`);
|
||||
poyaDays.forEach((poya, index) => {
|
||||
console.log(`{
|
||||
"name": "${poya.name} Full Moon Poya Day",
|
||||
"date": "${year}-??-??",
|
||||
"type": "Poya",
|
||||
"description": "${poya.description}",
|
||||
"is_recurring": false
|
||||
},`);
|
||||
});
|
||||
|
||||
console.log(`\n// Religious holidays (need to verify dates)`);
|
||||
console.log(`{
|
||||
"name": "Good Friday",
|
||||
"date": "${year}-??-??", // Based on Easter calculation
|
||||
"type": "Public",
|
||||
"description": "Christian commemoration of the crucifixion of Jesus Christ",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Eid al-Fitr",
|
||||
"date": "${year}-??-??", // Islamic lunar calendar
|
||||
"type": "Public",
|
||||
"description": "Festival marking the end of Ramadan",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Eid al-Adha",
|
||||
"date": "${year}-??-??", // Islamic lunar calendar
|
||||
"type": "Public",
|
||||
"description": "Islamic festival of sacrifice",
|
||||
"is_recurring": false
|
||||
},
|
||||
{
|
||||
"name": "Deepavali",
|
||||
"date": "${year}-??-??", // Hindu lunar calendar
|
||||
"type": "Public",
|
||||
"description": "Hindu Festival of Lights",
|
||||
"is_recurring": false
|
||||
}`);
|
||||
|
||||
console.log(`\n=== NOTES ===`);
|
||||
console.log(`1. Sinhala & Tamil New Year: Check official gazette or Department of Meteorology`);
|
||||
console.log(`2. Poya Days: Check Buddhist calendar or astronomical calculations`);
|
||||
console.log(`3. Good Friday: Calculate based on Easter (Western calendar)`);
|
||||
console.log(`4. Islamic holidays: Check Islamic calendar or local mosque announcements`);
|
||||
console.log(`5. Deepavali: Check Hindu calendar or Tamil cultural organizations`);
|
||||
console.log(`\nReliable sources:`);
|
||||
console.log(`- Sri Lanka Department of Meteorology`);
|
||||
console.log(`- Central Bank of Sri Lanka holiday circulars`);
|
||||
console.log(`- Ministry of Public Administration gazette notifications`);
|
||||
}
|
||||
|
||||
// Show information about variable holidays
|
||||
showVariableHolidayInfo() {
|
||||
console.log(`\n=== SRI LANKAN VARIABLE HOLIDAYS INFO ===\n`);
|
||||
|
||||
console.log(`🗓️ SINHALA & TAMIL NEW YEAR:`);
|
||||
console.log(` • Usually April 13-14, but can vary to April 12-13 or April 14-15`);
|
||||
console.log(` • Based on astrological calculations`);
|
||||
console.log(` • Check: Department of Meteorology or official gazette\n`);
|
||||
|
||||
console.log(`🌕 POYA DAYS (12 per year):`);
|
||||
console.log(` • Follow Buddhist lunar calendar`);
|
||||
console.log(` • Dates change every year`);
|
||||
console.log(` • Usually fall on full moon days\n`);
|
||||
|
||||
console.log(`🕊️ GOOD FRIDAY:`);
|
||||
console.log(` • Based on Easter calculation (Western Christianity)`);
|
||||
console.log(` • First Sunday after first full moon after March 21\n`);
|
||||
|
||||
console.log(`☪️ ISLAMIC HOLIDAYS (Eid al-Fitr, Eid al-Adha):`);
|
||||
console.log(` • Follow Islamic lunar calendar (Hijri)`);
|
||||
console.log(` • Dates shift ~11 days earlier each year`);
|
||||
console.log(` • Depend on moon sighting\n`);
|
||||
|
||||
console.log(`🪔 DEEPAVALI:`);
|
||||
console.log(` • Hindu Festival of Lights`);
|
||||
console.log(` • Based on Hindu lunar calendar`);
|
||||
console.log(` • Usually October/November\n`);
|
||||
|
||||
console.log(`📋 RECOMMENDED WORKFLOW:`);
|
||||
console.log(` 1. Use --add-year to create basic structure`);
|
||||
console.log(` 2. Research accurate dates from official sources`);
|
||||
console.log(` 3. Manually edit the JSON file with correct dates`);
|
||||
console.log(` 4. Use --validate to check the data`);
|
||||
console.log(` 5. Use --generate-sql to create migration`);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
if (require.main === module) {
|
||||
const updater = new SriLankanHolidayUpdater();
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes("--list")) {
|
||||
updater.listYears();
|
||||
} else if (args.includes("--validate")) {
|
||||
updater.validate();
|
||||
} else if (args.includes("--add-year")) {
|
||||
const yearIndex = args.indexOf("--add-year") + 1;
|
||||
const year = parseInt(args[yearIndex]);
|
||||
if (year) {
|
||||
updater.addYear(year);
|
||||
updater.saveHolidayData();
|
||||
} else {
|
||||
console.log("Please provide a year: --add-year 2029");
|
||||
}
|
||||
} else if (args.includes("--generate-sql")) {
|
||||
const yearIndex = args.indexOf("--generate-sql") + 1;
|
||||
const year = parseInt(args[yearIndex]);
|
||||
if (year) {
|
||||
updater.generateSQL(year);
|
||||
} else {
|
||||
console.log("Please provide a year: --generate-sql 2029");
|
||||
}
|
||||
} else if (args.includes("--poya-template")) {
|
||||
const yearIndex = args.indexOf("--poya-template") + 1;
|
||||
const year = parseInt(args[yearIndex]);
|
||||
if (year) {
|
||||
updater.getPoyaDayTemplate(year);
|
||||
} else {
|
||||
console.log("Please provide a year: --poya-template 2029");
|
||||
}
|
||||
} else if (args.includes("--holiday-info")) {
|
||||
updater.showVariableHolidayInfo();
|
||||
} else {
|
||||
console.log(`
|
||||
Sri Lankan Holiday Updater
|
||||
|
||||
Usage:
|
||||
node update-sri-lankan-holidays.js --list # List all years
|
||||
node update-sri-lankan-holidays.js --validate # Validate data
|
||||
node update-sri-lankan-holidays.js --holiday-info # Show variable holiday info
|
||||
node update-sri-lankan-holidays.js --add-year 2029 # Add basic holidays for year
|
||||
node update-sri-lankan-holidays.js --generate-sql 2029 # Generate SQL for year
|
||||
node update-sri-lankan-holidays.js --poya-template 2029 # Show complete template for year
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SriLankanHolidayUpdater;
|
||||
@@ -1,225 +0,0 @@
|
||||
import moment from "moment";
|
||||
import db from "../config/db";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
interface HolidayData {
|
||||
name: string;
|
||||
date: string;
|
||||
description: string;
|
||||
is_recurring: boolean;
|
||||
}
|
||||
|
||||
export class HolidayDataProvider {
|
||||
/**
|
||||
* Fetch Sri Lankan holidays from external API or database
|
||||
* This provides a centralized way to get accurate holiday data
|
||||
*/
|
||||
public static async getSriLankanHolidays(year: number): Promise<HolidayData[]> {
|
||||
try {
|
||||
// First, check if we have data in the database for this year
|
||||
const dbHolidays = await this.getHolidaysFromDatabase("LK", year);
|
||||
if (dbHolidays.length > 0) {
|
||||
return dbHolidays;
|
||||
}
|
||||
|
||||
// Load holidays from JSON file
|
||||
const holidaysFromFile = this.getHolidaysFromFile(year);
|
||||
if (holidaysFromFile.length > 0) {
|
||||
// Store in database for future use
|
||||
await this.storeHolidaysInDatabase("LK", holidaysFromFile);
|
||||
return holidaysFromFile;
|
||||
}
|
||||
|
||||
// If specific year not found, generate from fixed holidays + fallback
|
||||
return this.generateHolidaysFromFixed(year);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Sri Lankan holidays:", error);
|
||||
// Fallback to basic holidays
|
||||
return this.getBasicSriLankanHolidays(year);
|
||||
}
|
||||
}
|
||||
|
||||
private static async getHolidaysFromDatabase(countryCode: string, year: number): Promise<HolidayData[]> {
|
||||
const query = `
|
||||
SELECT name, date, description, is_recurring
|
||||
FROM country_holidays
|
||||
WHERE country_code = $1
|
||||
AND EXTRACT(YEAR FROM date) = $2
|
||||
ORDER BY date
|
||||
`;
|
||||
const result = await db.query(query, [countryCode, year]);
|
||||
return result.rows.map(row => ({
|
||||
name: row.name,
|
||||
date: moment(row.date).format("YYYY-MM-DD"),
|
||||
description: row.description,
|
||||
is_recurring: row.is_recurring
|
||||
}));
|
||||
}
|
||||
|
||||
private static async storeHolidaysInDatabase(countryCode: string, holidays: HolidayData[]): Promise<void> {
|
||||
for (const holiday of holidays) {
|
||||
const query = `
|
||||
INSERT INTO country_holidays (country_code, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING
|
||||
`;
|
||||
await db.query(query, [
|
||||
countryCode,
|
||||
holiday.name,
|
||||
holiday.description,
|
||||
holiday.date,
|
||||
holiday.is_recurring
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private static getHolidaysFromFile(year: number): HolidayData[] {
|
||||
try {
|
||||
const filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
const holidayData = JSON.parse(fileContent);
|
||||
|
||||
// Check if we have data for the specific year
|
||||
if (holidayData[year.toString()]) {
|
||||
return holidayData[year.toString()].map((holiday: any) => ({
|
||||
name: holiday.name,
|
||||
date: holiday.date,
|
||||
description: holiday.description,
|
||||
is_recurring: holiday.is_recurring
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error("Error reading holidays from file:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static generateHolidaysFromFixed(year: number): HolidayData[] {
|
||||
try {
|
||||
const filePath = path.join(__dirname, "..", "data", "sri-lankan-holidays.json");
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
const holidayData = JSON.parse(fileContent);
|
||||
|
||||
// Generate holidays from fixed_holidays for the given year
|
||||
if (holidayData.fixed_holidays) {
|
||||
const fixedHolidays = holidayData.fixed_holidays.map((holiday: any) => ({
|
||||
name: holiday.name,
|
||||
date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`,
|
||||
description: holiday.description,
|
||||
is_recurring: true
|
||||
}));
|
||||
|
||||
// Log warning about incomplete data
|
||||
console.warn(`⚠️ Using only fixed holidays for Sri Lankan year ${year}. Poya days and religious holidays not included.`);
|
||||
console.warn(` To add complete data, see: docs/sri-lankan-holiday-update-process.md`);
|
||||
|
||||
return fixedHolidays;
|
||||
}
|
||||
|
||||
return this.getBasicSriLankanHolidays(year);
|
||||
} catch (error) {
|
||||
console.error("Error generating holidays from fixed data:", error);
|
||||
return this.getBasicSriLankanHolidays(year);
|
||||
}
|
||||
}
|
||||
|
||||
private static getSriLankan2025Holidays(): HolidayData[] {
|
||||
// Import the 2025 data we already have
|
||||
return [
|
||||
// Poya Days
|
||||
{ name: "Duruthu Full Moon Poya Day", date: "2025-01-13", description: "Commemorates the first visit of Buddha to Sri Lanka", is_recurring: false },
|
||||
{ name: "Navam Full Moon Poya Day", date: "2025-02-12", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples", is_recurring: false },
|
||||
{ name: "Medin Full Moon Poya Day", date: "2025-03-14", description: "Commemorates Buddha's first visit to his father's palace after enlightenment", is_recurring: false },
|
||||
{ name: "Bak Full Moon Poya Day", date: "2025-04-12", description: "Commemorates Buddha's second visit to Sri Lanka", is_recurring: false },
|
||||
{ name: "Vesak Full Moon Poya Day", date: "2025-05-12", description: "Most sacred day for Buddhists", is_recurring: false },
|
||||
{ name: "Poson Full Moon Poya Day", date: "2025-06-11", description: "Commemorates the introduction of Buddhism to Sri Lanka", is_recurring: false },
|
||||
{ name: "Esala Full Moon Poya Day", date: "2025-07-10", description: "Commemorates Buddha's first sermon", is_recurring: false },
|
||||
{ name: "Nikini Full Moon Poya Day", date: "2025-08-09", description: "Commemorates the first Buddhist council", is_recurring: false },
|
||||
{ name: "Binara Full Moon Poya Day", date: "2025-09-07", description: "Commemorates Buddha's visit to heaven", is_recurring: false },
|
||||
{ name: "Vap Full Moon Poya Day", date: "2025-10-07", description: "Marks the end of Buddhist Lent", is_recurring: false },
|
||||
{ name: "Il Full Moon Poya Day", date: "2025-11-05", description: "Commemorates Buddha's ordination of sixty disciples", is_recurring: false },
|
||||
{ name: "Unduvap Full Moon Poya Day", date: "2025-12-04", description: "Commemorates the arrival of Sanghamitta Theri", is_recurring: false },
|
||||
|
||||
// Fixed holidays
|
||||
{ name: "Independence Day", date: "2025-02-04", description: "Sri Lankan Independence Day", is_recurring: true },
|
||||
{ name: "Sinhala and Tamil New Year Day", date: "2025-04-13", description: "Traditional New Year", is_recurring: true },
|
||||
{ name: "Day after Sinhala and Tamil New Year", date: "2025-04-14", description: "New Year celebrations", is_recurring: true },
|
||||
{ name: "May Day", date: "2025-05-01", description: "International Workers' Day", is_recurring: true },
|
||||
{ name: "Christmas Day", date: "2025-12-25", description: "Christmas", is_recurring: true },
|
||||
|
||||
// Variable holidays
|
||||
{ name: "Good Friday", date: "2025-04-18", description: "Christian holiday", is_recurring: false },
|
||||
{ name: "Day after Vesak Full Moon Poya Day", date: "2025-05-13", description: "Vesak celebrations", is_recurring: false },
|
||||
{ name: "Eid al-Fitr", date: "2025-03-31", description: "End of Ramadan", is_recurring: false },
|
||||
{ name: "Deepavali", date: "2025-10-20", description: "Hindu Festival of Lights", is_recurring: false }
|
||||
];
|
||||
}
|
||||
|
||||
private static generateApproximateHolidays(year: number): HolidayData[] {
|
||||
// This is a fallback method that generates approximate dates
|
||||
// In production, you should use accurate astronomical calculations or external data
|
||||
const holidays: HolidayData[] = [];
|
||||
|
||||
// Fixed holidays
|
||||
holidays.push(
|
||||
{ name: "Independence Day", date: `${year}-02-04`, description: "Sri Lankan Independence Day", is_recurring: true },
|
||||
{ name: "Sinhala and Tamil New Year Day", date: `${year}-04-13`, description: "Traditional New Year", is_recurring: true },
|
||||
{ name: "Day after Sinhala and Tamil New Year", date: `${year}-04-14`, description: "New Year celebrations", is_recurring: true },
|
||||
{ name: "May Day", date: `${year}-05-01`, description: "International Workers' Day", is_recurring: true },
|
||||
{ name: "Christmas Day", date: `${year}-12-25`, description: "Christmas", is_recurring: true }
|
||||
);
|
||||
|
||||
// Note: For Poya days and other religious holidays, you would need
|
||||
// astronomical calculations or reliable external data sources
|
||||
|
||||
return holidays;
|
||||
}
|
||||
|
||||
private static getBasicSriLankanHolidays(year: number): HolidayData[] {
|
||||
// Return only the fixed holidays that don't change
|
||||
return [
|
||||
{ name: "Independence Day", date: `${year}-02-04`, description: "Sri Lankan Independence Day", is_recurring: true },
|
||||
{ name: "Sinhala and Tamil New Year Day", date: `${year}-04-13`, description: "Traditional New Year", is_recurring: true },
|
||||
{ name: "Day after Sinhala and Tamil New Year", date: `${year}-04-14`, description: "New Year celebrations", is_recurring: true },
|
||||
{ name: "May Day", date: `${year}-05-01`, description: "International Workers' Day", is_recurring: true },
|
||||
{ name: "Christmas Day", date: `${year}-12-25`, description: "Christmas", is_recurring: true }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update organization holidays for a specific year
|
||||
* This can be called periodically to ensure holiday data is up to date
|
||||
*/
|
||||
public static async updateOrganizationHolidays(organizationId: string, countryCode: string, year: number): Promise<void> {
|
||||
if (countryCode !== "LK") return;
|
||||
|
||||
const holidays = await this.getSriLankanHolidays(year);
|
||||
|
||||
// Get default holiday type
|
||||
const typeQuery = `SELECT id FROM holiday_types WHERE name = 'Public Holiday' LIMIT 1`;
|
||||
const typeResult = await db.query(typeQuery);
|
||||
const holidayTypeId = typeResult.rows[0]?.id;
|
||||
|
||||
if (!holidayTypeId) return;
|
||||
|
||||
// Insert holidays into organization_holidays
|
||||
for (const holiday of holidays) {
|
||||
const query = `
|
||||
INSERT INTO organization_holidays (organization_id, holiday_type_id, name, description, date, is_recurring)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (organization_id, date) DO NOTHING
|
||||
`;
|
||||
await db.query(query, [
|
||||
organizationId,
|
||||
holidayTypeId,
|
||||
holiday.name,
|
||||
holiday.description,
|
||||
holiday.date,
|
||||
holiday.is_recurring
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import moment from "moment";
|
||||
|
||||
interface SriLankanHoliday {
|
||||
name: string;
|
||||
date: string;
|
||||
type: "Public" | "Bank" | "Mercantile" | "Poya";
|
||||
description: string;
|
||||
is_recurring: boolean;
|
||||
is_poya: boolean;
|
||||
country_code: string;
|
||||
color_code: string;
|
||||
}
|
||||
|
||||
export class SriLankanHolidayService {
|
||||
private static readonly COUNTRY_CODE = "LK";
|
||||
|
||||
// Fixed recurring holidays (same date every year)
|
||||
private static readonly FIXED_HOLIDAYS = [
|
||||
{
|
||||
name: "Independence Day",
|
||||
month: 2,
|
||||
day: 4,
|
||||
type: "Public" as const,
|
||||
description: "Commemorates the independence of Sri Lanka from British rule in 1948",
|
||||
color_code: "#DC143C"
|
||||
},
|
||||
{
|
||||
name: "Sinhala and Tamil New Year Day",
|
||||
month: 4,
|
||||
day: 13,
|
||||
type: "Public" as const,
|
||||
description: "Traditional New Year celebrated by Sinhalese and Tamil communities",
|
||||
color_code: "#DC143C"
|
||||
},
|
||||
{
|
||||
name: "Day after Sinhala and Tamil New Year",
|
||||
month: 4,
|
||||
day: 14,
|
||||
type: "Public" as const,
|
||||
description: "Second day of traditional New Year celebrations",
|
||||
color_code: "#DC143C"
|
||||
},
|
||||
{
|
||||
name: "May Day",
|
||||
month: 5,
|
||||
day: 1,
|
||||
type: "Public" as const,
|
||||
description: "International Workers' Day",
|
||||
color_code: "#DC143C"
|
||||
},
|
||||
{
|
||||
name: "Christmas Day",
|
||||
month: 12,
|
||||
day: 25,
|
||||
type: "Public" as const,
|
||||
description: "Christian celebration of the birth of Jesus Christ",
|
||||
color_code: "#DC143C"
|
||||
}
|
||||
];
|
||||
|
||||
// Poya days names (in order of Buddhist months)
|
||||
private static readonly POYA_NAMES = [
|
||||
{ name: "Duruthu", description: "Commemorates the first visit of Buddha to Sri Lanka" },
|
||||
{ name: "Navam", description: "Commemorates the appointment of Sariputta and Moggallana as Buddha's chief disciples" },
|
||||
{ name: "Medin", description: "Commemorates Buddha's first visit to his father's palace after enlightenment" },
|
||||
{ name: "Bak", description: "Commemorates Buddha's second visit to Sri Lanka" },
|
||||
{ name: "Vesak", description: "Most sacred day for Buddhists - commemorates birth, enlightenment and passing of Buddha" },
|
||||
{ name: "Poson", description: "Commemorates the introduction of Buddhism to Sri Lanka by Arahat Mahinda" },
|
||||
{ name: "Esala", description: "Commemorates Buddha's first sermon and the arrival of the Sacred Tooth Relic" },
|
||||
{ name: "Nikini", description: "Commemorates the first Buddhist council" },
|
||||
{ name: "Binara", description: "Commemorates Buddha's visit to heaven to preach to his mother" },
|
||||
{ name: "Vap", description: "Marks the end of Buddhist Lent and Buddha's return from heaven" },
|
||||
{ name: "Il", description: "Commemorates Buddha's ordination of sixty disciples" },
|
||||
{ name: "Unduvap", description: "Commemorates the arrival of Sanghamitta Theri with the Sacred Bo sapling" }
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate Poya days for a given year
|
||||
* Note: This is a simplified calculation. For production use, consider using
|
||||
* astronomical calculations or an API that provides accurate lunar calendar dates
|
||||
*/
|
||||
private static calculatePoyaDays(year: number): SriLankanHoliday[] {
|
||||
const poyaDays: SriLankanHoliday[] = [];
|
||||
|
||||
// This is a simplified approach - in reality, you would need astronomical calculations
|
||||
// or use a service that provides accurate Buddhist lunar calendar dates
|
||||
// For now, we'll use approximate dates based on lunar month cycles
|
||||
|
||||
// Starting from a known Vesak date (May full moon)
|
||||
// and calculating other Poya days based on lunar month intervals
|
||||
const baseVesakDate = this.getVesakDate(year);
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const monthsFromVesak = i - 4; // Vesak is the 5th month
|
||||
const poyaDate = moment(baseVesakDate).add(monthsFromVesak * 29.53, "days"); // Lunar month average
|
||||
|
||||
// Adjust to the nearest full moon date (would need proper calculation in production)
|
||||
const poyaInfo = this.POYA_NAMES[i];
|
||||
|
||||
poyaDays.push({
|
||||
name: `${poyaInfo.name} Full Moon Poya Day`,
|
||||
date: poyaDate.format("YYYY-MM-DD"),
|
||||
type: "Poya",
|
||||
description: poyaInfo.description,
|
||||
is_recurring: false,
|
||||
is_poya: true,
|
||||
country_code: this.COUNTRY_CODE,
|
||||
color_code: "#8B4513"
|
||||
});
|
||||
}
|
||||
|
||||
return poyaDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approximate Vesak date for a year
|
||||
* Vesak typically falls on the full moon in May
|
||||
*/
|
||||
private static getVesakDate(year: number): Date {
|
||||
// This is a simplified calculation
|
||||
// In production, use astronomical calculations or a reliable API
|
||||
const may1 = new Date(year, 4, 1); // May 1st
|
||||
const fullMoonDay = 15; // Approximate - would need proper lunar calculation
|
||||
return new Date(year, 4, fullMoonDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Easter date for a year (Western/Gregorian calendar)
|
||||
* Using Computus algorithm
|
||||
*/
|
||||
private static getEasterDate(year: number): Date {
|
||||
const a = year % 19;
|
||||
const b = Math.floor(year / 100);
|
||||
const c = year % 100;
|
||||
const d = Math.floor(b / 4);
|
||||
const e = b % 4;
|
||||
const f = Math.floor((b + 8) / 25);
|
||||
const g = Math.floor((b - f + 1) / 3);
|
||||
const h = (19 * a + b - d - g + 15) % 30;
|
||||
const i = Math.floor(c / 4);
|
||||
const k = c % 4;
|
||||
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
||||
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
||||
const day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Sri Lankan holidays for a given year
|
||||
*/
|
||||
public static getHolidaysForYear(year: number): SriLankanHoliday[] {
|
||||
const holidays: SriLankanHoliday[] = [];
|
||||
|
||||
// Add fixed holidays
|
||||
for (const holiday of this.FIXED_HOLIDAYS) {
|
||||
holidays.push({
|
||||
...holiday,
|
||||
date: `${year}-${String(holiday.month).padStart(2, "0")}-${String(holiday.day).padStart(2, "0")}`,
|
||||
is_recurring: true,
|
||||
is_poya: false,
|
||||
country_code: this.COUNTRY_CODE
|
||||
});
|
||||
}
|
||||
|
||||
// Add Poya days
|
||||
const poyaDays = this.calculatePoyaDays(year);
|
||||
holidays.push(...poyaDays);
|
||||
|
||||
// Add Good Friday (2 days before Easter)
|
||||
const easter = this.getEasterDate(year);
|
||||
const goodFriday = moment(easter).subtract(2, "days");
|
||||
holidays.push({
|
||||
name: "Good Friday",
|
||||
date: goodFriday.format("YYYY-MM-DD"),
|
||||
type: "Public",
|
||||
description: "Christian commemoration of the crucifixion of Jesus Christ",
|
||||
is_recurring: false,
|
||||
is_poya: false,
|
||||
country_code: this.COUNTRY_CODE,
|
||||
color_code: "#DC143C"
|
||||
});
|
||||
|
||||
// Add day after Vesak
|
||||
const vesakDay = poyaDays.find(p => p.name.includes("Vesak"));
|
||||
if (vesakDay) {
|
||||
const dayAfterVesak = moment(vesakDay.date).add(1, "day");
|
||||
holidays.push({
|
||||
name: "Day after Vesak Full Moon Poya Day",
|
||||
date: dayAfterVesak.format("YYYY-MM-DD"),
|
||||
type: "Public",
|
||||
description: "Additional day for Vesak celebrations",
|
||||
is_recurring: false,
|
||||
is_poya: false,
|
||||
country_code: this.COUNTRY_CODE,
|
||||
color_code: "#DC143C"
|
||||
});
|
||||
}
|
||||
|
||||
// Note: Eid and Deepavali dates would need to be calculated based on
|
||||
// Islamic and Hindu calendars respectively, or fetched from an external source
|
||||
|
||||
return holidays.sort((a, b) => a.date.localeCompare(b.date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SQL insert statements for holidays
|
||||
*/
|
||||
public static generateSQL(year: number, tableName = "country_holidays"): string {
|
||||
const holidays = this.getHolidaysForYear(year);
|
||||
const values = holidays.map(holiday => {
|
||||
return `('${this.COUNTRY_CODE}', '${holiday.name.replace(/'/g, "''")}', '${holiday.description.replace(/'/g, "''")}', '${holiday.date}', ${holiday.is_recurring})`;
|
||||
}).join(",\n ");
|
||||
|
||||
return `INSERT INTO ${tableName} (country_code, name, description, date, is_recurring)
|
||||
VALUES
|
||||
${values}
|
||||
ON CONFLICT (country_code, name, date) DO NOTHING;`;
|
||||
}
|
||||
}
|
||||
@@ -160,9 +160,6 @@ export const PASSWORD_POLICY = "Minimum of 8 characters, with upper and lowercas
|
||||
// paddle status to exclude
|
||||
export const statusExclude = ["past_due", "paused", "deleted"];
|
||||
|
||||
// Trial user team member limit
|
||||
export const TRIAL_MEMBER_LIMIT = 10;
|
||||
|
||||
export const HTML_TAG_REGEXP = /<\/?[^>]+>/gi;
|
||||
|
||||
export const UNMAPPED = "Unmapped";
|
||||
|
||||
@@ -15,8 +15,4 @@ VITE_RECAPTCHA_SITE_KEY=recaptcha-site-key
|
||||
VITE_WORKLENZ_SESSION_ID=worklenz-session-id
|
||||
|
||||
# Google Login
|
||||
VITE_ENABLE_GOOGLE_LOGIN=false
|
||||
|
||||
# Survey Modal Configuration
|
||||
# Set to true to enable the survey modal, false to disable it
|
||||
VITE_ENABLE_SURVEY_MODAL=false
|
||||
VITE_ENABLE_GOOGLE_LOGIN=false
|
||||
@@ -1,22 +0,0 @@
|
||||
VITE_API_URL=http://localhost:3000
|
||||
VITE_SOCKET_URL=ws://localhost:3000
|
||||
|
||||
VITE_APP_TITLE=Worklenz
|
||||
VITE_APP_ENV=development
|
||||
|
||||
# Mixpanel
|
||||
VITE_MIXPANEL_TOKEN=mixpanel-token
|
||||
|
||||
# Recaptcha
|
||||
VITE_ENABLE_RECAPTCHA=false
|
||||
VITE_RECAPTCHA_SITE_KEY=recaptcha-site-key
|
||||
|
||||
# Session ID
|
||||
VITE_WORKLENZ_SESSION_ID=worklenz-session-id
|
||||
|
||||
# Google Login
|
||||
VITE_ENABLE_GOOGLE_LOGIN=false
|
||||
|
||||
# Survey Modal Configuration
|
||||
# Set to true to enable the survey modal, false to disable it
|
||||
VITE_ENABLE_SURVEY_MODAL=false
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2b2b2b" />
|
||||
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="application-name" content="Worklenz" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
@@ -17,45 +17,27 @@
|
||||
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||
<meta name="msapplication-TileColor" content="#2b2b2b" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/favicon.ico" />
|
||||
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
|
||||
<!-- Resource hints for better loading performance -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||
<link rel="dns-prefetch" href="https://js.hs-scripts.com" />
|
||||
|
||||
|
||||
<!-- Preload critical resources -->
|
||||
<link
|
||||
rel="preload"
|
||||
href="/locales/en/common.json"
|
||||
as="fetch"
|
||||
type="application/json"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/locales/en/auth/login.json"
|
||||
as="fetch"
|
||||
type="application/json"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/locales/en/navbar.json"
|
||||
as="fetch"
|
||||
type="application/json"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<link rel="preload" href="/locales/en/common.json" as="fetch" type="application/json" crossorigin />
|
||||
<link rel="preload" href="/locales/en/auth/login.json" as="fetch" type="application/json" crossorigin />
|
||||
<link rel="preload" href="/locales/en/navbar.json" as="fetch" type="application/json" crossorigin />
|
||||
|
||||
<!-- Optimized font loading with font-display: swap -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"
|
||||
@@ -69,12 +51,12 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</noscript>
|
||||
|
||||
|
||||
<title>Worklenz</title>
|
||||
|
||||
|
||||
<!-- Environment configuration -->
|
||||
<script src="/env-config.js"></script>
|
||||
|
||||
|
||||
<!-- Analytics Module -->
|
||||
<script src="/js/analytics.js"></script>
|
||||
</head>
|
||||
|
||||
554
worklenz-frontend/package-lock.json
generated
554
worklenz-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,7 @@
|
||||
"build": "vite build",
|
||||
"dev-build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:ui": "vitest --ui"
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.1.0",
|
||||
@@ -81,10 +77,7 @@
|
||||
"@types/react-dom": "19.0.0",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"rollup": "^4.40.2",
|
||||
|
||||
@@ -66,7 +66,7 @@ class AnalyticsManager {
|
||||
|
||||
// Add event listener to button
|
||||
const btn = notice.querySelector('#analytics-notice-btn');
|
||||
btn.addEventListener('click', e => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
localStorage.setItem('privacyNoticeShown', 'true');
|
||||
notice.remove();
|
||||
@@ -77,7 +77,7 @@ class AnalyticsManager {
|
||||
* Check if privacy notice should be shown
|
||||
*/
|
||||
checkPrivacyNotice() {
|
||||
const isProduction =
|
||||
const isProduction =
|
||||
window.location.hostname === 'worklenz.com' ||
|
||||
window.location.hostname === 'app.worklenz.com';
|
||||
const noticeShown = localStorage.getItem('privacyNoticeShown') === 'true';
|
||||
@@ -94,4 +94,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const analytics = new AnalyticsManager();
|
||||
analytics.init();
|
||||
analytics.checkPrivacyNotice();
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,7 @@ class HubSpotManager {
|
||||
* Load HubSpot script with dark mode support
|
||||
*/
|
||||
init() {
|
||||
// if (!this.isProduction) return;
|
||||
if (!this.isProduction) return;
|
||||
|
||||
const loadHubSpot = () => {
|
||||
const script = document.createElement('script');
|
||||
@@ -24,10 +24,10 @@ class HubSpotManager {
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.src = this.scriptSrc;
|
||||
|
||||
|
||||
// Configure dark mode after script loads
|
||||
script.onload = () => this.setupDarkModeSupport();
|
||||
|
||||
|
||||
document.body.appendChild(script);
|
||||
};
|
||||
|
||||
@@ -45,27 +45,26 @@ class HubSpotManager {
|
||||
setupDarkModeSupport() {
|
||||
const applyTheme = () => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
|
||||
// Remove existing theme styles
|
||||
const existingStyle = document.getElementById(this.styleId);
|
||||
if (existingStyle) {
|
||||
existingStyle.remove();
|
||||
}
|
||||
|
||||
// Apply dark mode CSS if dark theme is active
|
||||
if (isDark) {
|
||||
this.injectDarkModeCSS();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Apply initial theme after delay to ensure widget is loaded
|
||||
setTimeout(applyTheme, 1000);
|
||||
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(applyTheme);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,27 +76,40 @@ class HubSpotManager {
|
||||
style.id = this.styleId;
|
||||
style.textContent = `
|
||||
/* HubSpot Chat Widget Dark Mode Override */
|
||||
/*
|
||||
Note: We can only style the container backgrounds, not the widget UI inside the iframe.
|
||||
HubSpot does not currently support external dark mode theming for the chat UI itself.
|
||||
*/
|
||||
#hubspot-conversations-inline-parent,
|
||||
#hubspot-conversations-iframe-container {
|
||||
background: #141414 !important;
|
||||
#hubspot-conversations-iframe-container,
|
||||
.shadow-2xl.widget-align-right.widget-align-bottom,
|
||||
[data-test-id="chat-widget"],
|
||||
[class*="VizExCollapsedChat"],
|
||||
[class*="VizExExpandedChat"],
|
||||
iframe[src*="hubspot"] {
|
||||
filter: invert(1) hue-rotate(180deg) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Target HubSpot widget container backgrounds */
|
||||
#hubspot-conversations-inline-parent div,
|
||||
#hubspot-conversations-iframe-container div,
|
||||
[data-test-id="chat-widget"] div {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/* Ensure Worklenz app elements are not affected by HubSpot styles */
|
||||
.ant-menu,
|
||||
.ant-menu *,
|
||||
[class*="settings"],
|
||||
[class*="sidebar"],
|
||||
.worklenz-app *:not([id*="hubspot"]):not([class*="widget"]) {
|
||||
filter: none !important;
|
||||
|
||||
/* Prevent double inversion of images, avatars, and icons */
|
||||
#hubspot-conversations-iframe-container img,
|
||||
#hubspot-conversations-iframe-container [style*="background-image"],
|
||||
#hubspot-conversations-iframe-container svg,
|
||||
iframe[src*="hubspot"] img,
|
||||
iframe[src*="hubspot"] svg,
|
||||
[data-test-id="chat-widget"] img,
|
||||
[data-test-id="chat-widget"] svg {
|
||||
filter: invert(1) hue-rotate(180deg) !important;
|
||||
}
|
||||
|
||||
/* Additional targeting for widget launcher and chat bubble */
|
||||
div[class*="shadow-2xl"],
|
||||
div[class*="widget-align"],
|
||||
div[style*="position: fixed"] {
|
||||
filter: invert(1) hue-rotate(180deg) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -109,7 +121,7 @@ class HubSpotManager {
|
||||
cleanup() {
|
||||
const script = document.getElementById(this.scriptId);
|
||||
const style = document.getElementById(this.styleId);
|
||||
|
||||
|
||||
if (script) script.remove();
|
||||
if (style) style.remove();
|
||||
}
|
||||
@@ -119,14 +131,7 @@ class HubSpotManager {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const hubspot = new HubSpotManager();
|
||||
hubspot.init();
|
||||
|
||||
|
||||
// Make available globally for potential cleanup
|
||||
window.HubSpotManager = hubspot;
|
||||
});
|
||||
|
||||
// Add this style to ensure the chat widget uses the light color scheme
|
||||
(function() {
|
||||
var style = document.createElement('style');
|
||||
style.innerHTML = '#hubspot-messages-iframe-container { color-scheme: light !important; }';
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
});
|
||||
@@ -1,194 +1,31 @@
|
||||
{
|
||||
"continue": "Vazhdo",
|
||||
|
||||
"setupYourAccount": "Konfiguro llogarinë tënde.",
|
||||
"organizationStepTitle": "Emërto organizatën tënde",
|
||||
"organizationStepLabel": "Zgjidh një emër për llogarinë tënde në Worklenz.",
|
||||
"organizationStepWelcome": "Konfiguro llogarinë tënde në Worklenz.",
|
||||
"organizationStepDescription": "Le të fillojmë duke konfiguruar organizatën tënde. Kjo do të jetë hapësira kryesore e punës për ekipin tënd.",
|
||||
"organizationStepTooltip": "Ky emër do të shfaqet në hapësirën tënde të punës dhe mund të ndryshohet më vonë në cilësime.",
|
||||
"organizationStepNeedIdeas": "Keni nevojë për ide?",
|
||||
"organizationStepUseDetected": "Përdorimi i zbuluar:",
|
||||
"organizationStepCharacters": "karaktere",
|
||||
"organizationStepGoodLength": "Gjatësi e mirë",
|
||||
"organizationStepTooShort": "Shumë i shkurtër",
|
||||
"organizationStepNamingTips": "Këshilla për emërtimin",
|
||||
"organizationStepTip1": "Mbaje të thjeshtë dhe të lehtë për t'u mbajtur mend",
|
||||
"organizationStepTip2": "Përfaqëso industrinë ose vlerat e tua",
|
||||
"organizationStepTip3": "Mendo për rritjen në të ardhmen",
|
||||
"organizationStepTip4": "Bëje unik dhe të përshtatshëm për markë",
|
||||
"organizationStepSuggestionsTitle": "Sugjerime për emra",
|
||||
"organizationStepCategory1": "Kompani Teknologjie",
|
||||
"organizationStepCategory2": "Agjenci Kreative",
|
||||
"organizationStepCategory3": "Konsulencë",
|
||||
"organizationStepCategory4": "Startupe",
|
||||
"organizationStepSuggestionsNote": "Këto janë vetëm shembuj për të të ndihmuar të fillosh. Zgjidh diçka që përfaqëson organizatën tënde.",
|
||||
"organizationStepPrivacyNote": "Emri i organizatës tënde është privat dhe i dukshëm vetëm për anëtarët e ekipit.",
|
||||
"projectStepTitle": "Krijo projektin tënd të parë",
|
||||
"projectStepLabel": "Në cilin projekt po punon tani?",
|
||||
"setupYourAccount": "Konfiguro Llogarinë Tënde në Worklenz.",
|
||||
"organizationStepTitle": "Emërtoni Organizatën Tuaj",
|
||||
"organizationStepLabel": "Zgjidhni një emër për llogarinë tuaj në Worklenz.",
|
||||
|
||||
"projectStepTitle": "Krijoni projektin tuaj të parë",
|
||||
"projectStepLabel": "Në cilin projekt po punoni aktualisht?",
|
||||
"projectStepPlaceholder": "p.sh. Plani i Marketingut",
|
||||
"tasksStepTitle": "Krijo detyrat e tua të para",
|
||||
"tasksStepLabel": "Shkruaj disa detyra që do të kryesh në",
|
||||
|
||||
"tasksStepTitle": "Krijoni detyrat tuaja të para",
|
||||
"tasksStepLabel": "Shkruani disa detyra që do të kryeni në",
|
||||
"tasksStepAddAnother": "Shto një tjetër",
|
||||
"emailPlaceholder": "Adresa e emailit",
|
||||
"invalidEmail": "Ju lutem vendosni një adresë emaili të vlefshme",
|
||||
|
||||
"emailPlaceholder": "Adresa email",
|
||||
"invalidEmail": "Ju lutemi vendosni një adresë email të vlefshme",
|
||||
"or": "ose",
|
||||
"templateButton": "Importo nga shablloni",
|
||||
"goBack": "Kthehu mbrapa",
|
||||
"goBack": "Kthehu Mbrapa",
|
||||
"cancel": "Anulo",
|
||||
"create": "Krijo",
|
||||
"templateDrawerTitle": "Zgjidh nga shabllonet",
|
||||
"step3InputLabel": "Fto me email",
|
||||
"addAnother": "Shto një tjetër",
|
||||
"skipForNow": "Kalo për tani",
|
||||
"skipping": "Duke kaluar...",
|
||||
"formTitle": "Krijo detyrën tënde të parë.",
|
||||
"step3Title": "Fto ekipin tënd për të punuar së bashku",
|
||||
"skipForNow": "Kalo tani për tani",
|
||||
"formTitle": "Krijoni detyrën tuaj të parë.",
|
||||
"step3Title": "Fto ekipin tënd të punojë me",
|
||||
"maxMembers": " (Mund të ftoni deri në 5 anëtarë)",
|
||||
"maxTasks": " (Mund të krijoni deri në 5 detyra)",
|
||||
"membersStepTitle": "Fto ekipin tënd",
|
||||
"membersStepDescription": "Shto anëtarë ekipi në \"{{organizationName}}\" dhe filloni bashkëpunimin",
|
||||
"memberPlaceholder": "Anëtari i ekipit {{index}} - Shkruani adresën e emailit",
|
||||
"validEmailAddress": "Adresë emaili e vlefshme",
|
||||
"addAnotherTeamMember": "Shto një anëtar tjetër të ekipit ({{current}}/{{max}})",
|
||||
"canInviteLater": "Gjithmonë mund të ftoni anëtarë të ekipit më vonë",
|
||||
"skipStepDescription": "Nuk i keni adresat e emailit gati? Asnjë problem! Mund ta kaloni këtë hap dhe të ftoni anëtarë nga paneli i projektit më vonë.",
|
||||
"orgCategoryTech": "Kompani Teknologjie",
|
||||
"orgCategoryCreative": "Agjenci Kreative",
|
||||
"orgCategoryConsulting": "Konsulencë",
|
||||
"orgCategoryStartups": "Startupe",
|
||||
"namingTip1": "Mbaje të thjeshtë dhe të lehtë për t'u mbajtur mend",
|
||||
"namingTip2": "Përfaqëso industrinë ose vlerat e tua",
|
||||
"namingTip3": "Mendo për rritjen në të ardhmen",
|
||||
"namingTip4": "Bëje unik dhe të përshtatshëm për markë",
|
||||
"aboutYouTitle": "Na trego për veten tënde",
|
||||
"aboutYouDescription": "Na ndihmo të personalizojmë përvojën tënde",
|
||||
"orgTypeQuestion": "Cila përshkruan më mirë organizatën tënde?",
|
||||
"userRoleQuestion": "Cili është roli yt?",
|
||||
"yourNeedsTitle": "Cilat janë nevojat e tua kryesore?",
|
||||
"yourNeedsDescription": "Zgjidh të gjitha që aplikohen për të na ndihmuar të konfigurojmë hapësirën tënde të punës",
|
||||
"yourNeedsQuestion": "Si do ta përdorësh kryesisht Worklenz?",
|
||||
"useCaseTaskOrg": "Organizo dhe ndiq detyrat",
|
||||
"useCaseTeamCollab": "Puno së bashku pa pengesa",
|
||||
"useCaseResourceMgmt": "Menaxho kohën dhe burimet",
|
||||
"useCaseClientComm": "Qëndro i lidhur me klientët",
|
||||
"useCaseTimeTrack": "Monitoro orët e projektit",
|
||||
"useCaseOther": "Diçka tjetër",
|
||||
"selectedText": "zgjedhur",
|
||||
"previousToolsQuestion": "Çfarë mjetesh ke përdorur më parë? (Opsionale)",
|
||||
"discoveryTitle": "Edhe një gjë e fundit...",
|
||||
"discoveryDescription": "Na ndihmo të kuptojmë si e zbulove Worklenz",
|
||||
"discoveryQuestion": "Si dëgjove për ne?",
|
||||
"allSetTitle": "Çdo gjë gati!",
|
||||
"allSetDescription": "Le të krijojmë projektin tënd të parë dhe të fillojmë me Worklenz",
|
||||
"surveyCompleteTitle": "Faleminderit!",
|
||||
"surveyCompleteDescription": "Përgjigjet tuaja na ndihmojnë të përmirësojmë Worklenz për të gjithë",
|
||||
"aboutYouStepName": "Rreth teje",
|
||||
"yourNeedsStepName": "Nevojat e tua",
|
||||
"discoveryStepName": "Zbulimi",
|
||||
"stepProgress": "Hapi {step} nga 3: {title}",
|
||||
"projectStepHeader": "Le të krijojmë projektin tënd të parë",
|
||||
"projectStepSubheader": "Fillo nga e para ose përdor një shabllon për të filluar më shpejt",
|
||||
"startFromScratch": "Fillo nga e para",
|
||||
"templateSelected": "Shablloni i zgjedhur më poshtë",
|
||||
"quickSuggestions": "Sugjerime të shpejta:",
|
||||
"orText": "OSE",
|
||||
"startWithTemplate": "Fillo me një shabllon",
|
||||
"clearToSelectTemplate": "Pastro emrin e projektit më sipër për të zgjedhur një shabllon",
|
||||
"templateHeadStart": "Fillo më shpejt me struktura të gatshme projekti",
|
||||
"browseAllTemplates": "Shfleto të gjitha shabllonet",
|
||||
"templatesAvailable": "15+ shabllone të specializuara sipas industrisë në dispozicion",
|
||||
"chooseTemplate": "Zgjidh një shabllon që i përshtatet llojit të projektit tënd",
|
||||
"createProject": "Krijo projekt",
|
||||
"templateSoftwareDev": "Zhvillim Softueri",
|
||||
"templateSoftwareDesc": "Sprint-e agile, ndjekje gabimesh, lëshime",
|
||||
"templateMarketing": "Fushatë Marketingu",
|
||||
"templateMarketingDesc": "Planifikim fushate, kalendar përmbajtjesh",
|
||||
"templateConstruction": "Projekt Ndërtimi",
|
||||
"templateConstructionDesc": "Faza, leje, kontraktorë",
|
||||
"templateStartup": "Lansim Startup-i",
|
||||
"templateStartupDesc": "Zhvillim MVP, financim, rritje",
|
||||
"tasksStepDescription": "Ndaji \"{{projectName}}\" në detyra të veprueshme për të filluar",
|
||||
"taskPlaceholder": "Detyra {{index}} - p.sh., Çfarë duhet bërë?",
|
||||
"addAnotherTask": "Shto një detyrë tjetër ({{current}}/{{max}})",
|
||||
"surveyStepTitle": "Na trego për veten tënde",
|
||||
"surveyStepLabel": "Na ndihmo të personalizojmë përvojën tënde në Worklenz duke iu përgjigjur disa pyetjeve.",
|
||||
"organizationType": "Cila përshkruan më mirë organizatën tënde?",
|
||||
"organizationTypeFreelancer": "Freelancer",
|
||||
"organizationTypeStartup": "Startup",
|
||||
"organizationTypeSmallMediumBusiness": "Biznes i Vogël ose i Mesëm",
|
||||
"organizationTypeAgency": "Agjenci",
|
||||
"organizationTypeEnterprise": "Ndërmarrje",
|
||||
"organizationTypeOther": "Tjetër",
|
||||
"userRole": "Cili është roli yt?",
|
||||
"userRoleFounderCeo": "Themelues / CEO",
|
||||
"userRoleProjectManager": "Menaxher Projekti",
|
||||
"userRoleSoftwareDeveloper": "Zhvillues Softueri",
|
||||
"userRoleDesigner": "Dizajner",
|
||||
"userRoleOperations": "Operacionet",
|
||||
"userRoleOther": "Tjetër",
|
||||
"mainUseCases": "Për çfarë do ta përdorësh kryesisht Worklenz?",
|
||||
"mainUseCasesTaskManagement": "Menaxhim detyrash",
|
||||
"mainUseCasesTeamCollaboration": "Bashkëpunim ekipi",
|
||||
"mainUseCasesResourcePlanning": "Planifikim burimesh",
|
||||
"mainUseCasesClientCommunication": "Komunikim & raportim me klientët",
|
||||
"mainUseCasesTimeTracking": "Ndjekje kohe",
|
||||
"mainUseCasesOther": "Tjetër",
|
||||
"previousTools": "Çfarë mjetesh ke përdorur para Worklenz?",
|
||||
"previousToolsPlaceholder": "p.sh. Trello, Asana, Monday.com",
|
||||
"howHeardAbout": "Si dëgjove për Worklenz?",
|
||||
"howHeardAboutGoogleSearch": "Kërkim në Google",
|
||||
"howHeardAboutTwitter": "Twitter",
|
||||
"howHeardAboutLinkedin": "LinkedIn",
|
||||
"howHeardAboutFriendColleague": "Një mik ose koleg",
|
||||
"howHeardAboutBlogArticle": "Një blog ose artikull",
|
||||
"howHeardAboutOther": "Tjetër",
|
||||
|
||||
"aboutYouStepTitle": "Na trego për veten",
|
||||
"aboutYouStepDescription": "Na ndihmo të personalizojmë përvojën tënde",
|
||||
"yourNeedsStepTitle": "Cilat janë nevojat e tua kryesore?",
|
||||
"yourNeedsStepDescription": "Zgjidh të gjitha që aplikohen për të na ndihmuar të konfigurojmë hapësirën tënde të punës",
|
||||
"selected": "zgjedhur",
|
||||
"previousToolsLabel": "Çfarë mjetesh ke përdorur më parë? (Opsionale)",
|
||||
|
||||
"roleSuggestions": {
|
||||
"designer": "UI/UX, Grafikë, Kreativ",
|
||||
"developer": "Frontend, Backend, Full-stack",
|
||||
"projectManager": "Planifikim, Koordinim",
|
||||
"marketing": "Përmbajtje, Media Sociale, Rritje",
|
||||
"sales": "Zhvillim Biznesi, Marrëdhënie me Klientë",
|
||||
"operations": "Administratë, HR, Financa"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
"en": "Anglisht",
|
||||
"es": "Spanjisht",
|
||||
"pt": "Portugalisht",
|
||||
"de": "Gjermanisht",
|
||||
"alb": "Shqip",
|
||||
"zh": "Kinezçe"
|
||||
},
|
||||
|
||||
"orgSuggestions": {
|
||||
"tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"],
|
||||
"creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"],
|
||||
"consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"],
|
||||
"startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"]
|
||||
},
|
||||
|
||||
"projectSuggestions": {
|
||||
"freelancer": ["Projekti i Klientit", "Përditësim Portfolio", "Markë Personale"],
|
||||
"startup": ["Zhvillim MVP", "Lansim Produkti", "Kërkim Tregu"],
|
||||
"agency": ["Fushatë Klienti", "Strategji Markë", "Ridizajnim Website"],
|
||||
"enterprise": ["Migrim Sistemi", "Optimizim Procesesh", "Trajnim Ekipi"]
|
||||
},
|
||||
|
||||
"useCaseDescriptions": {
|
||||
"taskManagement": "Organizoj dhe ndjek detyrat",
|
||||
"teamCollaboration": "Punojmë së bashku pa probleme",
|
||||
"resourcePlanning": "Menaxhoj kohën dhe burimet",
|
||||
"clientCommunication": "Qëndroj i lidhur me klientët",
|
||||
"timeTracking": "Monitoroj orët e projektit",
|
||||
"other": "Diçka tjetër"
|
||||
}
|
||||
"maxTasks": " (Mund të krijoni deri në 5 detyra)"
|
||||
}
|
||||
|
||||
@@ -4,69 +4,5 @@
|
||||
"owner": "Pronari i Organizatës",
|
||||
"admins": "Administruesit e Organizatës",
|
||||
"contactNumber": "Shto Numrin e Kontaktit",
|
||||
"edit": "Redakto",
|
||||
"organizationWorkingDaysAndHours": "Ditët dhe Orët e Punës së Organizatës",
|
||||
"workingDays": "Ditët e Punës",
|
||||
"workingHours": "Orët e Punës",
|
||||
"monday": "E Hënë",
|
||||
"tuesday": "E Martë",
|
||||
"wednesday": "E Mërkurë",
|
||||
"thursday": "E Enjte",
|
||||
"friday": "E Premte",
|
||||
"saturday": "E Shtunë",
|
||||
"sunday": "E Dielë",
|
||||
"hours": "orë",
|
||||
"saveButton": "Ruaj",
|
||||
"saved": "Cilësimet u ruajtën me sukses",
|
||||
"errorSaving": "Gabim gjatë ruajtjes së cilësimeve",
|
||||
"organizationCalculationMethod": "Metoda e Llogaritjes së Organizatës",
|
||||
"calculationMethod": "Metoda e Llogaritjes",
|
||||
"hourlyRates": "Normat Orërore",
|
||||
"manDays": "Ditët e Njeriut",
|
||||
"saveChanges": "Ruaj Ndryshimet",
|
||||
"hourlyCalculationDescription": "Të gjitha kostot e projektit do të llogariten duke përdorur orët e vlerësuara × normat orërore",
|
||||
"manDaysCalculationDescription": "Të gjitha kostot e projektit do të llogariten duke përdorur ditët e vlerësuara të njeriut × normat ditore",
|
||||
"calculationMethodTooltip": "Ky cilësim zbatohet për të gjitha projektet në organizatën tuaj",
|
||||
"calculationMethodUpdated": "Metoda e llogaritjes së organizatës u përditësua me sukses",
|
||||
"calculationMethodUpdateError": "Dështoi përditësimi i metodës së llogaritjes",
|
||||
"holidayCalendar": "Kalnedari i Festave",
|
||||
"addHoliday": "Shto Festë",
|
||||
"editHoliday": "Redakto Festë",
|
||||
"holidayName": "Emri i Festës",
|
||||
"holidayNameRequired": "Ju lutemi shkruani emrin e festës",
|
||||
"description": "Përshkrim",
|
||||
"date": "Data",
|
||||
"dateRequired": "Ju lutemi zgjidhni një datë",
|
||||
"holidayType": "Lloji i Festës",
|
||||
"holidayTypeRequired": "Ju lutemi zgjidhni një lloj feste",
|
||||
"recurring": "Përsëritëse",
|
||||
"save": "Ruaj",
|
||||
"update": "Përditëso",
|
||||
"cancel": "Anulo",
|
||||
"holidayCreated": "Festa u krijua me sukses",
|
||||
"holidayUpdated": "Festa u përditësua me sukses",
|
||||
"holidayDeleted": "Festa u fshi me sukses",
|
||||
"errorCreatingHoliday": "Gabim gjatë krijimit të festës",
|
||||
"errorUpdatingHoliday": "Gabim gjatë përditësimit të festës",
|
||||
"errorDeletingHoliday": "Gabim gjatë fshirjes së festës",
|
||||
"importCountryHolidays": "Importo Festat e Vendit",
|
||||
"country": "Vendi",
|
||||
"countryRequired": "Ju lutemi zgjidhni një vend",
|
||||
"selectCountry": "Zgjidhni një vend",
|
||||
"year": "Viti",
|
||||
"import": "Importo",
|
||||
"holidaysImported": "U importuan me sukses {{count}} festa",
|
||||
"errorImportingHolidays": "Gabim gjatë importimit të festave",
|
||||
"addCustomHoliday": "Shto Festë të Përshtatur",
|
||||
"officialHolidaysFrom": "Festat zyrtare nga",
|
||||
"workingDay": "Ditë Pune",
|
||||
"holiday": "Festë",
|
||||
"today": "Sot",
|
||||
"cannotEditOfficialHoliday": "Nuk mund të redaktoni festat zyrtare",
|
||||
"customHoliday": "Festë e Përshtatur",
|
||||
"officialHoliday": "Festë Zyrtare",
|
||||
"delete": "Fshi",
|
||||
"deleteHolidayConfirm": "A jeni i sigurt që dëshironi të fshini këtë festë?",
|
||||
"yes": "Po",
|
||||
"no": "Jo"
|
||||
"edit": "Redakto"
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"settings": "Cilësimet",
|
||||
"organizationWorkingDaysAndHours": "Ditët dhe Orët e Punës së Organizatës",
|
||||
"workingDays": "Ditët e Punës",
|
||||
"workingHours": "Orët e Punës",
|
||||
"hours": "orë",
|
||||
"monday": "E Hënë",
|
||||
"tuesday": "E Martë",
|
||||
"wednesday": "E Mërkurë",
|
||||
"thursday": "E Enjte",
|
||||
"friday": "E Premte",
|
||||
"saturday": "E Shtunë",
|
||||
"sunday": "E Dielë",
|
||||
"saveButton": "Ruaj",
|
||||
"saved": "Cilësimet u ruajtën me sukses",
|
||||
"errorSaving": "Gabim gjatë ruajtjes së cilësimeve",
|
||||
"holidaySettings": "Cilësimet e pushimeve",
|
||||
"country": "Vendi",
|
||||
"countryRequired": "Ju lutemi zgjidhni një vend",
|
||||
"selectCountry": "Zgjidhni vendin",
|
||||
"state": "Shteti/Provinca",
|
||||
"selectState": "Zgjidhni shtetin/provincën (opsionale)",
|
||||
"autoSyncHolidays": "Sinkronizo automatikisht pushimet zyrtare",
|
||||
"saveHolidaySettings": "Ruaj cilësimet e pushimeve",
|
||||
"holidaySettingsSaved": "Cilësimet e pushimeve u ruajtën me sukses",
|
||||
"errorSavingHolidaySettings": "Gabim gjatë ruajtjes së cilësimeve të pushimeve",
|
||||
"addCustomHoliday": "Shto Festë të Përshtatur",
|
||||
"officialHolidaysFrom": "Festat zyrtare nga",
|
||||
"workingDay": "Ditë Pune",
|
||||
"holiday": "Festë",
|
||||
"today": "Sot",
|
||||
"cannotEditOfficialHoliday": "Nuk mund të redaktoni festat zyrtare"
|
||||
}
|
||||
@@ -4,6 +4,5 @@
|
||||
"teams": "Ekipet",
|
||||
"billing": "Faturimi",
|
||||
"projects": "Projektet",
|
||||
"settings": "Cilësimet",
|
||||
"adminCenter": "Qendra Administrative"
|
||||
}
|
||||
|
||||
@@ -5,13 +5,5 @@
|
||||
"signup-failed": "Regjistrimi dështoi. Ju lutemi sigurohuni që të gjitha fushat e nevojshme janë plotësuar dhe provoni përsëri.",
|
||||
"reconnecting": "Jeni shkëputur nga serveri.",
|
||||
"connection-lost": "Lidhja me serverin dështoi. Ju lutemi kontrolloni lidhjen tuaj me internet.",
|
||||
"connection-restored": "U lidhët me serverin me sukses",
|
||||
"cancel": "Anulo",
|
||||
"update-available": "Worklenz u përditesua!",
|
||||
"update-description": "Një version i ri i Worklenz është i disponueshëm me karakteristikat dhe përmirësimet më të fundit.",
|
||||
"update-instruction": "Për eksperiencën më të mirë, ju lutemi rifreskoni faqen për të aplikuar ndryshimet e reja.",
|
||||
"update-whats-new": "💡 <1>Çfarë ka të re:</1> Përmirësim i performancës, rregullime të gabimeve dhe eksperiencön e përmirësuar e përdoruesit",
|
||||
"update-now": "Përditeso tani",
|
||||
"update-later": "Më vonë",
|
||||
"updating": "Duke u përditesuar..."
|
||||
"connection-restored": "U lidhët me serverin me sukses"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,6 @@
|
||||
"list": "Listë",
|
||||
"calendar": "Kalendar",
|
||||
"tasks": "Detyrat",
|
||||
"refresh": "Rifresko",
|
||||
"recentActivity": "Aktiviteti i Fundit",
|
||||
"recentTasks": "Detyrat e Fundit",
|
||||
"recentTasksSegment": "Detyrat e Fundit",
|
||||
"timeLogged": "Koha e Regjistruar",
|
||||
"timeLoggedSegment": "Koha e Regjistruar",
|
||||
"noRecentTasks": "Asnjë detyrë e fundit",
|
||||
"noTimeLoggedTasks": "Asnjë detyrë me kohë të regjistruar",
|
||||
"activityTag": "Aktiviteti",
|
||||
"timeLogTag": "Regjistrim Kohe",
|
||||
"timerTag": "Kohëmatës",
|
||||
"activitySingular": "aktivitet",
|
||||
"activityPlural": "aktivitete",
|
||||
"recentTaskAriaLabel": "Detyrë e fundit:",
|
||||
"timeLoggedTaskAriaLabel": "Detyrë me kohë të regjistruar:",
|
||||
"errorLoadingRecentTasks": "Gabim në ngarkimin e detyrave të fundit",
|
||||
"errorLoadingTimeLoggedTasks": "Gabim në ngarkimin e detyrave me kohë të regjistruar"
|
||||
"refresh": "Rifresko"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,6 @@
|
||||
"deleteConfirmationOk": "Po",
|
||||
"deleteConfirmationCancel": "Anulo",
|
||||
|
||||
"deleteTaskTitle": "Fshi Detyrën",
|
||||
"deleteTaskContent": "Jeni i sigurt që doni të fshini këtë detyrë? Kjo veprim nuk mund të zhbëhet.",
|
||||
"deleteTaskConfirm": "Fshi",
|
||||
"deleteTaskCancel": "Anulo",
|
||||
|
||||
"deleteStatusTitle": "Fshi Statusin",
|
||||
"deleteStatusContent": "Jeni i sigurt që doni të fshini këtë status? Kjo veprim nuk mund të zhbëhet.",
|
||||
|
||||
"deletePhaseTitle": "Fshi Fazen",
|
||||
"deletePhaseContent": "Jeni i sigurt që doni të fshini këtë fazë? Kjo veprim nuk mund të zhbëhet.",
|
||||
|
||||
"dueDate": "Data e përfundimit",
|
||||
"cancel": "Anulo",
|
||||
|
||||
@@ -37,16 +26,5 @@
|
||||
"noDueDate": "Pa datë përfundimi",
|
||||
"save": "Ruaj",
|
||||
"clear": "Pastro",
|
||||
"nextWeek": "Javën e ardhshme",
|
||||
"noSubtasks": "Pa nëndetyra",
|
||||
"showSubtasks": "Shfaq nëndetyrat",
|
||||
"hideSubtasks": "Fshih nëndetyrat",
|
||||
"errorLoadingTasks": "Gabim gjatë ngarkimit të detyrave",
|
||||
"noTasksFound": "Nuk u gjetën detyra",
|
||||
"loadingFilters": "Duke ngarkuar filtra...",
|
||||
"failedToUpdateColumnOrder": "Dështoi përditësimi i rendit të kolonave",
|
||||
"failedToUpdatePhaseOrder": "Dështoi përditësimi i rendit të fazave",
|
||||
"pleaseTryAgain": "Ju lutemi provoni përsëri",
|
||||
"taskNotCompleted": "Detyra nuk është përfunduar",
|
||||
"completeTaskDependencies": "Ju lutemi përfundoni varësitë e detyrës para se të vazhdoni"
|
||||
"nextWeek": "Javën e ardhshme"
|
||||
}
|
||||
|
||||
@@ -38,13 +38,5 @@
|
||||
"createClient": "Krijo klient",
|
||||
"searchInputPlaceholder": "Kërko sipas emrit ose emailit",
|
||||
"hoursPerDayValidationMessage": "Orët në ditë duhet të jenë një numër midis 1 dhe 24",
|
||||
"workingDaysValidationMessage": "Ditët e punës duhet të jenë një numër pozitiv",
|
||||
"manDaysValidationMessage": "Ditët e punëtorëve duhet të jenë një numër pozitiv",
|
||||
"noPermission": "Nuk ka leje",
|
||||
"progressSettings": "Cilësimet e Progresit",
|
||||
"manualProgress": "Progresi Manual",
|
||||
"manualProgressTooltip": "Lejo përditësimet manuale të progresit për detyrat pa nëndetyra",
|
||||
"weightedProgress": "Progresi i Ponderuar",
|
||||
"weightedProgressTooltip": "Llogarit progresin bazuar në peshat e nëndetyrave",
|
||||
"timeProgress": "Progresi i Bazuar në Kohë"
|
||||
"noPermission": "Nuk ka leje"
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"financeText": "Finance",
|
||||
"ratecardSingularText": "Rate Card",
|
||||
"groupByText": "Group by",
|
||||
"statusText": "Status",
|
||||
"phaseText": "Phase",
|
||||
"priorityText": "Priority",
|
||||
"exportButton": "Export",
|
||||
"currencyText": "Currency",
|
||||
"importButton": "Import",
|
||||
"filterText": "Filter",
|
||||
"billableOnlyText": "Billable Only",
|
||||
"nonBillableOnlyText": "Non-Billable Only",
|
||||
"allTasksText": "All Tasks",
|
||||
"projectBudgetOverviewText": "Project Budget Overview",
|
||||
"taskColumn": "Task",
|
||||
"membersColumn": "Members",
|
||||
"hoursColumn": "Estimated Hours",
|
||||
"manDaysColumn": "Estimated Man Days",
|
||||
"actualManDaysColumn": "Actual Man Days",
|
||||
"effortVarianceColumn": "Effort Variance",
|
||||
"totalTimeLoggedColumn": "Total Time Logged",
|
||||
"costColumn": "Actual Cost",
|
||||
"estimatedCostColumn": "Estimated Cost",
|
||||
"fixedCostColumn": "Fixed Cost",
|
||||
"totalBudgetedCostColumn": "Total Budgeted Cost",
|
||||
"totalActualCostColumn": "Total Actual Cost",
|
||||
"varianceColumn": "Variance",
|
||||
"totalText": "Total",
|
||||
"noTasksFound": "No tasks found",
|
||||
"addRoleButton": "+ Add Role",
|
||||
"ratecardImportantNotice": "* This rate card is generated based on the company's standard job titles and rates. However, you have the flexibility to modify it according to the project. These changes will not impact the organization's standard job titles and rates.",
|
||||
"saveButton": "Save",
|
||||
"jobTitleColumn": "Job Title",
|
||||
"ratePerHourColumn": "Rate per hour",
|
||||
"ratePerManDayColumn": "Tarifa për ditë-njeri",
|
||||
"calculationMethodText": "Calculation Method",
|
||||
"hourlyRatesText": "Hourly Rates",
|
||||
"manDaysText": "Man Days",
|
||||
"hoursPerDayText": "Hours per Day",
|
||||
"ratecardPluralText": "Rate Cards",
|
||||
"labourHoursColumn": "Labour Hours",
|
||||
"actions": "Actions",
|
||||
"selectJobTitle": "Select Job Title",
|
||||
"ratecardsPluralText": "Rate Card Templates",
|
||||
"deleteConfirm": "Are you sure ?",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"alreadyImportedRateCardMessage": "A rate card has already been imported. Clear all imported rate cards to add a new one.",
|
||||
"budgetOverviewTooltips": {
|
||||
"manualBudget": "Manual project budget amount set by project manager",
|
||||
"totalActualCost": "Total actual cost including fixed costs",
|
||||
"variance": "Difference between manual budget and actual cost",
|
||||
"utilization": "Percentage of manual budget utilized",
|
||||
"estimatedHours": "Total estimated hours from all tasks",
|
||||
"fixedCosts": "Total fixed costs from all tasks",
|
||||
"timeBasedCost": "Actual cost from time tracking (excluding fixed costs)",
|
||||
"remainingBudget": "Remaining budget amount"
|
||||
},
|
||||
"budgetModal": {
|
||||
"title": "Edit Project Budget",
|
||||
"description": "Set a manual budget for this project. This budget will be used for all financial calculations and should include both time-based costs and fixed costs.",
|
||||
"placeholder": "Enter budget amount",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
"budgetStatistics": {
|
||||
"manualBudget": "Manual Budget",
|
||||
"totalActualCost": "Total Actual Cost",
|
||||
"variance": "Variance",
|
||||
"budgetUtilization": "Budget Utilization",
|
||||
"estimatedHours": "Estimated Hours",
|
||||
"fixedCosts": "Fixed Costs",
|
||||
"timeBasedCost": "Time-based Cost",
|
||||
"remainingBudget": "Remaining Budget",
|
||||
"noManualBudgetSet": "(No Manual Budget Set)"
|
||||
},
|
||||
"budgetSettingsDrawer": {
|
||||
"title": "Project Budget Settings",
|
||||
"budgetConfiguration": "Budget Configuration",
|
||||
"projectBudget": "Project Budget",
|
||||
"projectBudgetTooltip": "Total budget allocated for this project",
|
||||
"currency": "Currency",
|
||||
"costCalculationMethod": "Cost Calculation Method",
|
||||
"calculationMethod": "Calculation Method",
|
||||
"workingHoursPerDay": "Working Hours per Day",
|
||||
"workingHoursPerDayTooltip": "Number of working hours in a day for man-day calculations",
|
||||
"hourlyCalculationInfo": "Costs will be calculated using estimated hours × hourly rates",
|
||||
"manDaysCalculationInfo": "Costs will be calculated using estimated man days × daily rates",
|
||||
"importantNotes": "Important Notes",
|
||||
"calculationMethodChangeNote": "• Changing the calculation method will affect how costs are calculated for all tasks in this project",
|
||||
"immediateEffectNote": "• Changes take effect immediately and will recalculate all project totals",
|
||||
"projectWideNote": "• Budget settings apply to the entire project and all its tasks",
|
||||
"cancel": "Cancel",
|
||||
"saveChanges": "Save Changes",
|
||||
"budgetSettingsUpdated": "Budget settings updated successfully",
|
||||
"budgetSettingsUpdateFailed": "Failed to update budget settings"
|
||||
},
|
||||
"columnTooltips": {
|
||||
"hours": "Total estimated hours for all tasks. Calculated from task time estimates.",
|
||||
"manDays": "Total estimated man days for all tasks. Based on {{hoursPerDay}} hours per working day.",
|
||||
"actualManDays": "Actual man days spent based on time logged. Calculated as: Total Time Logged ÷ {{hoursPerDay}} hours per day.",
|
||||
"effortVariance": "Difference between estimated and actual man days. Positive values indicate over-estimation, negative values indicate under-estimation.",
|
||||
"totalTimeLogged": "Total time actually logged by team members across all tasks.",
|
||||
"estimatedCostHourly": "Estimated cost calculated as: Estimated Hours × Hourly Rates for assigned team members.",
|
||||
"estimatedCostManDays": "Estimated cost calculated as: Estimated Man Days × Daily Rates for assigned team members.",
|
||||
"actualCost": "Actual cost based on time logged. Calculated as: Time Logged × Hourly Rates for team members.",
|
||||
"fixedCost": "Fixed costs that don't depend on time spent. Added manually per task.",
|
||||
"totalBudgetHourly": "Total budgeted cost including estimated cost (Hours × Hourly Rates) + Fixed Costs.",
|
||||
"totalBudgetManDays": "Total budgeted cost including estimated cost (Man Days × Daily Rates) + Fixed Costs.",
|
||||
"totalActual": "Total actual cost including time-based cost + Fixed Costs.",
|
||||
"variance": "Cost variance: Total Budgeted Costs - Total Actual Cost. Positive values indicate under-budget, negative values indicate over-budget."
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,5 @@
|
||||
"deleteButtonTooltip": "Hiq nga projekti",
|
||||
"memberCount": "Anëtar",
|
||||
"membersCountPlural": "Anëtarë",
|
||||
"emptyText": "Nuk ka bashkëngjitje në projekt.",
|
||||
"searchPlaceholder": "Kërko anëtarë"
|
||||
"emptyText": "Nuk ka bashkëngjitje në projekt."
|
||||
}
|
||||
|
||||
@@ -10,6 +10,5 @@
|
||||
"error": "Gabim në ngarkimin e projektit",
|
||||
"pinnedTab": "E fiksuar si tab i parazgjedhur",
|
||||
"pinTab": "Fikso si tab i parazgjedhur",
|
||||
"unpinTab": "Hiqe fiksimin e tab-it të parazgjedhur",
|
||||
"finance": "Finance"
|
||||
}
|
||||
"unpinTab": "Hiqe fiksimin e tab-it të parazgjedhur"
|
||||
}
|
||||
@@ -3,9 +3,5 @@
|
||||
"searchLabel": "Shtoni anëtarë duke shkruar emrin ose email-in e tyre",
|
||||
"searchPlaceholder": "Shkruani emrin ose email-in",
|
||||
"inviteAsAMember": "Fto si anëtar",
|
||||
"inviteNewMemberByEmail": "Fto anëtar të ri me email",
|
||||
"members": "Anëtarë",
|
||||
"copyProjectLink": "Kopjo lidhjen e projektit",
|
||||
"inviteMember": "Fto anëtar",
|
||||
"alsoInviteToProject": "Fto edhe në projekt"
|
||||
"inviteNewMemberByEmail": "Fto anëtar të ri me email"
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"nameColumn": "Emri",
|
||||
"createdColumn": "Krijuar",
|
||||
"noProjectsAvailable": "Nuk ka projekte të disponueshme",
|
||||
"deleteConfirmationTitle": "Jeni i sigurt që doni të fshini këtë rate card?",
|
||||
"deleteConfirmationOk": "Po, fshij",
|
||||
"deleteConfirmationCancel": "Anulo",
|
||||
"searchPlaceholder": "Kërko rate cards sipas emrit",
|
||||
"createRatecard": "Krijo Rate Card",
|
||||
"editTooltip": "Redakto rate card",
|
||||
"deleteTooltip": "Fshi rate card",
|
||||
"fetchError": "Dështoi të merret rate card",
|
||||
"createError": "Dështoi të krijohet rate card",
|
||||
"deleteSuccess": "Rate card u fshi me sukses",
|
||||
"deleteError": "Dështoi të fshihet rate card",
|
||||
|
||||
"jobTitleColumn": "Titulli i punës",
|
||||
"ratePerHourColumn": "Tarifa për orë",
|
||||
"ratePerDayColumn": "Tarifa për ditë",
|
||||
"ratePerManDayColumn": "Tarifa për ditë-njeri",
|
||||
"saveButton": "Ruaj",
|
||||
"addRoleButton": "Shto rol",
|
||||
"createRatecardSuccessMessage": "Rate card u krijua me sukses",
|
||||
"createRatecardErrorMessage": "Dështoi të krijohet rate card",
|
||||
"updateRatecardSuccessMessage": "Rate card u përditësua me sukses",
|
||||
"updateRatecardErrorMessage": "Dështoi të përditësohet rate card",
|
||||
"currency": "Monedha",
|
||||
"actionsColumn": "Veprime",
|
||||
"addAllButton": "Shto të gjitha",
|
||||
"removeAllButton": "Hiq të gjitha",
|
||||
"selectJobTitle": "Zgjidh titullin e punës",
|
||||
"unsavedChangesTitle": "Keni ndryshime të paruajtura",
|
||||
"unsavedChangesMessage": "Dëshironi të ruani ndryshimet para se të largoheni?",
|
||||
"unsavedChangesSave": "Ruaj",
|
||||
"unsavedChangesDiscard": "Hidh poshtë",
|
||||
"ratecardNameRequired": "Emri i rate card është i detyrueshëm",
|
||||
"ratecardNamePlaceholder": "Shkruani emrin e rate card",
|
||||
"noRatecardsFound": "Nuk u gjetën rate cards",
|
||||
"loadingRateCards": "Duke ngarkuar rate cards...",
|
||||
"noJobTitlesAvailable": "Nuk ka tituj pune të disponueshëm",
|
||||
"noRolesAdded": "Ende nuk janë shtuar role",
|
||||
"createFirstJobTitle": "Krijo titullin e parë të punës",
|
||||
"jobRolesTitle": "Rolet e punës",
|
||||
"noJobTitlesMessage": "Ju lutemi krijoni tituj pune së pari në cilësimet përpara se të shtoni role në rate cards.",
|
||||
"createNewJobTitle": "Krijo titull të ri pune",
|
||||
"jobTitleNamePlaceholder": "Shkruani emrin e titullit të punës",
|
||||
"jobTitleNameRequired": "Emri i titullit të punës është i detyrueshëm",
|
||||
"jobTitleCreatedSuccess": "Titulli i punës u krijua me sukses",
|
||||
"jobTitleCreateError": "Dështoi të krijohet titulli i punës",
|
||||
"createButton": "Krijo",
|
||||
"cancelButton": "Anulo",
|
||||
"discardButton": "Hidh poshtë",
|
||||
"manDaysCalculationMessage": "Organizata po përdor llogaritjen e ditëve-njeri ({{hours}}h/ditë). Tarifat më sipër përfaqësojnë tarifa ditore.",
|
||||
"hourlyCalculationMessage": "Organizata po përdor llogaritjen orore. Tarifat më sipër përfaqësojnë tarifa orore."
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
"jobTitleLabel": "Titulli i Punës",
|
||||
"jobTitlePlaceholder": "Zgjidh ose kërko titull pune (Opsionale)",
|
||||
"memberAccessLabel": "Niveli i Qasjes",
|
||||
"addToTeamButton": "Dërgo ftesën",
|
||||
"addToTeamButton": "Shto Anëtar në Ekip",
|
||||
"updateButton": "Ruaj Ndryshimet",
|
||||
"resendInvitationButton": "Dërgo Përsëri Email-in e Ftesës",
|
||||
"invitationSentSuccessMessage": "Ftesa për ekip u dërgua me sukses!",
|
||||
@@ -43,6 +43,5 @@
|
||||
"updatedText": "Përditësuar",
|
||||
"noResultFound": "Shkruani një adresë email dhe shtypni Enter...",
|
||||
"jobTitlesFetchError": "Dështoi marrja e titujve të punës",
|
||||
"invitationResent": "Ftesa u dërgua sërish me sukses!",
|
||||
"copyTeamLink": "Kopjo lidhjen e ekipit"
|
||||
"invitationResent": "Ftesa u dërgua sërish me sukses!"
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
"namePlaceholder": "Emri",
|
||||
"nameRequired": "Ju lutem shkruani një Emër",
|
||||
"updateFailed": "Ndryshimi i emrit të ekipit dështoi!"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"modalTitle": "Ndihmoni të përmirësojmë përvojën tuaj",
|
||||
"skip": "Kalo për tani",
|
||||
"previous": "Prapa",
|
||||
"next": "Tjetra",
|
||||
"completeSurvey": "Përfundo Anketën",
|
||||
"submitting": "Duke dërguar përgjigjet tuaja...",
|
||||
"submitSuccessTitle": "Faleminderit!",
|
||||
"submitSuccessSubtitle": "Feedback-u juaj na ndihmon të përmirësojmë Worklenz për të gjithë.",
|
||||
"submitSuccessMessage": "Faleminderit që plotësuat anketën!",
|
||||
"submitErrorMessage": "Dështoi dërgimi i anketës. Ju lutemi provoni përsëri.",
|
||||
"submitErrorLog": "Dështoi dërgimi i anketës",
|
||||
"fetchErrorLog": "Dështoi marrja e anketës"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"recurring": "Përsëritës",
|
||||
"recurringTaskConfiguration": "Konfigurimi i detyrës përsëritëse",
|
||||
"repeats": "Përsëritet",
|
||||
"daily": "Ditore",
|
||||
"weekly": "Javore",
|
||||
"everyXDays": "Çdo X ditë",
|
||||
"everyXWeeks": "Çdo X javë",
|
||||
"everyXMonths": "Çdo X muaj",
|
||||
"monthly": "Mujore",
|
||||
"selectDaysOfWeek": "Zgjidh ditët e javës",
|
||||
"mon": "Hën",
|
||||
"tue": "Mar",
|
||||
"wed": "Mër",
|
||||
"thu": "Enj",
|
||||
"fri": "Pre",
|
||||
"sat": "Sht",
|
||||
"sun": "Die",
|
||||
"monthlyRepeatType": "Lloji i përsëritjes mujore",
|
||||
"onSpecificDate": "Në një datë specifike",
|
||||
"onSpecificDay": "Në një ditë specifike",
|
||||
"dateOfMonth": "Data e muajit",
|
||||
"weekOfMonth": "Java e muajit",
|
||||
"dayOfWeek": "Dita e javës",
|
||||
"first": "E para",
|
||||
"second": "E dyta",
|
||||
"third": "E treta",
|
||||
"fourth": "E katërta",
|
||||
"last": "E fundit",
|
||||
"intervalDays": "Intervali (ditë)",
|
||||
"intervalWeeks": "Intervali (javë)",
|
||||
"intervalMonths": "Intervali (muaj)",
|
||||
"saveChanges": "Ruaj ndryshimet"
|
||||
}
|
||||
@@ -84,12 +84,5 @@
|
||||
"close": "Mbyll",
|
||||
"cannotMoveStatus": "Nuk mund të lëvizet statusi",
|
||||
"cannotMoveStatusMessage": "Nuk mund të lëvizet ky status sepse do të linte kategorinë '{{categoryName}}' bosh. Çdo kategori duhet të ketë të paktën një status.",
|
||||
"ok": "OK",
|
||||
"clearSort": "Pastro Renditjen",
|
||||
"sortAscending": "Rendit në Rritje",
|
||||
"sortDescending": "Rendit në Zbritje",
|
||||
"sortByField": "Rendit sipas {{field}}",
|
||||
"ascendingOrder": "Rritës",
|
||||
"descendingOrder": "Zbritës",
|
||||
"currentSort": "Renditja aktuale: {{field}} {{order}}"
|
||||
"ok": "OK"
|
||||
}
|
||||
|
||||
@@ -57,9 +57,6 @@
|
||||
|
||||
"contextMenu": {
|
||||
"assignToMe": "Cakto mua",
|
||||
"copyLink": "Kopjo lidhjen e detyrës",
|
||||
"linkCopied": "Lidhja u kopjua në clipboard",
|
||||
"linkCopyFailed": "Dështoi kopjimi i lidhjes",
|
||||
"moveTo": "Zhvendos në",
|
||||
"unarchive": "Ç'arkivizo",
|
||||
"archive": "Arkivizo",
|
||||
@@ -90,7 +87,7 @@
|
||||
"peopleField": "Fusha e njerëzve",
|
||||
"noDate": "Asnjë datë",
|
||||
"unsupportedField": "Lloj fushe i pambështetur",
|
||||
|
||||
|
||||
"modal": {
|
||||
"addFieldTitle": "Shto fushë",
|
||||
"editFieldTitle": "Redakto fushën",
|
||||
@@ -111,7 +108,7 @@
|
||||
"createErrorMessage": "Dështoi në krijimin e kolonës së personalizuar",
|
||||
"updateErrorMessage": "Dështoi në përditësimin e kolonës së personalizuar"
|
||||
},
|
||||
|
||||
|
||||
"fieldTypes": {
|
||||
"people": "Njerëz",
|
||||
"number": "Numër",
|
||||
@@ -136,11 +133,5 @@
|
||||
"dependencies": "Detyra ka varësi",
|
||||
"recurring": "Detyrë përsëritëse"
|
||||
}
|
||||
},
|
||||
|
||||
"timer": {
|
||||
"conflictTitle": "Kronómetr Tashë Në Ecuri",
|
||||
"conflictMessage": "Ju keni një kronómetr në ecuri për \"{{taskName}}\" në projektin \"{{projectName}}\". Dëshironi ta ndaloni atë kronómetr dhe të filloni një të ri për këtë detyrë?",
|
||||
"stopAndStart": "Ndalo & Fillo Kronómetr të Ri"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
"searchByName": "Kërko sipas emrit",
|
||||
"selectAll": "Zgjidh të Gjitha",
|
||||
"clearAll": "Pastro të Gjitha",
|
||||
"teams": "Ekipet",
|
||||
|
||||
"searchByProject": "Kërko sipas emrit të projektit",
|
||||
@@ -16,8 +15,6 @@
|
||||
|
||||
"billable": "Fakturueshme",
|
||||
"nonBillable": "Jo Fakturueshme",
|
||||
"allBillableTypes": "Të Gjitha Llojet e Fakturueshme",
|
||||
"filterByBillableStatus": "Filtro sipas statusit të fakturueshmërisë",
|
||||
|
||||
"total": "Total",
|
||||
|
||||
@@ -31,9 +28,6 @@
|
||||
|
||||
"membersTimeSheet": "Fletë Kohore e Anëtarëve",
|
||||
"member": "Anëtar",
|
||||
"members": "Anëtarët",
|
||||
"searchByMember": "Kërko sipas anëtarit",
|
||||
"utilization": "Përdorimi",
|
||||
|
||||
"estimatedVsActual": "Vlerësuar vs Aktual",
|
||||
"workingDays": "Ditë Pune",
|
||||
@@ -46,32 +40,5 @@
|
||||
"noCategory": "Pa Kategori",
|
||||
"noProjects": "Nuk u gjetën projekte",
|
||||
"noTeams": "Nuk u gjetën ekipe",
|
||||
"noData": "Nuk u gjetën të dhëna",
|
||||
"groupBy": "Gruppo sipas",
|
||||
"groupByCategory": "Kategori",
|
||||
"groupByTeam": "Ekip",
|
||||
"groupByStatus": "Status",
|
||||
"groupByNone": "Asnjë",
|
||||
"clearSearch": "Pastro kërkimin",
|
||||
"selectedProjects": "Projektet e Zgjedhura",
|
||||
"projectsSelected": "projekte të zgjedhura",
|
||||
"showSelected": "Shfaq Vetëm të Zgjedhurat",
|
||||
"expandAll": "Zgjero të Gjitha",
|
||||
"collapseAll": "Mbyll të Gjitha",
|
||||
"ungrouped": "Pa Grupuar",
|
||||
|
||||
"totalTimeLogged": "Koha Totale e Regjistruar",
|
||||
"acrossAllTeamMembers": "Në të gjithë anëtarët e ekipit",
|
||||
"expectedCapacity": "Kapaciteti i Pritur",
|
||||
"basedOnWorkingSchedule": "Bazuar në orarin e punës",
|
||||
"teamUtilization": "Përdorimi i Ekipit",
|
||||
"targetRange": "Gama e Objektivit",
|
||||
"variance": "Varianca",
|
||||
"overCapacity": "Mbi Kapacitetin",
|
||||
"underCapacity": "Nën Kapacitetin",
|
||||
"considerWorkloadRedistribution": "Konsidero rishpërndarjen e ngarkesës së punës",
|
||||
"capacityAvailableForNewProjects": "Kapaciteti i disponueshëm për projekte të reja",
|
||||
"optimal": "Optimal",
|
||||
"underUtilized": "I Përdorur Pak",
|
||||
"overUtilized": "I Përdorur Shumë"
|
||||
"noData": "Nuk u gjetën të dhëna"
|
||||
}
|
||||
|
||||
@@ -3,28 +3,7 @@
|
||||
|
||||
"setupYourAccount": "Richten Sie Ihr Worklenz-Konto ein.",
|
||||
"organizationStepTitle": "Organisation benennen",
|
||||
"organizationStepWelcome": "Willkommen bei Worklenz!",
|
||||
"organizationStepDescription": "Beginnen wir mit der Einrichtung Ihrer Organisation. Dies wird der Hauptarbeitsplatz für Ihr Team.",
|
||||
"organizationStepLabel": "Organisationsname",
|
||||
"organizationStepPlaceholder": "z.B. Acme Corporation",
|
||||
"organizationStepTooltip": "Dieser Name wird in Ihrem Arbeitsbereich angezeigt und kann später in den Einstellungen geändert werden.",
|
||||
"organizationStepNeedIdeas": "Brauchen Sie Ideen?",
|
||||
"organizationStepUseDetected": "Erkannt verwenden:",
|
||||
"organizationStepCharacters": "Zeichen",
|
||||
"organizationStepGoodLength": "Gute Länge",
|
||||
"organizationStepTooShort": "Zu kurz",
|
||||
"organizationStepNamingTips": "Namensgebungstipps",
|
||||
"organizationStepTip1": "Halten Sie es einfach und einprägsam",
|
||||
"organizationStepTip2": "Spiegeln Sie Ihre Branche oder Werte wider",
|
||||
"organizationStepTip3": "Denken Sie an zukünftiges Wachstum",
|
||||
"organizationStepTip4": "Machen Sie es einzigartig und markenfähig",
|
||||
"organizationStepSuggestionsTitle": "Namensvorschläge",
|
||||
"organizationStepCategory1": "Tech-Unternehmen",
|
||||
"organizationStepCategory2": "Kreativagenturen",
|
||||
"organizationStepCategory3": "Beratung",
|
||||
"organizationStepCategory4": "Startups",
|
||||
"organizationStepSuggestionsNote": "Dies sind nur Beispiele für den Einstieg. Wählen Sie etwas, das Ihre Organisation repräsentiert.",
|
||||
"organizationStepPrivacyNote": "Ihr Organisationsname ist privat und nur für Ihre Teammitglieder sichtbar.",
|
||||
"organizationStepLabel": "Wählen Sie einen Namen für Ihr Worklenz-Konto.",
|
||||
|
||||
"projectStepTitle": "Erstellen Sie Ihr erstes Projekt",
|
||||
"projectStepLabel": "An welchem Projekt arbeiten Sie gerade?",
|
||||
@@ -45,170 +24,8 @@
|
||||
"step3InputLabel": "Per E-Mail einladen",
|
||||
"addAnother": "Weitere hinzufügen",
|
||||
"skipForNow": "Jetzt überspringen",
|
||||
"skipping": "Überspringen...",
|
||||
"formTitle": "Erstellen Sie Ihre erste Aufgabe.",
|
||||
"step3Title": "Laden Sie Ihr Team zur Zusammenarbeit ein",
|
||||
"maxMembers": " (Sie können bis zu 5 Mitglieder einladen)",
|
||||
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)",
|
||||
|
||||
"membersStepTitle": "Laden Sie Ihr Team ein",
|
||||
"membersStepDescription": "Teammitglieder zu \"{{organizationName}}\" hinzufügen und mit der Zusammenarbeit beginnen",
|
||||
"memberPlaceholder": "Teammitglied {{index}} - E-Mail-Adresse eingeben",
|
||||
"validEmailAddress": "Gültige E-Mail-Adresse",
|
||||
"addAnotherTeamMember": "Weiteres Teammitglied hinzufügen ({{current}}/{{max}})",
|
||||
"canInviteLater": "Sie können Teammitglieder jederzeit später einladen",
|
||||
"skipStepDescription": "Haben Sie keine E-Mail-Adressen bereit? Kein Problem! Sie können diesen Schritt überspringen und Teammitglieder später über Ihr Projekt-Dashboard einladen.",
|
||||
|
||||
"orgCategoryTech": "Technologieunternehmen",
|
||||
"orgCategoryCreative": "Kreativagenturen",
|
||||
"orgCategoryConsulting": "Beratung",
|
||||
"orgCategoryStartups": "Startups",
|
||||
"namingTip1": "Halten Sie es einfach und einprägsam",
|
||||
"namingTip2": "Spiegeln Sie Ihre Branche oder Werte wider",
|
||||
"namingTip3": "Denken Sie an zukünftiges Wachstum",
|
||||
"namingTip4": "Machen Sie es einzigartig und markenfähig",
|
||||
|
||||
"aboutYouTitle": "Erzählen Sie uns von sich",
|
||||
"aboutYouDescription": "Helfen Sie uns, Ihre Erfahrung zu personalisieren",
|
||||
"orgTypeQuestion": "Was beschreibt Ihre Organisation am besten?",
|
||||
"userRoleQuestion": "Was ist Ihre Rolle?",
|
||||
|
||||
"yourNeedsTitle": "Was sind Ihre Hauptbedürfnisse?",
|
||||
"yourNeedsDescription": "Wählen Sie alle zutreffenden aus, um uns bei der Einrichtung Ihres Arbeitsbereichs zu helfen",
|
||||
"yourNeedsQuestion": "Wie werden Sie Worklenz hauptsächlich nutzen?",
|
||||
"useCaseTaskOrg": "Aufgaben organisieren und verfolgen",
|
||||
"useCaseTeamCollab": "Nahtlos zusammenarbeiten",
|
||||
"useCaseResourceMgmt": "Zeit und Ressourcen verwalten",
|
||||
"useCaseClientComm": "Mit Kunden in Verbindung bleiben",
|
||||
"useCaseTimeTrack": "Projektstunden überwachen",
|
||||
"useCaseOther": "Etwas anderes",
|
||||
"selectedText": "ausgewählt",
|
||||
"previousToolsQuestion": "Welche Tools haben Sie zuvor verwendet? (Optional)",
|
||||
"previousToolsPlaceholder": "z.B. Asana, Trello, Jira, Monday.com, etc.",
|
||||
|
||||
"discoveryTitle": "Eine letzte Sache...",
|
||||
"discoveryDescription": "Helfen Sie uns zu verstehen, wie Sie Worklenz entdeckt haben",
|
||||
"discoveryQuestion": "Wie haben Sie von uns erfahren?",
|
||||
"allSetTitle": "Sie sind bereit!",
|
||||
"allSetDescription": "Lassen Sie uns Ihr erstes Projekt erstellen und mit Worklenz beginnen",
|
||||
"surveyCompleteTitle": "Vielen Dank!",
|
||||
"surveyCompleteDescription": "Ihr Feedback hilft uns, Worklenz für alle zu verbessern",
|
||||
"aboutYouStepName": "Über Sie",
|
||||
"yourNeedsStepName": "Ihre Bedürfnisse",
|
||||
"discoveryStepName": "Entdeckung",
|
||||
"stepProgress": "Schritt {step} von 3: {title}",
|
||||
|
||||
"projectStepHeader": "Lassen Sie uns Ihr erstes Projekt erstellen",
|
||||
"projectStepSubheader": "Von Grund auf beginnen oder eine Vorlage verwenden, um schneller voranzukommen",
|
||||
"startFromScratch": "Von Grund auf beginnen",
|
||||
"templateSelected": "Vorlage unten ausgewählt",
|
||||
"quickSuggestions": "Schnelle Vorschläge:",
|
||||
"orText": "ODER",
|
||||
"startWithTemplate": "Mit einer Vorlage beginnen",
|
||||
"clearToSelectTemplate": "Projektname oben löschen, um eine Vorlage auszuwählen",
|
||||
"templateHeadStart": "Verschaffen Sie sich einen Vorsprung mit vorgefertigten Projektstrukturen",
|
||||
"browseAllTemplates": "Alle Vorlagen durchsuchen",
|
||||
"templatesAvailable": "15+ branchenspezifische Vorlagen verfügbar",
|
||||
"chooseTemplate": "Wählen Sie eine Vorlage, die zu Ihrem Projekttyp passt",
|
||||
"createProject": "Projekt erstellen",
|
||||
|
||||
"templateSoftwareDev": "Softwareentwicklung",
|
||||
"templateSoftwareDesc": "Agile Sprints, Fehlerverfolgung, Releases",
|
||||
"templateMarketing": "Marketing-Kampagne",
|
||||
"templateMarketingDesc": "Kampagnenplanung, Content-Kalender",
|
||||
"templateConstruction": "Bauprojekt",
|
||||
"templateConstructionDesc": "Phasen, Genehmigungen, Auftragnehmer",
|
||||
"templateStartup": "Startup-Launch",
|
||||
"templateStartupDesc": "MVP-Entwicklung, Finanzierung, Wachstum",
|
||||
|
||||
"tasksStepTitle": "Fügen Sie Ihre ersten Aufgaben hinzu",
|
||||
"tasksStepDescription": "Unterteilen Sie \"{{projectName}}\" in umsetzbare Aufgaben, um zu beginnen",
|
||||
"taskPlaceholder": "Aufgabe {{index}} - z.B., Was muss getan werden?",
|
||||
"addAnotherTask": "Weitere Aufgabe hinzufügen ({{current}}/{{max}})",
|
||||
|
||||
"surveyStepTitle": "Erzählen Sie uns von sich",
|
||||
"surveyStepLabel": "Helfen Sie uns, Ihre Worklenz-Erfahrung zu personalisieren, indem Sie ein paar Fragen beantworten.",
|
||||
|
||||
"organizationType": "Was beschreibt Ihre Organisation am besten?",
|
||||
"organizationTypeFreelancer": "Freelancer",
|
||||
"organizationTypeStartup": "Startup",
|
||||
"organizationTypeSmallMediumBusiness": "Kleines oder mittleres Unternehmen",
|
||||
"organizationTypeAgency": "Agentur",
|
||||
"organizationTypeEnterprise": "Unternehmen",
|
||||
"organizationTypeOther": "Andere",
|
||||
|
||||
"userRole": "Was ist Ihre Rolle?",
|
||||
"userRoleFounderCeo": "Gründer / CEO",
|
||||
"userRoleProjectManager": "Projektmanager",
|
||||
"userRoleSoftwareDeveloper": "Software-Entwickler",
|
||||
"userRoleDesigner": "Designer",
|
||||
"userRoleOperations": "Betrieb",
|
||||
"userRoleOther": "Andere",
|
||||
|
||||
"mainUseCases": "Wofür werden Sie Worklenz hauptsächlich verwenden?",
|
||||
"mainUseCasesTaskManagement": "Aufgabenverwaltung",
|
||||
"mainUseCasesTeamCollaboration": "Teamzusammenarbeit",
|
||||
"mainUseCasesResourcePlanning": "Ressourcenplanung",
|
||||
"mainUseCasesClientCommunication": "Kundenkommunikation & Berichterstattung",
|
||||
"mainUseCasesTimeTracking": "Zeiterfassung",
|
||||
"mainUseCasesOther": "Andere",
|
||||
|
||||
"previousTools": "Welche Tools haben Sie vor Worklenz verwendet?",
|
||||
"previousToolsPlaceholder": "z.B. Trello, Asana, Monday.com",
|
||||
|
||||
"howHeardAbout": "Wie haben Sie von Worklenz erfahren?",
|
||||
"howHeardAboutGoogleSearch": "Google-Suche",
|
||||
"howHeardAboutTwitter": "Twitter",
|
||||
"howHeardAboutLinkedin": "LinkedIn",
|
||||
"howHeardAboutFriendColleague": "Ein Freund oder Kollege",
|
||||
"howHeardAboutBlogArticle": "Ein Blog oder Artikel",
|
||||
"howHeardAboutOther": "Andere",
|
||||
|
||||
"aboutYouStepTitle": "Erzählen Sie uns von sich",
|
||||
"aboutYouStepDescription": "Helfen Sie uns, Ihre Erfahrung zu personalisieren",
|
||||
"yourNeedsStepTitle": "Was sind Ihre Hauptbedürfnisse?",
|
||||
"yourNeedsStepDescription": "Wählen Sie alle zutreffenden aus, um uns bei der Einrichtung Ihres Arbeitsbereichs zu helfen",
|
||||
"selected": "ausgewählt",
|
||||
"previousToolsLabel": "Welche Tools haben Sie zuvor verwendet? (Optional)",
|
||||
|
||||
"roleSuggestions": {
|
||||
"designer": "UI/UX, Grafiken, Kreativ",
|
||||
"developer": "Frontend, Backend, Full-stack",
|
||||
"projectManager": "Planung, Koordination",
|
||||
"marketing": "Inhalt, Social Media, Wachstum",
|
||||
"sales": "Geschäftsentwicklung, Kundenbeziehungen",
|
||||
"operations": "Admin, HR, Finanzen"
|
||||
},
|
||||
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"pt": "Português",
|
||||
"de": "Deutsch",
|
||||
"alb": "Shqip",
|
||||
"zh": "简体中文"
|
||||
},
|
||||
|
||||
"orgSuggestions": {
|
||||
"tech": ["TechCorp", "DevStudio", "CodeCraft", "PixelForge"],
|
||||
"creative": ["Creative Hub", "Design Studio", "Brand Works", "Visual Arts"],
|
||||
"consulting": ["Strategy Group", "Business Solutions", "Expert Advisors", "Growth Partners"],
|
||||
"startup": ["Innovation Labs", "Future Works", "Venture Co", "Next Gen"]
|
||||
},
|
||||
|
||||
"projectSuggestions": {
|
||||
"freelancer": ["Kundenprojekt", "Portfolio-Update", "Persönliche Marke"],
|
||||
"startup": ["MVP-Entwicklung", "Produktlaunch", "Marktforschung"],
|
||||
"agency": ["Kundenkampagne", "Markenstrategie", "Website-Redesign"],
|
||||
"enterprise": ["Systemumstellung", "Prozessoptimierung", "Teamschulung"]
|
||||
},
|
||||
|
||||
"useCaseDescriptions": {
|
||||
"taskManagement": "Aufgaben organisieren und verfolgen",
|
||||
"teamCollaboration": "Nahtlos zusammenarbeiten",
|
||||
"resourcePlanning": "Zeit und Ressourcen verwalten",
|
||||
"clientCommunication": "Mit Kunden in Verbindung bleiben",
|
||||
"timeTracking": "Projektstunden überwachen",
|
||||
"other": "Etwas anderes"
|
||||
}
|
||||
"maxTasks": " (Sie können bis zu 5 Aufgaben erstellen)"
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"billingDetails": "Abrechnungsdetails",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Name",
|
||||
"emailAddress": "E-Mail-Adresse",
|
||||
"emailPlaceholder": "E-Mail-Adresse",
|
||||
"contactNumber": "Telefonnummer",
|
||||
"phoneNumberPlaceholder": "Telefonnummer",
|
||||
"phoneValidationError": "Telefonnummer muss genau 10 Ziffern haben",
|
||||
"companyDetails": "Firmendetails",
|
||||
"companyName": "Firmenname",
|
||||
"companyNamePlaceholder": "Firmenname",
|
||||
"addressLine01": "Adresszeile 01",
|
||||
"addressLine01Placeholder": "Adresszeile 01",
|
||||
"addressLine02": "Adresszeile 02",
|
||||
"addressLine02Placeholder": "Adresszeile 02",
|
||||
"country": "Land",
|
||||
"countryPlaceholder": "Land",
|
||||
"city": "Stadt",
|
||||
"cityPlaceholder": "Stadt",
|
||||
"state": "Bundesland",
|
||||
"statePlaceholder": "Bundesland",
|
||||
"postalCode": "Postleitzahl",
|
||||
"postalCodePlaceholder": "Postleitzahl",
|
||||
"save": "Speichern"
|
||||
}
|
||||
@@ -4,69 +4,5 @@
|
||||
"owner": "Organisationsinhaber",
|
||||
"admins": "Organisationsadministratoren",
|
||||
"contactNumber": "Kontaktnummer hinzufügen",
|
||||
"edit": "Bearbeiten",
|
||||
"organizationWorkingDaysAndHours": "Arbeitstage und -stunden der Organisation",
|
||||
"workingDays": "Arbeitstage",
|
||||
"workingHours": "Arbeitsstunden",
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag",
|
||||
"sunday": "Sonntag",
|
||||
"hours": "Stunden",
|
||||
"saveButton": "Speichern",
|
||||
"saved": "Einstellungen erfolgreich gespeichert",
|
||||
"errorSaving": "Fehler beim Speichern der Einstellungen",
|
||||
"organizationCalculationMethod": "Organisations-Berechnungsmethode",
|
||||
"calculationMethod": "Berechnungsmethode",
|
||||
"hourlyRates": "Stundensätze",
|
||||
"manDays": "Mann-Tage",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"hourlyCalculationDescription": "Alle Projektkosten werden anhand geschätzter Stunden × Stundensätze berechnet",
|
||||
"manDaysCalculationDescription": "Alle Projektkosten werden anhand geschätzter Mann-Tage × Tagessätze berechnet",
|
||||
"calculationMethodTooltip": "Diese Einstellung gilt für alle Projekte in Ihrer Organisation",
|
||||
"calculationMethodUpdated": "Organisations-Berechnungsmethode erfolgreich aktualisiert",
|
||||
"calculationMethodUpdateError": "Fehler beim Aktualisieren der Berechnungsmethode",
|
||||
"holidayCalendar": "Feiertagskalender",
|
||||
"addHoliday": "Feiertag hinzufügen",
|
||||
"editHoliday": "Feiertag bearbeiten",
|
||||
"holidayName": "Feiertagsname",
|
||||
"holidayNameRequired": "Bitte geben Sie den Feiertagsnamen ein",
|
||||
"description": "Beschreibung",
|
||||
"date": "Datum",
|
||||
"dateRequired": "Bitte wählen Sie ein Datum aus",
|
||||
"holidayType": "Feiertagstyp",
|
||||
"holidayTypeRequired": "Bitte wählen Sie einen Feiertagstyp aus",
|
||||
"recurring": "Wiederkehrend",
|
||||
"save": "Speichern",
|
||||
"update": "Aktualisieren",
|
||||
"cancel": "Abbrechen",
|
||||
"holidayCreated": "Feiertag erfolgreich erstellt",
|
||||
"holidayUpdated": "Feiertag erfolgreich aktualisiert",
|
||||
"holidayDeleted": "Feiertag erfolgreich gelöscht",
|
||||
"errorCreatingHoliday": "Fehler beim Erstellen des Feiertags",
|
||||
"errorUpdatingHoliday": "Fehler beim Aktualisieren des Feiertags",
|
||||
"errorDeletingHoliday": "Fehler beim Löschen des Feiertags",
|
||||
"importCountryHolidays": "Landesfeiertage importieren",
|
||||
"country": "Land",
|
||||
"countryRequired": "Bitte wählen Sie ein Land aus",
|
||||
"selectCountry": "Ein Land auswählen",
|
||||
"year": "Jahr",
|
||||
"import": "Importieren",
|
||||
"holidaysImported": "{{count}} Feiertage erfolgreich importiert",
|
||||
"errorImportingHolidays": "Fehler beim Importieren der Feiertage",
|
||||
"addCustomHoliday": "Benutzerdefinierten Feiertag hinzufügen",
|
||||
"officialHolidaysFrom": "Offizielle Feiertage aus",
|
||||
"workingDay": "Arbeitstag",
|
||||
"holiday": "Feiertag",
|
||||
"today": "Heute",
|
||||
"cannotEditOfficialHoliday": "Offizielle Feiertage können nicht bearbeitet werden",
|
||||
"customHoliday": "Benutzerdefinierter Feiertag",
|
||||
"officialHoliday": "Offizieller Feiertag",
|
||||
"delete": "Löschen",
|
||||
"deleteHolidayConfirm": "Sind Sie sicher, dass Sie diesen Feiertag löschen möchten?",
|
||||
"yes": "Ja",
|
||||
"no": "Nein"
|
||||
"edit": "Bearbeiten"
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"settings": "Einstellungen",
|
||||
"organizationWorkingDaysAndHours": "Arbeitstage und -stunden der Organisation",
|
||||
"workingDays": "Arbeitstage",
|
||||
"workingHours": "Arbeitsstunden",
|
||||
"hours": "Stunden",
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag",
|
||||
"sunday": "Sonntag",
|
||||
"saveButton": "Speichern",
|
||||
"saved": "Einstellungen erfolgreich gespeichert",
|
||||
"errorSaving": "Fehler beim Speichern der Einstellungen",
|
||||
"holidaySettings": "Feiertagseinstellungen",
|
||||
"country": "Land",
|
||||
"countryRequired": "Bitte wählen Sie ein Land aus",
|
||||
"selectCountry": "Land auswählen",
|
||||
"state": "Bundesland/Provinz",
|
||||
"selectState": "Bundesland/Provinz auswählen (optional)",
|
||||
"autoSyncHolidays": "Offizielle Feiertage automatisch synchronisieren",
|
||||
"saveHolidaySettings": "Feiertagseinstellungen speichern",
|
||||
"holidaySettingsSaved": "Feiertagseinstellungen erfolgreich gespeichert",
|
||||
"errorSavingHolidaySettings": "Fehler beim Speichern der Feiertagseinstellungen",
|
||||
"addCustomHoliday": "Benutzerdefinierten Feiertag hinzufügen",
|
||||
"officialHolidaysFrom": "Offizielle Feiertage aus",
|
||||
"workingDay": "Arbeitstag",
|
||||
"holiday": "Feiertag",
|
||||
"today": "Heute",
|
||||
"cannotEditOfficialHoliday": "Offizielle Feiertage können nicht bearbeitet werden"
|
||||
}
|
||||
@@ -4,6 +4,5 @@
|
||||
"teams": "Teams",
|
||||
"billing": "Abrechnung",
|
||||
"projects": "Projekte",
|
||||
"settings": "Einstellungen",
|
||||
"adminCenter": "Admin-Center"
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"emailLabel": "E-Mail",
|
||||
"emailPlaceholder": "Ihre E-Mail-Adresse eingeben",
|
||||
"emailRequired": "Bitte geben Sie Ihre E-Mail-Adresse ein!",
|
||||
"passwordLabel": "Passwort",
|
||||
"passwordGuideline": "Das Passwort muss mindestens 8 Zeichen lang sein und Groß- und Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten.",
|
||||
"passwordPlaceholder": "Geben Sie Ihr Passwort ein",
|
||||
"passwordLabel": "Password",
|
||||
"passwordGuideline": "Password must be at least 8 characters, include uppercase and lowercase letters, a number, and a special character.",
|
||||
"passwordPlaceholder": "Enter your password",
|
||||
"passwordRequired": "Bitte geben Sie Ihr Passwort ein!",
|
||||
"passwordMinCharacterRequired": "Das Passwort muss mindestens 8 Zeichen lang sein!",
|
||||
"passwordMaxCharacterRequired": "Das Passwort darf maximal 32 Zeichen lang sein!",
|
||||
"passwordMaxCharacterRequired": "Password must be at most 32 characters!",
|
||||
"passwordPatternRequired": "Das Passwort entspricht nicht den Anforderungen!",
|
||||
"strongPasswordPlaceholder": "Ein stärkeres Passwort eingeben",
|
||||
"passwordValidationAltText": "Das Passwort muss mindestens 8 Zeichen enthalten, mit Groß- und Kleinbuchstaben, einer Zahl und einem Sonderzeichen.",
|
||||
|
||||
@@ -5,13 +5,5 @@
|
||||
"signup-failed": "Registrierung fehlgeschlagen. Bitte füllen Sie alle erforderlichen Felder aus und versuchen Sie es erneut.",
|
||||
"reconnecting": "Vom Server getrennt.",
|
||||
"connection-lost": "Verbindung zum Server fehlgeschlagen. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||
"connection-restored": "Erfolgreich mit dem Server verbunden",
|
||||
"cancel": "Abbrechen",
|
||||
"update-available": "Worklenz aktualisiert!",
|
||||
"update-description": "Eine neue Version von Worklenz ist verfügbar mit den neuesten Funktionen und Verbesserungen.",
|
||||
"update-instruction": "Für die beste Erfahrung laden Sie bitte die Seite neu, um die neuen Änderungen zu übernehmen.",
|
||||
"update-whats-new": "💡 <1>Was ist neu:</1> Verbesserte Leistung, Fehlerbehebungen und verbesserte Benutzererfahrung",
|
||||
"update-now": "Jetzt aktualisieren",
|
||||
"update-later": "Später",
|
||||
"updating": "Wird aktualisiert..."
|
||||
"connection-restored": "Erfolgreich mit dem Server verbunden"
|
||||
}
|
||||
|
||||
@@ -41,22 +41,6 @@
|
||||
"list": "Liste",
|
||||
"calendar": "Kalender",
|
||||
"tasks": "Aufgaben",
|
||||
"refresh": "Aktualisieren",
|
||||
"recentActivity": "Aktuelle Aktivitäten",
|
||||
"recentTasks": "Aktuelle Aufgaben",
|
||||
"recentTasksSegment": "Aktuelle Aufgaben",
|
||||
"timeLogged": "Erfasste Zeit",
|
||||
"timeLoggedSegment": "Erfasste Zeit",
|
||||
"noRecentTasks": "Keine aktuellen Aufgaben",
|
||||
"noTimeLoggedTasks": "Keine Aufgaben mit erfasster Zeit",
|
||||
"activityTag": "Aktivität",
|
||||
"timeLogTag": "Zeiterfassung",
|
||||
"timerTag": "Timer",
|
||||
"activitySingular": "Aktivität",
|
||||
"activityPlural": "Aktivitäten",
|
||||
"recentTaskAriaLabel": "Aktuelle Aufgabe:",
|
||||
"timeLoggedTaskAriaLabel": "Aufgabe mit erfasster Zeit:",
|
||||
"errorLoadingRecentTasks": "Fehler beim Laden aktueller Aufgaben",
|
||||
"errorLoadingTimeLoggedTasks": "Fehler beim Laden der Zeiterfassung"
|
||||
"refresh": "Aktualisieren"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,6 @@
|
||||
"deleteConfirmationOk": "Ja",
|
||||
"deleteConfirmationCancel": "Abbrechen",
|
||||
|
||||
"deleteTaskTitle": "Aufgabe löschen",
|
||||
"deleteTaskContent": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteTaskConfirm": "Löschen",
|
||||
"deleteTaskCancel": "Abbrechen",
|
||||
|
||||
"deleteStatusTitle": "Status löschen",
|
||||
"deleteStatusContent": "Sind Sie sicher, dass Sie diesen Status löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
|
||||
"deletePhaseTitle": "Phase löschen",
|
||||
"deletePhaseContent": "Sind Sie sicher, dass Sie diese Phase löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
|
||||
"dueDate": "Fälligkeitsdatum",
|
||||
"cancel": "Abbrechen",
|
||||
|
||||
@@ -37,17 +26,5 @@
|
||||
"noDueDate": "Kein Fälligkeitsdatum",
|
||||
"save": "Speichern",
|
||||
"clear": "Löschen",
|
||||
"nextWeek": "Nächste Woche",
|
||||
"noSubtasks": "Keine Unteraufgaben",
|
||||
"showSubtasks": "Unteraufgaben anzeigen",
|
||||
"hideSubtasks": "Unteraufgaben ausblenden",
|
||||
|
||||
"errorLoadingTasks": "Fehler beim Laden der Aufgaben",
|
||||
"noTasksFound": "Keine Aufgaben gefunden",
|
||||
"loadingFilters": "Filter werden geladen...",
|
||||
"failedToUpdateColumnOrder": "Fehler beim Aktualisieren der Spaltenreihenfolge",
|
||||
"failedToUpdatePhaseOrder": "Fehler beim Aktualisieren der Phasenreihenfolge",
|
||||
"pleaseTryAgain": "Bitte versuchen Sie es erneut",
|
||||
"taskNotCompleted": "Aufgabe ist nicht abgeschlossen",
|
||||
"completeTaskDependencies": "Bitte schließen Sie die Aufgabenabhängigkeiten ab, bevor Sie fortfahren"
|
||||
"nextWeek": "Nächste Woche"
|
||||
}
|
||||
|
||||
@@ -38,13 +38,5 @@
|
||||
"createClient": "Kunde erstellen",
|
||||
"searchInputPlaceholder": "Nach Name oder E-Mail suchen",
|
||||
"hoursPerDayValidationMessage": "Stunden pro Tag müssen zwischen 1 und 24",
|
||||
"workingDaysValidationMessage": "Arbeitstage müssen eine positive Zahl sein",
|
||||
"manDaysValidationMessage": "Personentage müssen eine positive Zahl sein",
|
||||
"noPermission": "Keine Berechtigung",
|
||||
"progressSettings": "Fortschrittseinstellungen",
|
||||
"manualProgress": "Manueller Fortschritt",
|
||||
"manualProgressTooltip": "Manuelle Fortschrittsaktualisierungen für Aufgaben ohne Unteraufgaben erlauben",
|
||||
"weightedProgress": "Gewichteter Fortschritt",
|
||||
"weightedProgressTooltip": "Fortschritt basierend auf Unteraufgaben-Gewichten berechnen",
|
||||
"timeProgress": "Zeitbasierter Fortschritt"
|
||||
"noPermission": "Keine Berechtigung"
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"financeText": "Finance",
|
||||
"ratecardSingularText": "Rate Card",
|
||||
"groupByText": "Group by",
|
||||
"statusText": "Status",
|
||||
"phaseText": "Phase",
|
||||
"priorityText": "Priority",
|
||||
"exportButton": "Export",
|
||||
"currencyText": "Currency",
|
||||
"importButton": "Import",
|
||||
"filterText": "Filter",
|
||||
"billableOnlyText": "Billable Only",
|
||||
"nonBillableOnlyText": "Non-Billable Only",
|
||||
"allTasksText": "All Tasks",
|
||||
"projectBudgetOverviewText": "Project Budget Overview",
|
||||
"taskColumn": "Task",
|
||||
"membersColumn": "Members",
|
||||
"hoursColumn": "Estimated Hours",
|
||||
"manDaysColumn": "Estimated Man Days",
|
||||
"actualManDaysColumn": "Actual Man Days",
|
||||
"effortVarianceColumn": "Effort Variance",
|
||||
"totalTimeLoggedColumn": "Total Time Logged",
|
||||
"costColumn": "Actual Cost",
|
||||
"estimatedCostColumn": "Estimated Cost",
|
||||
"fixedCostColumn": "Fixed Cost",
|
||||
"totalBudgetedCostColumn": "Total Budgeted Cost",
|
||||
"totalActualCostColumn": "Total Actual Cost",
|
||||
"varianceColumn": "Variance",
|
||||
"totalText": "Total",
|
||||
"noTasksFound": "No tasks found",
|
||||
"addRoleButton": "+ Add Role",
|
||||
"ratecardImportantNotice": "* This rate card is generated based on the company's standard job titles and rates. However, you have the flexibility to modify it according to the project. These changes will not impact the organization's standard job titles and rates.",
|
||||
"saveButton": "Save",
|
||||
"jobTitleColumn": "Job Title",
|
||||
"ratePerHourColumn": "Rate per hour",
|
||||
"ratePerManDayColumn": "Satz pro Manntag",
|
||||
"calculationMethodText": "Calculation Method",
|
||||
"hourlyRatesText": "Hourly Rates",
|
||||
"manDaysText": "Man Days",
|
||||
"hoursPerDayText": "Hours per Day",
|
||||
"ratecardPluralText": "Rate Cards",
|
||||
"labourHoursColumn": "Labour Hours",
|
||||
"actions": "Actions",
|
||||
"selectJobTitle": "Select Job Title",
|
||||
"ratecardsPluralText": "Rate Card Templates",
|
||||
"deleteConfirm": "Are you sure ?",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"alreadyImportedRateCardMessage": "A rate card has already been imported. Clear all imported rate cards to add a new one.",
|
||||
"budgetOverviewTooltips": {
|
||||
"manualBudget": "Manual project budget amount set by project manager",
|
||||
"totalActualCost": "Total actual cost including fixed costs",
|
||||
"variance": "Difference between manual budget and actual cost",
|
||||
"utilization": "Percentage of manual budget utilized",
|
||||
"estimatedHours": "Total estimated hours from all tasks",
|
||||
"fixedCosts": "Total fixed costs from all tasks",
|
||||
"timeBasedCost": "Actual cost from time tracking (excluding fixed costs)",
|
||||
"remainingBudget": "Remaining budget amount"
|
||||
},
|
||||
"budgetModal": {
|
||||
"title": "Edit Project Budget",
|
||||
"description": "Set a manual budget for this project. This budget will be used for all financial calculations and should include both time-based costs and fixed costs.",
|
||||
"placeholder": "Enter budget amount",
|
||||
"saveButton": "Save",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
"budgetStatistics": {
|
||||
"manualBudget": "Manual Budget",
|
||||
"totalActualCost": "Total Actual Cost",
|
||||
"variance": "Variance",
|
||||
"budgetUtilization": "Budget Utilization",
|
||||
"estimatedHours": "Estimated Hours",
|
||||
"fixedCosts": "Fixed Costs",
|
||||
"timeBasedCost": "Time-based Cost",
|
||||
"remainingBudget": "Remaining Budget",
|
||||
"noManualBudgetSet": "(No Manual Budget Set)"
|
||||
},
|
||||
"budgetSettingsDrawer": {
|
||||
"title": "Project Budget Settings",
|
||||
"budgetConfiguration": "Budget Configuration",
|
||||
"projectBudget": "Project Budget",
|
||||
"projectBudgetTooltip": "Total budget allocated for this project",
|
||||
"currency": "Currency",
|
||||
"costCalculationMethod": "Cost Calculation Method",
|
||||
"calculationMethod": "Calculation Method",
|
||||
"workingHoursPerDay": "Working Hours per Day",
|
||||
"workingHoursPerDayTooltip": "Number of working hours in a day for man-day calculations",
|
||||
"hourlyCalculationInfo": "Costs will be calculated using estimated hours × hourly rates",
|
||||
"manDaysCalculationInfo": "Costs will be calculated using estimated man days × daily rates",
|
||||
"importantNotes": "Important Notes",
|
||||
"calculationMethodChangeNote": "• Changing the calculation method will affect how costs are calculated for all tasks in this project",
|
||||
"immediateEffectNote": "• Changes take effect immediately and will recalculate all project totals",
|
||||
"projectWideNote": "• Budget settings apply to the entire project and all its tasks",
|
||||
"cancel": "Cancel",
|
||||
"saveChanges": "Save Changes",
|
||||
"budgetSettingsUpdated": "Budget settings updated successfully",
|
||||
"budgetSettingsUpdateFailed": "Failed to update budget settings"
|
||||
},
|
||||
"columnTooltips": {
|
||||
"hours": "Total estimated hours for all tasks. Calculated from task time estimates.",
|
||||
"manDays": "Total estimated man days for all tasks. Based on {{hoursPerDay}} hours per working day.",
|
||||
"actualManDays": "Actual man days spent based on time logged. Calculated as: Total Time Logged ÷ {{hoursPerDay}} hours per day.",
|
||||
"effortVariance": "Difference between estimated and actual man days. Positive values indicate over-estimation, negative values indicate under-estimation.",
|
||||
"totalTimeLogged": "Total time actually logged by team members across all tasks.",
|
||||
"estimatedCostHourly": "Estimated cost calculated as: Estimated Hours × Hourly Rates for assigned team members.",
|
||||
"estimatedCostManDays": "Estimated cost calculated as: Estimated Man Days × Daily Rates for assigned team members.",
|
||||
"actualCost": "Actual cost based on time logged. Calculated as: Time Logged × Hourly Rates for team members.",
|
||||
"fixedCost": "Fixed costs that don't depend on time spent. Added manually per task.",
|
||||
"totalBudgetHourly": "Total budgeted cost including estimated cost (Hours × Hourly Rates) + Fixed Costs.",
|
||||
"totalBudgetManDays": "Total budgeted cost including estimated cost (Man Days × Daily Rates) + Fixed Costs.",
|
||||
"totalActual": "Total actual cost including time-based cost + Fixed Costs.",
|
||||
"variance": "Cost variance: Total Budgeted Costs - Total Actual Cost. Positive values indicate under-budget, negative values indicate over-budget."
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,5 @@
|
||||
"deleteButtonTooltip": "Aus Projekt entfernen",
|
||||
"memberCount": "Mitglied",
|
||||
"membersCountPlural": "Mitglieder",
|
||||
"emptyText": "Es gibt keine Anhänge in diesem Projekt.",
|
||||
"searchPlaceholder": "Mitglieder suchen"
|
||||
"emptyText": "Es gibt keine Anhänge in diesem Projekt."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user