Linting cleanup (#99)
* Add eslint and prettier dependencies and configs * Lint project.
This commit is contained in:
parent
dd6ec117d0
commit
e594d1075b
97 changed files with 26554 additions and 39840 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.idea
|
||||
*.sqlite3
|
||||
*.sqlite3-shm
|
||||
*.sqlite3-wal
|
||||
|
|
|
|||
6
backend/.prettierrc.json
Normal file
6
backend/.prettierrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
|
|
@ -19,37 +19,60 @@ const sessionStore = new SequelizeStore({
|
|||
});
|
||||
|
||||
// Middlewares
|
||||
const sslEnabled = process.env.NODE_ENV === 'production' && process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
|
||||
app.use(helmet({
|
||||
const sslEnabled =
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
|
||||
app.use(
|
||||
helmet({
|
||||
hsts: sslEnabled, // Only enable HSTS when SSL is enabled
|
||||
forceHTTPS: sslEnabled, // Only force HTTPS when SSL is enabled
|
||||
contentSecurityPolicy: false // Disable CSP for now to avoid conflicts
|
||||
}));
|
||||
contentSecurityPolicy: false, // Disable CSP for now to avoid conflicts
|
||||
})
|
||||
);
|
||||
app.use(compression());
|
||||
app.use(morgan('combined'));
|
||||
|
||||
// CORS configuration
|
||||
const allowedOrigins = process.env.TUDUDI_ALLOWED_ORIGINS
|
||||
? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map(origin => origin.trim())
|
||||
: ['http://localhost:8080', 'http://localhost:9292', 'http://127.0.0.1:8080', 'http://127.0.0.1:9292'];
|
||||
? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map((origin) =>
|
||||
origin.trim()
|
||||
)
|
||||
: [
|
||||
'http://localhost:8080',
|
||||
'http://localhost:9292',
|
||||
'http://127.0.0.1:8080',
|
||||
'http://127.0.0.1:9292',
|
||||
];
|
||||
|
||||
app.use(cors({
|
||||
app.use(
|
||||
cors({
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'X-Requested-With'],
|
||||
allowedHeaders: [
|
||||
'Authorization',
|
||||
'Content-Type',
|
||||
'Accept',
|
||||
'X-Requested-With',
|
||||
],
|
||||
exposedHeaders: ['Content-Type'],
|
||||
maxAge: 1728000
|
||||
}));
|
||||
maxAge: 1728000,
|
||||
})
|
||||
);
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Session configuration
|
||||
const secureFlag = process.env.NODE_ENV === 'production' && process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
|
||||
app.use(session({
|
||||
secret: process.env.TUDUDI_SESSION_SECRET || require('crypto').randomBytes(64).toString('hex'),
|
||||
const secureFlag =
|
||||
process.env.NODE_ENV === 'production' &&
|
||||
process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
|
||||
app.use(
|
||||
session({
|
||||
secret:
|
||||
process.env.TUDUDI_SESSION_SECRET ||
|
||||
require('crypto').randomBytes(64).toString('hex'),
|
||||
store: sessionStore,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
|
|
@ -57,9 +80,10 @@ app.use(session({
|
|||
httpOnly: true,
|
||||
secure: secureFlag,
|
||||
maxAge: 2592000000, // 30 days
|
||||
sameSite: secureFlag ? 'none' : 'lax'
|
||||
}
|
||||
}));
|
||||
sameSite: secureFlag ? 'none' : 'lax',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Static files
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
|
@ -72,7 +96,10 @@ if (process.env.NODE_ENV === 'production') {
|
|||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use('/locales', express.static(path.join(__dirname, 'dist/locales')));
|
||||
} else {
|
||||
app.use('/locales', express.static(path.join(__dirname, '../public/locales')));
|
||||
app.use(
|
||||
'/locales',
|
||||
express.static(path.join(__dirname, '../public/locales'))
|
||||
);
|
||||
}
|
||||
|
||||
// Serve uploaded files
|
||||
|
|
@ -87,7 +114,7 @@ app.get('/api/health', (req, res) => {
|
|||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -108,21 +135,30 @@ app.use('/api/calendar', require('./routes/calendar'));
|
|||
|
||||
// SPA fallback
|
||||
app.get('*', (req, res) => {
|
||||
if (!req.path.startsWith('/api/') && !req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) {
|
||||
if (
|
||||
!req.path.startsWith('/api/') &&
|
||||
!req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)
|
||||
) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||
} else {
|
||||
res.sendFile(path.join(__dirname, '../public', 'index.html'));
|
||||
}
|
||||
} else {
|
||||
res.status(404).json({ error: 'Not Found', message: 'The requested resource could not be found.' });
|
||||
res.status(404).json({
|
||||
error: 'Not Found',
|
||||
message: 'The requested resource could not be found.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ error: 'Internal Server Error', message: err.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal Server Error',
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3002;
|
||||
|
|
@ -145,8 +181,11 @@ async function startServer() {
|
|||
where: { email: process.env.TUDUDI_USER_EMAIL },
|
||||
defaults: {
|
||||
email: process.env.TUDUDI_USER_EMAIL,
|
||||
password_digest: await bcrypt.hash(process.env.TUDUDI_USER_PASSWORD, 10)
|
||||
}
|
||||
password_digest: await bcrypt.hash(
|
||||
process.env.TUDUDI_USER_PASSWORD,
|
||||
10
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
if (created) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ module.exports = {
|
|||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
dialect: 'sqlite',
|
||||
|
|
@ -25,8 +25,8 @@ module.exports = {
|
|||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
},
|
||||
production: {
|
||||
dialect: 'sqlite',
|
||||
|
|
@ -36,7 +36,7 @@ module.exports = {
|
|||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
}
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
quotes:
|
||||
- "Believe you can and you're halfway there."
|
||||
- "The only way to do great work is to love what you do."
|
||||
- "Success is not final, failure is not fatal: It is the courage to continue that counts."
|
||||
- 'The only way to do great work is to love what you do.'
|
||||
- 'Success is not final, failure is not fatal: It is the courage to continue that counts.'
|
||||
- "It always seems impossible until it's done."
|
||||
- "Your time is limited, don't waste it living someone else's life."
|
||||
- "The future belongs to those who believe in the beauty of their dreams."
|
||||
- 'The future belongs to those who believe in the beauty of their dreams.'
|
||||
- "Don't watch the clock; do what it does. Keep going."
|
||||
- "Quality is not an act, it is a habit."
|
||||
- "The only limit to our realization of tomorrow is our doubts of today."
|
||||
- "Act as if what you do makes a difference. It does."
|
||||
- "The best way to predict the future is to create it."
|
||||
- "Success is walking from failure to failure with no loss of enthusiasm."
|
||||
- "You are never too old to set another goal or to dream a new dream."
|
||||
- "The secret of getting ahead is getting started."
|
||||
- 'Quality is not an act, it is a habit.'
|
||||
- 'The only limit to our realization of tomorrow is our doubts of today.'
|
||||
- 'Act as if what you do makes a difference. It does.'
|
||||
- 'The best way to predict the future is to create it.'
|
||||
- 'Success is walking from failure to failure with no loss of enthusiasm.'
|
||||
- 'You are never too old to set another goal or to dream a new dream.'
|
||||
- 'The secret of getting ahead is getting started.'
|
||||
- "Don't let yesterday take up too much of today."
|
||||
- "You don't have to be great to start, but you have to start to be great."
|
||||
- "Focus on progress, not perfection."
|
||||
- "One task at a time leads to great accomplishments."
|
||||
- 'Focus on progress, not perfection.'
|
||||
- 'One task at a time leads to great accomplishments.'
|
||||
- "Today's effort is tomorrow's success."
|
||||
- "Small steps every day lead to big results."
|
||||
|
||||
- 'Small steps every day lead to big results.'
|
||||
|
|
|
|||
40
backend/eslint.config.js
Normal file
40
backend/eslint.config.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
module.exports = [
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
require: 'readonly',
|
||||
module: 'readonly',
|
||||
exports: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
global: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
prettier: require('eslint-plugin-prettier'),
|
||||
jest: require('eslint-plugin-jest'),
|
||||
},
|
||||
rules: {
|
||||
...require('eslint-plugin-prettier').configs.recommended.rules,
|
||||
...require('eslint-plugin-jest').configs.recommended.rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.js', '**/*.spec.js', 'tests/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...require('eslint-plugin-jest').environments.globals.globals,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/helpers/setup.js'],
|
||||
testMatch: [
|
||||
'<rootDir>/tests/**/*.test.js',
|
||||
'<rootDir>/tests/**/*.spec.js'
|
||||
],
|
||||
testMatch: ['<rootDir>/tests/**/*.test.js', '<rootDir>/tests/**/*.spec.js'],
|
||||
maxWorkers: 1,
|
||||
collectCoverageFrom: [
|
||||
'routes/**/*.js',
|
||||
|
|
@ -13,7 +10,7 @@ module.exports = {
|
|||
'services/**/*.js',
|
||||
'!models/index.js',
|
||||
'!**/*.test.js',
|
||||
'!**/*.spec.js'
|
||||
'!**/*.spec.js',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
|
|
@ -21,5 +18,5 @@ module.exports = {
|
|||
forceExit: true,
|
||||
clearMocks: true,
|
||||
resetMocks: true,
|
||||
restoreMocks: true
|
||||
restoreMocks: true,
|
||||
};
|
||||
|
|
@ -27,5 +27,5 @@ const requireAuth = async (req, res, next) => {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
requireAuth
|
||||
requireAuth,
|
||||
};
|
||||
|
|
@ -7,55 +7,55 @@ module.exports = {
|
|||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
email: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
telegram_bot_token: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
telegram_chat_id: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
task_summary_enabled: {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
task_summary_frequency: {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: 'daily'
|
||||
defaultValue: 'daily',
|
||||
},
|
||||
task_summary_last_run: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
task_summary_next_run: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('users');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -6,32 +6,39 @@ module.exports = {
|
|||
await queryInterface.addColumn('tasks', 'recurrence_weekday', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Day of week (0=Sunday, 1=Monday, ..., 6=Saturday) for weekly recurrence'
|
||||
comment:
|
||||
'Day of week (0=Sunday, 1=Monday, ..., 6=Saturday) for weekly recurrence',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('tasks', 'recurrence_month_day', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Day of month (1-31) for monthly recurrence, -1 for last day'
|
||||
comment:
|
||||
'Day of month (1-31) for monthly recurrence, -1 for last day',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('tasks', 'recurrence_week_of_month', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
comment: 'Week of month (1-5) for monthly weekday recurrence'
|
||||
comment: 'Week of month (1-5) for monthly weekday recurrence',
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('tasks', 'completion_based', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: 'Whether recurrence is based on completion date (true) or due date (false)'
|
||||
comment:
|
||||
'Whether recurrence is based on completion date (true) or due date (false)',
|
||||
});
|
||||
|
||||
// Add index for efficient recurring task queries
|
||||
await queryInterface.addIndex('tasks', ['recurrence_type', 'last_generated_date'], {
|
||||
name: 'idx_tasks_recurrence_lookup'
|
||||
});
|
||||
await queryInterface.addIndex(
|
||||
'tasks',
|
||||
['recurrence_type', 'last_generated_date'],
|
||||
{
|
||||
name: 'idx_tasks_recurrence_lookup',
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
|
|
@ -42,6 +49,9 @@ module.exports = {
|
|||
await queryInterface.removeColumn('tasks', 'completion_based');
|
||||
|
||||
// Remove the index
|
||||
await queryInterface.removeIndex('tasks', 'idx_tasks_recurrence_lookup');
|
||||
}
|
||||
await queryInterface.removeIndex(
|
||||
'tasks',
|
||||
'idx_tasks_recurrence_lookup'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -7,10 +7,10 @@ module.exports = {
|
|||
allowNull: true,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL'
|
||||
onDelete: 'SET NULL',
|
||||
});
|
||||
|
||||
// Add index for performance
|
||||
|
|
@ -20,5 +20,5 @@ module.exports = {
|
|||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeIndex('tasks', ['recurring_parent_id']);
|
||||
await queryInterface.removeColumn('tasks', 'recurring_parent_id');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -4,11 +4,11 @@ module.exports = {
|
|||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('projects', 'image_url', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('projects', 'image_url');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -5,11 +5,11 @@ module.exports = {
|
|||
await queryInterface.addColumn('users', 'task_intelligence_enabled', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
defaultValue: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('users', 'task_intelligence_enabled');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -2,14 +2,21 @@
|
|||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('users', 'auto_suggest_next_actions_enabled', {
|
||||
await queryInterface.addColumn(
|
||||
'users',
|
||||
'auto_suggest_next_actions_enabled',
|
||||
{
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
defaultValue: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('users', 'auto_suggest_next_actions_enabled');
|
||||
}
|
||||
await queryInterface.removeColumn(
|
||||
'users',
|
||||
'auto_suggest_next_actions_enabled'
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ module.exports = {
|
|||
// Add completed_at column to tasks table
|
||||
await queryInterface.addColumn('tasks', 'completed_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Add an index for better query performance
|
||||
|
|
@ -18,5 +18,5 @@ module.exports = {
|
|||
|
||||
// Remove the completed_at column
|
||||
await queryInterface.removeColumn('tasks', 'completed_at');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -7,73 +7,73 @@ module.exports = {
|
|||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
provider: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'google'
|
||||
defaultValue: 'google',
|
||||
},
|
||||
access_token: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
refresh_token: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
token_type: {
|
||||
type: Sequelize.STRING,
|
||||
defaultValue: 'Bearer'
|
||||
defaultValue: 'Bearer',
|
||||
},
|
||||
expires_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
scope: {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
connected_email: {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
|
||||
// Add unique index for user_id + provider combination
|
||||
await queryInterface.addIndex('calendar_tokens', {
|
||||
fields: ['user_id', 'provider'],
|
||||
unique: true,
|
||||
name: 'calendar_tokens_user_provider_unique'
|
||||
name: 'calendar_tokens_user_provider_unique',
|
||||
});
|
||||
|
||||
// Add index for faster lookups by user_id
|
||||
await queryInterface.addIndex('calendar_tokens', {
|
||||
fields: ['user_id'],
|
||||
name: 'calendar_tokens_user_id_index'
|
||||
name: 'calendar_tokens_user_id_index',
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.dropTable('calendar_tokens');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -8,27 +8,27 @@ module.exports = {
|
|||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
task_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
user_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
event_type: {
|
||||
type: Sequelize.STRING,
|
||||
|
|
@ -60,8 +60,8 @@ module.exports = {
|
|||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
|
||||
// Add indexes for better query performance
|
||||
|
|
@ -75,8 +75,14 @@ module.exports = {
|
|||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove indexes first
|
||||
await queryInterface.removeIndex('task_events', ['task_id', 'created_at']);
|
||||
await queryInterface.removeIndex('task_events', ['task_id', 'event_type']);
|
||||
await queryInterface.removeIndex('task_events', [
|
||||
'task_id',
|
||||
'created_at',
|
||||
]);
|
||||
await queryInterface.removeIndex('task_events', [
|
||||
'task_id',
|
||||
'event_type',
|
||||
]);
|
||||
await queryInterface.removeIndex('task_events', ['created_at']);
|
||||
await queryInterface.removeIndex('task_events', ['event_type']);
|
||||
await queryInterface.removeIndex('task_events', ['user_id']);
|
||||
|
|
@ -84,5 +90,5 @@ module.exports = {
|
|||
|
||||
// Drop the table
|
||||
await queryInterface.dropTable('task_events');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -6,11 +6,11 @@ module.exports = {
|
|||
await queryInterface.addColumn('users', 'pomodoro_enabled', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
defaultValue: true,
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.removeColumn('users', 'pomodoro_enabled');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ module.exports = {
|
|||
// Add UUID column to tasks table (without unique constraint initially)
|
||||
await queryInterface.addColumn('tasks', 'uuid', {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Backfill existing tasks with UUIDs
|
||||
|
|
@ -27,7 +27,7 @@ module.exports = {
|
|||
// Add unique index for UUID
|
||||
await queryInterface.addIndex('tasks', ['uuid'], {
|
||||
unique: true,
|
||||
name: 'tasks_uuid_unique'
|
||||
name: 'tasks_uuid_unique',
|
||||
});
|
||||
},
|
||||
|
||||
|
|
@ -37,5 +37,5 @@ module.exports = {
|
|||
|
||||
// Remove UUID column
|
||||
await queryInterface.removeColumn('tasks', 'uuid');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -10,39 +10,39 @@ module.exports = {
|
|||
type: Sequelize.INTEGER,
|
||||
references: {
|
||||
model: 'notes',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
tag_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
references: {
|
||||
model: 'tags',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
created_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
updated_at: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
|
||||
// Add unique index
|
||||
await queryInterface.addIndex('notes_tags', ['note_id', 'tag_id'], {
|
||||
unique: true,
|
||||
name: 'notes_tags_unique_idx'
|
||||
name: 'notes_tags_unique_idx',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.dropTable('notes_tags');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -7,13 +7,13 @@ module.exports = {
|
|||
await queryInterface.addColumn('notes_tags', 'created_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
});
|
||||
|
||||
await queryInterface.addColumn('notes_tags', 'updated_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
});
|
||||
|
||||
console.log('Successfully added timestamps to notes_tags table');
|
||||
|
|
@ -25,5 +25,5 @@ module.exports = {
|
|||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.removeColumn('notes_tags', 'created_at');
|
||||
await queryInterface.removeColumn('notes_tags', 'updated_at');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -3,8 +3,9 @@
|
|||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Create the projects_tags table if it doesn't exist
|
||||
const tableExists = await queryInterface.showAllTables()
|
||||
.then(tables => tables.includes('projects_tags'));
|
||||
const tableExists = await queryInterface
|
||||
.showAllTables()
|
||||
.then((tables) => tables.includes('projects_tags'));
|
||||
|
||||
if (!tableExists) {
|
||||
await queryInterface.createTable('projects_tags', {
|
||||
|
|
@ -13,38 +14,38 @@ module.exports = {
|
|||
allowNull: false,
|
||||
references: {
|
||||
model: 'projects',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
tag_id: {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tags',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
created_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
updated_at: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
}
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
},
|
||||
});
|
||||
|
||||
// Add composite primary key
|
||||
await queryInterface.addConstraint('projects_tags', {
|
||||
fields: ['project_id', 'tag_id'],
|
||||
type: 'primary key',
|
||||
name: 'projects_tags_pkey'
|
||||
name: 'projects_tags_pkey',
|
||||
});
|
||||
} else {
|
||||
// Add timestamps if table exists but doesn't have them
|
||||
|
|
@ -52,7 +53,7 @@ module.exports = {
|
|||
await queryInterface.addColumn('projects_tags', 'created_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
});
|
||||
} catch (error) {
|
||||
// Column might already exist
|
||||
|
|
@ -62,7 +63,7 @@ module.exports = {
|
|||
await queryInterface.addColumn('projects_tags', 'updated_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
|
||||
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
|
||||
});
|
||||
} catch (error) {
|
||||
// Column might already exist
|
||||
|
|
@ -78,5 +79,5 @@ module.exports = {
|
|||
} catch (error) {
|
||||
// Columns might not exist
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,36 +1,40 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Area = sequelize.define('Area', {
|
||||
const Area = sequelize.define(
|
||||
'Area',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'areas',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
return Area;
|
||||
};
|
||||
|
|
@ -1,59 +1,62 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const CalendarToken = sequelize.define('CalendarToken', {
|
||||
const CalendarToken = sequelize.define(
|
||||
'CalendarToken',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
provider: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'google'
|
||||
defaultValue: 'google',
|
||||
},
|
||||
access_token: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
refresh_token: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
token_type: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'Bearer'
|
||||
defaultValue: 'Bearer',
|
||||
},
|
||||
expires_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
scope: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
connected_email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
defaultValue: DataTypes.NOW,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'calendar_tokens',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
|
|
@ -61,16 +64,17 @@ const CalendarToken = sequelize.define('CalendarToken', {
|
|||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['user_id', 'provider']
|
||||
fields: ['user_id', 'provider'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
// Associations
|
||||
CalendarToken.associate = function (models) {
|
||||
CalendarToken.belongsTo(models.User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user'
|
||||
as: 'user',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,42 +1,46 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const InboxItem = sequelize.define('InboxItem', {
|
||||
const InboxItem = sequelize.define(
|
||||
'InboxItem',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'added'
|
||||
defaultValue: 'added',
|
||||
},
|
||||
source: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'tududi'
|
||||
defaultValue: 'tududi',
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'inbox_items',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
return InboxItem;
|
||||
};
|
||||
|
|
@ -15,13 +15,19 @@ if (process.env.NODE_ENV === 'test') {
|
|||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const dbPath = process.env.DATABASE_URL
|
||||
? process.env.DATABASE_URL.replace('sqlite:///', '')
|
||||
: path.join(__dirname, '../db', process.env.NODE_ENV === 'production' ? 'production.sqlite3' : 'development.sqlite3');
|
||||
: path.join(
|
||||
__dirname,
|
||||
'../db',
|
||||
process.env.NODE_ENV === 'production'
|
||||
? 'production.sqlite3'
|
||||
: 'development.sqlite3'
|
||||
);
|
||||
|
||||
dbConfig = {
|
||||
dialect: 'sqlite',
|
||||
|
|
@ -31,8 +37,8 @@ if (process.env.NODE_ENV === 'test') {
|
|||
timestamps: true,
|
||||
underscored: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: 'updated_at'
|
||||
}
|
||||
updatedAt: 'updated_at',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -80,14 +86,38 @@ Task.hasMany(TaskEvent, { foreignKey: 'task_id', as: 'TaskEvents' });
|
|||
TaskEvent.belongsTo(Task, { foreignKey: 'task_id', as: 'Task' });
|
||||
|
||||
// Many-to-many associations
|
||||
Task.belongsToMany(Tag, { through: 'tasks_tags', foreignKey: 'task_id', otherKey: 'tag_id' });
|
||||
Tag.belongsToMany(Task, { through: 'tasks_tags', foreignKey: 'tag_id', otherKey: 'task_id' });
|
||||
Task.belongsToMany(Tag, {
|
||||
through: 'tasks_tags',
|
||||
foreignKey: 'task_id',
|
||||
otherKey: 'tag_id',
|
||||
});
|
||||
Tag.belongsToMany(Task, {
|
||||
through: 'tasks_tags',
|
||||
foreignKey: 'tag_id',
|
||||
otherKey: 'task_id',
|
||||
});
|
||||
|
||||
Note.belongsToMany(Tag, { through: 'notes_tags', foreignKey: 'note_id', otherKey: 'tag_id' });
|
||||
Tag.belongsToMany(Note, { through: 'notes_tags', foreignKey: 'tag_id', otherKey: 'note_id' });
|
||||
Note.belongsToMany(Tag, {
|
||||
through: 'notes_tags',
|
||||
foreignKey: 'note_id',
|
||||
otherKey: 'tag_id',
|
||||
});
|
||||
Tag.belongsToMany(Note, {
|
||||
through: 'notes_tags',
|
||||
foreignKey: 'tag_id',
|
||||
otherKey: 'note_id',
|
||||
});
|
||||
|
||||
Project.belongsToMany(Tag, { through: 'projects_tags', foreignKey: 'project_id', otherKey: 'tag_id' });
|
||||
Tag.belongsToMany(Project, { through: 'projects_tags', foreignKey: 'tag_id', otherKey: 'project_id' });
|
||||
Project.belongsToMany(Tag, {
|
||||
through: 'projects_tags',
|
||||
foreignKey: 'project_id',
|
||||
otherKey: 'tag_id',
|
||||
});
|
||||
Tag.belongsToMany(Project, {
|
||||
through: 'projects_tags',
|
||||
foreignKey: 'tag_id',
|
||||
otherKey: 'project_id',
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
|
|
@ -98,5 +128,5 @@ module.exports = {
|
|||
Tag,
|
||||
Note,
|
||||
InboxItem,
|
||||
TaskEvent
|
||||
TaskEvent,
|
||||
};
|
||||
|
|
@ -1,47 +1,51 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Note = sequelize.define('Note', {
|
||||
const Note = sequelize.define(
|
||||
'Note',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
project_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'projects',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'notes',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
{
|
||||
fields: ['project_id']
|
||||
fields: ['project_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
return Note;
|
||||
};
|
||||
|
|
@ -1,73 +1,77 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Project = sequelize.define('Project', {
|
||||
const Project = sequelize.define(
|
||||
'Project',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
pin_to_sidebar: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 2
|
||||
}
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
due_date_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
area_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'areas',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
image_url: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'projects',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
{
|
||||
fields: ['area_id']
|
||||
fields: ['area_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
return Project;
|
||||
};
|
||||
|
|
@ -1,32 +1,36 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Tag = sequelize.define('Tag', {
|
||||
const Tag = sequelize.define(
|
||||
'Tag',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'tags',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
return Tag;
|
||||
};
|
||||
|
|
@ -1,34 +1,36 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const Task = sequelize.define('Task', {
|
||||
const Task = sequelize.define(
|
||||
'Task',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
uuid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
defaultValue: DataTypes.UUIDV4
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
due_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
today: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
|
@ -36,8 +38,8 @@ module.exports = (sequelize) => {
|
|||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 2
|
||||
}
|
||||
max: 2,
|
||||
},
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.INTEGER,
|
||||
|
|
@ -45,116 +47,118 @@ module.exports = (sequelize) => {
|
|||
defaultValue: 0,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 4
|
||||
}
|
||||
max: 4,
|
||||
},
|
||||
},
|
||||
note: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
recurrence_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'none'
|
||||
defaultValue: 'none',
|
||||
},
|
||||
recurrence_interval: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
recurrence_end_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
last_generated_date: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
recurrence_weekday: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: 0,
|
||||
max: 6
|
||||
}
|
||||
max: 6,
|
||||
},
|
||||
},
|
||||
recurrence_month_day: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: -1,
|
||||
max: 31
|
||||
}
|
||||
max: 31,
|
||||
},
|
||||
},
|
||||
recurrence_week_of_month: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
completion_based: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
project_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'projects',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
recurring_parent_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
completed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'tasks',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
{
|
||||
fields: ['project_id']
|
||||
fields: ['project_id'],
|
||||
},
|
||||
{
|
||||
fields: ['recurrence_type']
|
||||
fields: ['recurrence_type'],
|
||||
},
|
||||
{
|
||||
fields: ['last_generated_date']
|
||||
fields: ['last_generated_date'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
// Define associations
|
||||
Task.associate = function (models) {
|
||||
// Self-referencing association for recurring tasks
|
||||
Task.belongsTo(models.Task, {
|
||||
as: 'RecurringParent',
|
||||
foreignKey: 'recurring_parent_id'
|
||||
foreignKey: 'recurring_parent_id',
|
||||
});
|
||||
|
||||
Task.hasMany(models.Task, {
|
||||
as: 'RecurringChildren',
|
||||
foreignKey: 'recurring_parent_id'
|
||||
foreignKey: 'recurring_parent_id',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -162,7 +166,7 @@ module.exports = (sequelize) => {
|
|||
Task.PRIORITY = {
|
||||
LOW: 0,
|
||||
MEDIUM: 1,
|
||||
HIGH: 2
|
||||
HIGH: 2,
|
||||
};
|
||||
|
||||
Task.STATUS = {
|
||||
|
|
@ -170,7 +174,7 @@ module.exports = (sequelize) => {
|
|||
IN_PROGRESS: 1,
|
||||
DONE: 2,
|
||||
ARCHIVED: 3,
|
||||
WAITING: 4
|
||||
WAITING: 4,
|
||||
};
|
||||
|
||||
Task.RECURRENCE_TYPE = {
|
||||
|
|
@ -179,7 +183,7 @@ module.exports = (sequelize) => {
|
|||
WEEKLY: 'weekly',
|
||||
MONTHLY: 'monthly',
|
||||
MONTHLY_WEEKDAY: 'monthly_weekday',
|
||||
MONTHLY_LAST_DAY: 'monthly_last_day'
|
||||
MONTHLY_LAST_DAY: 'monthly_last_day',
|
||||
};
|
||||
|
||||
// priority and status
|
||||
|
|
@ -189,22 +193,30 @@ module.exports = (sequelize) => {
|
|||
};
|
||||
|
||||
const getStatusName = (statusValue) => {
|
||||
const statuses = ['not_started', 'in_progress', 'done', 'archived', 'waiting'];
|
||||
const statuses = [
|
||||
'not_started',
|
||||
'in_progress',
|
||||
'done',
|
||||
'archived',
|
||||
'waiting',
|
||||
];
|
||||
return statuses[statusValue] || 'not_started';
|
||||
};
|
||||
|
||||
const getPriorityValue = (priorityName) => {
|
||||
const priorities = { 'low': 0, 'medium': 1, 'high': 2 };
|
||||
return priorities[priorityName] !== undefined ? priorities[priorityName] : 0;
|
||||
const priorities = { low: 0, medium: 1, high: 2 };
|
||||
return priorities[priorityName] !== undefined
|
||||
? priorities[priorityName]
|
||||
: 0;
|
||||
};
|
||||
|
||||
const getStatusValue = (statusName) => {
|
||||
const statuses = {
|
||||
'not_started': 0,
|
||||
'in_progress': 1,
|
||||
'done': 2,
|
||||
'archived': 3,
|
||||
'waiting': 4
|
||||
not_started: 0,
|
||||
in_progress: 1,
|
||||
done: 2,
|
||||
archived: 3,
|
||||
waiting: 4,
|
||||
};
|
||||
return statuses[statusName] !== undefined ? statuses[statusName] : 0;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,38 +1,57 @@
|
|||
const { DataTypes } = require('sequelize');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const TaskEvent = sequelize.define('TaskEvent', {
|
||||
const TaskEvent = sequelize.define(
|
||||
'TaskEvent',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
task_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'users',
|
||||
key: 'id'
|
||||
}
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
event_type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
validate: {
|
||||
isIn: [['created', 'status_changed', 'priority_changed', 'due_date_changed',
|
||||
'project_changed', 'name_changed', 'description_changed', 'note_changed',
|
||||
'completed', 'archived', 'deleted', 'restored', 'today_changed',
|
||||
'tags_changed', 'recurrence_changed', 'recurrence_type_changed',
|
||||
'completion_based_changed', 'recurrence_end_date_changed']]
|
||||
}
|
||||
isIn: [
|
||||
[
|
||||
'created',
|
||||
'status_changed',
|
||||
'priority_changed',
|
||||
'due_date_changed',
|
||||
'project_changed',
|
||||
'name_changed',
|
||||
'description_changed',
|
||||
'note_changed',
|
||||
'completed',
|
||||
'archived',
|
||||
'deleted',
|
||||
'restored',
|
||||
'today_changed',
|
||||
'tags_changed',
|
||||
'recurrence_changed',
|
||||
'recurrence_type_changed',
|
||||
'completion_based_changed',
|
||||
'recurrence_end_date_changed',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
old_value: {
|
||||
type: DataTypes.TEXT,
|
||||
|
|
@ -42,8 +61,11 @@ module.exports = (sequelize) => {
|
|||
return rawValue ? JSON.parse(rawValue) : null;
|
||||
},
|
||||
set(value) {
|
||||
this.setDataValue('old_value', value ? JSON.stringify(value) : null);
|
||||
}
|
||||
this.setDataValue(
|
||||
'old_value',
|
||||
value ? JSON.stringify(value) : null
|
||||
);
|
||||
},
|
||||
},
|
||||
new_value: {
|
||||
type: DataTypes.TEXT,
|
||||
|
|
@ -53,18 +75,37 @@ module.exports = (sequelize) => {
|
|||
return rawValue ? JSON.parse(rawValue) : null;
|
||||
},
|
||||
set(value) {
|
||||
this.setDataValue('new_value', value ? JSON.stringify(value) : null);
|
||||
}
|
||||
this.setDataValue(
|
||||
'new_value',
|
||||
value ? JSON.stringify(value) : null
|
||||
);
|
||||
},
|
||||
},
|
||||
field_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
validate: {
|
||||
isIn: [['status', 'priority', 'due_date', 'project_id', 'name', 'description',
|
||||
'note', 'today', 'tags', 'recurrence_type', 'recurrence_interval',
|
||||
'recurrence_end_date', 'recurrence_weekday', 'recurrence_month_day',
|
||||
'recurrence_week_of_month', 'completion_based']]
|
||||
}
|
||||
isIn: [
|
||||
[
|
||||
'status',
|
||||
'priority',
|
||||
'due_date',
|
||||
'project_id',
|
||||
'name',
|
||||
'description',
|
||||
'note',
|
||||
'today',
|
||||
'tags',
|
||||
'recurrence_type',
|
||||
'recurrence_interval',
|
||||
'recurrence_end_date',
|
||||
'recurrence_weekday',
|
||||
'recurrence_month_day',
|
||||
'recurrence_week_of_month',
|
||||
'completion_based',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.TEXT,
|
||||
|
|
@ -74,53 +115,64 @@ module.exports = (sequelize) => {
|
|||
return rawValue ? JSON.parse(rawValue) : null;
|
||||
},
|
||||
set(value) {
|
||||
this.setDataValue('metadata', value ? JSON.stringify(value) : null);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
this.setDataValue(
|
||||
'metadata',
|
||||
value ? JSON.stringify(value) : null
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'task_events',
|
||||
timestamps: true,
|
||||
createdAt: 'created_at',
|
||||
updatedAt: false, // We don't need updated_at for events (they're immutable)
|
||||
indexes: [
|
||||
{
|
||||
fields: ['task_id']
|
||||
fields: ['task_id'],
|
||||
},
|
||||
{
|
||||
fields: ['user_id']
|
||||
fields: ['user_id'],
|
||||
},
|
||||
{
|
||||
fields: ['event_type']
|
||||
fields: ['event_type'],
|
||||
},
|
||||
{
|
||||
fields: ['created_at']
|
||||
fields: ['created_at'],
|
||||
},
|
||||
{
|
||||
fields: ['task_id', 'event_type']
|
||||
fields: ['task_id', 'event_type'],
|
||||
},
|
||||
{
|
||||
fields: ['task_id', 'created_at']
|
||||
fields: ['task_id', 'created_at'],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
});
|
||||
);
|
||||
|
||||
// Define associations
|
||||
TaskEvent.associate = function (models) {
|
||||
// TaskEvent belongs to Task
|
||||
TaskEvent.belongsTo(models.Task, {
|
||||
foreignKey: 'task_id',
|
||||
as: 'Task'
|
||||
as: 'Task',
|
||||
});
|
||||
|
||||
// TaskEvent belongs to User
|
||||
TaskEvent.belongsTo(models.User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'User'
|
||||
as: 'User',
|
||||
});
|
||||
};
|
||||
|
||||
// Helper methods for common event types
|
||||
TaskEvent.createStatusChangeEvent = async function(taskId, userId, oldStatus, newStatus, metadata = {}) {
|
||||
TaskEvent.createStatusChangeEvent = async function (
|
||||
taskId,
|
||||
userId,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
metadata = {}
|
||||
) {
|
||||
return await TaskEvent.create({
|
||||
task_id: taskId,
|
||||
user_id: userId,
|
||||
|
|
@ -128,11 +180,16 @@ module.exports = (sequelize) => {
|
|||
field_name: 'status',
|
||||
old_value: { status: oldStatus },
|
||||
new_value: { status: newStatus },
|
||||
metadata: metadata
|
||||
metadata: metadata,
|
||||
});
|
||||
};
|
||||
|
||||
TaskEvent.createTaskCreatedEvent = async function(taskId, userId, taskData, metadata = {}) {
|
||||
TaskEvent.createTaskCreatedEvent = async function (
|
||||
taskId,
|
||||
userId,
|
||||
taskData,
|
||||
metadata = {}
|
||||
) {
|
||||
return await TaskEvent.create({
|
||||
task_id: taskId,
|
||||
user_id: userId,
|
||||
|
|
@ -140,14 +197,24 @@ module.exports = (sequelize) => {
|
|||
field_name: null,
|
||||
old_value: null,
|
||||
new_value: taskData,
|
||||
metadata: metadata
|
||||
metadata: metadata,
|
||||
});
|
||||
};
|
||||
|
||||
TaskEvent.createFieldChangeEvent = async function(taskId, userId, fieldName, oldValue, newValue, metadata = {}) {
|
||||
const eventType = fieldName === 'status' && newValue === 2 ? 'completed' :
|
||||
fieldName === 'status' && newValue === 3 ? 'archived' :
|
||||
`${fieldName}_changed`;
|
||||
TaskEvent.createFieldChangeEvent = async function (
|
||||
taskId,
|
||||
userId,
|
||||
fieldName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata = {}
|
||||
) {
|
||||
const eventType =
|
||||
fieldName === 'status' && newValue === 2
|
||||
? 'completed'
|
||||
: fieldName === 'status' && newValue === 3
|
||||
? 'archived'
|
||||
: `${fieldName}_changed`;
|
||||
|
||||
return await TaskEvent.create({
|
||||
task_id: taskId,
|
||||
|
|
@ -156,7 +223,7 @@ module.exports = (sequelize) => {
|
|||
field_name: fieldName,
|
||||
old_value: { [fieldName]: oldValue },
|
||||
new_value: { [fieldName]: newValue },
|
||||
metadata: metadata
|
||||
metadata: metadata,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -165,11 +232,13 @@ module.exports = (sequelize) => {
|
|||
return await TaskEvent.findAll({
|
||||
where: { task_id: taskId },
|
||||
order: [['created_at', 'ASC']],
|
||||
include: [{
|
||||
include: [
|
||||
{
|
||||
model: sequelize.models.User,
|
||||
as: 'User',
|
||||
attributes: ['id', 'name', 'email']
|
||||
}]
|
||||
attributes: ['id', 'name', 'email'],
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -177,19 +246,21 @@ module.exports = (sequelize) => {
|
|||
const events = await TaskEvent.findAll({
|
||||
where: {
|
||||
task_id: taskId,
|
||||
event_type: ['status_changed', 'created', 'completed']
|
||||
event_type: ['status_changed', 'created', 'completed'],
|
||||
},
|
||||
order: [['created_at', 'ASC']]
|
||||
order: [['created_at', 'ASC']],
|
||||
});
|
||||
|
||||
if (events.length === 0) return null;
|
||||
|
||||
const startEvent = events.find(e =>
|
||||
const startEvent = events.find(
|
||||
(e) =>
|
||||
e.event_type === 'created' ||
|
||||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
|
||||
);
|
||||
|
||||
const completedEvent = events.find(e =>
|
||||
const completedEvent = events.find(
|
||||
(e) =>
|
||||
e.event_type === 'completed' ||
|
||||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done
|
||||
);
|
||||
|
|
@ -203,7 +274,7 @@ module.exports = (sequelize) => {
|
|||
started_at: startTime,
|
||||
completed_at: endTime,
|
||||
duration_ms: endTime - startTime,
|
||||
duration_hours: (endTime - startTime) / (1000 * 60 * 60)
|
||||
duration_hours: (endTime - startTime) / (1000 * 60 * 60),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,98 +2,111 @@ const { DataTypes } = require('sequelize');
|
|||
const bcrypt = require('bcrypt');
|
||||
|
||||
module.exports = (sequelize) => {
|
||||
const User = sequelize.define('User', {
|
||||
const User = sequelize.define(
|
||||
'User',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
isEmail: true,
|
||||
},
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.VIRTUAL,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
password_digest: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
field: 'password_digest'
|
||||
field: 'password_digest',
|
||||
},
|
||||
appearance: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'light',
|
||||
validate: {
|
||||
isIn: [['light', 'dark']]
|
||||
}
|
||||
isIn: [['light', 'dark']],
|
||||
},
|
||||
},
|
||||
language: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'en'
|
||||
defaultValue: 'en',
|
||||
},
|
||||
timezone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'UTC'
|
||||
defaultValue: 'UTC',
|
||||
},
|
||||
avatar_image: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
telegram_bot_token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
telegram_chat_id: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
task_summary_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
task_summary_frequency: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'daily',
|
||||
validate: {
|
||||
isIn: [['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h']]
|
||||
}
|
||||
isIn: [
|
||||
[
|
||||
'daily',
|
||||
'weekdays',
|
||||
'weekly',
|
||||
'1h',
|
||||
'2h',
|
||||
'4h',
|
||||
'8h',
|
||||
'12h',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
task_summary_last_run: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
task_summary_next_run: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
allowNull: true,
|
||||
},
|
||||
task_intelligence_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
defaultValue: true,
|
||||
},
|
||||
auto_suggest_next_actions_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
pomodoro_enabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
defaultValue: true,
|
||||
},
|
||||
today_settings: {
|
||||
type: DataTypes.JSON,
|
||||
|
|
@ -105,19 +118,24 @@ module.exports = (sequelize) => {
|
|||
showDueToday: true,
|
||||
showCompleted: true,
|
||||
showProgressBar: true,
|
||||
showDailyQuote: true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
showDailyQuote: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
tableName: 'users',
|
||||
hooks: {
|
||||
beforeValidate: async (user) => {
|
||||
if (user.password) {
|
||||
user.password_digest = await bcrypt.hash(user.password, 10);
|
||||
user.password_digest = await bcrypt.hash(
|
||||
user.password,
|
||||
10
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
// password operations
|
||||
const hashPassword = async (password) => {
|
||||
|
|
|
|||
4370
backend/package-lock.json
generated
4370
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,37 +23,43 @@
|
|||
"migration:undo": "npx sequelize-cli db:migrate:undo",
|
||||
"migration:undo:all": "npx sequelize-cli db:migrate:undo:all",
|
||||
"migration:status": "npx sequelize-cli db:migrate:status",
|
||||
"seed:dev": "node scripts/seed-dev-data.js"
|
||||
"seed:dev": "node scripts/seed-dev-data.js",
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cheerio": "^1.1.0",
|
||||
"compression": "^1.8.0",
|
||||
"connect-session-sequelize": "^7.1.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.5.0",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.18.1",
|
||||
"googleapis": "^144.0.0",
|
||||
"helmet": "^8.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^2.0.1",
|
||||
"node-cron": "^4.1.0",
|
||||
"recharts": "^2.15.4",
|
||||
"sequelize": "^6.37.7",
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^11.1.0"
|
||||
"bcrypt": "~6.0.0",
|
||||
"compression": "~1.8.0",
|
||||
"connect-session-sequelize": "~7.1.7",
|
||||
"cors": "~2.8.5",
|
||||
"dotenv": "~16.5.0",
|
||||
"eslint": "^8.0.0",
|
||||
"express": "~4.18.2",
|
||||
"express-session": "~1.18.1",
|
||||
"googleapis": "~144.0.0",
|
||||
"helmet": "~8.1.0",
|
||||
"js-yaml": "~4.1.0",
|
||||
"moment-timezone": "~0.6.0",
|
||||
"morgan": "~1.10.0",
|
||||
"multer": "~2.0.1",
|
||||
"node-cron": "~4.1.0",
|
||||
"recharts": "~2.15.4",
|
||||
"sequelize": "~6.37.7",
|
||||
"sqlite3": "~5.1.7",
|
||||
"uuid": "~11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^30.0.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"sequelize-cli": "^6.6.2",
|
||||
"supertest": "^7.1.1"
|
||||
"cross-env": "~7.0.3",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"jest": "~30.0.0",
|
||||
"nodemon": "~3.0.1",
|
||||
"prettier": "~3.6.2",
|
||||
"sequelize-cli": "~6.6.2",
|
||||
"supertest": "~7.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ router.get('/areas', async (req, res) => {
|
|||
|
||||
const areas = await Area.findAll({
|
||||
where: { user_id: req.session.userId },
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
res.json(areas);
|
||||
|
|
@ -29,11 +29,13 @@ router.get('/areas/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const area = await Area.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!area) {
|
||||
return res.status(404).json({ error: "Area not found or doesn't belong to the current user." });
|
||||
return res.status(404).json({
|
||||
error: "Area not found or doesn't belong to the current user.",
|
||||
});
|
||||
}
|
||||
|
||||
res.json(area);
|
||||
|
|
@ -59,7 +61,7 @@ router.post('/areas', async (req, res) => {
|
|||
const area = await Area.create({
|
||||
name: name.trim(),
|
||||
description: description || '',
|
||||
user_id: req.session.userId
|
||||
user_id: req.session.userId,
|
||||
});
|
||||
|
||||
res.status(201).json(area);
|
||||
|
|
@ -67,7 +69,9 @@ router.post('/areas', async (req, res) => {
|
|||
console.error('Error creating area:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem creating the area.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -80,7 +84,7 @@ router.patch('/areas/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const area = await Area.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!area) {
|
||||
|
|
@ -99,7 +103,9 @@ router.patch('/areas/:id', async (req, res) => {
|
|||
console.error('Error updating area:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem updating the area.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -112,7 +118,7 @@ router.delete('/areas/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const area = await Area.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!area) {
|
||||
|
|
@ -123,7 +129,9 @@ router.delete('/areas/:id', async (req, res) => {
|
|||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Error deleting area:', error);
|
||||
res.status(400).json({ error: 'There was a problem deleting the area.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem deleting the area.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ router.get('/current_user', async (req, res) => {
|
|||
email: user.email,
|
||||
language: user.language,
|
||||
appearance: user.appearance,
|
||||
timezone: user.timezone
|
||||
}
|
||||
timezone: user.timezone,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,10 @@ router.post('/login', async (req, res) => {
|
|||
return res.status(401).json({ errors: ['Invalid credentials'] });
|
||||
}
|
||||
|
||||
const isValidPassword = await User.checkPassword(password, user.password_digest);
|
||||
const isValidPassword = await User.checkPassword(
|
||||
password,
|
||||
user.password_digest
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({ errors: ['Invalid credentials'] });
|
||||
}
|
||||
|
|
@ -54,8 +57,8 @@ router.post('/login', async (req, res) => {
|
|||
email: user.email,
|
||||
language: user.language,
|
||||
appearance: user.appearance,
|
||||
timezone: user.timezone
|
||||
}
|
||||
timezone: user.timezone,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ const getOAuth2Client = () => {
|
|||
return new google.auth.OAuth2(
|
||||
process.env.GOOGLE_CLIENT_ID,
|
||||
process.env.GOOGLE_CLIENT_SECRET,
|
||||
process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3002/api/calendar/oauth/callback'
|
||||
process.env.GOOGLE_REDIRECT_URI ||
|
||||
'http://localhost:3002/api/calendar/oauth/callback'
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -19,16 +20,23 @@ const getOAuth2Client = () => {
|
|||
router.get('/auth', requireAuth, (req, res) => {
|
||||
try {
|
||||
// Check if Google credentials are configured
|
||||
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
||||
if (
|
||||
!process.env.GOOGLE_CLIENT_ID ||
|
||||
!process.env.GOOGLE_CLIENT_SECRET
|
||||
) {
|
||||
// Demo mode - simulate successful connection
|
||||
console.log('Demo mode: Simulating Google Calendar connection for user:', req.currentUser.id);
|
||||
console.log(
|
||||
'Demo mode: Simulating Google Calendar connection for user:',
|
||||
req.currentUser.id
|
||||
);
|
||||
|
||||
// Simulate the callback redirect with success
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:8080';
|
||||
const frontendUrl =
|
||||
process.env.FRONTEND_URL || 'http://localhost:8080';
|
||||
return res.json({
|
||||
authUrl: `${frontendUrl}/calendar?demo=true&connected=true`,
|
||||
demo: true,
|
||||
message: 'Demo mode: Google Calendar integration simulated'
|
||||
message: 'Demo mode: Google Calendar integration simulated',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +46,7 @@ router.get('/auth', requireAuth, (req, res) => {
|
|||
const authUrl = oauth2Client.generateAuthUrl({
|
||||
access_type: 'offline',
|
||||
scope: SCOPES,
|
||||
state: JSON.stringify({ userId: req.currentUser.id })
|
||||
state: JSON.stringify({ userId: req.currentUser.id }),
|
||||
});
|
||||
|
||||
res.json({ authUrl });
|
||||
|
|
@ -54,7 +62,9 @@ router.get('/oauth/callback', async (req, res) => {
|
|||
const { code, state } = req.query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: 'Authorization code not provided' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Authorization code not provided' });
|
||||
}
|
||||
|
||||
const oauth2Client = getOAuth2Client();
|
||||
|
|
@ -72,10 +82,14 @@ router.get('/oauth/callback', async (req, res) => {
|
|||
// await saveGoogleTokensForUser(userId, tokens);
|
||||
|
||||
// Redirect to frontend with success
|
||||
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:8080'}/calendar?connected=true`);
|
||||
res.redirect(
|
||||
`${process.env.FRONTEND_URL || 'http://localhost:8080'}/calendar?connected=true`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error handling OAuth callback:', error);
|
||||
res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:8080'}/calendar?error=auth_failed`);
|
||||
res.redirect(
|
||||
`${process.env.FRONTEND_URL || 'http://localhost:8080'}/calendar?error=auth_failed`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -83,13 +97,16 @@ router.get('/oauth/callback', async (req, res) => {
|
|||
router.get('/status', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Check if we're in demo mode or have real Google integration
|
||||
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
||||
if (
|
||||
!process.env.GOOGLE_CLIENT_ID ||
|
||||
!process.env.GOOGLE_CLIENT_SECRET
|
||||
) {
|
||||
// Demo mode - check if user has been "connected" in this session
|
||||
// For demo purposes, we'll simulate connection status
|
||||
res.json({
|
||||
connected: false, // Will be set to true after demo connection
|
||||
email: null,
|
||||
demo: true
|
||||
demo: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -99,7 +116,7 @@ router.get('/status', requireAuth, async (req, res) => {
|
|||
|
||||
res.json({
|
||||
connected: false, // Change to true when tokens exist and are valid
|
||||
email: null // Return connected Google account email when available
|
||||
email: null, // Return connected Google account email when available
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking calendar status:', error);
|
||||
|
|
@ -126,8 +143,8 @@ router.get('/events', requireAuth, async (req, res) => {
|
|||
start: new Date().toISOString(),
|
||||
end: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
||||
type: 'google',
|
||||
color: '#ea4335'
|
||||
}
|
||||
color: '#ea4335',
|
||||
},
|
||||
];
|
||||
|
||||
res.json({ events: sampleEvents });
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ router.get('/inbox', async (req, res) => {
|
|||
const items = await InboxItem.findAll({
|
||||
where: {
|
||||
user_id: req.session.userId,
|
||||
status: 'added'
|
||||
status: 'added',
|
||||
},
|
||||
order: [['created_at', 'DESC']]
|
||||
order: [['created_at', 'DESC']],
|
||||
});
|
||||
|
||||
res.json(items);
|
||||
|
|
@ -40,7 +40,7 @@ router.post('/inbox', async (req, res) => {
|
|||
const item = await InboxItem.create({
|
||||
content: content.trim(),
|
||||
source: source || 'tududi',
|
||||
user_id: req.session.userId
|
||||
user_id: req.session.userId,
|
||||
});
|
||||
|
||||
res.status(201).json(item);
|
||||
|
|
@ -48,7 +48,9 @@ router.post('/inbox', async (req, res) => {
|
|||
console.error('Error creating inbox item:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem creating the inbox item.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -61,7 +63,7 @@ router.get('/inbox/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const item = await InboxItem.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
|
|
@ -83,7 +85,7 @@ router.patch('/inbox/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const item = await InboxItem.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
|
|
@ -102,7 +104,9 @@ router.patch('/inbox/:id', async (req, res) => {
|
|||
console.error('Error updating inbox item:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem updating the inbox item.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -115,7 +119,7 @@ router.delete('/inbox/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const item = await InboxItem.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
|
|
@ -127,7 +131,9 @@ router.delete('/inbox/:id', async (req, res) => {
|
|||
res.json({ message: 'Inbox item successfully deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting inbox item:', error);
|
||||
res.status(400).json({ error: 'There was a problem deleting the inbox item.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem deleting the inbox item.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -139,7 +145,7 @@ router.patch('/inbox/:id/process', async (req, res) => {
|
|||
}
|
||||
|
||||
const item = await InboxItem.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
|
|
@ -150,7 +156,9 @@ router.patch('/inbox/:id/process', async (req, res) => {
|
|||
res.json(item);
|
||||
} catch (error) {
|
||||
console.error('Error processing inbox item:', error);
|
||||
res.status(400).json({ error: 'There was a problem processing the inbox item.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem processing the inbox item.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ async function updateNoteTags(note, tagsArray, userId) {
|
|||
}
|
||||
|
||||
try {
|
||||
const tagNames = tagsArray.filter((name, index, arr) => arr.indexOf(name) === index); // unique
|
||||
const tagNames = tagsArray.filter(
|
||||
(name, index, arr) => arr.indexOf(name) === index
|
||||
); // unique
|
||||
const tags = await Promise.all(
|
||||
tagNames.map(async (name) => {
|
||||
const [tag] = await Tag.findOrCreate({
|
||||
where: { name, user_id: userId },
|
||||
defaults: { name, user_id: userId }
|
||||
defaults: { name, user_id: userId },
|
||||
});
|
||||
return tag;
|
||||
})
|
||||
|
|
@ -40,7 +42,7 @@ router.get('/notes', async (req, res) => {
|
|||
let whereClause = { user_id: req.session.userId };
|
||||
let includeClause = [
|
||||
{ model: Tag, through: { attributes: [] } },
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] }
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] },
|
||||
];
|
||||
|
||||
// Filter by tag
|
||||
|
|
@ -53,7 +55,7 @@ router.get('/notes', async (req, res) => {
|
|||
where: whereClause,
|
||||
include: includeClause,
|
||||
order: [[orderColumn, orderDirection.toUpperCase()]],
|
||||
distinct: true
|
||||
distinct: true,
|
||||
});
|
||||
|
||||
res.json(notes);
|
||||
|
|
@ -74,8 +76,8 @@ router.get('/note/:id', async (req, res) => {
|
|||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
include: [
|
||||
{ model: Tag, through: { attributes: [] } },
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] }
|
||||
]
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] },
|
||||
],
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
|
|
@ -101,13 +103,13 @@ router.post('/note', async (req, res) => {
|
|||
const noteAttributes = {
|
||||
title,
|
||||
content,
|
||||
user_id: req.session.userId
|
||||
user_id: req.session.userId,
|
||||
};
|
||||
|
||||
// Handle project assignment
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.session.userId }
|
||||
where: { id: project_id, user_id: req.session.userId },
|
||||
});
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
|
|
@ -120,10 +122,10 @@ router.post('/note', async (req, res) => {
|
|||
// Handle tags - can be array of strings or array of objects with name property
|
||||
let tagNames = [];
|
||||
if (Array.isArray(tags)) {
|
||||
if (tags.every(t => typeof t === 'string')) {
|
||||
if (tags.every((t) => typeof t === 'string')) {
|
||||
tagNames = tags;
|
||||
} else if (tags.every(t => typeof t === 'object' && t.name)) {
|
||||
tagNames = tags.map(t => t.name);
|
||||
} else if (tags.every((t) => typeof t === 'object' && t.name)) {
|
||||
tagNames = tags.map((t) => t.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,8 +135,8 @@ router.post('/note', async (req, res) => {
|
|||
const noteWithAssociations = await Note.findByPk(note.id, {
|
||||
include: [
|
||||
{ model: Tag, through: { attributes: [] } },
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] }
|
||||
]
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] },
|
||||
],
|
||||
});
|
||||
|
||||
res.status(201).json(noteWithAssociations);
|
||||
|
|
@ -142,7 +144,9 @@ router.post('/note', async (req, res) => {
|
|||
console.error('Error creating note:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem creating the note.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -155,7 +159,7 @@ router.patch('/note/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
|
|
@ -172,7 +176,7 @@ router.patch('/note/:id', async (req, res) => {
|
|||
if (project_id !== undefined) {
|
||||
if (project_id && project_id.toString().trim()) {
|
||||
const project = await Project.findOne({
|
||||
where: { id: project_id, user_id: req.session.userId }
|
||||
where: { id: project_id, user_id: req.session.userId },
|
||||
});
|
||||
if (!project) {
|
||||
return res.status(400).json({ error: 'Invalid project.' });
|
||||
|
|
@ -189,10 +193,10 @@ router.patch('/note/:id', async (req, res) => {
|
|||
if (tags !== undefined) {
|
||||
let tagNames = [];
|
||||
if (Array.isArray(tags)) {
|
||||
if (tags.every(t => typeof t === 'string')) {
|
||||
if (tags.every((t) => typeof t === 'string')) {
|
||||
tagNames = tags;
|
||||
} else if (tags.every(t => typeof t === 'object' && t.name)) {
|
||||
tagNames = tags.map(t => t.name);
|
||||
} else if (tags.every((t) => typeof t === 'object' && t.name)) {
|
||||
tagNames = tags.map((t) => t.name);
|
||||
}
|
||||
}
|
||||
await updateNoteTags(note, tagNames, req.session.userId);
|
||||
|
|
@ -202,8 +206,8 @@ router.patch('/note/:id', async (req, res) => {
|
|||
const noteWithAssociations = await Note.findByPk(note.id, {
|
||||
include: [
|
||||
{ model: Tag, through: { attributes: [] } },
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] }
|
||||
]
|
||||
{ model: Project, required: false, attributes: ['id', 'name'] },
|
||||
],
|
||||
});
|
||||
|
||||
res.json(noteWithAssociations);
|
||||
|
|
@ -211,7 +215,9 @@ router.patch('/note/:id', async (req, res) => {
|
|||
console.error('Error updating note:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem updating the note.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -224,7 +230,7 @@ router.delete('/note/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!note) {
|
||||
|
|
@ -235,7 +241,9 @@ router.delete('/note/:id', async (req, res) => {
|
|||
res.json({ message: 'Note deleted successfully.' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error);
|
||||
res.status(400).json({ error: 'There was a problem deleting the note.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem deleting the note.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ const storage = multer.diskStorage({
|
|||
cb(null, uploadDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, 'project-' + uniqueSuffix + path.extname(file.originalname));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
|
|
@ -40,7 +40,9 @@ const upload = multer({
|
|||
},
|
||||
fileFilter: function (req, file, cb) {
|
||||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
||||
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
||||
const extname = allowedTypes.test(
|
||||
path.extname(file.originalname).toLowerCase()
|
||||
);
|
||||
const mimetype = allowedTypes.test(file.mimetype);
|
||||
|
||||
if (mimetype && extname) {
|
||||
|
|
@ -48,7 +50,7 @@ const upload = multer({
|
|||
} else {
|
||||
cb(new Error('Only image files are allowed!'));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Helper function to update project tags
|
||||
|
|
@ -56,8 +58,8 @@ async function updateProjectTags(project, tagsData, userId) {
|
|||
if (!tagsData) return;
|
||||
|
||||
const tagNames = tagsData
|
||||
.map(tag => tag.name)
|
||||
.filter(name => name && name.trim())
|
||||
.map((tag) => tag.name)
|
||||
.filter((name) => name && name.trim())
|
||||
.filter((name, index, arr) => arr.indexOf(name) === index); // unique
|
||||
|
||||
if (tagNames.length === 0) {
|
||||
|
|
@ -67,15 +69,17 @@ async function updateProjectTags(project, tagsData, userId) {
|
|||
|
||||
// Find existing tags
|
||||
const existingTags = await Tag.findAll({
|
||||
where: { user_id: userId, name: tagNames }
|
||||
where: { user_id: userId, name: tagNames },
|
||||
});
|
||||
|
||||
// Create new tags
|
||||
const existingTagNames = existingTags.map(tag => tag.name);
|
||||
const newTagNames = tagNames.filter(name => !existingTagNames.includes(name));
|
||||
const existingTagNames = existingTags.map((tag) => tag.name);
|
||||
const newTagNames = tagNames.filter(
|
||||
(name) => !existingTagNames.includes(name)
|
||||
);
|
||||
|
||||
const createdTags = await Promise.all(
|
||||
newTagNames.map(name => Tag.create({ name, user_id: userId }))
|
||||
newTagNames.map((name) => Tag.create({ name, user_id: userId }))
|
||||
);
|
||||
|
||||
// Set all tags to project
|
||||
|
|
@ -139,33 +143,33 @@ router.get('/projects', async (req, res) => {
|
|||
{
|
||||
model: Task,
|
||||
required: false,
|
||||
attributes: ['id', 'status']
|
||||
attributes: ['id', 'status'],
|
||||
},
|
||||
{
|
||||
model: Area,
|
||||
required: false,
|
||||
attributes: ['name']
|
||||
attributes: ['name'],
|
||||
},
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] }
|
||||
}
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
const { grouped } = req.query;
|
||||
|
||||
// Calculate task status counts for each project
|
||||
const taskStatusCounts = {};
|
||||
const enhancedProjects = projects.map(project => {
|
||||
const enhancedProjects = projects.map((project) => {
|
||||
const tasks = project.Tasks || [];
|
||||
const taskStatus = {
|
||||
total: tasks.length,
|
||||
done: tasks.filter(t => t.status === 2).length,
|
||||
in_progress: tasks.filter(t => t.status === 1).length,
|
||||
not_started: tasks.filter(t => t.status === 0).length
|
||||
done: tasks.filter((t) => t.status === 2).length,
|
||||
in_progress: tasks.filter((t) => t.status === 1).length,
|
||||
not_started: tasks.filter((t) => t.status === 0).length,
|
||||
};
|
||||
|
||||
taskStatusCounts[project.id] = taskStatus;
|
||||
|
|
@ -176,14 +180,17 @@ router.get('/projects', async (req, res) => {
|
|||
tags: projectJson.Tags || [], // Normalize Tags to tags
|
||||
due_date_at: formatDate(project.due_date_at),
|
||||
task_status: taskStatus,
|
||||
completion_percentage: taskStatus.total > 0 ? Math.round((taskStatus.done / taskStatus.total) * 100) : 0
|
||||
completion_percentage:
|
||||
taskStatus.total > 0
|
||||
? Math.round((taskStatus.done / taskStatus.total) * 100)
|
||||
: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// If grouped=true, return grouped format
|
||||
if (grouped === 'true') {
|
||||
const groupedProjects = {};
|
||||
enhancedProjects.forEach(project => {
|
||||
enhancedProjects.forEach((project) => {
|
||||
const areaName = project.Area ? project.Area.name : 'No Area';
|
||||
if (!groupedProjects[areaName]) {
|
||||
groupedProjects[areaName] = [];
|
||||
|
|
@ -193,7 +200,7 @@ router.get('/projects', async (req, res) => {
|
|||
res.json(groupedProjects);
|
||||
} else {
|
||||
res.json({
|
||||
projects: enhancedProjects
|
||||
projects: enhancedProjects,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -220,13 +227,17 @@ router.get('/project/:id', async (req, res) => {
|
|||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
required: false
|
||||
}
|
||||
]
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ model: Area, required: false, attributes: ['id', 'name'] },
|
||||
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
|
||||
]
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
|
@ -237,7 +248,7 @@ router.get('/project/:id', async (req, res) => {
|
|||
const result = {
|
||||
...projectJson,
|
||||
tags: projectJson.Tags || [], // Normalize Tags to tags
|
||||
due_date_at: formatDate(project.due_date_at)
|
||||
due_date_at: formatDate(project.due_date_at),
|
||||
};
|
||||
|
||||
res.json(result);
|
||||
|
|
@ -254,7 +265,16 @@ router.post('/project', async (req, res) => {
|
|||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
const { name, description, area_id, priority, due_date_at, image_url, tags, Tags } = req.body;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
area_id,
|
||||
priority,
|
||||
due_date_at,
|
||||
image_url,
|
||||
tags,
|
||||
Tags,
|
||||
} = req.body;
|
||||
|
||||
// Handle both tags and Tags (Sequelize association format)
|
||||
const tagsData = tags || Tags;
|
||||
|
|
@ -272,7 +292,7 @@ router.post('/project', async (req, res) => {
|
|||
priority: priority || null,
|
||||
due_date_at: due_date_at || null,
|
||||
image_url: image_url || null,
|
||||
user_id: req.session.userId
|
||||
user_id: req.session.userId,
|
||||
};
|
||||
|
||||
const project = await Project.create(projectData);
|
||||
|
|
@ -281,8 +301,12 @@ router.post('/project', async (req, res) => {
|
|||
// Reload project with associations
|
||||
const projectWithAssociations = await Project.findByPk(project.id, {
|
||||
include: [
|
||||
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
|
||||
]
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const projectJson = projectWithAssociations.toJSON();
|
||||
|
|
@ -290,13 +314,15 @@ router.post('/project', async (req, res) => {
|
|||
res.status(201).json({
|
||||
...projectJson,
|
||||
tags: projectJson.Tags || [], // Normalize Tags to tags
|
||||
due_date_at: formatDate(projectWithAssociations.due_date_at)
|
||||
due_date_at: formatDate(projectWithAssociations.due_date_at),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem creating the project.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -309,14 +335,25 @@ router.patch('/project/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const project = await Project.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return res.status(404).json({ error: 'Project not found.' });
|
||||
}
|
||||
|
||||
const { name, description, area_id, active, pin_to_sidebar, priority, due_date_at, image_url, tags, Tags } = req.body;
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
area_id,
|
||||
active,
|
||||
pin_to_sidebar,
|
||||
priority,
|
||||
due_date_at,
|
||||
image_url,
|
||||
tags,
|
||||
Tags,
|
||||
} = req.body;
|
||||
|
||||
// Handle both tags and Tags (Sequelize association format)
|
||||
const tagsData = tags || Tags;
|
||||
|
|
@ -326,7 +363,8 @@ router.patch('/project/:id', async (req, res) => {
|
|||
if (description !== undefined) updateData.description = description;
|
||||
if (area_id !== undefined) updateData.area_id = area_id;
|
||||
if (active !== undefined) updateData.active = active;
|
||||
if (pin_to_sidebar !== undefined) updateData.pin_to_sidebar = pin_to_sidebar;
|
||||
if (pin_to_sidebar !== undefined)
|
||||
updateData.pin_to_sidebar = pin_to_sidebar;
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (due_date_at !== undefined) updateData.due_date_at = due_date_at;
|
||||
if (image_url !== undefined) updateData.image_url = image_url;
|
||||
|
|
@ -337,8 +375,12 @@ router.patch('/project/:id', async (req, res) => {
|
|||
// Reload project with associations
|
||||
const projectWithAssociations = await Project.findByPk(project.id, {
|
||||
include: [
|
||||
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } }
|
||||
]
|
||||
{
|
||||
model: Tag,
|
||||
attributes: ['id', 'name'],
|
||||
through: { attributes: [] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const projectJson = projectWithAssociations.toJSON();
|
||||
|
|
@ -346,13 +388,15 @@ router.patch('/project/:id', async (req, res) => {
|
|||
res.json({
|
||||
...projectJson,
|
||||
tags: projectJson.Tags || [], // Normalize Tags to tags
|
||||
due_date_at: formatDate(projectWithAssociations.due_date_at)
|
||||
due_date_at: formatDate(projectWithAssociations.due_date_at),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating project:', error);
|
||||
res.status(400).json({
|
||||
error: 'There was a problem updating the project.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -365,7 +409,7 @@ router.delete('/project/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
const project = await Project.findOne({
|
||||
where: { id: req.params.id, user_id: req.session.userId }
|
||||
where: { id: req.params.id, user_id: req.session.userId },
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
|
|
@ -376,7 +420,9 @@ router.delete('/project/:id', async (req, res) => {
|
|||
res.json({ message: 'Project successfully deleted' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
res.status(400).json({ error: 'There was a problem deleting the project.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem deleting the project.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ router.get('/quotes', (req, res) => {
|
|||
const quotes = quotesService.getAllQuotes();
|
||||
res.json({
|
||||
quotes,
|
||||
count: quotesService.getQuotesCount()
|
||||
count: quotesService.getQuotesCount(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting quotes:', error);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ router.get('/tags', async (req, res) => {
|
|||
const tags = await Tag.findAll({
|
||||
where: { user_id: req.currentUser.id },
|
||||
attributes: ['id', 'name'],
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
res.json(tags);
|
||||
|
|
@ -23,7 +23,7 @@ router.get('/tag/:id', async (req, res) => {
|
|||
try {
|
||||
const tag = await Tag.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
attributes: ['id', 'name']
|
||||
attributes: ['id', 'name'],
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
|
|
@ -48,16 +48,18 @@ router.post('/tag', async (req, res) => {
|
|||
|
||||
const tag = await Tag.create({
|
||||
name: name.trim(),
|
||||
user_id: req.currentUser.id
|
||||
user_id: req.currentUser.id,
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: tag.id,
|
||||
name: tag.name
|
||||
name: tag.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating tag:', error);
|
||||
res.status(400).json({ error: 'There was a problem creating the tag.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem creating the tag.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ router.post('/tag', async (req, res) => {
|
|||
router.patch('/tag/:id', async (req, res) => {
|
||||
try {
|
||||
const tag = await Tag.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id }
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
|
|
@ -82,11 +84,13 @@ router.patch('/tag/:id', async (req, res) => {
|
|||
|
||||
res.json({
|
||||
id: tag.id,
|
||||
name: tag.name
|
||||
name: tag.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating tag:', error);
|
||||
res.status(400).json({ error: 'There was a problem updating the tag.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem updating the tag.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -96,7 +100,7 @@ router.delete('/tag/:id', async (req, res) => {
|
|||
|
||||
try {
|
||||
const tag = await Tag.findOne({
|
||||
where: { id: req.params.id, user_id: req.currentUser.id }
|
||||
where: { id: req.params.id, user_id: req.currentUser.id },
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
|
|
@ -111,7 +115,7 @@ router.delete('/tag/:id', async (req, res) => {
|
|||
await sequelize.query('DELETE FROM tasks_tags WHERE tag_id = ?', {
|
||||
replacements: [tag.id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
transaction,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore if table doesn't exist
|
||||
|
|
@ -122,7 +126,7 @@ router.delete('/tag/:id', async (req, res) => {
|
|||
await sequelize.query('DELETE FROM notes_tags WHERE tag_id = ?', {
|
||||
replacements: [tag.id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
transaction,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore if table doesn't exist
|
||||
|
|
@ -130,11 +134,14 @@ router.delete('/tag/:id', async (req, res) => {
|
|||
}
|
||||
|
||||
try {
|
||||
await sequelize.query('DELETE FROM projects_tags WHERE tag_id = ?', {
|
||||
await sequelize.query(
|
||||
'DELETE FROM projects_tags WHERE tag_id = ?',
|
||||
{
|
||||
replacements: [tag.id],
|
||||
type: sequelize.QueryTypes.DELETE,
|
||||
transaction
|
||||
});
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
// Ignore if table doesn't exist
|
||||
console.log('projects_tags table not found, skipping');
|
||||
|
|
@ -148,7 +155,9 @@ router.delete('/tag/:id', async (req, res) => {
|
|||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
console.error('Error deleting tag:', error);
|
||||
res.status(400).json({ error: 'There was a problem deleting the tag.' });
|
||||
res.status(400).json({
|
||||
error: 'There was a problem deleting the tag.',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ router.get('/task/:id/timeline', async (req, res) => {
|
|||
const timeline = await TaskEventService.getTaskTimeline(req.params.id);
|
||||
|
||||
// Filter to only show events for tasks owned by the current user
|
||||
const userTimeline = timeline.filter(event => event.user_id === req.currentUser.id);
|
||||
const userTimeline = timeline.filter(
|
||||
(event) => event.user_id === req.currentUser.id
|
||||
);
|
||||
|
||||
res.json(userTimeline);
|
||||
} catch (error) {
|
||||
|
|
@ -21,10 +23,14 @@ router.get('/task/:id/timeline', async (req, res) => {
|
|||
// GET /api/task/:id/completion-time - Get task completion analytics
|
||||
router.get('/task/:id/completion-time', async (req, res) => {
|
||||
try {
|
||||
const completionTime = await TaskEventService.getTaskCompletionTime(req.params.id);
|
||||
const completionTime = await TaskEventService.getTaskCompletionTime(
|
||||
req.params.id
|
||||
);
|
||||
|
||||
if (!completionTime) {
|
||||
return res.status(404).json({ error: 'Task completion data not found' });
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: 'Task completion data not found' });
|
||||
}
|
||||
|
||||
res.json(completionTime);
|
||||
|
|
@ -58,7 +64,9 @@ router.get('/user/activity-summary', async (req, res) => {
|
|||
const { startDate, endDate } = req.query;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return res.status(400).json({ error: 'startDate and endDate are required' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'startDate and endDate are required' });
|
||||
}
|
||||
|
||||
const activitySummary = await TaskEventService.getTaskActivitySummary(
|
||||
|
|
@ -85,7 +93,7 @@ router.get('/tasks/completion-analytics', async (req, res) => {
|
|||
|
||||
const whereClause = {
|
||||
user_id: req.currentUser.id,
|
||||
status: 2 // completed
|
||||
status: 2, // completed
|
||||
};
|
||||
|
||||
if (projectId) {
|
||||
|
|
@ -95,23 +103,25 @@ router.get('/tasks/completion-analytics', async (req, res) => {
|
|||
const completedTasks = await Task.findAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{ model: Project, attributes: ['name'], required: false }
|
||||
{ model: Project, attributes: ['name'], required: false },
|
||||
],
|
||||
order: [['completed_at', 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset)
|
||||
offset: parseInt(offset),
|
||||
});
|
||||
|
||||
// Get completion time analytics for each task
|
||||
const analytics = [];
|
||||
for (const task of completedTasks) {
|
||||
const completionTime = await TaskEventService.getTaskCompletionTime(task.id);
|
||||
const completionTime = await TaskEventService.getTaskCompletionTime(
|
||||
task.id
|
||||
);
|
||||
if (completionTime) {
|
||||
analytics.push({
|
||||
task_id: task.id,
|
||||
task_name: task.name,
|
||||
project_name: task.Project?.name || null,
|
||||
...completionTime
|
||||
...completionTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -119,30 +129,37 @@ router.get('/tasks/completion-analytics', async (req, res) => {
|
|||
// Calculate summary statistics
|
||||
const summary = {
|
||||
total_tasks: analytics.length,
|
||||
average_completion_hours: analytics.length > 0
|
||||
? analytics.reduce((sum, a) => sum + a.duration_hours, 0) / analytics.length
|
||||
average_completion_hours:
|
||||
analytics.length > 0
|
||||
? analytics.reduce((sum, a) => sum + a.duration_hours, 0) /
|
||||
analytics.length
|
||||
: 0,
|
||||
median_completion_hours: 0,
|
||||
fastest_completion: analytics.length > 0
|
||||
? Math.min(...analytics.map(a => a.duration_hours))
|
||||
fastest_completion:
|
||||
analytics.length > 0
|
||||
? Math.min(...analytics.map((a) => a.duration_hours))
|
||||
: 0,
|
||||
slowest_completion:
|
||||
analytics.length > 0
|
||||
? Math.max(...analytics.map((a) => a.duration_hours))
|
||||
: 0,
|
||||
slowest_completion: analytics.length > 0
|
||||
? Math.max(...analytics.map(a => a.duration_hours))
|
||||
: 0
|
||||
};
|
||||
|
||||
// Calculate median
|
||||
if (analytics.length > 0) {
|
||||
const sorted = analytics.map(a => a.duration_hours).sort((a, b) => a - b);
|
||||
const sorted = analytics
|
||||
.map((a) => a.duration_hours)
|
||||
.sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
summary.median_completion_hours = sorted.length % 2 === 0
|
||||
summary.median_completion_hours =
|
||||
sorted.length % 2 === 0
|
||||
? (sorted[middle - 1] + sorted[middle]) / 2
|
||||
: sorted[middle];
|
||||
}
|
||||
|
||||
res.json({
|
||||
tasks: analytics,
|
||||
summary
|
||||
summary,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching completion analytics:', error);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -12,7 +12,9 @@ router.post('/telegram/start-polling', async (req, res) => {
|
|||
|
||||
const user = await User.findByPk(req.session.userId);
|
||||
if (!user || !user.telegram_bot_token) {
|
||||
return res.status(400).json({ error: 'Telegram bot token not set.' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Telegram bot token not set.' });
|
||||
}
|
||||
|
||||
const success = await telegramPoller.addUser(user);
|
||||
|
|
@ -21,10 +23,12 @@ router.post('/telegram/start-polling', async (req, res) => {
|
|||
res.json({
|
||||
success: true,
|
||||
message: 'Telegram polling started',
|
||||
status: telegramPoller.getStatus()
|
||||
status: telegramPoller.getStatus(),
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({ error: 'Failed to start Telegram polling.' });
|
||||
res.status(500).json({
|
||||
error: 'Failed to start Telegram polling.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting Telegram polling:', error);
|
||||
|
|
@ -44,7 +48,7 @@ router.post('/telegram/stop-polling', async (req, res) => {
|
|||
res.json({
|
||||
success: true,
|
||||
message: 'Telegram polling stopped',
|
||||
status: telegramPoller.getStatus()
|
||||
status: telegramPoller.getStatus(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error stopping Telegram polling:', error);
|
||||
|
|
@ -61,7 +65,7 @@ router.get('/telegram/polling-status', async (req, res) => {
|
|||
|
||||
res.json({
|
||||
success: true,
|
||||
status: telegramPoller.getStatus()
|
||||
status: telegramPoller.getStatus(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting Telegram polling status:', error);
|
||||
|
|
@ -79,7 +83,9 @@ router.post('/telegram/setup', async (req, res) => {
|
|||
const { token } = req.body;
|
||||
|
||||
if (!token) {
|
||||
return res.status(400).json({ error: 'Telegram bot token is required.' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Telegram bot token is required.' });
|
||||
}
|
||||
|
||||
const user = await User.findByPk(req.session.userId);
|
||||
|
|
@ -89,7 +95,9 @@ router.post('/telegram/setup', async (req, res) => {
|
|||
|
||||
// Basic token validation - check if it looks like a Telegram bot token
|
||||
if (!/^\d+:[A-Za-z0-9_-]{35}$/.test(token)) {
|
||||
return res.status(400).json({ error: 'Invalid Telegram bot token format.' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Invalid Telegram bot token format.' });
|
||||
}
|
||||
|
||||
// Update user's telegram bot token
|
||||
|
|
@ -98,7 +106,7 @@ router.post('/telegram/setup', async (req, res) => {
|
|||
res.json({
|
||||
success: true,
|
||||
message: 'Telegram bot token updated successfully',
|
||||
token: token
|
||||
token: token,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting up Telegram:', error);
|
||||
|
|
|
|||
|
|
@ -11,12 +11,16 @@ function extractMetadataFromHtml(html) {
|
|||
let title = null;
|
||||
|
||||
// Try og:title first
|
||||
const ogTitleMatch = html.match(/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i);
|
||||
const ogTitleMatch = html.match(
|
||||
/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (ogTitleMatch) {
|
||||
title = ogTitleMatch[1];
|
||||
} else {
|
||||
// Try twitter:title
|
||||
const twitterTitleMatch = html.match(/<meta[^>]+name=["']twitter:title["'][^>]+content=["']([^"']+)["']/i);
|
||||
const twitterTitleMatch = html.match(
|
||||
/<meta[^>]+name=["']twitter:title["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (twitterTitleMatch) {
|
||||
title = twitterTitleMatch[1];
|
||||
} else {
|
||||
|
|
@ -46,11 +50,15 @@ function extractMetadataFromHtml(html) {
|
|||
|
||||
// Extract image with priority: og:image > twitter:image
|
||||
let image = null;
|
||||
const ogImageMatch = html.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i);
|
||||
const ogImageMatch = html.match(
|
||||
/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (ogImageMatch) {
|
||||
image = ogImageMatch[1];
|
||||
} else {
|
||||
const twitterImageMatch = html.match(/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i);
|
||||
const twitterImageMatch = html.match(
|
||||
/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (twitterImageMatch) {
|
||||
image = twitterImageMatch[1];
|
||||
}
|
||||
|
|
@ -58,15 +66,21 @@ function extractMetadataFromHtml(html) {
|
|||
|
||||
// Extract description
|
||||
let description = null;
|
||||
const ogDescMatch = html.match(/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i);
|
||||
const ogDescMatch = html.match(
|
||||
/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (ogDescMatch) {
|
||||
description = ogDescMatch[1];
|
||||
} else {
|
||||
const twitterDescMatch = html.match(/<meta[^>]+name=["']twitter:description["'][^>]+content=["']([^"']+)["']/i);
|
||||
const twitterDescMatch = html.match(
|
||||
/<meta[^>]+name=["']twitter:description["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (twitterDescMatch) {
|
||||
description = twitterDescMatch[1];
|
||||
} else {
|
||||
const metaDescMatch = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i);
|
||||
const metaDescMatch = html.match(
|
||||
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i
|
||||
);
|
||||
if (metaDescMatch) {
|
||||
description = metaDescMatch[1];
|
||||
}
|
||||
|
|
@ -80,7 +94,7 @@ function extractMetadataFromHtml(html) {
|
|||
return {
|
||||
title,
|
||||
image,
|
||||
description
|
||||
description,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing HTML:', error);
|
||||
|
|
@ -90,7 +104,8 @@ function extractMetadataFromHtml(html) {
|
|||
|
||||
// Helper function to check if text is a URL
|
||||
function isUrl(text) {
|
||||
const urlRegex = /^(https?:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i;
|
||||
const urlRegex =
|
||||
/^(https?:\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/i;
|
||||
return urlRegex.test(text.trim());
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +120,8 @@ function resolveUrl(baseUrl, relativeUrl) {
|
|||
|
||||
// Helper function to handle YouTube URLs specially
|
||||
function handleYouTubeUrl(url) {
|
||||
const youtubeRegex = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
|
||||
const youtubeRegex =
|
||||
/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
|
||||
const match = url.match(youtubeRegex);
|
||||
|
||||
if (match) {
|
||||
|
|
@ -115,7 +131,7 @@ function handleYouTubeUrl(url) {
|
|||
return {
|
||||
title: 'YouTube Video',
|
||||
image: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
|
||||
description: 'YouTube video'
|
||||
description: 'YouTube video',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -165,14 +181,21 @@ async function fetchUrlMetadata(url, maxRedirects = 5) {
|
|||
method: 'GET',
|
||||
timeout: 2000, // Reduced from 5000ms to 2000ms
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
}
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
},
|
||||
};
|
||||
|
||||
const req = client.request(options, (res) => {
|
||||
// Handle redirects (301, 302, 303, 307, 308)
|
||||
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
||||
const redirectUrl = new URL(res.headers.location, currentUrl).href;
|
||||
if (
|
||||
[301, 302, 303, 307, 308].includes(res.statusCode) &&
|
||||
res.headers.location
|
||||
) {
|
||||
const redirectUrl = new URL(
|
||||
res.headers.location,
|
||||
currentUrl
|
||||
).href;
|
||||
makeRequest(redirectUrl, redirectCount + 1);
|
||||
return;
|
||||
}
|
||||
|
|
@ -199,7 +222,12 @@ async function fetchUrlMetadata(url, maxRedirects = 5) {
|
|||
data += chunk;
|
||||
|
||||
// Early termination if we've found essential meta tags and closed head
|
||||
if (!foundMeta && (data.includes('og:title') || data.includes('twitter:title') || data.includes('</title>'))) {
|
||||
if (
|
||||
!foundMeta &&
|
||||
(data.includes('og:title') ||
|
||||
data.includes('twitter:title') ||
|
||||
data.includes('</title>'))
|
||||
) {
|
||||
foundMeta = true;
|
||||
}
|
||||
|
||||
|
|
@ -216,8 +244,14 @@ async function fetchUrlMetadata(url, maxRedirects = 5) {
|
|||
const metadata = extractMetadataFromHtml(data);
|
||||
|
||||
// Resolve relative image URLs to absolute
|
||||
if (metadata.image && !metadata.image.startsWith('http')) {
|
||||
metadata.image = resolveUrl(currentUrl, metadata.image);
|
||||
if (
|
||||
metadata.image &&
|
||||
!metadata.image.startsWith('http')
|
||||
) {
|
||||
metadata.image = resolveUrl(
|
||||
currentUrl,
|
||||
metadata.image
|
||||
);
|
||||
}
|
||||
|
||||
resolve(metadata);
|
||||
|
|
@ -266,10 +300,16 @@ router.get('/url/title', async (req, res) => {
|
|||
url,
|
||||
title: metadata.title,
|
||||
image: metadata.image,
|
||||
description: metadata.description
|
||||
description: metadata.description,
|
||||
});
|
||||
} else {
|
||||
res.json({ url, title: null, image: null, description: null, error: 'Could not extract metadata' });
|
||||
res.json({
|
||||
url,
|
||||
title: null,
|
||||
image: null,
|
||||
description: null,
|
||||
error: 'Could not extract metadata',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting URL title:', error);
|
||||
|
|
@ -287,12 +327,15 @@ router.post('/url/extract-from-text', async (req, res) => {
|
|||
const { text } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Text parameter is required' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Text parameter is required' });
|
||||
}
|
||||
|
||||
// Enhanced URL extraction - look for URLs with or without protocol
|
||||
const urlWithProtocolRegex = /(https?:\/\/[^\s]+)/gi;
|
||||
const urlWithoutProtocolRegex = /(?:^|\s)((?:www\.)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(?::[0-9]{1,5})?(?:\/[^\s]*)?)/gi;
|
||||
const urlWithoutProtocolRegex =
|
||||
/(?:^|\s)((?:www\.)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(?::[0-9]{1,5})?(?:\/[^\s]*)?)/gi;
|
||||
|
||||
let urls = text.match(urlWithProtocolRegex);
|
||||
|
||||
|
|
@ -301,7 +344,7 @@ router.post('/url/extract-from-text', async (req, res) => {
|
|||
const matches = text.match(urlWithoutProtocolRegex);
|
||||
if (matches) {
|
||||
// Clean up the matches (remove leading whitespace)
|
||||
urls = matches.map(match => match.trim());
|
||||
urls = matches.map((match) => match.trim());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +359,7 @@ router.post('/url/extract-from-text', async (req, res) => {
|
|||
title: metadata.title,
|
||||
image: metadata.image,
|
||||
description: metadata.description,
|
||||
originalText: text
|
||||
originalText: text,
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
|
|
@ -325,7 +368,7 @@ router.post('/url/extract-from-text', async (req, res) => {
|
|||
title: null,
|
||||
image: null,
|
||||
description: null,
|
||||
originalText: text
|
||||
originalText: text,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,16 @@ const { User } = require('../models');
|
|||
const taskSummaryService = require('../services/taskSummaryService');
|
||||
const router = express.Router();
|
||||
|
||||
const VALID_FREQUENCIES = ['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h'];
|
||||
const VALID_FREQUENCIES = [
|
||||
'daily',
|
||||
'weekdays',
|
||||
'weekly',
|
||||
'1h',
|
||||
'2h',
|
||||
'4h',
|
||||
'8h',
|
||||
'12h',
|
||||
];
|
||||
|
||||
// GET /api/profile
|
||||
router.get('/profile', async (req, res) => {
|
||||
|
|
@ -14,11 +23,21 @@ router.get('/profile', async (req, res) => {
|
|||
|
||||
const user = await User.findByPk(req.session.userId, {
|
||||
attributes: [
|
||||
'id', 'email', 'appearance', 'language', 'timezone',
|
||||
'avatar_image', 'telegram_bot_token', 'telegram_chat_id',
|
||||
'task_summary_enabled', 'task_summary_frequency', 'task_intelligence_enabled',
|
||||
'auto_suggest_next_actions_enabled', 'pomodoro_enabled', 'today_settings'
|
||||
]
|
||||
'id',
|
||||
'email',
|
||||
'appearance',
|
||||
'language',
|
||||
'timezone',
|
||||
'avatar_image',
|
||||
'telegram_bot_token',
|
||||
'telegram_chat_id',
|
||||
'task_summary_enabled',
|
||||
'task_summary_frequency',
|
||||
'task_intelligence_enabled',
|
||||
'auto_suggest_next_actions_enabled',
|
||||
'pomodoro_enabled',
|
||||
'today_settings',
|
||||
],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -54,35 +73,60 @@ router.patch('/profile', async (req, res) => {
|
|||
return res.status(404).json({ error: 'Profile not found.' });
|
||||
}
|
||||
|
||||
const { appearance, language, timezone, avatar_image, telegram_bot_token, task_intelligence_enabled, task_summary_enabled, task_summary_frequency, auto_suggest_next_actions_enabled, pomodoro_enabled, currentPassword, newPassword } = req.body;
|
||||
const {
|
||||
appearance,
|
||||
language,
|
||||
timezone,
|
||||
avatar_image,
|
||||
telegram_bot_token,
|
||||
task_intelligence_enabled,
|
||||
task_summary_enabled,
|
||||
task_summary_frequency,
|
||||
auto_suggest_next_actions_enabled,
|
||||
pomodoro_enabled,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
} = req.body;
|
||||
|
||||
const allowedUpdates = {};
|
||||
if (appearance !== undefined) allowedUpdates.appearance = appearance;
|
||||
if (language !== undefined) allowedUpdates.language = language;
|
||||
if (timezone !== undefined) allowedUpdates.timezone = timezone;
|
||||
if (avatar_image !== undefined) allowedUpdates.avatar_image = avatar_image;
|
||||
if (telegram_bot_token !== undefined) allowedUpdates.telegram_bot_token = telegram_bot_token;
|
||||
if (task_intelligence_enabled !== undefined) allowedUpdates.task_intelligence_enabled = task_intelligence_enabled;
|
||||
if (task_summary_enabled !== undefined) allowedUpdates.task_summary_enabled = task_summary_enabled;
|
||||
if (task_summary_frequency !== undefined) allowedUpdates.task_summary_frequency = task_summary_frequency;
|
||||
if (auto_suggest_next_actions_enabled !== undefined) allowedUpdates.auto_suggest_next_actions_enabled = auto_suggest_next_actions_enabled;
|
||||
if (pomodoro_enabled !== undefined) allowedUpdates.pomodoro_enabled = pomodoro_enabled;
|
||||
if (avatar_image !== undefined)
|
||||
allowedUpdates.avatar_image = avatar_image;
|
||||
if (telegram_bot_token !== undefined)
|
||||
allowedUpdates.telegram_bot_token = telegram_bot_token;
|
||||
if (task_intelligence_enabled !== undefined)
|
||||
allowedUpdates.task_intelligence_enabled =
|
||||
task_intelligence_enabled;
|
||||
if (task_summary_enabled !== undefined)
|
||||
allowedUpdates.task_summary_enabled = task_summary_enabled;
|
||||
if (task_summary_frequency !== undefined)
|
||||
allowedUpdates.task_summary_frequency = task_summary_frequency;
|
||||
if (auto_suggest_next_actions_enabled !== undefined)
|
||||
allowedUpdates.auto_suggest_next_actions_enabled =
|
||||
auto_suggest_next_actions_enabled;
|
||||
if (pomodoro_enabled !== undefined)
|
||||
allowedUpdates.pomodoro_enabled = pomodoro_enabled;
|
||||
|
||||
// Handle password change if provided
|
||||
if (currentPassword && newPassword) {
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
field: 'newPassword',
|
||||
error: 'Password must be at least 6 characters'
|
||||
error: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isValidPassword = await User.checkPassword(currentPassword, user.password_digest);
|
||||
const isValidPassword = await User.checkPassword(
|
||||
currentPassword,
|
||||
user.password_digest
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({
|
||||
field: 'currentPassword',
|
||||
error: 'Current password is incorrect'
|
||||
error: 'Current password is incorrect',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +139,21 @@ router.patch('/profile', async (req, res) => {
|
|||
|
||||
// Return updated user with limited fields
|
||||
const updatedUser = await User.findByPk(user.id, {
|
||||
attributes: ['id', 'email', 'appearance', 'language', 'timezone', 'avatar_image', 'telegram_bot_token', 'telegram_chat_id', 'task_intelligence_enabled', 'task_summary_enabled', 'task_summary_frequency', 'auto_suggest_next_actions_enabled', 'pomodoro_enabled']
|
||||
attributes: [
|
||||
'id',
|
||||
'email',
|
||||
'appearance',
|
||||
'language',
|
||||
'timezone',
|
||||
'avatar_image',
|
||||
'telegram_bot_token',
|
||||
'telegram_chat_id',
|
||||
'task_intelligence_enabled',
|
||||
'task_summary_enabled',
|
||||
'task_summary_frequency',
|
||||
'auto_suggest_next_actions_enabled',
|
||||
'pomodoro_enabled',
|
||||
],
|
||||
});
|
||||
|
||||
res.json(updatedUser);
|
||||
|
|
@ -103,7 +161,9 @@ router.patch('/profile', async (req, res) => {
|
|||
console.error('Error updating profile:', error);
|
||||
res.status(400).json({
|
||||
error: 'Failed to update profile.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -118,13 +178,15 @@ router.post('/profile/change-password', async (req, res) => {
|
|||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ error: 'Current password and new password are required' });
|
||||
return res.status(400).json({
|
||||
error: 'Current password and new password are required',
|
||||
});
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return res.status(400).json({
|
||||
field: 'newPassword',
|
||||
error: 'Password must be at least 6 characters'
|
||||
error: 'Password must be at least 6 characters',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -134,11 +196,14 @@ router.post('/profile/change-password', async (req, res) => {
|
|||
}
|
||||
|
||||
// Verify current password
|
||||
const isValidPassword = await User.checkPassword(currentPassword, user.password_digest);
|
||||
const isValidPassword = await User.checkPassword(
|
||||
currentPassword,
|
||||
user.password_digest
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({
|
||||
field: 'currentPassword',
|
||||
error: 'Current password is incorrect'
|
||||
error: 'Current password is incorrect',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -177,13 +242,15 @@ router.post('/profile/task-summary/toggle', async (req, res) => {
|
|||
res.json({
|
||||
success: true,
|
||||
enabled: enabled,
|
||||
message: message
|
||||
message: message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling task summary:', error);
|
||||
res.status(400).json({
|
||||
error: 'Failed to update task summary settings.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -215,13 +282,15 @@ router.post('/profile/task-summary/frequency', async (req, res) => {
|
|||
res.json({
|
||||
success: true,
|
||||
frequency: frequency,
|
||||
message: `Task summary frequency has been set to ${frequency}.`
|
||||
message: `Task summary frequency has been set to ${frequency}.`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating task summary frequency:', error);
|
||||
res.status(400).json({
|
||||
error: 'Failed to update task summary frequency.',
|
||||
details: error.errors ? error.errors.map(e => e.message) : [error.message]
|
||||
details: error.errors
|
||||
? error.errors.map((e) => e.message)
|
||||
: [error.message],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -239,7 +308,9 @@ router.post('/profile/task-summary/send-now', async (req, res) => {
|
|||
}
|
||||
|
||||
if (!user.telegram_bot_token || !user.telegram_chat_id) {
|
||||
return res.status(400).json({ error: 'Telegram bot is not properly configured.' });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: 'Telegram bot is not properly configured.' });
|
||||
}
|
||||
|
||||
// Send the task summary
|
||||
|
|
@ -248,16 +319,18 @@ router.post('/profile/task-summary/send-now', async (req, res) => {
|
|||
if (success) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Task summary was sent to your Telegram.'
|
||||
message: 'Task summary was sent to your Telegram.',
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({ error: 'Failed to send message to Telegram.' });
|
||||
res.status(400).json({
|
||||
error: 'Failed to send message to Telegram.',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending task summary:', error);
|
||||
res.status(400).json({
|
||||
error: 'Error sending message to Telegram.',
|
||||
details: error.message
|
||||
details: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -279,7 +352,7 @@ router.get('/profile/task-summary/status', async (req, res) => {
|
|||
enabled: user.task_summary_enabled,
|
||||
frequency: user.task_summary_frequency,
|
||||
last_run: user.task_summary_last_run,
|
||||
next_run: user.task_summary_next_run
|
||||
next_run: user.task_summary_next_run,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching task summary status:', error);
|
||||
|
|
@ -306,24 +379,42 @@ router.put('/profile/today-settings', async (req, res) => {
|
|||
showDueToday,
|
||||
showCompleted,
|
||||
showProgressBar,
|
||||
showDailyQuote
|
||||
showDailyQuote,
|
||||
} = req.body;
|
||||
|
||||
const todaySettings = {
|
||||
showMetrics: showMetrics !== undefined ? showMetrics : user.today_settings?.showMetrics || false,
|
||||
showProductivity: showProductivity !== undefined ? showProductivity : user.today_settings?.showProductivity || false,
|
||||
showIntelligence: showIntelligence !== undefined ? showIntelligence : user.today_settings?.showIntelligence || false,
|
||||
showDueToday: showDueToday !== undefined ? showDueToday : user.today_settings?.showDueToday || true,
|
||||
showCompleted: showCompleted !== undefined ? showCompleted : user.today_settings?.showCompleted || true,
|
||||
showMetrics:
|
||||
showMetrics !== undefined
|
||||
? showMetrics
|
||||
: user.today_settings?.showMetrics || false,
|
||||
showProductivity:
|
||||
showProductivity !== undefined
|
||||
? showProductivity
|
||||
: user.today_settings?.showProductivity || false,
|
||||
showIntelligence:
|
||||
showIntelligence !== undefined
|
||||
? showIntelligence
|
||||
: user.today_settings?.showIntelligence || false,
|
||||
showDueToday:
|
||||
showDueToday !== undefined
|
||||
? showDueToday
|
||||
: user.today_settings?.showDueToday || true,
|
||||
showCompleted:
|
||||
showCompleted !== undefined
|
||||
? showCompleted
|
||||
: user.today_settings?.showCompleted || true,
|
||||
showProgressBar: true, // Always enabled - ignore any attempts to disable it
|
||||
showDailyQuote: showDailyQuote !== undefined ? showDailyQuote : user.today_settings?.showDailyQuote || true
|
||||
showDailyQuote:
|
||||
showDailyQuote !== undefined
|
||||
? showDailyQuote
|
||||
: user.today_settings?.showDailyQuote || true,
|
||||
};
|
||||
|
||||
await user.update({ today_settings: todaySettings });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
today_settings: todaySettings
|
||||
today_settings: todaySettings,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating today settings:', error);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ async function initDatabase() {
|
|||
await sequelize.sync({ force: true });
|
||||
|
||||
console.log('✅ Database initialized successfully');
|
||||
console.log('All tables have been created and existing data has been cleared');
|
||||
console.log(
|
||||
'All tables have been created and existing data has been cleared'
|
||||
);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error initializing database:', error.message);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@
|
|||
*/
|
||||
|
||||
require('dotenv').config();
|
||||
const { sequelize, User, Task, Project, Area, Note, Tag, InboxItem } = require('../models');
|
||||
const {
|
||||
sequelize,
|
||||
User,
|
||||
Task,
|
||||
Project,
|
||||
Area,
|
||||
Note,
|
||||
Tag,
|
||||
InboxItem,
|
||||
} = require('../models');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
|
@ -20,7 +29,9 @@ async function checkDatabaseStatus() {
|
|||
|
||||
console.log('📂 Database Configuration:');
|
||||
console.log(` Storage: ${dbPath}`);
|
||||
console.log(` Dialect: ${dbConfig.dialect || sequelize.options.dialect || 'sqlite'}`);
|
||||
console.log(
|
||||
` Dialect: ${dbConfig.dialect || sequelize.options.dialect || 'sqlite'}`
|
||||
);
|
||||
console.log(` Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
|
||||
// Check if database file exists
|
||||
|
|
@ -45,7 +56,7 @@ async function checkDatabaseStatus() {
|
|||
{ name: 'Tasks', model: Task },
|
||||
{ name: 'Notes', model: Note },
|
||||
{ name: 'Tags', model: Tag },
|
||||
{ name: 'Inbox Items', model: InboxItem }
|
||||
{ name: 'Inbox Items', model: InboxItem },
|
||||
];
|
||||
|
||||
for (const { name, model } of models) {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,16 @@ function createMigration() {
|
|||
|
||||
if (!migrationName) {
|
||||
console.error('❌ Usage: npm run migration:create <migration-name>');
|
||||
console.error('Example: npm run migration:create add-description-to-tasks');
|
||||
console.error(
|
||||
'Example: npm run migration:create add-description-to-tasks'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate timestamp (YYYYMMDDHHMMSS format)
|
||||
const now = new Date();
|
||||
const timestamp = now.getFullYear().toString() +
|
||||
const timestamp =
|
||||
now.getFullYear().toString() +
|
||||
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||||
now.getDate().toString().padStart(2, '0') +
|
||||
now.getHours().toString().padStart(2, '0') +
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ async function createUser() {
|
|||
|
||||
if (!email || password === undefined) {
|
||||
console.error('❌ Usage: npm run user:create <email> <password>');
|
||||
console.error('Example: npm run user:create admin@example.com mypassword123');
|
||||
console.error(
|
||||
'Example: npm run user:create admin@example.com mypassword123'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +31,8 @@ async function createUser() {
|
|||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
|
||||
|
||||
// Check for common invalid patterns
|
||||
if (!email.includes('@') ||
|
||||
if (
|
||||
!email.includes('@') ||
|
||||
!email.includes('.') ||
|
||||
email.includes('@@') ||
|
||||
email.includes(' ') ||
|
||||
|
|
@ -38,7 +41,8 @@ async function createUser() {
|
|||
email.endsWith('.') ||
|
||||
email.includes('@.') ||
|
||||
email.includes('.@') ||
|
||||
!emailRegex.test(email)) {
|
||||
!emailRegex.test(email)
|
||||
) {
|
||||
console.error('❌ Invalid email format');
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -59,7 +63,7 @@ async function createUser() {
|
|||
// Create the user
|
||||
const user = await User.create({
|
||||
email,
|
||||
password_digest: hashedPassword
|
||||
password_digest: hashedPassword,
|
||||
});
|
||||
|
||||
console.log('✅ User created successfully');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
const { User, Area, Project, Task, Tag, Note, InboxItem } = require('../models');
|
||||
const {
|
||||
User,
|
||||
Area,
|
||||
Project,
|
||||
Task,
|
||||
Tag,
|
||||
Note,
|
||||
InboxItem,
|
||||
} = require('../models');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { createMassiveTaskData } = require('./massive-tasks');
|
||||
|
||||
|
|
@ -19,7 +27,7 @@ async function seedDatabase() {
|
|||
password_digest: await bcrypt.hash('password123', 10),
|
||||
appearance: 'light',
|
||||
language: 'en',
|
||||
timezone: 'Europe/Athens'
|
||||
timezone: 'Europe/Athens',
|
||||
});
|
||||
console.log('✅ Created new test user with ID:', testUser.id);
|
||||
} else {
|
||||
|
|
@ -46,7 +54,7 @@ async function seedDatabase() {
|
|||
{ name: 'Travel', user_id: testUser.id },
|
||||
{ name: 'Hobbies', user_id: testUser.id },
|
||||
{ name: 'Social', user_id: testUser.id },
|
||||
{ name: 'Career', user_id: testUser.id }
|
||||
{ name: 'Career', user_id: testUser.id },
|
||||
]);
|
||||
|
||||
// Create projects
|
||||
|
|
@ -58,14 +66,14 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[1].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days from now
|
||||
due_date_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days from now
|
||||
},
|
||||
{
|
||||
name: 'Learn React Native',
|
||||
description: 'Master mobile app development',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[3].id,
|
||||
active: true
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'Home Renovation',
|
||||
|
|
@ -73,7 +81,7 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[4].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) // 60 days from now
|
||||
due_date_at: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000), // 60 days from now
|
||||
},
|
||||
{
|
||||
name: 'Fitness Challenge',
|
||||
|
|
@ -81,14 +89,14 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[2].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days from now
|
||||
due_date_at: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), // 90 days from now
|
||||
},
|
||||
{
|
||||
name: 'Side Business',
|
||||
description: 'Launch online consulting service',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[1].id,
|
||||
active: true
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'Investment Portfolio',
|
||||
|
|
@ -96,7 +104,7 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[5].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 120 * 24 * 60 * 60 * 1000) // 120 days from now
|
||||
due_date_at: new Date(Date.now() + 120 * 24 * 60 * 60 * 1000), // 120 days from now
|
||||
},
|
||||
{
|
||||
name: 'Europe Trip 2024',
|
||||
|
|
@ -104,14 +112,14 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[6].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000) // 180 days from now
|
||||
due_date_at: new Date(Date.now() + 180 * 24 * 60 * 60 * 1000), // 180 days from now
|
||||
},
|
||||
{
|
||||
name: 'Photography Mastery',
|
||||
description: 'Learn advanced photography techniques',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[7].id,
|
||||
active: true
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'Professional Certification',
|
||||
|
|
@ -119,7 +127,7 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[9].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 150 * 24 * 60 * 60 * 1000) // 150 days from now
|
||||
due_date_at: new Date(Date.now() + 150 * 24 * 60 * 60 * 1000), // 150 days from now
|
||||
},
|
||||
{
|
||||
name: 'Garden Makeover',
|
||||
|
|
@ -127,21 +135,21 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[4].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000) // 45 days from now
|
||||
due_date_at: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000), // 45 days from now
|
||||
},
|
||||
{
|
||||
name: 'Blog Launch',
|
||||
description: 'Start personal tech blog',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[0].id,
|
||||
active: true
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'Language Learning Spanish',
|
||||
description: 'Become conversational in Spanish',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[3].id,
|
||||
active: false // Paused project
|
||||
active: false, // Paused project
|
||||
},
|
||||
{
|
||||
name: 'Wedding Planning',
|
||||
|
|
@ -149,14 +157,14 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[8].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year from now
|
||||
due_date_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year from now
|
||||
},
|
||||
{
|
||||
name: 'Meal Prep System',
|
||||
description: 'Establish weekly meal preparation routine',
|
||||
user_id: testUser.id,
|
||||
area_id: areas[2].id,
|
||||
active: true
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: 'Smart Home Setup',
|
||||
|
|
@ -164,8 +172,8 @@ async function seedDatabase() {
|
|||
user_id: testUser.id,
|
||||
area_id: areas[4].id,
|
||||
active: true,
|
||||
due_date_at: new Date(Date.now() + 21 * 24 * 60 * 60 * 1000) // 21 days from now
|
||||
}
|
||||
due_date_at: new Date(Date.now() + 21 * 24 * 60 * 60 * 1000), // 21 days from now
|
||||
},
|
||||
]);
|
||||
|
||||
// Create tags
|
||||
|
|
@ -195,7 +203,7 @@ async function seedDatabase() {
|
|||
{ name: 'review', user_id: testUser.id },
|
||||
{ name: 'automation', user_id: testUser.id },
|
||||
{ name: 'documentation', user_id: testUser.id },
|
||||
{ name: 'bug-fix', user_id: testUser.id }
|
||||
{ name: 'bug-fix', user_id: testUser.id },
|
||||
]);
|
||||
|
||||
// Helper function to get random date
|
||||
|
|
@ -211,14 +219,18 @@ async function seedDatabase() {
|
|||
|
||||
// Create tasks
|
||||
console.log('✅ Creating massive task dataset...');
|
||||
const taskData = createMassiveTaskData(projects, getRandomDate, getPastDate);
|
||||
const taskData = createMassiveTaskData(
|
||||
projects,
|
||||
getRandomDate,
|
||||
getPastDate
|
||||
);
|
||||
|
||||
const tasks = [];
|
||||
for (const taskInfo of taskData) {
|
||||
const task = await Task.create({
|
||||
...taskInfo,
|
||||
user_id: testUser.id,
|
||||
note: taskInfo.note || null
|
||||
note: taskInfo.note || null,
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
|
|
@ -255,22 +267,28 @@ async function seedDatabase() {
|
|||
'Plan workshop or shed organization',
|
||||
'Research travel planning tools',
|
||||
'Update subscription management',
|
||||
'Plan digital decluttering project'
|
||||
'Plan digital decluttering project',
|
||||
];
|
||||
|
||||
for (let i = 0; i < backlogTaskNames.length; i++) {
|
||||
const daysAgo = Math.floor(Math.random() * 120) + 31; // 31-150 days ago
|
||||
const oldDate = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
|
||||
const oldDate = new Date(
|
||||
Date.now() - daysAgo * 24 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
const backlogTask = await Task.create({
|
||||
name: backlogTaskNames[i],
|
||||
priority: Math.floor(Math.random() * 3),
|
||||
status: Math.random() < 0.9 ? 0 : 1, // 90% not started, 10% in progress
|
||||
user_id: testUser.id,
|
||||
project_id: Math.random() < 0.3 ? projects[Math.floor(Math.random() * projects.length)].id : null,
|
||||
project_id:
|
||||
Math.random() < 0.3
|
||||
? projects[Math.floor(Math.random() * projects.length)]
|
||||
.id
|
||||
: null,
|
||||
due_date: Math.random() < 0.2 ? getRandomDate(30) : null,
|
||||
created_at: oldDate,
|
||||
updated_at: oldDate
|
||||
updated_at: oldDate,
|
||||
});
|
||||
tasks.push(backlogTask);
|
||||
}
|
||||
|
|
@ -287,7 +305,7 @@ async function seedDatabase() {
|
|||
'Complete expense report submission',
|
||||
'Follow up on pending client emails',
|
||||
'Review contract terms and conditions',
|
||||
'Update project timeline document'
|
||||
'Update project timeline document',
|
||||
];
|
||||
|
||||
const today = new Date();
|
||||
|
|
@ -299,10 +317,14 @@ async function seedDatabase() {
|
|||
priority: Math.floor(Math.random() * 3),
|
||||
status: Math.random() < 0.8 ? 0 : 1, // 80% not started, 20% in progress
|
||||
user_id: testUser.id,
|
||||
project_id: Math.random() < 0.4 ? projects[Math.floor(Math.random() * projects.length)].id : null,
|
||||
project_id:
|
||||
Math.random() < 0.4
|
||||
? projects[Math.floor(Math.random() * projects.length)]
|
||||
.id
|
||||
: null,
|
||||
due_date: today,
|
||||
created_at: getPastDate(7), // Created within last week
|
||||
updated_at: getPastDate(7)
|
||||
updated_at: getPastDate(7),
|
||||
});
|
||||
tasks.push(todayTask);
|
||||
}
|
||||
|
|
@ -318,7 +340,11 @@ async function seedDatabase() {
|
|||
const taskTags = [];
|
||||
|
||||
// Pattern-based tagging for AI trigger recognition
|
||||
if (taskName.includes('urgent') || taskName.includes('asap') || task.due_date && new Date(task.due_date) < new Date()) {
|
||||
if (
|
||||
taskName.includes('urgent') ||
|
||||
taskName.includes('asap') ||
|
||||
(task.due_date && new Date(task.due_date) < new Date())
|
||||
) {
|
||||
taskTags.push(tags[0]); // urgent
|
||||
}
|
||||
|
||||
|
|
@ -326,56 +352,113 @@ async function seedDatabase() {
|
|||
taskTags.push(tags[5]); // phone-call
|
||||
}
|
||||
|
||||
if (taskName.includes('meeting') || taskName.includes('standup') || taskName.includes('conference')) {
|
||||
if (
|
||||
taskName.includes('meeting') ||
|
||||
taskName.includes('standup') ||
|
||||
taskName.includes('conference')
|
||||
) {
|
||||
taskTags.push(tags[3]); // meeting
|
||||
}
|
||||
|
||||
if (taskName.includes('research') || taskName.includes('study') || taskName.includes('learn')) {
|
||||
if (
|
||||
taskName.includes('research') ||
|
||||
taskName.includes('study') ||
|
||||
taskName.includes('learn')
|
||||
) {
|
||||
taskTags.push(tags[2]); // research
|
||||
taskTags.push(tags[15]); // learning
|
||||
}
|
||||
|
||||
if (taskName.includes('buy') || taskName.includes('purchase') || taskName.includes('shop')) {
|
||||
if (
|
||||
taskName.includes('buy') ||
|
||||
taskName.includes('purchase') ||
|
||||
taskName.includes('shop')
|
||||
) {
|
||||
taskTags.push(tags[8]); // shopping
|
||||
}
|
||||
|
||||
if (taskName.includes('design') || taskName.includes('create') || taskName.includes('write') || taskName.includes('paint')) {
|
||||
if (
|
||||
taskName.includes('design') ||
|
||||
taskName.includes('create') ||
|
||||
taskName.includes('write') ||
|
||||
taskName.includes('paint')
|
||||
) {
|
||||
taskTags.push(tags[4]); // creative
|
||||
}
|
||||
|
||||
if (taskName.includes('health') || taskName.includes('doctor') || taskName.includes('medical') || taskName.includes('fitness') || taskName.includes('workout')) {
|
||||
if (
|
||||
taskName.includes('health') ||
|
||||
taskName.includes('doctor') ||
|
||||
taskName.includes('medical') ||
|
||||
taskName.includes('fitness') ||
|
||||
taskName.includes('workout')
|
||||
) {
|
||||
taskTags.push(tags[18]); // health
|
||||
}
|
||||
|
||||
if (taskName.includes('financial') || taskName.includes('budget') || taskName.includes('invest') || taskName.includes('money') || taskName.includes('pay')) {
|
||||
if (
|
||||
taskName.includes('financial') ||
|
||||
taskName.includes('budget') ||
|
||||
taskName.includes('invest') ||
|
||||
taskName.includes('money') ||
|
||||
taskName.includes('pay')
|
||||
) {
|
||||
taskTags.push(tags[17]); // financial
|
||||
}
|
||||
|
||||
if (taskName.includes('outdoor') || taskName.includes('garden') || taskName.includes('hiking') || taskName.includes('park')) {
|
||||
if (
|
||||
taskName.includes('outdoor') ||
|
||||
taskName.includes('garden') ||
|
||||
taskName.includes('hiking') ||
|
||||
taskName.includes('park')
|
||||
) {
|
||||
taskTags.push(tags[19]); // outdoor
|
||||
}
|
||||
|
||||
if (taskName.includes('plan') || taskName.includes('schedule') || taskName.includes('organize')) {
|
||||
if (
|
||||
taskName.includes('plan') ||
|
||||
taskName.includes('schedule') ||
|
||||
taskName.includes('organize')
|
||||
) {
|
||||
taskTags.push(tags[20]); // planning
|
||||
}
|
||||
|
||||
if (taskName.includes('review') || taskName.includes('check') || taskName.includes('audit')) {
|
||||
if (
|
||||
taskName.includes('review') ||
|
||||
taskName.includes('check') ||
|
||||
taskName.includes('audit')
|
||||
) {
|
||||
taskTags.push(tags[21]); // review
|
||||
}
|
||||
|
||||
if (taskName.includes('fix') || taskName.includes('repair') || taskName.includes('maintain') || taskName.includes('clean')) {
|
||||
if (
|
||||
taskName.includes('fix') ||
|
||||
taskName.includes('repair') ||
|
||||
taskName.includes('maintain') ||
|
||||
taskName.includes('clean')
|
||||
) {
|
||||
taskTags.push(tags[16]); // maintenance
|
||||
}
|
||||
|
||||
if (taskName.includes('weekend') || task.due_date && [0, 6].includes(new Date(task.due_date).getDay())) {
|
||||
if (
|
||||
taskName.includes('weekend') ||
|
||||
(task.due_date &&
|
||||
[0, 6].includes(new Date(task.due_date).getDay()))
|
||||
) {
|
||||
taskTags.push(tags[7]); // weekend
|
||||
}
|
||||
|
||||
if (taskName.includes('online') || taskName.includes('website') || taskName.includes('digital') || taskName.includes('app')) {
|
||||
if (
|
||||
taskName.includes('online') ||
|
||||
taskName.includes('website') ||
|
||||
taskName.includes('digital') ||
|
||||
taskName.includes('app')
|
||||
) {
|
||||
taskTags.push(tags[6]); // online
|
||||
}
|
||||
|
||||
if (task.status === 4) { // waiting status
|
||||
if (task.status === 4) {
|
||||
// waiting status
|
||||
taskTags.push(tags[10]); // waiting-for
|
||||
}
|
||||
|
||||
|
|
@ -383,31 +466,59 @@ async function seedDatabase() {
|
|||
taskTags.push(tags[11]); // someday-maybe
|
||||
}
|
||||
|
||||
if (taskName.includes('team') || taskName.includes('group') || taskName.includes('collaborate')) {
|
||||
if (
|
||||
taskName.includes('team') ||
|
||||
taskName.includes('group') ||
|
||||
taskName.includes('collaborate')
|
||||
) {
|
||||
taskTags.push(tags[14]); // collaboration
|
||||
}
|
||||
|
||||
if (taskName.includes('quick') || taskName.includes('fast') || taskName.includes('simple')) {
|
||||
if (
|
||||
taskName.includes('quick') ||
|
||||
taskName.includes('fast') ||
|
||||
taskName.includes('simple')
|
||||
) {
|
||||
taskTags.push(tags[1]); // quick-win
|
||||
}
|
||||
|
||||
if (taskName.includes('energy') || taskName.includes('intensive') || taskName.includes('focus')) {
|
||||
if (
|
||||
taskName.includes('energy') ||
|
||||
taskName.includes('intensive') ||
|
||||
taskName.includes('focus')
|
||||
) {
|
||||
taskTags.push(tags[12]); // high-energy
|
||||
}
|
||||
|
||||
if (taskName.includes('relax') || taskName.includes('easy') || taskName.includes('light')) {
|
||||
if (
|
||||
taskName.includes('relax') ||
|
||||
taskName.includes('easy') ||
|
||||
taskName.includes('light')
|
||||
) {
|
||||
taskTags.push(tags[13]); // low-energy
|
||||
}
|
||||
|
||||
if (taskName.includes('automate') || taskName.includes('script') || taskName.includes('automation')) {
|
||||
if (
|
||||
taskName.includes('automate') ||
|
||||
taskName.includes('script') ||
|
||||
taskName.includes('automation')
|
||||
) {
|
||||
taskTags.push(tags[22]); // automation
|
||||
}
|
||||
|
||||
if (taskName.includes('document') || taskName.includes('write') || taskName.includes('manual')) {
|
||||
if (
|
||||
taskName.includes('document') ||
|
||||
taskName.includes('write') ||
|
||||
taskName.includes('manual')
|
||||
) {
|
||||
taskTags.push(tags[23]); // documentation
|
||||
}
|
||||
|
||||
if (taskName.includes('bug') || taskName.includes('fix') || taskName.includes('error')) {
|
||||
if (
|
||||
taskName.includes('bug') ||
|
||||
taskName.includes('fix') ||
|
||||
taskName.includes('error')
|
||||
) {
|
||||
taskTags.push(tags[24]); // bug-fix
|
||||
}
|
||||
|
||||
|
|
@ -425,43 +536,77 @@ async function seedDatabase() {
|
|||
const TaskEventService = require('../services/taskEventService');
|
||||
|
||||
// Create events for completed tasks to show user patterns
|
||||
const completedTasks = tasks.filter(t => t.status === 2);
|
||||
for (const task of completedTasks.slice(0, 20)) { // Just first 20 to avoid too much data
|
||||
const completedTasks = tasks.filter((t) => t.status === 2);
|
||||
for (const task of completedTasks.slice(0, 20)) {
|
||||
// Just first 20 to avoid too much data
|
||||
try {
|
||||
// Create task creation event
|
||||
await TaskEventService.logTaskCreated(task.id, testUser.id, {
|
||||
await TaskEventService.logTaskCreated(
|
||||
task.id,
|
||||
testUser.id,
|
||||
{
|
||||
name: task.name,
|
||||
status: 0,
|
||||
priority: task.priority,
|
||||
project_id: task.project_id
|
||||
}, { source: 'web' });
|
||||
project_id: task.project_id,
|
||||
},
|
||||
{ source: 'web' }
|
||||
);
|
||||
|
||||
// Create status change to in_progress
|
||||
if (Math.random() < 0.7) { // 70% had in_progress phase
|
||||
await TaskEventService.logStatusChange(task.id, testUser.id, 0, 1, { source: 'web' });
|
||||
if (Math.random() < 0.7) {
|
||||
// 70% had in_progress phase
|
||||
await TaskEventService.logStatusChange(
|
||||
task.id,
|
||||
testUser.id,
|
||||
0,
|
||||
1,
|
||||
{ source: 'web' }
|
||||
);
|
||||
}
|
||||
|
||||
// Create completion event
|
||||
await TaskEventService.logStatusChange(task.id, testUser.id, 1, 2, { source: 'web' });
|
||||
await TaskEventService.logStatusChange(
|
||||
task.id,
|
||||
testUser.id,
|
||||
1,
|
||||
2,
|
||||
{ source: 'web' }
|
||||
);
|
||||
} catch (eventError) {
|
||||
console.log(`Skipping event creation for task ${task.id}: ${eventError.message}`);
|
||||
console.log(
|
||||
`Skipping event creation for task ${task.id}: ${eventError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create events for some in-progress tasks
|
||||
const inProgressTasks = tasks.filter(t => t.status === 1);
|
||||
const inProgressTasks = tasks.filter((t) => t.status === 1);
|
||||
for (const task of inProgressTasks.slice(0, 10)) {
|
||||
try {
|
||||
await TaskEventService.logTaskCreated(task.id, testUser.id, {
|
||||
await TaskEventService.logTaskCreated(
|
||||
task.id,
|
||||
testUser.id,
|
||||
{
|
||||
name: task.name,
|
||||
status: 0,
|
||||
priority: task.priority,
|
||||
project_id: task.project_id
|
||||
}, { source: 'web' });
|
||||
project_id: task.project_id,
|
||||
},
|
||||
{ source: 'web' }
|
||||
);
|
||||
|
||||
await TaskEventService.logStatusChange(task.id, testUser.id, 0, 1, { source: 'web' });
|
||||
await TaskEventService.logStatusChange(
|
||||
task.id,
|
||||
testUser.id,
|
||||
0,
|
||||
1,
|
||||
{ source: 'web' }
|
||||
);
|
||||
} catch (eventError) {
|
||||
console.log(`Skipping event creation for task ${task.id}: ${eventError.message}`);
|
||||
console.log(
|
||||
`Skipping event creation for task ${task.id}: ${eventError.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -470,74 +615,86 @@ async function seedDatabase() {
|
|||
await Note.bulkCreate([
|
||||
{
|
||||
title: 'Meeting Notes - Website Redesign',
|
||||
content: 'Key decisions:\n- Use blue and white color scheme\n- Include customer testimonials\n- Mobile-first approach\n- Launch date: End of next month\n\nAction items:\n- Get approval from stakeholders\n- Create prototype by Friday\n- Schedule user testing session',
|
||||
content:
|
||||
'Key decisions:\n- Use blue and white color scheme\n- Include customer testimonials\n- Mobile-first approach\n- Launch date: End of next month\n\nAction items:\n- Get approval from stakeholders\n- Create prototype by Friday\n- Schedule user testing session',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[0].id
|
||||
project_id: projects[0].id,
|
||||
},
|
||||
{
|
||||
title: 'React Native Learning Resources',
|
||||
content: 'Useful links:\n- Official documentation\n- Expo.dev for quick prototyping\n- React Navigation library\n- AsyncStorage for local data\n\nTutorials to check out:\n- React Native School\n- The Net Ninja series\n- React Native Express',
|
||||
content:
|
||||
'Useful links:\n- Official documentation\n- Expo.dev for quick prototyping\n- React Navigation library\n- AsyncStorage for local data\n\nTutorials to check out:\n- React Native School\n- The Net Ninja series\n- React Native Express',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[1].id
|
||||
project_id: projects[1].id,
|
||||
},
|
||||
{
|
||||
title: 'Home Renovation Budget',
|
||||
content: 'Budget breakdown:\n- Kitchen: $15,000\n- Bathroom: $8,000\n- Contingency: $3,000\n- Total: $26,000\n\nContractors to contact:\n- ABC Construction: 555-0123\n- Quality Home Builders: 555-0456\n- Elite Renovations: 555-0789',
|
||||
content:
|
||||
'Budget breakdown:\n- Kitchen: $15,000\n- Bathroom: $8,000\n- Contingency: $3,000\n- Total: $26,000\n\nContractors to contact:\n- ABC Construction: 555-0123\n- Quality Home Builders: 555-0456\n- Elite Renovations: 555-0789',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[2].id
|
||||
project_id: projects[2].id,
|
||||
},
|
||||
{
|
||||
title: 'Investment Strategy Notes',
|
||||
content: 'Portfolio allocation goals:\n- 60% Stock index funds\n- 30% Bond index funds\n- 10% International funds\n\nPlatforms to consider:\n- Vanguard\n- Fidelity\n- Charles Schwab\n\nMonthly investment: $1,000',
|
||||
content:
|
||||
'Portfolio allocation goals:\n- 60% Stock index funds\n- 30% Bond index funds\n- 10% International funds\n\nPlatforms to consider:\n- Vanguard\n- Fidelity\n- Charles Schwab\n\nMonthly investment: $1,000',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[5].id
|
||||
project_id: projects[5].id,
|
||||
},
|
||||
{
|
||||
title: 'Europe Trip Planning',
|
||||
content: 'Destinations:\n1. Paris, France (5 days)\n2. Rome, Italy (4 days)\n3. Barcelona, Spain (4 days)\n4. Amsterdam, Netherlands (3 days)\n5. Prague, Czech Republic (3 days)\n\nEstimated costs:\n- Flights: $1,200\n- Hotels: $2,500\n- Food: $1,000\n- Activities: $800\n- Total: $5,500',
|
||||
content:
|
||||
'Destinations:\n1. Paris, France (5 days)\n2. Rome, Italy (4 days)\n3. Barcelona, Spain (4 days)\n4. Amsterdam, Netherlands (3 days)\n5. Prague, Czech Republic (3 days)\n\nEstimated costs:\n- Flights: $1,200\n- Hotels: $2,500\n- Food: $1,000\n- Activities: $800\n- Total: $5,500',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[6].id
|
||||
project_id: projects[6].id,
|
||||
},
|
||||
{
|
||||
title: 'Photography Equipment Wishlist',
|
||||
content: 'Camera gear to consider:\n- Canon EOS R6 Mark II\n- 24-70mm f/2.8 lens\n- 85mm f/1.8 portrait lens\n- Tripod: Manfrotto MT055CXPRO4\n- Editing software: Lightroom + Photoshop\n\nLearning resources:\n- Sean Tucker YouTube channel\n- Peter McKinnon tutorials\n- Local photography meetups',
|
||||
content:
|
||||
'Camera gear to consider:\n- Canon EOS R6 Mark II\n- 24-70mm f/2.8 lens\n- 85mm f/1.8 portrait lens\n- Tripod: Manfrotto MT055CXPRO4\n- Editing software: Lightroom + Photoshop\n\nLearning resources:\n- Sean Tucker YouTube channel\n- Peter McKinnon tutorials\n- Local photography meetups',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[7].id
|
||||
project_id: projects[7].id,
|
||||
},
|
||||
{
|
||||
title: 'Book Recommendations',
|
||||
content: 'To read:\n- "Deep Work" by Cal Newport\n- "The Lean Startup" by Eric Ries\n- "Atomic Habits" by James Clear\n- "Clean Code" by Robert Martin\n- "The Psychology of Money" by Morgan Housel\n- "Educated" by Tara Westover\n- "Sapiens" by Yuval Noah Harari',
|
||||
user_id: testUser.id
|
||||
content:
|
||||
'To read:\n- "Deep Work" by Cal Newport\n- "The Lean Startup" by Eric Ries\n- "Atomic Habits" by James Clear\n- "Clean Code" by Robert Martin\n- "The Psychology of Money" by Morgan Housel\n- "Educated" by Tara Westover\n- "Sapiens" by Yuval Noah Harari',
|
||||
user_id: testUser.id,
|
||||
},
|
||||
{
|
||||
title: 'Recipe Ideas',
|
||||
content: 'Meals to try:\n- Mediterranean quinoa bowl\n- Thai green curry\n- Homemade pizza\n- Greek lemon chicken\n- Mushroom risotto\n- Korean bulgogi\n- Mexican street corn salad\n- Indian butter chicken\n- Japanese ramen',
|
||||
user_id: testUser.id
|
||||
content:
|
||||
'Meals to try:\n- Mediterranean quinoa bowl\n- Thai green curry\n- Homemade pizza\n- Greek lemon chicken\n- Mushroom risotto\n- Korean bulgogi\n- Mexican street corn salad\n- Indian butter chicken\n- Japanese ramen',
|
||||
user_id: testUser.id,
|
||||
},
|
||||
{
|
||||
title: 'Business Ideas',
|
||||
content: 'Potential side businesses:\n- Web development consulting\n- Online course creation\n- Photography services\n- Productivity coaching\n- Technical writing\n\nRevenue streams to explore:\n- Subscription services\n- One-time consulting\n- Product sales\n- Affiliate marketing',
|
||||
content:
|
||||
'Potential side businesses:\n- Web development consulting\n- Online course creation\n- Photography services\n- Productivity coaching\n- Technical writing\n\nRevenue streams to explore:\n- Subscription services\n- One-time consulting\n- Product sales\n- Affiliate marketing',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[4].id
|
||||
project_id: projects[4].id,
|
||||
},
|
||||
{
|
||||
title: 'Fitness Goals & Progress',
|
||||
content: 'Current stats:\n- Weight: 180 lbs\n- Body fat: 18%\n- Bench press: 185 lbs\n- Squat: 225 lbs\n- Deadlift: 275 lbs\n\nGoals (90 days):\n- Weight: 175 lbs\n- Body fat: 15%\n- Bench press: 205 lbs\n- Squat: 255 lbs\n- Deadlift: 315 lbs',
|
||||
content:
|
||||
'Current stats:\n- Weight: 180 lbs\n- Body fat: 18%\n- Bench press: 185 lbs\n- Squat: 225 lbs\n- Deadlift: 275 lbs\n\nGoals (90 days):\n- Weight: 175 lbs\n- Body fat: 15%\n- Bench press: 205 lbs\n- Squat: 255 lbs\n- Deadlift: 315 lbs',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[3].id
|
||||
project_id: projects[3].id,
|
||||
},
|
||||
{
|
||||
title: 'Weekly Meal Prep Ideas',
|
||||
content: 'Prep schedule:\nSunday: Protein prep (chicken, fish, tofu)\nMonday: Vegetable chopping\nWednesday: Mid-week refresh\n\nMeal rotation:\n- Breakfast: Overnight oats, egg muffins\n- Lunch: Buddha bowls, salads\n- Dinner: Stir-fries, sheet pan meals\n- Snacks: Greek yogurt, nuts, fruit',
|
||||
content:
|
||||
'Prep schedule:\nSunday: Protein prep (chicken, fish, tofu)\nMonday: Vegetable chopping\nWednesday: Mid-week refresh\n\nMeal rotation:\n- Breakfast: Overnight oats, egg muffins\n- Lunch: Buddha bowls, salads\n- Dinner: Stir-fries, sheet pan meals\n- Snacks: Greek yogurt, nuts, fruit',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[13].id
|
||||
project_id: projects[13].id,
|
||||
},
|
||||
{
|
||||
title: 'Smart Home Device List',
|
||||
content: 'Devices to install:\n- Smart thermostat (Nest)\n- Smart doorbell (Ring)\n- Smart locks (August)\n- Smart lights (Philips Hue)\n- Smart speakers (Echo Dot)\n- Security cameras (Arlo)\n- Smart switches (TP-Link Kasa)\n\nEstimated cost: $2,500\nInstallation timeline: 3 weeks',
|
||||
content:
|
||||
'Devices to install:\n- Smart thermostat (Nest)\n- Smart doorbell (Ring)\n- Smart locks (August)\n- Smart lights (Philips Hue)\n- Smart speakers (Echo Dot)\n- Security cameras (Arlo)\n- Smart switches (TP-Link Kasa)\n\nEstimated cost: $2,500\nInstallation timeline: 3 weeks',
|
||||
user_id: testUser.id,
|
||||
project_id: projects[14].id
|
||||
}
|
||||
project_id: projects[14].id,
|
||||
},
|
||||
]);
|
||||
|
||||
// Create inbox items
|
||||
|
|
@ -546,103 +703,103 @@ async function seedDatabase() {
|
|||
{
|
||||
content: 'Research new project management tools',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Plan team building activity for Q4',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Look into cloud storage solutions',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider learning TypeScript',
|
||||
user_id: testUser.id,
|
||||
processed: true
|
||||
processed: true,
|
||||
},
|
||||
{
|
||||
content: 'Update emergency contact information',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Research sustainable investing options',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Look into ergonomic desk setup',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider getting a pet',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Research meditation retreats',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Look into renewable energy for home',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider starting a podcast',
|
||||
user_id: testUser.id,
|
||||
processed: true
|
||||
processed: true,
|
||||
},
|
||||
{
|
||||
content: 'Research local volunteer opportunities',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Look into professional coaching',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider learning a musical instrument',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Research minimalism lifestyle',
|
||||
user_id: testUser.id,
|
||||
processed: true
|
||||
processed: true,
|
||||
},
|
||||
{
|
||||
content: 'Look into starting a garden',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider learning sign language',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Research passive income strategies',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Look into digital nomad lifestyle',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
processed: false,
|
||||
},
|
||||
{
|
||||
content: 'Consider getting professional headshots',
|
||||
user_id: testUser.id,
|
||||
processed: false
|
||||
}
|
||||
processed: false,
|
||||
},
|
||||
]);
|
||||
|
||||
console.log('✨ Database seeding completed successfully!');
|
||||
|
|
@ -656,13 +813,14 @@ async function seedDatabase() {
|
|||
- 20 inbox items`);
|
||||
|
||||
console.log('\n🚀 You can now:');
|
||||
console.log('- Login with test@tududi.com / password123 to see test data');
|
||||
console.log(
|
||||
'- Login with test@tududi.com / password123 to see test data'
|
||||
);
|
||||
console.log('- Your original account data is preserved and untouched');
|
||||
console.log('- Explore the Today view with various task statuses');
|
||||
console.log('- Test task editing, priority changes, etc.');
|
||||
console.log('- View projects with different completion states');
|
||||
console.log('- Test the task timeline feature');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error seeding database:', error);
|
||||
}
|
||||
|
|
@ -672,10 +830,12 @@ module.exports = { seedDatabase };
|
|||
|
||||
// Allow running directly
|
||||
if (require.main === module) {
|
||||
seedDatabase().then(() => {
|
||||
seedDatabase()
|
||||
.then(() => {
|
||||
console.log('🏁 Seeding finished');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 Seeding failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,128 +2,600 @@
|
|||
function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
||||
return [
|
||||
// Website Redesign Project Tasks (Project 0)
|
||||
{ name: 'Research competitor websites', project_id: projects[0].id, priority: 1, status: 2, completed_at: getPastDate(5) },
|
||||
{ name: 'Create wireframes for homepage', project_id: projects[0].id, priority: 2, status: 1 },
|
||||
{ name: 'Design new color palette', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Write content for About page', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Set up staging environment', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(7) },
|
||||
{ name: 'Optimize images for web', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Implement responsive design', project_id: projects[0].id, priority: 2, status: 0 },
|
||||
{ name: 'Test cross-browser compatibility', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Setup Google Analytics', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Create contact form', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Write SEO meta descriptions', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Design mobile navigation', project_id: projects[0].id, priority: 2, status: 0 },
|
||||
{ name: 'Create footer section', project_id: projects[0].id, priority: 0, status: 0 },
|
||||
{ name: 'Add social media icons', project_id: projects[0].id, priority: 0, status: 0 },
|
||||
{ name: 'Setup SSL certificate', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(5) },
|
||||
{
|
||||
name: 'Research competitor websites',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Create wireframes for homepage',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Design new color palette',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Write content for About page',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up staging environment',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Optimize images for web',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Implement responsive design',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Test cross-browser compatibility',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup Google Analytics',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create contact form',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Write SEO meta descriptions',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Design mobile navigation',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create footer section',
|
||||
project_id: projects[0].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Add social media icons',
|
||||
project_id: projects[0].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup SSL certificate',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(5),
|
||||
},
|
||||
|
||||
// Learn React Native Project Tasks (Project 1)
|
||||
{ name: 'Complete React Native tutorial', project_id: projects[1].id, priority: 2, status: 1 },
|
||||
{ name: 'Build first mobile app', project_id: projects[1].id, priority: 2, status: 0 },
|
||||
{ name: 'Learn about navigation', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Study state management', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Practice with APIs', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Setup development environment', project_id: projects[1].id, priority: 2, status: 2, completed_at: getPastDate(10) },
|
||||
{ name: 'Learn about debugging tools', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Study push notifications', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Learn about app deployment', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{ name: 'Practice with AsyncStorage', project_id: projects[1].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Complete React Native tutorial',
|
||||
project_id: projects[1].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Build first mobile app',
|
||||
project_id: projects[1].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn about navigation',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Study state management',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Practice with APIs',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup development environment',
|
||||
project_id: projects[1].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Learn about debugging tools',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Study push notifications',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn about app deployment',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Practice with AsyncStorage',
|
||||
project_id: projects[1].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Home Renovation Project Tasks (Project 2)
|
||||
{ name: 'Get quotes from contractors', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(14) },
|
||||
{ name: 'Choose kitchen tiles', project_id: projects[2].id, priority: 1, status: 0 },
|
||||
{ name: 'Order new appliances', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(21) },
|
||||
{ name: 'Plan bathroom layout', project_id: projects[2].id, priority: 1, status: 0 },
|
||||
{ name: 'Select paint colors', project_id: projects[2].id, priority: 1, status: 1 },
|
||||
{ name: 'Research flooring options', project_id: projects[2].id, priority: 1, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Schedule plumbing inspection', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(10) },
|
||||
{ name: 'Order cabinet hardware', project_id: projects[2].id, priority: 0, status: 0 },
|
||||
{ name: 'Plan electrical upgrades', project_id: projects[2].id, priority: 2, status: 0 },
|
||||
{ name: 'Choose lighting fixtures', project_id: projects[2].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Get quotes from contractors',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(14),
|
||||
},
|
||||
{
|
||||
name: 'Choose kitchen tiles',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Order new appliances',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(21),
|
||||
},
|
||||
{
|
||||
name: 'Plan bathroom layout',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Select paint colors',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Research flooring options',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Schedule plumbing inspection',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Order cabinet hardware',
|
||||
project_id: projects[2].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan electrical upgrades',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Choose lighting fixtures',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Fitness Challenge Project Tasks (Project 3)
|
||||
{ name: 'Create workout schedule', project_id: projects[3].id, priority: 2, status: 2, completed_at: getPastDate(10) },
|
||||
{ name: 'Track daily calories', project_id: projects[3].id, priority: 1, status: 1 },
|
||||
{ name: 'Join gym membership', project_id: projects[3].id, priority: 2, status: 2, completed_at: getPastDate(15) },
|
||||
{ name: 'Buy workout equipment', project_id: projects[3].id, priority: 1, status: 0 },
|
||||
{ name: 'Plan meal prep schedule', project_id: projects[3].id, priority: 1, status: 1 },
|
||||
{ name: 'Find workout buddy', project_id: projects[3].id, priority: 0, status: 0 },
|
||||
{ name: 'Set up fitness tracking app', project_id: projects[3].id, priority: 1, status: 2, completed_at: getPastDate(8) },
|
||||
{ name: 'Schedule body composition test', project_id: projects[3].id, priority: 1, status: 0, due_date: getRandomDate(7) },
|
||||
{ name: 'Research supplements', project_id: projects[3].id, priority: 0, status: 0 },
|
||||
{ name: 'Plan recovery routine', project_id: projects[3].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Create workout schedule',
|
||||
project_id: projects[3].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Track daily calories',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Join gym membership',
|
||||
project_id: projects[3].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(15),
|
||||
},
|
||||
{
|
||||
name: 'Buy workout equipment',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan meal prep schedule',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Find workout buddy',
|
||||
project_id: projects[3].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up fitness tracking app',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(8),
|
||||
},
|
||||
{
|
||||
name: 'Schedule body composition test',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Research supplements',
|
||||
project_id: projects[3].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan recovery routine',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Side Business Project Tasks (Project 4)
|
||||
{ name: 'Define service offerings', project_id: projects[4].id, priority: 2, status: 1 },
|
||||
{ name: 'Create business website', project_id: projects[4].id, priority: 2, status: 0 },
|
||||
{ name: 'Set up payment processing', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Network with potential clients', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Register business name', project_id: projects[4].id, priority: 2, status: 2, completed_at: getPastDate(12) },
|
||||
{ name: 'Open business bank account', project_id: projects[4].id, priority: 2, status: 0, due_date: getRandomDate(5) },
|
||||
{ name: 'Create marketing materials', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Set up accounting system', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Research competitors', project_id: projects[4].id, priority: 1, status: 1 },
|
||||
{ name: 'Create pricing strategy', project_id: projects[4].id, priority: 2, status: 0 },
|
||||
{
|
||||
name: 'Define service offerings',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Create business website',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up payment processing',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Network with potential clients',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Register business name',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(12),
|
||||
},
|
||||
{
|
||||
name: 'Open business bank account',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Create marketing materials',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up accounting system',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research competitors',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Create pricing strategy',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Investment Portfolio Project Tasks (Project 5)
|
||||
{ name: 'Research investment platforms', project_id: projects[5].id, priority: 2, status: 1 },
|
||||
{ name: 'Open brokerage account', project_id: projects[5].id, priority: 2, status: 0, due_date: getRandomDate(10) },
|
||||
{ name: 'Study different asset classes', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Set investment goals', project_id: projects[5].id, priority: 2, status: 2, completed_at: getPastDate(7) },
|
||||
{ name: 'Create risk assessment', project_id: projects[5].id, priority: 1, status: 1 },
|
||||
{ name: 'Research ETFs and mutual funds', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Set up automatic investing', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Learn about tax implications', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Create emergency fund', project_id: projects[5].id, priority: 2, status: 1 },
|
||||
{ name: 'Review portfolio monthly', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Research investment platforms',
|
||||
project_id: projects[5].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Open brokerage account',
|
||||
project_id: projects[5].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Study different asset classes',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set investment goals',
|
||||
project_id: projects[5].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Create risk assessment',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Research ETFs and mutual funds',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up automatic investing',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn about tax implications',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create emergency fund',
|
||||
project_id: projects[5].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Review portfolio monthly',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Europe Trip 2024 Project Tasks (Project 6)
|
||||
{ name: 'Research destinations', project_id: projects[6].id, priority: 2, status: 1 },
|
||||
{ name: 'Book flights', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(30) },
|
||||
{ name: 'Reserve accommodations', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(45) },
|
||||
{ name: 'Apply for passport renewal', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(60) },
|
||||
{ name: 'Plan itinerary', project_id: projects[6].id, priority: 1, status: 0 },
|
||||
{ name: 'Research local customs', project_id: projects[6].id, priority: 0, status: 0 },
|
||||
{ name: 'Learn basic phrases', project_id: projects[6].id, priority: 0, status: 0 },
|
||||
{ name: 'Check visa requirements', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(90) },
|
||||
{ name: 'Get travel insurance', project_id: projects[6].id, priority: 1, status: 0, due_date: getRandomDate(21) },
|
||||
{ name: 'Plan budget', project_id: projects[6].id, priority: 1, status: 1 },
|
||||
{
|
||||
name: 'Research destinations',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Book flights',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Reserve accommodations',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(45),
|
||||
},
|
||||
{
|
||||
name: 'Apply for passport renewal',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(60),
|
||||
},
|
||||
{
|
||||
name: 'Plan itinerary',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research local customs',
|
||||
project_id: projects[6].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn basic phrases',
|
||||
project_id: projects[6].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Check visa requirements',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(90),
|
||||
},
|
||||
{
|
||||
name: 'Get travel insurance',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(21),
|
||||
},
|
||||
{
|
||||
name: 'Plan budget',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
|
||||
// Photography Mastery Project Tasks (Project 7)
|
||||
{ name: 'Learn camera basics', project_id: projects[7].id, priority: 2, status: 2, completed_at: getPastDate(14) },
|
||||
{ name: 'Practice composition rules', project_id: projects[7].id, priority: 1, status: 1 },
|
||||
{ name: 'Study lighting techniques', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Learn photo editing', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Build portfolio', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Join photography community', project_id: projects[7].id, priority: 0, status: 0 },
|
||||
{ name: 'Experiment with different styles', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Learn about gear', project_id: projects[7].id, priority: 0, status: 0 },
|
||||
{ name: 'Practice street photography', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Study famous photographers', project_id: projects[7].id, priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Learn camera basics',
|
||||
project_id: projects[7].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(14),
|
||||
},
|
||||
{
|
||||
name: 'Practice composition rules',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Study lighting techniques',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn photo editing',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Build portfolio',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Join photography community',
|
||||
project_id: projects[7].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Experiment with different styles',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn about gear',
|
||||
project_id: projects[7].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Practice street photography',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Study famous photographers',
|
||||
project_id: projects[7].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Non-project tasks - Personal productivity and life management
|
||||
{ name: 'Call dentist for appointment', priority: 1, status: 0, due_date: getRandomDate(3) },
|
||||
{
|
||||
name: 'Call dentist for appointment',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(3),
|
||||
},
|
||||
{ name: 'Buy groceries for the week', priority: 0, status: 0 },
|
||||
{ name: 'Clean garage', priority: 0, status: 0 },
|
||||
{ name: 'Update resume', priority: 1, status: 0 },
|
||||
{ name: 'Read "Atomic Habits" book', priority: 0, status: 0 },
|
||||
{ name: 'Organize digital photos', priority: 0, status: 0 },
|
||||
{ name: 'Schedule car maintenance', priority: 1, status: 0, due_date: getRandomDate(7) },
|
||||
{
|
||||
name: 'Schedule car maintenance',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{ name: 'Plan weekend trip', priority: 0, status: 0 },
|
||||
{ name: 'Learn basic Spanish', priority: 0, status: 0 },
|
||||
{ name: 'Backup computer files', priority: 1, status: 0 },
|
||||
{ name: 'Donate old clothes', priority: 0, status: 0 },
|
||||
{ name: 'Research investment options', priority: 1, status: 0 },
|
||||
{ name: 'Call mom and dad', priority: 1, status: 0, due_date: getRandomDate(2) },
|
||||
{
|
||||
name: 'Call mom and dad',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(2),
|
||||
},
|
||||
{ name: 'Fix leaky faucet', priority: 0, status: 0 },
|
||||
{ name: 'Try new restaurant', priority: 0, status: 0 },
|
||||
{ name: 'Update LinkedIn profile', priority: 1, status: 0 },
|
||||
{ name: 'Review monthly expenses', priority: 1, status: 0, due_date: getRandomDate(5) },
|
||||
{
|
||||
name: 'Review monthly expenses',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(5),
|
||||
},
|
||||
{ name: 'Organize desk workspace', priority: 0, status: 0 },
|
||||
{ name: 'Plan birthday party', priority: 1, status: 0 },
|
||||
{ name: 'Research new phone', priority: 0, status: 0 },
|
||||
{ name: 'Schedule eye exam', priority: 1, status: 0, due_date: getRandomDate(14) },
|
||||
{
|
||||
name: 'Schedule eye exam',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(14),
|
||||
},
|
||||
{ name: 'Update emergency contacts', priority: 1, status: 0 },
|
||||
{ name: 'Clean out email inbox', priority: 0, status: 0 },
|
||||
{ name: 'Research vacation destinations', priority: 0, status: 0 },
|
||||
|
|
@ -133,15 +605,30 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
{ name: 'Update password manager', priority: 1, status: 0 },
|
||||
{ name: 'Organize physical documents', priority: 0, status: 0 },
|
||||
{ name: 'Research new coffee maker', priority: 0, status: 0 },
|
||||
{ name: 'Schedule oil change', priority: 1, status: 0, due_date: getRandomDate(10) },
|
||||
{
|
||||
name: 'Schedule oil change',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(10),
|
||||
},
|
||||
{ name: 'Plan gift for anniversary', priority: 1, status: 0 },
|
||||
{ name: 'Research home security system', priority: 0, status: 0 },
|
||||
{ name: 'Update will and testament', priority: 2, status: 0, due_date: getRandomDate(30) },
|
||||
{
|
||||
name: 'Update will and testament',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{ name: 'Learn keyboard shortcuts', priority: 0, status: 0 },
|
||||
{ name: 'Research meditation apps', priority: 0, status: 0 },
|
||||
{ name: 'Plan date night', priority: 1, status: 0 },
|
||||
{ name: 'Research side income ideas', priority: 0, status: 0 },
|
||||
{ name: 'Update insurance policies', priority: 1, status: 0, due_date: getRandomDate(21) },
|
||||
{
|
||||
name: 'Update insurance policies',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(21),
|
||||
},
|
||||
{ name: 'Learn new cooking recipe', priority: 0, status: 0 },
|
||||
{ name: 'Research productivity tools', priority: 0, status: 0 },
|
||||
{ name: 'Plan garden for spring', priority: 0, status: 0 },
|
||||
|
|
@ -149,7 +636,12 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
{ name: 'Update social media profiles', priority: 0, status: 0 },
|
||||
{ name: 'Plan weekend activities', priority: 0, status: 0 },
|
||||
{ name: 'Research new podcast', priority: 0, status: 0 },
|
||||
{ name: 'Schedule annual checkup', priority: 1, status: 0, due_date: getRandomDate(45) },
|
||||
{
|
||||
name: 'Schedule annual checkup',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(45),
|
||||
},
|
||||
{ name: 'Learn new Excel functions', priority: 0, status: 0 },
|
||||
{ name: 'Research retirement planning', priority: 1, status: 0 },
|
||||
{ name: 'Plan family reunion', priority: 1, status: 0 },
|
||||
|
|
@ -158,26 +650,126 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
{ name: 'Plan workout routine', priority: 1, status: 0 },
|
||||
|
||||
// Completed tasks for metrics - spread across different dates
|
||||
{ name: 'Pay monthly bills', priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Submit expense reports', priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Weekly team meeting', priority: 1, status: 2, completed_at: getPastDate(2) },
|
||||
{ name: 'Review project proposal', priority: 2, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Update LinkedIn profile', priority: 0, status: 2, completed_at: getPastDate(4) },
|
||||
{ name: 'Clean kitchen', priority: 0, status: 2, completed_at: getPastDate(5) },
|
||||
{ name: 'Water plants', priority: 0, status: 2, completed_at: getPastDate(6) },
|
||||
{ name: 'Grocery shopping', priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Call insurance company', priority: 1, status: 2, completed_at: getPastDate(2) },
|
||||
{ name: 'Send birthday card', priority: 0, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Fix printer issue', priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Review budget', priority: 1, status: 2, completed_at: getPastDate(4) },
|
||||
{ name: 'Attend networking event', priority: 1, status: 2, completed_at: getPastDate(5) },
|
||||
{ name: 'Complete online training', priority: 1, status: 2, completed_at: getPastDate(6) },
|
||||
{ name: 'Schedule vet appointment', priority: 1, status: 2, completed_at: getPastDate(2) },
|
||||
{ name: 'Buy gift for colleague', priority: 0, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Update calendar', priority: 0, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Research vacation spots', priority: 0, status: 2, completed_at: getPastDate(4) },
|
||||
{ name: 'Backup important files', priority: 1, status: 2, completed_at: getPastDate(5) },
|
||||
{ name: 'Clean bathroom', priority: 0, status: 2, completed_at: getPastDate(1) },
|
||||
{
|
||||
name: 'Pay monthly bills',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Submit expense reports',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Weekly team meeting',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
},
|
||||
{
|
||||
name: 'Review project proposal',
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Update LinkedIn profile',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(4),
|
||||
},
|
||||
{
|
||||
name: 'Clean kitchen',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Water plants',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(6),
|
||||
},
|
||||
{
|
||||
name: 'Grocery shopping',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Call insurance company',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
},
|
||||
{
|
||||
name: 'Send birthday card',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Fix printer issue',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Review budget',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(4),
|
||||
},
|
||||
{
|
||||
name: 'Attend networking event',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Complete online training',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(6),
|
||||
},
|
||||
{
|
||||
name: 'Schedule vet appointment',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
},
|
||||
{
|
||||
name: 'Buy gift for colleague',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Update calendar',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Research vacation spots',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(4),
|
||||
},
|
||||
{
|
||||
name: 'Backup important files',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Clean bathroom',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
|
||||
// Recurring tasks
|
||||
{
|
||||
|
|
@ -187,7 +779,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
due_date: new Date(),
|
||||
project_id: projects[0].id
|
||||
project_id: projects[0].id,
|
||||
},
|
||||
{
|
||||
name: 'Weekly grocery shopping',
|
||||
|
|
@ -196,7 +788,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 6, // Saturday
|
||||
due_date: getRandomDate(7)
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Monthly budget review',
|
||||
|
|
@ -205,7 +797,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_month_day: 1,
|
||||
due_date: getRandomDate(30)
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Weekly meal prep',
|
||||
|
|
@ -214,7 +806,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 0, // Sunday
|
||||
due_date: getRandomDate(7)
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Daily workout',
|
||||
|
|
@ -223,7 +815,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
due_date: new Date(),
|
||||
project_id: projects[3].id
|
||||
project_id: projects[3].id,
|
||||
},
|
||||
{
|
||||
name: 'Weekly house cleaning',
|
||||
|
|
@ -232,11 +824,16 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 6, // Saturday
|
||||
due_date: getRandomDate(7)
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
|
||||
// Waiting and someday tasks
|
||||
{ name: 'Wait for contractor estimate', priority: 1, status: 4, project_id: projects[2].id },
|
||||
{
|
||||
name: 'Wait for contractor estimate',
|
||||
priority: 1,
|
||||
status: 4,
|
||||
project_id: projects[2].id,
|
||||
},
|
||||
{ name: 'Learn advanced photography', priority: 0, status: 0 },
|
||||
{ name: 'Write a book', priority: 0, status: 0 },
|
||||
{ name: 'Learn to play guitar', priority: 0, status: 0 },
|
||||
|
|
@ -245,7 +842,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
|
|||
{ name: 'Learn rock climbing', priority: 0, status: 0 },
|
||||
{ name: 'Start a podcast', priority: 0, status: 0 },
|
||||
{ name: 'Learn wine tasting', priority: 0, status: 0 },
|
||||
{ name: 'Take dance lessons', priority: 0, status: 0 }
|
||||
{ name: 'Take dance lessons', priority: 0, status: 0 },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Implement data validation',
|
||||
'Create audit logging',
|
||||
'Setup health checks',
|
||||
'Implement graceful shutdowns'
|
||||
'Implement graceful shutdowns',
|
||||
];
|
||||
|
||||
// Personal development and learning tasks
|
||||
|
|
@ -95,7 +95,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Learn about data visualization',
|
||||
'Study cybersecurity fundamentals',
|
||||
'Learn about scalability patterns',
|
||||
'Study database design principles'
|
||||
'Study database design principles',
|
||||
];
|
||||
|
||||
// Health and fitness tasks
|
||||
|
|
@ -139,7 +139,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Research healthy recipes',
|
||||
'Update meal planning app',
|
||||
'Schedule workout with trainer',
|
||||
'Join new fitness class'
|
||||
'Join new fitness class',
|
||||
];
|
||||
|
||||
// Home and family tasks
|
||||
|
|
@ -183,7 +183,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Clean dryer vent',
|
||||
'Organize medicine cabinet',
|
||||
'Check expiration dates on medications',
|
||||
'Update emergency contact list'
|
||||
'Update emergency contact list',
|
||||
];
|
||||
|
||||
// Financial and administrative tasks
|
||||
|
|
@ -217,7 +217,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Review online banking security',
|
||||
'Setup automatic bill pay',
|
||||
'Research high-yield savings',
|
||||
'Update direct deposit info'
|
||||
'Update direct deposit info',
|
||||
];
|
||||
|
||||
// Social and relationship tasks
|
||||
|
|
@ -244,14 +244,14 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Schedule catch-up with old friend',
|
||||
'Write recommendation letter',
|
||||
'Plan anniversary celebration',
|
||||
'Organize children\'s playdate',
|
||||
"Organize children's playdate",
|
||||
'Schedule babysitter',
|
||||
'Plan family photo session',
|
||||
'Organize neighborhood BBQ',
|
||||
'Plan holiday gathering',
|
||||
'Schedule couple\'s therapy',
|
||||
"Schedule couple's therapy",
|
||||
'Plan birthday celebration',
|
||||
'Organize team building activity'
|
||||
'Organize team building activity',
|
||||
];
|
||||
|
||||
// Creative and hobby tasks
|
||||
|
|
@ -285,7 +285,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Practice stand-up comedy',
|
||||
'Work on graphic design',
|
||||
'Learn new language phrases',
|
||||
'Practice mindful writing'
|
||||
'Practice mindful writing',
|
||||
];
|
||||
|
||||
// Travel and adventure tasks
|
||||
|
|
@ -314,7 +314,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
'Check weather forecast',
|
||||
'Pack travel documents',
|
||||
'Arrange airport transportation',
|
||||
'Update travel blog'
|
||||
'Update travel blog',
|
||||
];
|
||||
|
||||
// All task categories combined
|
||||
|
|
@ -326,119 +326,553 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
...financialTasks,
|
||||
...socialTasks,
|
||||
...creativeeTasks,
|
||||
...travelTasks
|
||||
...travelTasks,
|
||||
];
|
||||
|
||||
// Create base task data with existing project tasks
|
||||
const baseTaskData = [
|
||||
// Website Redesign Project (triggers collaboration, urgent deadlines)
|
||||
{ name: 'Research competitor websites', project_id: projects[0].id, priority: 1, status: 2, completed_at: getPastDate(5) },
|
||||
{ name: 'Create wireframes for homepage', project_id: projects[0].id, priority: 2, status: 1 },
|
||||
{ name: 'Design new color palette', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Write content for About page', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Set up staging environment', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(3) }, // Urgent deadline
|
||||
{ name: 'Optimize images for web', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Implement responsive design', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(7) },
|
||||
{ name: 'Test cross-browser compatibility', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Setup Google Analytics', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Create contact form', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Write SEO meta descriptions', project_id: projects[0].id, priority: 1, status: 0 },
|
||||
{ name: 'Design mobile navigation', project_id: projects[0].id, priority: 2, status: 0 },
|
||||
{ name: 'Create footer section', project_id: projects[0].id, priority: 0, status: 0 },
|
||||
{ name: 'Add social media icons', project_id: projects[0].id, priority: 0, status: 0 },
|
||||
{ name: 'Setup SSL certificate', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(2) }, // Very urgent
|
||||
{
|
||||
name: 'Research competitor websites',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Create wireframes for homepage',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Design new color palette',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Write content for About page',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Set up staging environment',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(3),
|
||||
}, // Urgent deadline
|
||||
{
|
||||
name: 'Optimize images for web',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Implement responsive design',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Test cross-browser compatibility',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup Google Analytics',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create contact form',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Write SEO meta descriptions',
|
||||
project_id: projects[0].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Design mobile navigation',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create footer section',
|
||||
project_id: projects[0].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Add social media icons',
|
||||
project_id: projects[0].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup SSL certificate',
|
||||
project_id: projects[0].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(2),
|
||||
}, // Very urgent
|
||||
|
||||
// Europe Trip 2024 - triggers travel planning AI features
|
||||
{ name: 'Research flight options to Paris', project_id: projects[6].id, priority: 2, status: 1 },
|
||||
{ name: 'Book hotel in Rome', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(14) },
|
||||
{ name: 'Apply for European travel insurance', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(30) },
|
||||
{ name: 'Learn basic Italian phrases', project_id: projects[6].id, priority: 1, status: 0 },
|
||||
{ name: 'Research train routes between cities', project_id: projects[6].id, priority: 1, status: 0 },
|
||||
{ name: 'Plan museum visits in Paris', project_id: projects[6].id, priority: 1, status: 0 },
|
||||
{ name: 'Book restaurant reservations', project_id: projects[6].id, priority: 1, status: 0 },
|
||||
{ name: 'Pack European travel adapter', project_id: projects[6].id, priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Research flight options to Paris',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Book hotel in Rome',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(14),
|
||||
},
|
||||
{
|
||||
name: 'Apply for European travel insurance',
|
||||
project_id: projects[6].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Learn basic Italian phrases',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research train routes between cities',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan museum visits in Paris',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Book restaurant reservations',
|
||||
project_id: projects[6].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Pack European travel adapter',
|
||||
project_id: projects[6].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Fitness Challenge - triggers health/wellness AI features
|
||||
{ name: 'Track daily protein intake', project_id: projects[3].id, priority: 1, status: 1 },
|
||||
{ name: 'Complete morning cardio workout', project_id: projects[3].id, priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Plan weekly meal prep', project_id: projects[3].id, priority: 1, status: 0 },
|
||||
{ name: 'Schedule body composition scan', project_id: projects[3].id, priority: 1, status: 0, due_date: getRandomDate(7) },
|
||||
{ name: 'Research new workout routines', project_id: projects[3].id, priority: 0, status: 0 },
|
||||
{ name: 'Update fitness tracker goals', project_id: projects[3].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Track daily protein intake',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Complete morning cardio workout',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Plan weekly meal prep',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Schedule body composition scan',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Research new workout routines',
|
||||
project_id: projects[3].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Update fitness tracker goals',
|
||||
project_id: projects[3].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Investment Portfolio - triggers financial AI features
|
||||
{ name: 'Research ESG investment options', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Rebalance portfolio allocation', project_id: projects[5].id, priority: 2, status: 0, due_date: getRandomDate(5) },
|
||||
{ name: 'Review quarterly performance', project_id: projects[5].id, priority: 1, status: 1 },
|
||||
{ name: 'Set up automatic dividend reinvestment', project_id: projects[5].id, priority: 1, status: 0 },
|
||||
{ name: 'Research international market exposure', project_id: projects[5].id, priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Research ESG investment options',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Rebalance portfolio allocation',
|
||||
project_id: projects[5].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Review quarterly performance',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Set up automatic dividend reinvestment',
|
||||
project_id: projects[5].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research international market exposure',
|
||||
project_id: projects[5].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Side Business - triggers entrepreneurship AI features
|
||||
{ name: 'Create business plan document', project_id: projects[4].id, priority: 2, status: 1 },
|
||||
{ name: 'Research target market demographics', project_id: projects[4].id, priority: 2, status: 0 },
|
||||
{ name: 'Design logo and branding', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Setup business social media accounts', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{ name: 'Register domain name', project_id: projects[4].id, priority: 2, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Create pricing strategy', project_id: projects[4].id, priority: 2, status: 0 },
|
||||
{ name: 'Draft service agreements', project_id: projects[4].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Create business plan document',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Research target market demographics',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Design logo and branding',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup business social media accounts',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Register domain name',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Create pricing strategy',
|
||||
project_id: projects[4].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Draft service agreements',
|
||||
project_id: projects[4].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Home Renovation - triggers home improvement AI features
|
||||
{ name: 'Get electrical work permit', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(10) },
|
||||
{ name: 'Choose bathroom tile pattern', project_id: projects[2].id, priority: 1, status: 1 },
|
||||
{ name: 'Schedule plumbing inspection', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(14) },
|
||||
{ name: 'Order kitchen countertops', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(21) },
|
||||
{ name: 'Research energy-efficient appliances', project_id: projects[2].id, priority: 1, status: 0 },
|
||||
{ name: 'Plan kitchen lighting layout', project_id: projects[2].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Get electrical work permit',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Choose bathroom tile pattern',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Schedule plumbing inspection',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(14),
|
||||
},
|
||||
{
|
||||
name: 'Order kitchen countertops',
|
||||
project_id: projects[2].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(21),
|
||||
},
|
||||
{
|
||||
name: 'Research energy-efficient appliances',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan kitchen lighting layout',
|
||||
project_id: projects[2].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Photography Mastery - triggers creative learning AI features
|
||||
{ name: 'Practice portrait lighting techniques', project_id: projects[7].id, priority: 1, status: 1 },
|
||||
{ name: 'Edit last weekend\'s photo shoot', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Research local photography groups', project_id: projects[7].id, priority: 0, status: 0 },
|
||||
{ name: 'Plan golden hour photo session', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{ name: 'Learn advanced Lightroom techniques', project_id: projects[7].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Practice portrait lighting techniques',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: "Edit last weekend's photo shoot",
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research local photography groups',
|
||||
project_id: projects[7].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan golden hour photo session',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Learn advanced Lightroom techniques',
|
||||
project_id: projects[7].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Smart Home Setup - triggers technology AI features
|
||||
{ name: 'Install smart thermostat', project_id: projects[14].id, priority: 2, status: 1 },
|
||||
{ name: 'Configure home security system', project_id: projects[14].id, priority: 2, status: 0, due_date: getRandomDate(7) },
|
||||
{ name: 'Setup voice assistant routines', project_id: projects[14].id, priority: 1, status: 0 },
|
||||
{ name: 'Install smart door locks', project_id: projects[14].id, priority: 2, status: 0 },
|
||||
{ name: 'Configure automated lighting', project_id: projects[14].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Install smart thermostat',
|
||||
project_id: projects[14].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Configure home security system',
|
||||
project_id: projects[14].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Setup voice assistant routines',
|
||||
project_id: projects[14].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Install smart door locks',
|
||||
project_id: projects[14].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Configure automated lighting',
|
||||
project_id: projects[14].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Blog Launch - triggers content creation AI features
|
||||
{ name: 'Write first blog post about productivity', project_id: projects[10].id, priority: 2, status: 1 },
|
||||
{ name: 'Design blog layout and theme', project_id: projects[10].id, priority: 1, status: 0 },
|
||||
{ name: 'Setup email newsletter signup', project_id: projects[10].id, priority: 1, status: 0 },
|
||||
{ name: 'Research SEO keywords for niche', project_id: projects[10].id, priority: 1, status: 0 },
|
||||
{ name: 'Create content calendar for 3 months', project_id: projects[10].id, priority: 2, status: 0 },
|
||||
{
|
||||
name: 'Write first blog post about productivity',
|
||||
project_id: projects[10].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Design blog layout and theme',
|
||||
project_id: projects[10].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Setup email newsletter signup',
|
||||
project_id: projects[10].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research SEO keywords for niche',
|
||||
project_id: projects[10].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Create content calendar for 3 months',
|
||||
project_id: projects[10].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Professional Certification - triggers career development AI features
|
||||
{ name: 'Complete AWS practice exams', project_id: projects[8].id, priority: 2, status: 1 },
|
||||
{ name: 'Schedule certification exam', project_id: projects[8].id, priority: 2, status: 0, due_date: getRandomDate(30) },
|
||||
{ name: 'Review cloud architecture patterns', project_id: projects[8].id, priority: 1, status: 0 },
|
||||
{ name: 'Practice hands-on labs', project_id: projects[8].id, priority: 1, status: 1 },
|
||||
{ name: 'Join AWS study group', project_id: projects[8].id, priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Complete AWS practice exams',
|
||||
project_id: projects[8].id,
|
||||
priority: 2,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Schedule certification exam',
|
||||
project_id: projects[8].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Review cloud architecture patterns',
|
||||
project_id: projects[8].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Practice hands-on labs',
|
||||
project_id: projects[8].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Join AWS study group',
|
||||
project_id: projects[8].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Meal Prep System - triggers nutrition AI features
|
||||
{ name: 'Plan balanced weekly menu', project_id: projects[13].id, priority: 1, status: 1 },
|
||||
{ name: 'Prep vegetables for the week', project_id: projects[13].id, priority: 1, status: 0 },
|
||||
{ name: 'Cook batch of protein sources', project_id: projects[13].id, priority: 1, status: 0 },
|
||||
{ name: 'Calculate macronutrient ratios', project_id: projects[13].id, priority: 1, status: 0 },
|
||||
{ name: 'Research meal prep containers', project_id: projects[13].id, priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Plan balanced weekly menu',
|
||||
project_id: projects[13].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Prep vegetables for the week',
|
||||
project_id: projects[13].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Cook batch of protein sources',
|
||||
project_id: projects[13].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Calculate macronutrient ratios',
|
||||
project_id: projects[13].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research meal prep containers',
|
||||
project_id: projects[13].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Wedding Planning - triggers event planning AI features
|
||||
{ name: 'Book wedding venue', project_id: projects[12].id, priority: 2, status: 2, completed_at: getPastDate(30) },
|
||||
{ name: 'Send save the date cards', project_id: projects[12].id, priority: 2, status: 0, due_date: getRandomDate(60) },
|
||||
{ name: 'Book wedding photographer', project_id: projects[12].id, priority: 2, status: 0, due_date: getRandomDate(45) },
|
||||
{ name: 'Choose wedding cake flavors', project_id: projects[12].id, priority: 1, status: 0 },
|
||||
{ name: 'Plan seating arrangement', project_id: projects[12].id, priority: 1, status: 0 },
|
||||
{ name: 'Book honeymoon flights', project_id: projects[12].id, priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Book wedding venue',
|
||||
project_id: projects[12].id,
|
||||
priority: 2,
|
||||
status: 2,
|
||||
completed_at: getPastDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Send save the date cards',
|
||||
project_id: projects[12].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(60),
|
||||
},
|
||||
{
|
||||
name: 'Book wedding photographer',
|
||||
project_id: projects[12].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(45),
|
||||
},
|
||||
{
|
||||
name: 'Choose wedding cake flavors',
|
||||
project_id: projects[12].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Plan seating arrangement',
|
||||
project_id: projects[12].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Book honeymoon flights',
|
||||
project_id: projects[12].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Garden Makeover - triggers gardening/sustainability AI features
|
||||
{ name: 'Plan vegetable garden layout', project_id: projects[9].id, priority: 1, status: 1 },
|
||||
{ name: 'Order seeds for spring planting', project_id: projects[9].id, priority: 2, status: 0, due_date: getRandomDate(14) },
|
||||
{ name: 'Install drip irrigation system', project_id: projects[9].id, priority: 1, status: 0 },
|
||||
{ name: 'Build raised garden beds', project_id: projects[9].id, priority: 2, status: 0 },
|
||||
{ name: 'Research companion planting', project_id: projects[9].id, priority: 0, status: 0 }
|
||||
{
|
||||
name: 'Plan vegetable garden layout',
|
||||
project_id: projects[9].id,
|
||||
priority: 1,
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
name: 'Order seeds for spring planting',
|
||||
project_id: projects[9].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(14),
|
||||
},
|
||||
{
|
||||
name: 'Install drip irrigation system',
|
||||
project_id: projects[9].id,
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Build raised garden beds',
|
||||
project_id: projects[9].id,
|
||||
priority: 2,
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
name: 'Research companion planting',
|
||||
project_id: projects[9].id,
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
];
|
||||
|
||||
// Generate massive additional tasks
|
||||
|
|
@ -446,7 +880,10 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
|
||||
// Add random tasks from all categories (including old tasks for backlog)
|
||||
for (let i = 0; i < 150; i++) {
|
||||
const taskName = allTaskCategories[Math.floor(Math.random() * allTaskCategories.length)];
|
||||
const taskName =
|
||||
allTaskCategories[
|
||||
Math.floor(Math.random() * allTaskCategories.length)
|
||||
];
|
||||
const hasProject = Math.random() < 0.4; // 40% chance of having a project
|
||||
const hasDueDate = Math.random() < 0.3; // 30% chance of having a due date
|
||||
const isCompleted = Math.random() < 0.08; // 8% chance of being completed
|
||||
|
|
@ -455,11 +892,15 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
name: taskName,
|
||||
priority: getRandomPriority(),
|
||||
status: isCompleted ? 2 : getRandomStatus(),
|
||||
note: Math.random() < 0.1 ? 'Added some notes during planning phase' : null
|
||||
note:
|
||||
Math.random() < 0.1
|
||||
? 'Added some notes during planning phase'
|
||||
: null,
|
||||
};
|
||||
|
||||
if (hasProject) {
|
||||
task.project_id = projects[Math.floor(Math.random() * projects.length)].id;
|
||||
task.project_id =
|
||||
projects[Math.floor(Math.random() * projects.length)].id;
|
||||
}
|
||||
|
||||
if (hasDueDate) {
|
||||
|
|
@ -468,7 +909,9 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
task.due_date = getPastDate(Math.floor(Math.random() * 30) + 1);
|
||||
} else {
|
||||
// Future due date
|
||||
task.due_date = getRandomDate(Math.floor(Math.random() * 60) + 1);
|
||||
task.due_date = getRandomDate(
|
||||
Math.floor(Math.random() * 60) + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -482,15 +925,50 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
// Add specific AI trigger tasks (tasks that should trigger intelligent suggestions)
|
||||
const aiTriggerTasks = [
|
||||
// Overdue tasks (AI should suggest prioritizing)
|
||||
{ name: 'Submit tax documents', priority: 2, status: 0, due_date: getPastDate(5) },
|
||||
{ name: 'Renew car registration', priority: 2, status: 0, due_date: getPastDate(3) },
|
||||
{ name: 'Pay property taxes', priority: 2, status: 0, due_date: getPastDate(10) },
|
||||
{ name: 'Submit insurance claim', priority: 2, status: 0, due_date: getPastDate(7) },
|
||||
{
|
||||
name: 'Submit tax documents',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getPastDate(5),
|
||||
},
|
||||
{
|
||||
name: 'Renew car registration',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Pay property taxes',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getPastDate(10),
|
||||
},
|
||||
{
|
||||
name: 'Submit insurance claim',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getPastDate(7),
|
||||
},
|
||||
|
||||
// High-priority tasks with near deadlines (AI should suggest immediate action)
|
||||
{ name: 'Prepare presentation for CEO', priority: 2, status: 0, due_date: getRandomDate(1) },
|
||||
{ name: 'Submit project proposal', priority: 2, status: 0, due_date: getRandomDate(2) },
|
||||
{ name: 'Complete performance review', priority: 2, status: 0, due_date: getRandomDate(3) },
|
||||
{
|
||||
name: 'Prepare presentation for CEO',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Submit project proposal',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(2),
|
||||
},
|
||||
{
|
||||
name: 'Complete performance review',
|
||||
priority: 2,
|
||||
status: 0,
|
||||
due_date: getRandomDate(3),
|
||||
},
|
||||
|
||||
// Health-related tasks (AI should suggest wellness patterns)
|
||||
{ name: 'Schedule annual checkup', priority: 1, status: 0 },
|
||||
|
|
@ -501,7 +979,11 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
// Financial tasks (AI should suggest money management)
|
||||
{ name: 'Review investment portfolio', priority: 1, status: 0 },
|
||||
{ name: 'Update budget spreadsheet', priority: 1, status: 0 },
|
||||
{ name: 'Research high-yield savings accounts', priority: 0, status: 0 },
|
||||
{
|
||||
name: 'Research high-yield savings accounts',
|
||||
priority: 0,
|
||||
status: 0,
|
||||
},
|
||||
{ name: 'Review insurance coverage', priority: 1, status: 0 },
|
||||
|
||||
// Learning tasks (AI should suggest skill development)
|
||||
|
|
@ -514,7 +996,11 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
{ name: 'Change air filter in HVAC', priority: 0, status: 0 },
|
||||
{ name: 'Test smoke detector batteries', priority: 1, status: 0 },
|
||||
{ name: 'Backup computer files', priority: 1, status: 0 },
|
||||
{ name: 'Update software and security patches', priority: 1, status: 0 },
|
||||
{
|
||||
name: 'Update software and security patches',
|
||||
priority: 1,
|
||||
status: 0,
|
||||
},
|
||||
|
||||
// Social/relationship tasks (AI should suggest work-life balance)
|
||||
{ name: 'Plan anniversary dinner', priority: 1, status: 0 },
|
||||
|
|
@ -535,7 +1021,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
status: 0,
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
due_date: new Date()
|
||||
due_date: new Date(),
|
||||
},
|
||||
{
|
||||
name: 'Review daily priorities',
|
||||
|
|
@ -543,7 +1029,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
status: 0,
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
due_date: new Date()
|
||||
due_date: new Date(),
|
||||
},
|
||||
{
|
||||
name: 'Log daily expenses',
|
||||
|
|
@ -551,7 +1037,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
status: 0,
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
due_date: new Date()
|
||||
due_date: new Date(),
|
||||
},
|
||||
|
||||
// Weekly recurring tasks
|
||||
|
|
@ -562,7 +1048,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 0, // Sunday
|
||||
due_date: getRandomDate(7)
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Weekly house cleaning',
|
||||
|
|
@ -571,7 +1057,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 6, // Saturday
|
||||
due_date: getRandomDate(7)
|
||||
due_date: getRandomDate(7),
|
||||
},
|
||||
{
|
||||
name: 'Weekly team standup',
|
||||
|
|
@ -581,7 +1067,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1, // Monday
|
||||
due_date: getRandomDate(7),
|
||||
project_id: projects[0].id
|
||||
project_id: projects[0].id,
|
||||
},
|
||||
|
||||
// Monthly recurring tasks
|
||||
|
|
@ -592,7 +1078,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_month_day: 1,
|
||||
due_date: getRandomDate(30)
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
{
|
||||
name: 'Monthly backup verification',
|
||||
|
|
@ -601,27 +1087,93 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
|
|||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_month_day: 15,
|
||||
due_date: getRandomDate(30)
|
||||
due_date: getRandomDate(30),
|
||||
},
|
||||
|
||||
// Waiting status tasks (AI should suggest follow-up actions)
|
||||
{ name: 'Wait for contractor estimate', priority: 1, status: 4, project_id: projects[2].id },
|
||||
{
|
||||
name: 'Wait for contractor estimate',
|
||||
priority: 1,
|
||||
status: 4,
|
||||
project_id: projects[2].id,
|
||||
},
|
||||
{ name: 'Wait for insurance approval', priority: 2, status: 4 },
|
||||
{ name: 'Wait for vendor response', priority: 1, status: 4, project_id: projects[0].id },
|
||||
{
|
||||
name: 'Wait for vendor response',
|
||||
priority: 1,
|
||||
status: 4,
|
||||
project_id: projects[0].id,
|
||||
},
|
||||
{ name: 'Wait for medical test results', priority: 1, status: 4 },
|
||||
{ name: 'Wait for loan approval', priority: 2, status: 4 },
|
||||
|
||||
// Recently completed tasks for learning patterns
|
||||
{ name: 'Complete weekly workout goal', priority: 1, status: 2, completed_at: getPastDate(1), project_id: projects[3].id },
|
||||
{ name: 'Finish reading productivity book', priority: 0, status: 2, completed_at: getPastDate(2) },
|
||||
{ name: 'Complete online course module', priority: 1, status: 2, completed_at: getPastDate(1) },
|
||||
{ name: 'Submit weekly report', priority: 1, status: 2, completed_at: getPastDate(1), project_id: projects[0].id },
|
||||
{ name: 'Complete meal prep for week', priority: 1, status: 2, completed_at: getPastDate(1), project_id: projects[13].id },
|
||||
{ name: 'Finish monthly budget', priority: 1, status: 2, completed_at: getPastDate(3) },
|
||||
{ name: 'Complete photography assignment', priority: 1, status: 2, completed_at: getPastDate(2), project_id: projects[7].id },
|
||||
{ name: 'Finish home organization project', priority: 0, status: 2, completed_at: getPastDate(4) },
|
||||
{ name: 'Complete investment research', priority: 1, status: 2, completed_at: getPastDate(5), project_id: projects[5].id },
|
||||
{ name: 'Finish blog post draft', priority: 1, status: 2, completed_at: getPastDate(2), project_id: projects[10].id }
|
||||
{
|
||||
name: 'Complete weekly workout goal',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
project_id: projects[3].id,
|
||||
},
|
||||
{
|
||||
name: 'Finish reading productivity book',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
},
|
||||
{
|
||||
name: 'Complete online course module',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
},
|
||||
{
|
||||
name: 'Submit weekly report',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
project_id: projects[0].id,
|
||||
},
|
||||
{
|
||||
name: 'Complete meal prep for week',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(1),
|
||||
project_id: projects[13].id,
|
||||
},
|
||||
{
|
||||
name: 'Finish monthly budget',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(3),
|
||||
},
|
||||
{
|
||||
name: 'Complete photography assignment',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
project_id: projects[7].id,
|
||||
},
|
||||
{
|
||||
name: 'Finish home organization project',
|
||||
priority: 0,
|
||||
status: 2,
|
||||
completed_at: getPastDate(4),
|
||||
},
|
||||
{
|
||||
name: 'Complete investment research',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(5),
|
||||
project_id: projects[5].id,
|
||||
},
|
||||
{
|
||||
name: 'Finish blog post draft',
|
||||
priority: 1,
|
||||
status: 2,
|
||||
completed_at: getPastDate(2),
|
||||
project_id: projects[10].id,
|
||||
},
|
||||
];
|
||||
|
||||
// Combine all tasks
|
||||
|
|
|
|||
|
|
@ -5,23 +5,20 @@ const yaml = require('js-yaml');
|
|||
// create default quotes
|
||||
const createDefaultQuotes = () => [
|
||||
"Believe you can and you're halfway there.",
|
||||
"The only way to do great work is to love what you do.",
|
||||
'The only way to do great work is to love what you do.',
|
||||
"It always seems impossible until it's done.",
|
||||
"Focus on progress, not perfection.",
|
||||
"One task at a time leads to great accomplishments."
|
||||
'Focus on progress, not perfection.',
|
||||
'One task at a time leads to great accomplishments.',
|
||||
];
|
||||
|
||||
// get quotes file path
|
||||
const getQuotesFilePath = () =>
|
||||
path.join(__dirname, '../config/quotes.yml');
|
||||
const getQuotesFilePath = () => path.join(__dirname, '../config/quotes.yml');
|
||||
|
||||
// Side effect function to check if file exists
|
||||
const fileExists = (filePath) =>
|
||||
fs.existsSync(filePath);
|
||||
const fileExists = (filePath) => fs.existsSync(filePath);
|
||||
|
||||
// Side effect function to read file contents
|
||||
const readFileContents = (filePath) =>
|
||||
fs.readFileSync(filePath, 'utf8');
|
||||
const readFileContents = (filePath) => fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// parse YAML content
|
||||
const parseYamlContent = (content) => {
|
||||
|
|
@ -72,13 +69,12 @@ const loadQuotesFromFile = () => {
|
|||
};
|
||||
|
||||
// get random index
|
||||
const getRandomIndex = (arrayLength) =>
|
||||
Math.floor(Math.random() * arrayLength);
|
||||
const getRandomIndex = (arrayLength) => Math.floor(Math.random() * arrayLength);
|
||||
|
||||
// get random quote from array
|
||||
const getRandomQuoteFromArray = (quotes) => {
|
||||
if (quotes.length === 0) {
|
||||
return "Stay focused and keep going!";
|
||||
return 'Stay focused and keep going!';
|
||||
}
|
||||
|
||||
const randomIndex = getRandomIndex(quotes.length);
|
||||
|
|
@ -86,12 +82,10 @@ const getRandomQuoteFromArray = (quotes) => {
|
|||
};
|
||||
|
||||
// get all quotes
|
||||
const getAllQuotesFromArray = (quotes) =>
|
||||
[...quotes]; // Return copy to maintain immutability
|
||||
const getAllQuotesFromArray = (quotes) => [...quotes]; // Return copy to maintain immutability
|
||||
|
||||
// get quotes count
|
||||
const getQuotesCount = (quotes) =>
|
||||
quotes.length;
|
||||
const getQuotesCount = (quotes) => quotes.length;
|
||||
|
||||
// Initialize quotes on module load
|
||||
let quotes = loadQuotesFromFile();
|
||||
|
|
@ -103,16 +97,13 @@ const reloadQuotes = () => {
|
|||
};
|
||||
|
||||
// get random quote
|
||||
const getRandomQuote = () =>
|
||||
getRandomQuoteFromArray(quotes);
|
||||
const getRandomQuote = () => getRandomQuoteFromArray(quotes);
|
||||
|
||||
// get all quotes
|
||||
const getAllQuotes = () =>
|
||||
getAllQuotesFromArray(quotes);
|
||||
const getAllQuotes = () => getAllQuotesFromArray(quotes);
|
||||
|
||||
// get count
|
||||
const getCount = () =>
|
||||
getQuotesCount(quotes);
|
||||
const getCount = () => getQuotesCount(quotes);
|
||||
|
||||
// Export functional interface
|
||||
module.exports = {
|
||||
|
|
@ -127,5 +118,5 @@ module.exports = {
|
|||
_validateQuotesData: validateQuotesData,
|
||||
_extractQuotes: extractQuotes,
|
||||
_getRandomIndex: getRandomIndex,
|
||||
_getRandomQuoteFromArray: getRandomQuoteFromArray
|
||||
_getRandomQuoteFromArray: getRandomQuoteFromArray,
|
||||
};
|
||||
|
|
@ -5,7 +5,6 @@ const { Op } = require('sequelize');
|
|||
* Service for managing recurring tasks
|
||||
*/
|
||||
class RecurringTaskService {
|
||||
|
||||
/**
|
||||
* Generate new tasks from recurring task templates
|
||||
* @param {number} userId - Optional user ID to limit processing
|
||||
|
|
@ -15,7 +14,7 @@ class RecurringTaskService {
|
|||
try {
|
||||
const whereClause = {
|
||||
recurrence_type: { [Op.ne]: 'none' },
|
||||
status: { [Op.ne]: Task.STATUS.ARCHIVED }
|
||||
status: { [Op.ne]: Task.STATUS.ARCHIVED },
|
||||
};
|
||||
|
||||
if (userId) {
|
||||
|
|
@ -25,14 +24,17 @@ class RecurringTaskService {
|
|||
// Find all recurring tasks that need processing
|
||||
const recurringTasks = await Task.findAll({
|
||||
where: whereClause,
|
||||
order: [['last_generated_date', 'ASC']]
|
||||
order: [['last_generated_date', 'ASC']],
|
||||
});
|
||||
|
||||
const newTasks = [];
|
||||
const now = new Date();
|
||||
|
||||
for (const task of recurringTasks) {
|
||||
const generatedTasks = await this.processRecurringTask(task, now);
|
||||
const generatedTasks = await this.processRecurringTask(
|
||||
task,
|
||||
now
|
||||
);
|
||||
newTasks.push(...generatedTasks);
|
||||
}
|
||||
|
||||
|
|
@ -67,12 +69,15 @@ class RecurringTaskService {
|
|||
user_id: task.user_id,
|
||||
name: task.name,
|
||||
due_date: nextDueDate,
|
||||
project_id: task.project_id
|
||||
}
|
||||
project_id: task.project_id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingTask) {
|
||||
const newTask = await this.createTaskInstance(task, nextDueDate);
|
||||
const newTask = await this.createTaskInstance(
|
||||
task,
|
||||
nextDueDate
|
||||
);
|
||||
newTasks.push(newTask);
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +90,9 @@ class RecurringTaskService {
|
|||
|
||||
// Safety check to prevent infinite loops
|
||||
if (newTasks.length > 100) {
|
||||
console.warn(`Generated 100+ tasks for recurring task ${task.id}, stopping to prevent overflow`);
|
||||
console.warn(
|
||||
`Generated 100+ tasks for recurring task ${task.id}, stopping to prevent overflow`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +118,7 @@ class RecurringTaskService {
|
|||
user_id: template.user_id,
|
||||
project_id: template.project_id,
|
||||
recurrence_type: 'none', // Instances are not recurring themselves
|
||||
recurring_parent_id: template.id // Link to the original recurring task
|
||||
recurring_parent_id: template.id, // Link to the original recurring task
|
||||
};
|
||||
|
||||
return await Task.create(taskData);
|
||||
|
|
@ -125,28 +132,44 @@ class RecurringTaskService {
|
|||
*/
|
||||
static calculateNextDueDate(task, fromDate) {
|
||||
// Handle invalid inputs
|
||||
if (!task || !task.recurrence_type || !fromDate || isNaN(fromDate.getTime())) {
|
||||
if (
|
||||
!task ||
|
||||
!task.recurrence_type ||
|
||||
!fromDate ||
|
||||
isNaN(fromDate.getTime())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseDate = task.completion_based ?
|
||||
(task.last_generated_date || task.created_at) :
|
||||
(task.due_date || task.created_at);
|
||||
const baseDate = task.completion_based
|
||||
? task.last_generated_date || task.created_at
|
||||
: task.due_date || task.created_at;
|
||||
|
||||
// If no base date is available, use fromDate
|
||||
const startDate = baseDate ?
|
||||
new Date(Math.max(fromDate.getTime(), baseDate.getTime())) :
|
||||
new Date(fromDate.getTime());
|
||||
const startDate = baseDate
|
||||
? new Date(Math.max(fromDate.getTime(), baseDate.getTime()))
|
||||
: new Date(fromDate.getTime());
|
||||
|
||||
switch (task.recurrence_type) {
|
||||
case 'daily':
|
||||
return this.calculateDailyRecurrence(startDate, task.recurrence_interval || 1);
|
||||
return this.calculateDailyRecurrence(
|
||||
startDate,
|
||||
task.recurrence_interval || 1
|
||||
);
|
||||
|
||||
case 'weekly':
|
||||
return this.calculateWeeklyRecurrence(startDate, task.recurrence_interval || 1, task.recurrence_weekday);
|
||||
return this.calculateWeeklyRecurrence(
|
||||
startDate,
|
||||
task.recurrence_interval || 1,
|
||||
task.recurrence_weekday
|
||||
);
|
||||
|
||||
case 'monthly':
|
||||
return this.calculateMonthlyRecurrence(startDate, task.recurrence_interval || 1, task.recurrence_month_day);
|
||||
return this.calculateMonthlyRecurrence(
|
||||
startDate,
|
||||
task.recurrence_interval || 1,
|
||||
task.recurrence_month_day
|
||||
);
|
||||
|
||||
case 'monthly_weekday':
|
||||
return this.calculateMonthlyWeekdayRecurrence(
|
||||
|
|
@ -157,7 +180,10 @@ class RecurringTaskService {
|
|||
);
|
||||
|
||||
case 'monthly_last_day':
|
||||
return this.calculateMonthlyLastDayRecurrence(startDate, task.recurrence_interval || 1);
|
||||
return this.calculateMonthlyLastDayRecurrence(
|
||||
startDate,
|
||||
task.recurrence_interval || 1
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
|
|
@ -191,18 +217,21 @@ class RecurringTaskService {
|
|||
const currentWeekday = nextDate.getDay();
|
||||
const daysUntilTarget = (weekday - currentWeekday + 7) % 7;
|
||||
|
||||
if (daysUntilTarget === 0 && nextDate.getTime() === fromDate.getTime()) {
|
||||
if (
|
||||
daysUntilTarget === 0 &&
|
||||
nextDate.getTime() === fromDate.getTime()
|
||||
) {
|
||||
// If today is the target weekday and we're calculating from today, add interval weeks
|
||||
nextDate.setDate(nextDate.getDate() + (interval * 7));
|
||||
nextDate.setDate(nextDate.getDate() + interval * 7);
|
||||
} else {
|
||||
nextDate.setDate(nextDate.getDate() + daysUntilTarget);
|
||||
if (nextDate <= fromDate) {
|
||||
nextDate.setDate(nextDate.getDate() + (interval * 7));
|
||||
nextDate.setDate(nextDate.getDate() + interval * 7);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No specific weekday, just add interval weeks
|
||||
nextDate.setDate(nextDate.getDate() + (interval * 7));
|
||||
nextDate.setDate(nextDate.getDate() + interval * 7);
|
||||
}
|
||||
|
||||
return nextDate;
|
||||
|
|
@ -221,15 +250,19 @@ class RecurringTaskService {
|
|||
|
||||
// Move to target month
|
||||
const targetMonth = nextDate.getUTCMonth() + interval;
|
||||
const targetYear = nextDate.getUTCFullYear() + Math.floor(targetMonth / 12);
|
||||
const targetYear =
|
||||
nextDate.getUTCFullYear() + Math.floor(targetMonth / 12);
|
||||
const finalMonth = targetMonth % 12;
|
||||
|
||||
// Get the max day for the target month
|
||||
const maxDay = new Date(Date.UTC(targetYear, finalMonth + 1, 0)).getUTCDate();
|
||||
const maxDay = new Date(
|
||||
Date.UTC(targetYear, finalMonth + 1, 0)
|
||||
).getUTCDate();
|
||||
const finalDay = Math.min(targetDay, maxDay);
|
||||
|
||||
// Create the new date
|
||||
const result = new Date(Date.UTC(
|
||||
const result = new Date(
|
||||
Date.UTC(
|
||||
targetYear,
|
||||
finalMonth,
|
||||
finalDay,
|
||||
|
|
@ -237,7 +270,8 @@ class RecurringTaskService {
|
|||
fromDate.getUTCMinutes(),
|
||||
fromDate.getUTCSeconds(),
|
||||
fromDate.getUTCMilliseconds()
|
||||
));
|
||||
)
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -250,12 +284,19 @@ class RecurringTaskService {
|
|||
* @param {number} weekOfMonth - Which occurrence in month (1-5)
|
||||
* @returns {Date} Next due date
|
||||
*/
|
||||
static calculateMonthlyWeekdayRecurrence(fromDate, interval, weekday, weekOfMonth) {
|
||||
static calculateMonthlyWeekdayRecurrence(
|
||||
fromDate,
|
||||
interval,
|
||||
weekday,
|
||||
weekOfMonth
|
||||
) {
|
||||
const nextDate = new Date(fromDate);
|
||||
nextDate.setUTCMonth(nextDate.getUTCMonth() + interval);
|
||||
|
||||
// Find the first day of the month
|
||||
const firstOfMonth = new Date(Date.UTC(nextDate.getUTCFullYear(), nextDate.getUTCMonth(), 1));
|
||||
const firstOfMonth = new Date(
|
||||
Date.UTC(nextDate.getUTCFullYear(), nextDate.getUTCMonth(), 1)
|
||||
);
|
||||
const firstWeekday = firstOfMonth.getUTCDay();
|
||||
|
||||
// Calculate the first occurrence of the target weekday
|
||||
|
|
@ -265,7 +306,9 @@ class RecurringTaskService {
|
|||
|
||||
// Add weeks to get to the target week of month
|
||||
const targetDate = new Date(firstOccurrence);
|
||||
targetDate.setUTCDate(firstOccurrence.getUTCDate() + ((weekOfMonth - 1) * 7));
|
||||
targetDate.setUTCDate(
|
||||
firstOccurrence.getUTCDate() + (weekOfMonth - 1) * 7
|
||||
);
|
||||
|
||||
// Make sure we're still in the same month
|
||||
if (targetDate.getUTCMonth() !== nextDate.getUTCMonth()) {
|
||||
|
|
@ -274,7 +317,12 @@ class RecurringTaskService {
|
|||
}
|
||||
|
||||
// Preserve the original time
|
||||
targetDate.setUTCHours(fromDate.getUTCHours(), fromDate.getUTCMinutes(), fromDate.getUTCSeconds(), fromDate.getUTCMilliseconds());
|
||||
targetDate.setUTCHours(
|
||||
fromDate.getUTCHours(),
|
||||
fromDate.getUTCMinutes(),
|
||||
fromDate.getUTCSeconds(),
|
||||
fromDate.getUTCMilliseconds()
|
||||
);
|
||||
|
||||
return targetDate;
|
||||
}
|
||||
|
|
@ -332,9 +380,13 @@ class RecurringTaskService {
|
|||
* @returns {Date} Nth occurrence of weekday in month
|
||||
*/
|
||||
static _getNthWeekdayOfMonth(year, month, weekday, n) {
|
||||
const firstOccurrence = this._getFirstWeekdayOfMonth(year, month, weekday);
|
||||
const firstOccurrence = this._getFirstWeekdayOfMonth(
|
||||
year,
|
||||
month,
|
||||
weekday
|
||||
);
|
||||
const targetDate = new Date(firstOccurrence);
|
||||
targetDate.setDate(firstOccurrence.getDate() + ((n - 1) * 7));
|
||||
targetDate.setDate(firstOccurrence.getDate() + (n - 1) * 7);
|
||||
|
||||
// If target date is in next month, return null
|
||||
if (targetDate.getMonth() !== month) {
|
||||
|
|
@ -388,7 +440,7 @@ class RecurringTaskService {
|
|||
const whereClause = {
|
||||
user_id: task.user_id,
|
||||
name: task.name,
|
||||
due_date: nextDueDate
|
||||
due_date: nextDueDate,
|
||||
};
|
||||
|
||||
// Only add project_id to where clause if it's not null/undefined
|
||||
|
|
@ -399,7 +451,7 @@ class RecurringTaskService {
|
|||
}
|
||||
|
||||
const existingTask = await Task.findOne({
|
||||
where: whereClause
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
if (existingTask) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,15 @@ class TaskEventService {
|
|||
* @param {any} eventData.newValue - New value (optional)
|
||||
* @param {Object} eventData.metadata - Additional metadata (optional)
|
||||
*/
|
||||
static async logEvent({ taskId, userId, eventType, fieldName = null, oldValue = null, newValue = null, metadata = {} }) {
|
||||
static async logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
eventType,
|
||||
fieldName = null,
|
||||
oldValue = null,
|
||||
newValue = null,
|
||||
metadata = {},
|
||||
}) {
|
||||
try {
|
||||
// Add source to metadata if not provided
|
||||
if (!metadata.source) {
|
||||
|
|
@ -24,9 +32,13 @@ class TaskEventService {
|
|||
user_id: userId,
|
||||
event_type: eventType,
|
||||
field_name: fieldName,
|
||||
old_value: oldValue ? { [fieldName || 'value']: oldValue } : null,
|
||||
new_value: newValue ? { [fieldName || 'value']: newValue } : null,
|
||||
metadata: metadata
|
||||
old_value: oldValue
|
||||
? { [fieldName || 'value']: oldValue }
|
||||
: null,
|
||||
new_value: newValue
|
||||
? { [fieldName || 'value']: newValue }
|
||||
: null,
|
||||
metadata: metadata,
|
||||
});
|
||||
|
||||
return event;
|
||||
|
|
@ -45,17 +57,26 @@ class TaskEventService {
|
|||
userId,
|
||||
eventType: 'created',
|
||||
newValue: taskData,
|
||||
metadata: { ...metadata, action: 'task_created' }
|
||||
metadata: { ...metadata, action: 'task_created' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log status change event
|
||||
*/
|
||||
static async logStatusChange(taskId, userId, oldStatus, newStatus, metadata = {}) {
|
||||
const eventType = newStatus === 2 ? 'completed' :
|
||||
newStatus === 3 ? 'archived' :
|
||||
'status_changed';
|
||||
static async logStatusChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldStatus,
|
||||
newStatus,
|
||||
metadata = {}
|
||||
) {
|
||||
const eventType =
|
||||
newStatus === 2
|
||||
? 'completed'
|
||||
: newStatus === 3
|
||||
? 'archived'
|
||||
: 'status_changed';
|
||||
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
|
|
@ -64,14 +85,20 @@ class TaskEventService {
|
|||
fieldName: 'status',
|
||||
oldValue: oldStatus,
|
||||
newValue: newStatus,
|
||||
metadata: { ...metadata, action: 'status_change' }
|
||||
metadata: { ...metadata, action: 'status_change' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log priority change event
|
||||
*/
|
||||
static async logPriorityChange(taskId, userId, oldPriority, newPriority, metadata = {}) {
|
||||
static async logPriorityChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldPriority,
|
||||
newPriority,
|
||||
metadata = {}
|
||||
) {
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
|
|
@ -79,14 +106,20 @@ class TaskEventService {
|
|||
fieldName: 'priority',
|
||||
oldValue: oldPriority,
|
||||
newValue: newPriority,
|
||||
metadata: { ...metadata, action: 'priority_change' }
|
||||
metadata: { ...metadata, action: 'priority_change' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log due date change event
|
||||
*/
|
||||
static async logDueDateChange(taskId, userId, oldDueDate, newDueDate, metadata = {}) {
|
||||
static async logDueDateChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldDueDate,
|
||||
newDueDate,
|
||||
metadata = {}
|
||||
) {
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
|
|
@ -94,14 +127,20 @@ class TaskEventService {
|
|||
fieldName: 'due_date',
|
||||
oldValue: oldDueDate,
|
||||
newValue: newDueDate,
|
||||
metadata: { ...metadata, action: 'due_date_change' }
|
||||
metadata: { ...metadata, action: 'due_date_change' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log project change event
|
||||
*/
|
||||
static async logProjectChange(taskId, userId, oldProjectId, newProjectId, metadata = {}) {
|
||||
static async logProjectChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldProjectId,
|
||||
newProjectId,
|
||||
metadata = {}
|
||||
) {
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
|
|
@ -109,14 +148,20 @@ class TaskEventService {
|
|||
fieldName: 'project_id',
|
||||
oldValue: oldProjectId,
|
||||
newValue: newProjectId,
|
||||
metadata: { ...metadata, action: 'project_change' }
|
||||
metadata: { ...metadata, action: 'project_change' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log task name change event
|
||||
*/
|
||||
static async logNameChange(taskId, userId, oldName, newName, metadata = {}) {
|
||||
static async logNameChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldName,
|
||||
newName,
|
||||
metadata = {}
|
||||
) {
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
|
|
@ -124,14 +169,20 @@ class TaskEventService {
|
|||
fieldName: 'name',
|
||||
oldValue: oldName,
|
||||
newValue: newName,
|
||||
metadata: { ...metadata, action: 'name_change' }
|
||||
metadata: { ...metadata, action: 'name_change' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log description change event
|
||||
*/
|
||||
static async logDescriptionChange(taskId, userId, oldDescription, newDescription, metadata = {}) {
|
||||
static async logDescriptionChange(
|
||||
taskId,
|
||||
userId,
|
||||
oldDescription,
|
||||
newDescription,
|
||||
metadata = {}
|
||||
) {
|
||||
return await this.logEvent({
|
||||
taskId,
|
||||
userId,
|
||||
|
|
@ -139,7 +190,7 @@ class TaskEventService {
|
|||
fieldName: 'description',
|
||||
oldValue: oldDescription,
|
||||
newValue: newDescription,
|
||||
metadata: { ...metadata, action: 'description_change' }
|
||||
metadata: { ...metadata, action: 'description_change' },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -149,16 +200,21 @@ class TaskEventService {
|
|||
static async logTaskUpdate(taskId, userId, changes, metadata = {}) {
|
||||
const events = [];
|
||||
|
||||
for (const [fieldName, { oldValue, newValue }] of Object.entries(changes)) {
|
||||
for (const [fieldName, { oldValue, newValue }] of Object.entries(
|
||||
changes
|
||||
)) {
|
||||
// Skip if values are the same
|
||||
if (oldValue === newValue) continue;
|
||||
|
||||
let eventType;
|
||||
switch (fieldName) {
|
||||
case 'status':
|
||||
eventType = newValue === 2 ? 'completed' :
|
||||
newValue === 3 ? 'archived' :
|
||||
'status_changed';
|
||||
eventType =
|
||||
newValue === 2
|
||||
? 'completed'
|
||||
: newValue === 3
|
||||
? 'archived'
|
||||
: 'status_changed';
|
||||
break;
|
||||
default:
|
||||
eventType = `${fieldName}_changed`;
|
||||
|
|
@ -171,7 +227,7 @@ class TaskEventService {
|
|||
fieldName,
|
||||
oldValue,
|
||||
newValue,
|
||||
metadata: { ...metadata, action: 'bulk_update' }
|
||||
metadata: { ...metadata, action: 'bulk_update' },
|
||||
});
|
||||
|
||||
events.push(event);
|
||||
|
|
@ -187,11 +243,13 @@ class TaskEventService {
|
|||
return await TaskEvent.findAll({
|
||||
where: { task_id: taskId },
|
||||
order: [['created_at', 'ASC']],
|
||||
include: [{
|
||||
include: [
|
||||
{
|
||||
model: require('../models').User,
|
||||
as: 'User',
|
||||
attributes: ['id', 'name', 'email']
|
||||
}]
|
||||
attributes: ['id', 'name', 'email'],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -202,21 +260,23 @@ class TaskEventService {
|
|||
const events = await TaskEvent.findAll({
|
||||
where: {
|
||||
task_id: taskId,
|
||||
event_type: ['status_changed', 'created', 'completed']
|
||||
event_type: ['status_changed', 'created', 'completed'],
|
||||
},
|
||||
order: [['created_at', 'ASC']]
|
||||
order: [['created_at', 'ASC']],
|
||||
});
|
||||
|
||||
if (events.length === 0) return null;
|
||||
|
||||
// Find when task was started (moved to in_progress or created)
|
||||
const startEvent = events.find(e =>
|
||||
const startEvent = events.find(
|
||||
(e) =>
|
||||
e.event_type === 'created' ||
|
||||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
|
||||
);
|
||||
|
||||
// Find when task was completed
|
||||
const completedEvent = events.find(e =>
|
||||
const completedEvent = events.find(
|
||||
(e) =>
|
||||
e.event_type === 'completed' ||
|
||||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done
|
||||
);
|
||||
|
|
@ -232,51 +292,67 @@ class TaskEventService {
|
|||
completed_at: endTime,
|
||||
duration_ms: endTime - startTime,
|
||||
duration_hours: (endTime - startTime) / (1000 * 60 * 60),
|
||||
duration_days: (endTime - startTime) / (1000 * 60 * 60 * 24)
|
||||
duration_days: (endTime - startTime) / (1000 * 60 * 60 * 24),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user productivity metrics
|
||||
*/
|
||||
static async getUserProductivityMetrics(userId, startDate = null, endDate = null) {
|
||||
static async getUserProductivityMetrics(
|
||||
userId,
|
||||
startDate = null,
|
||||
endDate = null
|
||||
) {
|
||||
const whereClause = { user_id: userId };
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereClause.created_at = {
|
||||
[require('sequelize').Op.between]: [startDate, endDate]
|
||||
[require('sequelize').Op.between]: [startDate, endDate],
|
||||
};
|
||||
}
|
||||
|
||||
const events = await TaskEvent.findAll({
|
||||
where: whereClause,
|
||||
order: [['created_at', 'ASC']]
|
||||
order: [['created_at', 'ASC']],
|
||||
});
|
||||
|
||||
// Calculate metrics
|
||||
const metrics = {
|
||||
total_events: events.length,
|
||||
tasks_created: events.filter(e => e.event_type === 'created').length,
|
||||
tasks_completed: events.filter(e => e.event_type === 'completed').length,
|
||||
status_changes: events.filter(e => e.event_type === 'status_changed').length,
|
||||
tasks_created: events.filter((e) => e.event_type === 'created')
|
||||
.length,
|
||||
tasks_completed: events.filter((e) => e.event_type === 'completed')
|
||||
.length,
|
||||
status_changes: events.filter(
|
||||
(e) => e.event_type === 'status_changed'
|
||||
).length,
|
||||
average_completion_time: null,
|
||||
completion_times: []
|
||||
completion_times: [],
|
||||
};
|
||||
|
||||
// Calculate completion times for all completed tasks
|
||||
const completedTasks = events.filter(e => e.event_type === 'completed');
|
||||
const completedTasks = events.filter(
|
||||
(e) => e.event_type === 'completed'
|
||||
);
|
||||
const completionTimes = [];
|
||||
|
||||
for (const completedEvent of completedTasks) {
|
||||
const taskCompletion = await this.getTaskCompletionTime(completedEvent.task_id);
|
||||
const taskCompletion = await this.getTaskCompletionTime(
|
||||
completedEvent.task_id
|
||||
);
|
||||
if (taskCompletion) {
|
||||
completionTimes.push(taskCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
if (completionTimes.length > 0) {
|
||||
const totalHours = completionTimes.reduce((sum, ct) => sum + ct.duration_hours, 0);
|
||||
metrics.average_completion_time = totalHours / completionTimes.length;
|
||||
const totalHours = completionTimes.reduce(
|
||||
(sum, ct) => sum + ct.duration_hours,
|
||||
0
|
||||
);
|
||||
metrics.average_completion_time =
|
||||
totalHours / completionTimes.length;
|
||||
metrics.completion_times = completionTimes;
|
||||
}
|
||||
|
||||
|
|
@ -291,16 +367,28 @@ class TaskEventService {
|
|||
where: {
|
||||
user_id: userId,
|
||||
created_at: {
|
||||
[require('sequelize').Op.between]: [startDate, endDate]
|
||||
}
|
||||
[require('sequelize').Op.between]: [startDate, endDate],
|
||||
},
|
||||
},
|
||||
attributes: [
|
||||
'event_type',
|
||||
[require('sequelize').fn('COUNT', require('sequelize').col('id')), 'count'],
|
||||
[require('sequelize').fn('DATE', require('sequelize').col('created_at')), 'date']
|
||||
[
|
||||
require('sequelize').fn(
|
||||
'COUNT',
|
||||
require('sequelize').col('id')
|
||||
),
|
||||
'count',
|
||||
],
|
||||
[
|
||||
require('sequelize').fn(
|
||||
'DATE',
|
||||
require('sequelize').col('created_at')
|
||||
),
|
||||
'date',
|
||||
],
|
||||
],
|
||||
group: ['event_type', 'date'],
|
||||
order: [['date', 'ASC']]
|
||||
order: [['date', 'ASC']],
|
||||
});
|
||||
|
||||
return events;
|
||||
|
|
@ -317,9 +405,9 @@ class TaskEventService {
|
|||
task_id: taskId,
|
||||
event_type: 'today_changed',
|
||||
new_value: {
|
||||
[Op.like]: '%"today":true%'
|
||||
}
|
||||
}
|
||||
[Op.like]: '%"today":true%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return count;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const RecurringTaskService = require('./recurringTaskService');
|
|||
// Create scheduler state
|
||||
const createSchedulerState = () => ({
|
||||
jobs: new Map(),
|
||||
isInitialized: false
|
||||
isInitialized: false,
|
||||
});
|
||||
|
||||
// Global mutable state (will be managed functionally)
|
||||
|
|
@ -19,7 +19,7 @@ const shouldDisableScheduler = () =>
|
|||
// Create job configuration
|
||||
const createJobConfig = () => ({
|
||||
scheduled: false,
|
||||
timezone: 'UTC'
|
||||
timezone: 'UTC',
|
||||
});
|
||||
|
||||
// Create cron expressions
|
||||
|
|
@ -33,7 +33,7 @@ const getCronExpression = (frequency) => {
|
|||
'4h': '0 */4 * * *',
|
||||
'8h': '0 */8 * * *',
|
||||
'12h': '0 */12 * * *',
|
||||
recurring_tasks: '0 6 * * *' // Daily at 6 AM for recurring task generation
|
||||
recurring_tasks: '0 6 * * *', // Daily at 6 AM for recurring task generation
|
||||
};
|
||||
return expressions[frequency];
|
||||
};
|
||||
|
|
@ -49,9 +49,19 @@ const createJobHandler = (frequency) => async () => {
|
|||
|
||||
// Create job entries
|
||||
const createJobEntries = () => {
|
||||
const frequencies = ['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h', 'recurring_tasks'];
|
||||
const frequencies = [
|
||||
'daily',
|
||||
'weekdays',
|
||||
'weekly',
|
||||
'1h',
|
||||
'2h',
|
||||
'4h',
|
||||
'8h',
|
||||
'12h',
|
||||
'recurring_tasks',
|
||||
];
|
||||
|
||||
return frequencies.map(frequency => {
|
||||
return frequencies.map((frequency) => {
|
||||
const cronExpression = getCronExpression(frequency);
|
||||
const jobHandler = createJobHandler(frequency);
|
||||
const jobConfig = createJobConfig();
|
||||
|
|
@ -82,8 +92,8 @@ const fetchUsersForFrequency = async (frequency) => {
|
|||
telegram_bot_token: { [require('sequelize').Op.ne]: null },
|
||||
telegram_chat_id: { [require('sequelize').Op.ne]: null },
|
||||
task_summary_enabled: true,
|
||||
task_summary_frequency: frequency
|
||||
}
|
||||
task_summary_frequency: frequency,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -103,7 +113,7 @@ const processSummariesForFrequency = async (frequency) => {
|
|||
const users = await fetchUsersForFrequency(frequency);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
users.map(user => sendSummaryToUser(user.id, frequency))
|
||||
users.map((user) => sendSummaryToUser(user.id, frequency))
|
||||
);
|
||||
|
||||
return results;
|
||||
|
|
@ -142,7 +152,7 @@ const initialize = async () => {
|
|||
// Update state immutably
|
||||
schedulerState = {
|
||||
jobs,
|
||||
isInitialized: true
|
||||
isInitialized: true,
|
||||
};
|
||||
|
||||
return schedulerState;
|
||||
|
|
@ -173,7 +183,7 @@ const restart = async () => {
|
|||
const getStatus = () => ({
|
||||
initialized: schedulerState.isInitialized,
|
||||
jobCount: schedulerState.jobs.size,
|
||||
jobs: Array.from(schedulerState.jobs.keys())
|
||||
jobs: Array.from(schedulerState.jobs.keys()),
|
||||
});
|
||||
|
||||
// Export functional interface
|
||||
|
|
@ -187,5 +197,5 @@ module.exports = {
|
|||
// For testing
|
||||
_createSchedulerState: createSchedulerState,
|
||||
_shouldDisableScheduler: shouldDisableScheduler,
|
||||
_getCronExpression: getCronExpression
|
||||
_getCronExpression: getCronExpression,
|
||||
};
|
||||
|
|
@ -14,7 +14,7 @@ const getPriorityEmoji = (priority) => {
|
|||
const emojiMap = {
|
||||
2: '🔴', // high
|
||||
1: '🟠', // medium
|
||||
0: '🟢' // low
|
||||
0: '🟢', // low
|
||||
};
|
||||
return emojiMap[priority] || '⚪';
|
||||
};
|
||||
|
|
@ -33,7 +33,9 @@ const formatTaskForDisplay = (task, index, includeStatus = false) => {
|
|||
const priorityEmoji = getPriorityEmoji(task.priority);
|
||||
const statusEmoji = includeStatus ? '✅ ' : '';
|
||||
const taskName = escapeMarkdown(task.name);
|
||||
const projectInfo = task.Project ? ` \\[${escapeMarkdown(task.Project.name)}\\]` : '';
|
||||
const projectInfo = task.Project
|
||||
? ` \\[${escapeMarkdown(task.Project.name)}\\]`
|
||||
: '';
|
||||
return `${index + 1}\\. ${statusEmoji}${priorityEmoji} ${taskName}${projectInfo}\n`;
|
||||
};
|
||||
|
||||
|
|
@ -42,9 +44,9 @@ const buildTaskSection = (tasks, title, includeStatus = false) => {
|
|||
if (tasks.length === 0) return '';
|
||||
|
||||
let section = `${title}\n`;
|
||||
section += tasks.map((task, index) =>
|
||||
formatTaskForDisplay(task, index, includeStatus)
|
||||
).join('');
|
||||
section += tasks
|
||||
.map((task, index) => formatTaskForDisplay(task, index, includeStatus))
|
||||
.join('');
|
||||
section += '\n';
|
||||
|
||||
return section;
|
||||
|
|
@ -53,7 +55,7 @@ const buildTaskSection = (tasks, title, includeStatus = false) => {
|
|||
// build summary message
|
||||
const buildSummaryMessage = (taskSections) => {
|
||||
let message = "📋 *Today's Task Summary*\n\n";
|
||||
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
|
||||
message += '━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
|
||||
message += "✏️ *Today's Plan*\n\n";
|
||||
|
||||
message += taskSections.dueToday;
|
||||
|
|
@ -61,8 +63,8 @@ const buildSummaryMessage = (taskSections) => {
|
|||
message += taskSections.suggested;
|
||||
message += taskSections.completed;
|
||||
|
||||
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n";
|
||||
message += "🎯 *Stay focused and make it happen\\!*";
|
||||
message += '━━━━━━━━━━━━━━━━━━━━━━━━\n';
|
||||
message += '🎯 *Stay focused and make it happen\\!*';
|
||||
|
||||
return message;
|
||||
};
|
||||
|
|
@ -83,9 +85,11 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
|
|||
weekdays: () => {
|
||||
const currentDay = from.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||
let daysToAdd = 1;
|
||||
if (currentDay === 5) { // Friday
|
||||
if (currentDay === 5) {
|
||||
// Friday
|
||||
daysToAdd = 3; // Skip to Monday
|
||||
} else if (currentDay === 6) { // Saturday
|
||||
} else if (currentDay === 6) {
|
||||
// Saturday
|
||||
daysToAdd = 2; // Skip to Monday
|
||||
}
|
||||
const nextWeekday = new Date(from);
|
||||
|
|
@ -129,7 +133,7 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
|
|||
const next = new Date(from);
|
||||
next.setHours(next.getHours() + 12);
|
||||
return next;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const calculator = calculations[frequency];
|
||||
|
|
@ -137,8 +141,7 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
|
|||
};
|
||||
|
||||
// Side effect function to fetch user by ID
|
||||
const fetchUser = async (userId) =>
|
||||
await User.findByPk(userId);
|
||||
const fetchUser = async (userId) => await User.findByPk(userId);
|
||||
|
||||
// Side effect function to fetch due today tasks
|
||||
const fetchDueTodayTasks = async (userId, today, tomorrow) =>
|
||||
|
|
@ -147,12 +150,12 @@ const fetchDueTodayTasks = async (userId, today, tomorrow) =>
|
|||
user_id: userId,
|
||||
due_date: {
|
||||
[Op.gte]: today,
|
||||
[Op.lt]: tomorrow
|
||||
[Op.lt]: tomorrow,
|
||||
},
|
||||
status: { [Op.ne]: 2 } // not done
|
||||
status: { [Op.ne]: 2 }, // not done
|
||||
},
|
||||
include: [{ model: Project, attributes: ['name'] }],
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
// Side effect function to fetch in progress tasks
|
||||
|
|
@ -160,10 +163,10 @@ const fetchInProgressTasks = async (userId) =>
|
|||
await Task.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
status: 1 // in_progress
|
||||
status: 1, // in_progress
|
||||
},
|
||||
include: [{ model: Project, attributes: ['name'] }],
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
// Side effect function to fetch completed today tasks
|
||||
|
|
@ -174,11 +177,11 @@ const fetchCompletedTodayTasks = async (userId, today, tomorrow) =>
|
|||
status: 2, // done
|
||||
updated_at: {
|
||||
[Op.gte]: today,
|
||||
[Op.lt]: tomorrow
|
||||
}
|
||||
[Op.lt]: tomorrow,
|
||||
},
|
||||
},
|
||||
include: [{ model: Project, attributes: ['name'] }],
|
||||
order: [['name', 'ASC']]
|
||||
order: [['name', 'ASC']],
|
||||
});
|
||||
|
||||
// Side effect function to fetch suggested tasks
|
||||
|
|
@ -187,11 +190,14 @@ const fetchSuggestedTasks = async (userId, excludedIds) =>
|
|||
where: {
|
||||
user_id: userId,
|
||||
status: { [Op.ne]: 2 }, // not done
|
||||
id: { [Op.notIn]: excludedIds }
|
||||
id: { [Op.notIn]: excludedIds },
|
||||
},
|
||||
include: [{ model: Project, attributes: ['name'] }],
|
||||
order: [['priority', 'DESC'], ['name', 'ASC']],
|
||||
limit: 5
|
||||
order: [
|
||||
['priority', 'DESC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
// Side effect function to send telegram message
|
||||
|
|
@ -204,7 +210,7 @@ const sendTelegramMessage = async (token, chatId, message) => {
|
|||
const updateUserTracking = async (user, lastRun, nextRun) =>
|
||||
await user.update({
|
||||
task_summary_last_run: lastRun,
|
||||
task_summary_next_run: nextRun
|
||||
task_summary_next_run: nextRun,
|
||||
});
|
||||
|
||||
// Function to generate summary for user (contains side effects)
|
||||
|
|
@ -219,19 +225,29 @@ const generateSummaryForUser = async (userId) => {
|
|||
const [dueToday, inProgress, completedToday] = await Promise.all([
|
||||
fetchDueTodayTasks(userId, today, tomorrow),
|
||||
fetchInProgressTasks(userId),
|
||||
fetchCompletedTodayTasks(userId, today, tomorrow)
|
||||
fetchCompletedTodayTasks(userId, today, tomorrow),
|
||||
]);
|
||||
|
||||
// Get suggested tasks (excluding already fetched ones)
|
||||
const excludedIds = [...dueToday.map(t => t.id), ...inProgress.map(t => t.id)];
|
||||
const excludedIds = [
|
||||
...dueToday.map((t) => t.id),
|
||||
...inProgress.map((t) => t.id),
|
||||
];
|
||||
const suggestedTasks = await fetchSuggestedTasks(userId, excludedIds);
|
||||
|
||||
// Build task sections
|
||||
const taskSections = {
|
||||
dueToday: buildTaskSection(dueToday, "🚀 *Tasks Due Today:*"),
|
||||
inProgress: buildTaskSection(inProgress, "⚙️ *In Progress Tasks:*"),
|
||||
suggested: buildTaskSection(suggestedTasks, "💡 *Suggested Tasks:*"),
|
||||
completed: buildTaskSection(completedToday, "✅ *Completed Today:*", true)
|
||||
dueToday: buildTaskSection(dueToday, '🚀 *Tasks Due Today:*'),
|
||||
inProgress: buildTaskSection(inProgress, '⚙️ *In Progress Tasks:*'),
|
||||
suggested: buildTaskSection(
|
||||
suggestedTasks,
|
||||
'💡 *Suggested Tasks:*'
|
||||
),
|
||||
completed: buildTaskSection(
|
||||
completedToday,
|
||||
'✅ *Completed Today:*',
|
||||
true
|
||||
),
|
||||
};
|
||||
|
||||
return buildSummaryMessage(taskSections);
|
||||
|
|
@ -266,7 +282,10 @@ const sendSummaryToUser = async (userId) => {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error sending task summary to user ${userId}:`, error.message);
|
||||
console.error(
|
||||
`Error sending task summary to user ${userId}:`,
|
||||
error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
@ -282,5 +301,5 @@ module.exports = {
|
|||
_createTodayDateRange: createTodayDateRange,
|
||||
_formatTaskForDisplay: formatTaskForDisplay,
|
||||
_buildTaskSection: buildTaskSection,
|
||||
_buildSummaryMessage: buildSummaryMessage
|
||||
_buildSummaryMessage: buildSummaryMessage,
|
||||
};
|
||||
|
|
@ -2,7 +2,10 @@ const telegramPoller = require('./telegramPoller');
|
|||
const { User } = require('../models');
|
||||
|
||||
async function initializeTelegramPolling() {
|
||||
if (process.env.NODE_ENV === 'test' || process.env.DISABLE_TELEGRAM === 'true') {
|
||||
if (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.DISABLE_TELEGRAM === 'true'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -11,9 +14,9 @@ async function initializeTelegramPolling() {
|
|||
const usersWithTelegram = await User.findAll({
|
||||
where: {
|
||||
telegram_bot_token: {
|
||||
[require('sequelize').Op.ne]: null
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.ne]: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (usersWithTelegram.length > 0) {
|
||||
|
|
|
|||
|
|
@ -8,15 +8,14 @@ const createPollerState = () => ({
|
|||
pollInterval: 5000, // 5 seconds
|
||||
usersToPool: [],
|
||||
userStatus: {},
|
||||
processedUpdates: new Set() // Track processed update IDs to prevent duplicates
|
||||
processedUpdates: new Set(), // Track processed update IDs to prevent duplicates
|
||||
});
|
||||
|
||||
// Global mutable state (managed functionally)
|
||||
let pollerState = createPollerState();
|
||||
|
||||
// Check if user exists in list
|
||||
const userExistsInList = (users, userId) =>
|
||||
users.some(u => u.id === userId);
|
||||
const userExistsInList = (users, userId) => users.some((u) => u.id === userId);
|
||||
|
||||
// Add user to list
|
||||
const addUserToList = (users, user) => {
|
||||
|
|
@ -28,7 +27,7 @@ const addUserToList = (users, user) => {
|
|||
|
||||
// Remove user from list
|
||||
const removeUserFromList = (users, userId) =>
|
||||
users.filter(u => u.id !== userId);
|
||||
users.filter((u) => u.id !== userId);
|
||||
|
||||
// Remove user status
|
||||
const removeUserStatus = (userStatus, userId) => {
|
||||
|
|
@ -41,14 +40,14 @@ const updateUserStatus = (userStatus, userId, updates) => ({
|
|||
...userStatus,
|
||||
[userId]: {
|
||||
...userStatus[userId],
|
||||
...updates
|
||||
}
|
||||
...updates,
|
||||
},
|
||||
});
|
||||
|
||||
// Get highest update ID from updates
|
||||
const getHighestUpdateId = (updates) => {
|
||||
if (!updates.length) return 0;
|
||||
return Math.max(...updates.map(u => u.update_id));
|
||||
return Math.max(...updates.map((u) => u.update_id));
|
||||
};
|
||||
|
||||
// Create message parameters
|
||||
|
|
@ -72,7 +71,8 @@ const createTelegramUrl = (token, endpoint, params = {}) => {
|
|||
// Side effect function to make HTTP GET request
|
||||
const makeHttpGetRequest = (url, timeout = 5000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, { timeout }, (res) => {
|
||||
https
|
||||
.get(url, { timeout }, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
|
|
@ -87,9 +87,11 @@ const makeHttpGetRequest = (url, timeout = 5000) => {
|
|||
reject(error);
|
||||
}
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
})
|
||||
.on('error', (error) => {
|
||||
reject(error);
|
||||
}).on('timeout', () => {
|
||||
})
|
||||
.on('timeout', () => {
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
|
|
@ -100,7 +102,7 @@ const makeHttpPostRequest = (url, postData, options) => {
|
|||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(url, options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
|
|
@ -122,7 +124,7 @@ const getTelegramUpdates = async (token, offset) => {
|
|||
try {
|
||||
const url = createTelegramUrl(token, 'getUpdates', {
|
||||
offset: offset.toString(),
|
||||
timeout: '1'
|
||||
timeout: '1',
|
||||
});
|
||||
|
||||
const response = await makeHttpGetRequest(url, 5000);
|
||||
|
|
@ -138,9 +140,18 @@ const getTelegramUpdates = async (token, offset) => {
|
|||
};
|
||||
|
||||
// Side effect function to send Telegram message
|
||||
const sendTelegramMessage = async (token, chatId, text, replyToMessageId = null) => {
|
||||
const sendTelegramMessage = async (
|
||||
token,
|
||||
chatId,
|
||||
text,
|
||||
replyToMessageId = null
|
||||
) => {
|
||||
try {
|
||||
const messageParams = createMessageParams(chatId, text, replyToMessageId);
|
||||
const messageParams = createMessageParams(
|
||||
chatId,
|
||||
text,
|
||||
replyToMessageId
|
||||
);
|
||||
const postData = JSON.stringify(messageParams);
|
||||
const url = createTelegramUrl(token, 'sendMessage');
|
||||
|
||||
|
|
@ -148,8 +159,8 @@ const sendTelegramMessage = async (token, chatId, text, replyToMessageId = null)
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
},
|
||||
};
|
||||
|
||||
return await makeHttpPostRequest(url, postData, options);
|
||||
|
|
@ -160,10 +171,7 @@ const sendTelegramMessage = async (token, chatId, text, replyToMessageId = null)
|
|||
|
||||
// Side effect function to update user chat ID
|
||||
const updateUserChatId = async (userId, chatId) => {
|
||||
await User.update(
|
||||
{ telegram_chat_id: chatId },
|
||||
{ where: { id: userId } }
|
||||
);
|
||||
await User.update({ telegram_chat_id: chatId }, { where: { id: userId } });
|
||||
};
|
||||
|
||||
// Side effect function to create inbox item
|
||||
|
|
@ -178,13 +186,15 @@ const createInboxItem = async (content, userId, messageId) => {
|
|||
user_id: userId,
|
||||
source: 'telegram',
|
||||
created_at: {
|
||||
[require('sequelize').Op.gte]: recentCutoff
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.gte]: recentCutoff,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingItem) {
|
||||
console.log(`Duplicate inbox item detected for user ${userId}, content: "${content}". Skipping creation.`);
|
||||
console.log(
|
||||
`Duplicate inbox item detected for user ${userId}, content: "${content}". Skipping creation.`
|
||||
);
|
||||
return existingItem;
|
||||
}
|
||||
|
||||
|
|
@ -192,7 +202,7 @@ const createInboxItem = async (content, userId, messageId) => {
|
|||
content: content,
|
||||
source: 'telegram',
|
||||
user_id: userId,
|
||||
metadata: { telegram_message_id: messageId } // Store message ID for reference
|
||||
metadata: { telegram_message_id: messageId }, // Store message ID for reference
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -221,7 +231,9 @@ const processMessage = async (user, update) => {
|
|||
messageId
|
||||
);
|
||||
|
||||
console.log(`Successfully processed message ${messageId} for user ${user.id}: "${text}"`);
|
||||
console.log(
|
||||
`Successfully processed message ${messageId} for user ${user.id}: "${text}"`
|
||||
);
|
||||
} catch (error) {
|
||||
// Send error message
|
||||
await sendTelegramMessage(
|
||||
|
|
@ -238,7 +250,7 @@ const processUpdates = async (user, updates) => {
|
|||
if (!updates.length) return;
|
||||
|
||||
// Filter out already processed updates
|
||||
const newUpdates = updates.filter(update => {
|
||||
const newUpdates = updates.filter((update) => {
|
||||
const updateKey = `${user.id}-${update.update_id}`;
|
||||
return !pollerState.processedUpdates.has(updateKey);
|
||||
});
|
||||
|
|
@ -252,8 +264,8 @@ const processUpdates = async (user, updates) => {
|
|||
pollerState = {
|
||||
...pollerState,
|
||||
userStatus: updateUserStatus(pollerState.userStatus, user.id, {
|
||||
lastUpdateId: highestUpdateId
|
||||
})
|
||||
lastUpdateId: highestUpdateId,
|
||||
}),
|
||||
};
|
||||
|
||||
// Process each new update
|
||||
|
|
@ -269,12 +281,19 @@ const processUpdates = async (user, updates) => {
|
|||
|
||||
// Clean up old processed updates (keep only last 1000 to prevent memory leak)
|
||||
if (pollerState.processedUpdates.size > 1000) {
|
||||
const oldestEntries = Array.from(pollerState.processedUpdates).slice(0, 100);
|
||||
oldestEntries.forEach(entry => pollerState.processedUpdates.delete(entry));
|
||||
const oldestEntries = Array.from(
|
||||
pollerState.processedUpdates
|
||||
).slice(0, 100);
|
||||
oldestEntries.forEach((entry) =>
|
||||
pollerState.processedUpdates.delete(entry)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing update ${update.update_id} for user ${user.id}:`, error);
|
||||
console.error(
|
||||
`Error processing update ${update.update_id} for user ${user.id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -286,11 +305,14 @@ const pollUpdates = async () => {
|
|||
if (!token) continue;
|
||||
|
||||
try {
|
||||
const lastUpdateId = pollerState.userStatus[user.id]?.lastUpdateId || 0;
|
||||
const lastUpdateId =
|
||||
pollerState.userStatus[user.id]?.lastUpdateId || 0;
|
||||
const updates = await getTelegramUpdates(token, lastUpdateId + 1);
|
||||
|
||||
if (updates && updates.length > 0) {
|
||||
console.log(`Processing ${updates.length} updates for user ${user.id}, starting from update ID ${lastUpdateId + 1}`);
|
||||
console.log(
|
||||
`Processing ${updates.length} updates for user ${user.id}, starting from update ID ${lastUpdateId + 1}`
|
||||
);
|
||||
await processUpdates(user, updates);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -314,7 +336,7 @@ const startPolling = () => {
|
|||
pollerState = {
|
||||
...pollerState,
|
||||
running: true,
|
||||
interval
|
||||
interval,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -329,7 +351,7 @@ const stopPolling = () => {
|
|||
pollerState = {
|
||||
...pollerState,
|
||||
running: false,
|
||||
interval: null
|
||||
interval: null,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -344,7 +366,7 @@ const addUser = async (user) => {
|
|||
|
||||
pollerState = {
|
||||
...pollerState,
|
||||
usersToPool: newUsersList
|
||||
usersToPool: newUsersList,
|
||||
};
|
||||
|
||||
// Start polling if not already running and we have users
|
||||
|
|
@ -364,7 +386,7 @@ const removeUser = (userId) => {
|
|||
pollerState = {
|
||||
...pollerState,
|
||||
usersToPool: newUsersList,
|
||||
userStatus: newUserStatus
|
||||
userStatus: newUserStatus,
|
||||
};
|
||||
|
||||
// Stop polling if no users left
|
||||
|
|
@ -380,7 +402,7 @@ const getStatus = () => ({
|
|||
running: pollerState.running,
|
||||
usersCount: pollerState.usersToPool.length,
|
||||
pollInterval: pollerState.pollInterval,
|
||||
userStatus: pollerState.userStatus
|
||||
userStatus: pollerState.userStatus,
|
||||
});
|
||||
|
||||
// Export functional interface
|
||||
|
|
@ -398,5 +420,5 @@ module.exports = {
|
|||
_removeUserFromList: removeUserFromList,
|
||||
_getHighestUpdateId: getHighestUpdateId,
|
||||
_createMessageParams: createMessageParams,
|
||||
_createTelegramUrl: createTelegramUrl
|
||||
_createTelegramUrl: createTelegramUrl,
|
||||
};
|
||||
|
|
@ -18,26 +18,31 @@ tests/
|
|||
## Running Tests
|
||||
|
||||
### All Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Unit Tests Only
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Integration Tests Only
|
||||
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
### Watch Mode (for development)
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
|
@ -45,6 +50,7 @@ npm run test:coverage
|
|||
## Test Environment
|
||||
|
||||
Tests run in a separate test environment with:
|
||||
|
||||
- In-memory SQLite database (isolated from development data)
|
||||
- Test-specific configuration from `.env.test`
|
||||
- Automatic database cleanup between tests
|
||||
|
|
@ -52,17 +58,20 @@ Tests run in a separate test environment with:
|
|||
## Writing Tests
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Test individual functions, models, or middleware in isolation
|
||||
- Mock external dependencies
|
||||
- Focus on business logic and edge cases
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test complete API endpoints
|
||||
- Use authenticated requests where needed
|
||||
- Test real database interactions
|
||||
- Verify response formats and status codes
|
||||
|
||||
### Test Utilities
|
||||
|
||||
- `tests/helpers/testUtils.js` provides utilities for creating test data
|
||||
- `tests/helpers/setup.js` handles database setup and cleanup
|
||||
- Use `createTestUser()` for creating authenticated test users
|
||||
|
|
|
|||
|
|
@ -14,8 +14,14 @@ beforeEach(async () => {
|
|||
// Clean all tables except Sessions to avoid conflicts
|
||||
try {
|
||||
const models = Object.values(sequelize.models);
|
||||
const nonSessionModels = models.filter(model => model.name !== 'Session');
|
||||
await Promise.all(nonSessionModels.map(model => model.destroy({ truncate: true, cascade: true })));
|
||||
const nonSessionModels = models.filter(
|
||||
(model) => model.name !== 'Session'
|
||||
);
|
||||
await Promise.all(
|
||||
nonSessionModels.map((model) =>
|
||||
model.destroy({ truncate: true, cascade: true })
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,16 @@ const createTestUser = async (userData = {}) => {
|
|||
const defaultUser = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123', // Use password field to trigger model hook
|
||||
...userData
|
||||
...userData,
|
||||
};
|
||||
|
||||
return await User.create(defaultUser);
|
||||
};
|
||||
|
||||
const authenticateUser = async (request, user) => {
|
||||
const response = await request
|
||||
.post('/api/login')
|
||||
.send({
|
||||
const response = await request.post('/api/login').send({
|
||||
email: user.email,
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
return response.headers['set-cookie'];
|
||||
|
|
@ -24,5 +22,5 @@ const authenticateUser = async (request, user) => {
|
|||
|
||||
module.exports = {
|
||||
createTestUser,
|
||||
authenticateUser
|
||||
authenticateUser,
|
||||
};
|
||||
|
|
@ -8,16 +8,14 @@ describe('Areas Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -25,12 +23,10 @@ describe('Areas Routes', () => {
|
|||
it('should create a new area', async () => {
|
||||
const areaData = {
|
||||
name: 'Work',
|
||||
description: 'Work related projects'
|
||||
description: 'Work related projects',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/areas')
|
||||
.send(areaData);
|
||||
const response = await agent.post('/api/areas').send(areaData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe(areaData.name);
|
||||
|
|
@ -40,7 +36,7 @@ describe('Areas Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const areaData = {
|
||||
name: 'Work'
|
||||
name: 'Work',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -53,12 +49,10 @@ describe('Areas Routes', () => {
|
|||
|
||||
it('should require area name', async () => {
|
||||
const areaData = {
|
||||
description: 'Area without name'
|
||||
description: 'Area without name',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/areas')
|
||||
.send(areaData);
|
||||
const response = await agent.post('/api/areas').send(areaData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Area name is required.');
|
||||
|
|
@ -72,13 +66,13 @@ describe('Areas Routes', () => {
|
|||
area1 = await Area.create({
|
||||
name: 'Work',
|
||||
description: 'Work projects',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
area2 = await Area.create({
|
||||
name: 'Personal',
|
||||
description: 'Personal projects',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -87,8 +81,8 @@ describe('Areas Routes', () => {
|
|||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body.map(a => a.id)).toContain(area1.id);
|
||||
expect(response.body.map(a => a.id)).toContain(area2.id);
|
||||
expect(response.body.map((a) => a.id)).toContain(area1.id);
|
||||
expect(response.body.map((a) => a.id)).toContain(area2.id);
|
||||
});
|
||||
|
||||
it('should order areas by name', async () => {
|
||||
|
|
@ -114,7 +108,7 @@ describe('Areas Routes', () => {
|
|||
area = await Area.create({
|
||||
name: 'Work',
|
||||
description: 'Work projects',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -131,25 +125,29 @@ describe('Areas Routes', () => {
|
|||
const response = await agent.get('/api/areas/999999');
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe("Area not found or doesn't belong to the current user.");
|
||||
expect(response.body.error).toBe(
|
||||
"Area not found or doesn't belong to the current user."
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow access to other user\'s areas', async () => {
|
||||
it("should not allow access to other user's areas", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherArea = await Area.create({
|
||||
name: 'Other Area',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/areas/${otherArea.id}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe("Area not found or doesn't belong to the current user.");
|
||||
expect(response.body.error).toBe(
|
||||
"Area not found or doesn't belong to the current user."
|
||||
);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
@ -167,14 +165,14 @@ describe('Areas Routes', () => {
|
|||
area = await Area.create({
|
||||
name: 'Work',
|
||||
description: 'Work projects',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update area', async () => {
|
||||
const updateData = {
|
||||
name: 'Updated Work',
|
||||
description: 'Updated description'
|
||||
description: 'Updated description',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -195,16 +193,16 @@ describe('Areas Routes', () => {
|
|||
expect(response.body.error).toBe('Area not found.');
|
||||
});
|
||||
|
||||
it('should not allow updating other user\'s areas', async () => {
|
||||
it("should not allow updating other user's areas", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherArea = await Area.create({
|
||||
name: 'Other Area',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -231,7 +229,7 @@ describe('Areas Routes', () => {
|
|||
beforeEach(async () => {
|
||||
area = await Area.create({
|
||||
name: 'Work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -252,16 +250,16 @@ describe('Areas Routes', () => {
|
|||
expect(response.body.error).toBe('Area not found.');
|
||||
});
|
||||
|
||||
it('should not allow deleting other user\'s areas', async () => {
|
||||
it("should not allow deleting other user's areas", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherArea = await Area.create({
|
||||
name: 'Other Area',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.delete(`/api/areas/${otherArea.id}`);
|
||||
|
|
|
|||
|
|
@ -9,16 +9,14 @@ describe('Auth Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should login with valid credentials', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/login')
|
||||
.send({
|
||||
const response = await request(app).post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
|
@ -31,10 +29,8 @@ describe('Auth Routes', () => {
|
|||
});
|
||||
|
||||
it('should return 400 for missing email', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/login')
|
||||
.send({
|
||||
password: 'password123'
|
||||
const response = await request(app).post('/api/login').send({
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
|
@ -42,10 +38,8 @@ describe('Auth Routes', () => {
|
|||
});
|
||||
|
||||
it('should return 400 for missing password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/login')
|
||||
.send({
|
||||
email: 'test@example.com'
|
||||
const response = await request(app).post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
|
@ -53,11 +47,9 @@ describe('Auth Routes', () => {
|
|||
});
|
||||
|
||||
it('should return 401 for non-existent user', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/login')
|
||||
.send({
|
||||
const response = await request(app).post('/api/login').send({
|
||||
email: 'nonexistent@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
|
|
@ -65,11 +57,9 @@ describe('Auth Routes', () => {
|
|||
});
|
||||
|
||||
it('should return 401 for invalid password', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/login')
|
||||
.send({
|
||||
const response = await request(app).post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword'
|
||||
password: 'wrongpassword',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
|
|
@ -82,7 +72,7 @@ describe('Auth Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -90,11 +80,9 @@ describe('Auth Routes', () => {
|
|||
const agent = request.agent(app);
|
||||
|
||||
// Login first
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
// Check current user
|
||||
|
|
@ -119,7 +107,7 @@ describe('Auth Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -127,11 +115,9 @@ describe('Auth Routes', () => {
|
|||
const agent = request.agent(app);
|
||||
|
||||
// Login first
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
// Logout
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
// Ensure no tags exist for this user (clean slate)
|
||||
|
|
@ -38,13 +36,13 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
const inboxItem1 = await InboxItem.create({
|
||||
content: 'Test item without tags',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const inboxItem2 = await InboxItem.create({
|
||||
content: 'Another item without tags',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const response = await agent.get('/api/inbox');
|
||||
|
|
@ -52,8 +50,12 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(2);
|
||||
expect(response.body.map(item => item.id)).toContain(inboxItem1.id);
|
||||
expect(response.body.map(item => item.id)).toContain(inboxItem2.id);
|
||||
expect(response.body.map((item) => item.id)).toContain(
|
||||
inboxItem1.id
|
||||
);
|
||||
expect(response.body.map((item) => item.id)).toContain(
|
||||
inboxItem2.id
|
||||
);
|
||||
expect(response.body[0].content).toBeDefined();
|
||||
expect(response.body[0].status).toBe('added');
|
||||
expect(response.body[0].user_id).toBe(user.id);
|
||||
|
|
@ -64,19 +66,19 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
const addedItem = await InboxItem.create({
|
||||
content: 'Added item',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
await InboxItem.create({
|
||||
content: 'Processed item',
|
||||
status: 'processed',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
await InboxItem.create({
|
||||
content: 'Deleted item',
|
||||
status: 'deleted',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const response = await agent.get('/api/inbox');
|
||||
|
|
@ -102,7 +104,7 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
await InboxItem.create({
|
||||
content: 'Test item',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
// Verify tags endpoint returns empty
|
||||
|
|
@ -122,12 +124,10 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
it('should create inbox items successfully when no tags exist', async () => {
|
||||
const inboxData = {
|
||||
content: 'New inbox item without tags',
|
||||
source: 'web'
|
||||
source: 'web',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/inbox')
|
||||
.send(inboxData);
|
||||
const response = await agent.post('/api/inbox').send(inboxData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.content).toBe(inboxData.content);
|
||||
|
|
@ -140,13 +140,11 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
const items = [
|
||||
{ content: 'First item', source: 'web' },
|
||||
{ content: 'Second item', source: 'telegram' },
|
||||
{ content: 'Third item', source: 'api' }
|
||||
{ content: 'Third item', source: 'api' },
|
||||
];
|
||||
|
||||
for (const item of items) {
|
||||
const response = await agent
|
||||
.post('/api/inbox')
|
||||
.send(item);
|
||||
const response = await agent.post('/api/inbox').send(item);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.content).toBe(item.content);
|
||||
|
|
@ -167,14 +165,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
inboxItem = await InboxItem.create({
|
||||
content: 'Original content',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update inbox items when no tags exist', async () => {
|
||||
const updateData = {
|
||||
content: 'Updated content without tags',
|
||||
status: 'processed'
|
||||
status: 'processed',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -194,12 +192,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
inboxItem = await InboxItem.create({
|
||||
content: 'Item to process',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should process inbox items when no tags exist', async () => {
|
||||
const response = await agent.patch(`/api/inbox/${inboxItem.id}/process`);
|
||||
const response = await agent.patch(
|
||||
`/api/inbox/${inboxItem.id}/process`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe('processed');
|
||||
|
|
@ -213,7 +213,7 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
inboxItem = await InboxItem.create({
|
||||
content: 'Item to delete',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -221,7 +221,9 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
const response = await agent.delete(`/api/inbox/${inboxItem.id}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Inbox item successfully deleted');
|
||||
expect(response.body.message).toBe(
|
||||
'Inbox item successfully deleted'
|
||||
);
|
||||
|
||||
// Verify item status is updated to deleted
|
||||
const deletedItem = await InboxItem.findByPk(inboxItem.id);
|
||||
|
|
@ -258,7 +260,9 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
expect(updateResponse.body.content).toBe('Updated workflow test');
|
||||
|
||||
// Step 5: Process inbox item
|
||||
const processResponse = await agent.patch(`/api/inbox/${itemId}/process`);
|
||||
const processResponse = await agent.patch(
|
||||
`/api/inbox/${itemId}/process`
|
||||
);
|
||||
expect(processResponse.status).toBe(200);
|
||||
expect(processResponse.body.status).toBe('processed');
|
||||
|
||||
|
|
@ -273,14 +277,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
const createPromises = Array.from({ length: 5 }, (_, i) =>
|
||||
agent.post('/api/inbox').send({
|
||||
content: `Concurrent item ${i + 1}`,
|
||||
source: 'test'
|
||||
source: 'test',
|
||||
})
|
||||
);
|
||||
|
||||
const createResponses = await Promise.all(createPromises);
|
||||
|
||||
// All should succeed
|
||||
createResponses.forEach(response => {
|
||||
createResponses.forEach((response) => {
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
|
||||
|
|
@ -290,15 +294,15 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
expect(getResponse.body.length).toBe(5);
|
||||
|
||||
// Process all items concurrently
|
||||
const itemIds = createResponses.map(response => response.body.id);
|
||||
const processPromises = itemIds.map(id =>
|
||||
const itemIds = createResponses.map((response) => response.body.id);
|
||||
const processPromises = itemIds.map((id) =>
|
||||
agent.patch(`/api/inbox/${id}/process`)
|
||||
);
|
||||
|
||||
const processResponses = await Promise.all(processPromises);
|
||||
|
||||
// All should succeed
|
||||
processResponses.forEach(response => {
|
||||
processResponses.forEach((response) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe('processed');
|
||||
});
|
||||
|
|
@ -325,7 +329,9 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
expect(updateResponse.body.error).toBe('Inbox item not found.');
|
||||
|
||||
// Try to process non-existent item
|
||||
const processResponse = await agent.patch('/api/inbox/999999/process');
|
||||
const processResponse = await agent.patch(
|
||||
'/api/inbox/999999/process'
|
||||
);
|
||||
expect(processResponse.status).toBe(404);
|
||||
expect(processResponse.body.error).toBe('Inbox item not found.');
|
||||
|
||||
|
|
@ -337,9 +343,7 @@ describe('Inbox Routes - No Tags Scenario', () => {
|
|||
|
||||
it('should validate required fields when creating inbox items (no tags scenario)', async () => {
|
||||
// Try to create item without content
|
||||
const response = await agent
|
||||
.post('/api/inbox')
|
||||
.send({});
|
||||
const response = await agent.post('/api/inbox').send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Content is required');
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Inbox Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -25,12 +23,10 @@ describe('Inbox Routes', () => {
|
|||
it('should create a new inbox item', async () => {
|
||||
const inboxData = {
|
||||
content: 'Remember to buy groceries',
|
||||
source: 'web'
|
||||
source: 'web',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/inbox')
|
||||
.send(inboxData);
|
||||
const response = await agent.post('/api/inbox').send(inboxData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.content).toBe(inboxData.content);
|
||||
|
|
@ -41,7 +37,7 @@ describe('Inbox Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const inboxData = {
|
||||
content: 'Test content'
|
||||
content: 'Test content',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -55,9 +51,7 @@ describe('Inbox Routes', () => {
|
|||
it('should require content', async () => {
|
||||
const inboxData = {};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/inbox')
|
||||
.send(inboxData);
|
||||
const response = await agent.post('/api/inbox').send(inboxData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Content is required');
|
||||
|
|
@ -71,13 +65,13 @@ describe('Inbox Routes', () => {
|
|||
inboxItem1 = await InboxItem.create({
|
||||
content: 'First item',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
inboxItem2 = await InboxItem.create({
|
||||
content: 'Second item',
|
||||
status: 'processed',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +81,7 @@ describe('Inbox Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(1); // Only items with status 'added' are returned
|
||||
expect(response.body.map(i => i.id)).toContain(inboxItem1.id);
|
||||
expect(response.body.map((i) => i.id)).toContain(inboxItem1.id);
|
||||
});
|
||||
|
||||
it('should only return items with added status', async () => {
|
||||
|
|
@ -113,7 +107,7 @@ describe('Inbox Routes', () => {
|
|||
beforeEach(async () => {
|
||||
inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -132,16 +126,16 @@ describe('Inbox Routes', () => {
|
|||
expect(response.body.error).toBe('Inbox item not found.');
|
||||
});
|
||||
|
||||
it('should not allow access to other user\'s inbox items', async () => {
|
||||
it("should not allow access to other user's inbox items", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherInboxItem = await InboxItem.create({
|
||||
content: 'Other content',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/inbox/${otherInboxItem.id}`);
|
||||
|
|
@ -151,7 +145,9 @@ describe('Inbox Routes', () => {
|
|||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).get(`/api/inbox/${inboxItem.id}`);
|
||||
const response = await request(app).get(
|
||||
`/api/inbox/${inboxItem.id}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -165,14 +161,14 @@ describe('Inbox Routes', () => {
|
|||
inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update inbox item', async () => {
|
||||
const updateData = {
|
||||
content: 'Updated content',
|
||||
status: 'processed'
|
||||
status: 'processed',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -209,7 +205,7 @@ describe('Inbox Routes', () => {
|
|||
beforeEach(async () => {
|
||||
inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -217,7 +213,9 @@ describe('Inbox Routes', () => {
|
|||
const response = await agent.delete(`/api/inbox/${inboxItem.id}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Inbox item successfully deleted');
|
||||
expect(response.body.message).toBe(
|
||||
'Inbox item successfully deleted'
|
||||
);
|
||||
|
||||
// Verify inbox item status is updated to deleted
|
||||
const deletedItem = await InboxItem.findByPk(inboxItem.id);
|
||||
|
|
@ -233,7 +231,9 @@ describe('Inbox Routes', () => {
|
|||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).delete(`/api/inbox/${inboxItem.id}`);
|
||||
const response = await request(app).delete(
|
||||
`/api/inbox/${inboxItem.id}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -247,12 +247,14 @@ describe('Inbox Routes', () => {
|
|||
inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
status: 'added',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should process inbox item', async () => {
|
||||
const response = await agent.patch(`/api/inbox/${inboxItem.id}/process`);
|
||||
const response = await agent.patch(
|
||||
`/api/inbox/${inboxItem.id}/process`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe('processed');
|
||||
|
|
@ -266,7 +268,9 @@ describe('Inbox Routes', () => {
|
|||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).patch(`/api/inbox/${inboxItem.id}/process`);
|
||||
const response = await request(app).patch(
|
||||
`/api/inbox/${inboxItem.id}/process`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
|
|||
|
|
@ -8,21 +8,19 @@ describe('Notes Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -31,12 +29,10 @@ describe('Notes Routes', () => {
|
|||
const noteData = {
|
||||
title: 'Test Note',
|
||||
content: 'This is a test note content',
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/note')
|
||||
.send(noteData);
|
||||
const response = await agent.post('/api/note').send(noteData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.title).toBe(noteData.title);
|
||||
|
|
@ -48,12 +44,10 @@ describe('Notes Routes', () => {
|
|||
it('should create note without project', async () => {
|
||||
const noteData = {
|
||||
title: 'Test Note',
|
||||
content: 'This is a test note content'
|
||||
content: 'This is a test note content',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/note')
|
||||
.send(noteData);
|
||||
const response = await agent.post('/api/note').send(noteData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.title).toBe(noteData.title);
|
||||
|
|
@ -65,7 +59,7 @@ describe('Notes Routes', () => {
|
|||
it('should require authentication', async () => {
|
||||
const noteData = {
|
||||
title: 'Test Note',
|
||||
content: 'Test content'
|
||||
content: 'Test content',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -85,13 +79,13 @@ describe('Notes Routes', () => {
|
|||
title: 'Note 1',
|
||||
content: 'First note content',
|
||||
user_id: user.id,
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
});
|
||||
|
||||
note2 = await Note.create({
|
||||
title: 'Note 2',
|
||||
content: 'Second note content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -101,15 +95,17 @@ describe('Notes Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(2);
|
||||
expect(response.body.map(n => n.id)).toContain(note1.id);
|
||||
expect(response.body.map(n => n.id)).toContain(note2.id);
|
||||
expect(response.body.map((n) => n.id)).toContain(note1.id);
|
||||
expect(response.body.map((n) => n.id)).toContain(note2.id);
|
||||
});
|
||||
|
||||
it('should include project information', async () => {
|
||||
const response = await agent.get('/api/notes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const noteWithProject = response.body.find(n => n.id === note1.id);
|
||||
const noteWithProject = response.body.find(
|
||||
(n) => n.id === note1.id
|
||||
);
|
||||
expect(noteWithProject.Project).toBeDefined();
|
||||
expect(noteWithProject.Project.name).toBe(project.name);
|
||||
});
|
||||
|
|
@ -119,8 +115,8 @@ describe('Notes Routes', () => {
|
|||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.length).toBe(2);
|
||||
expect(response.body.map(n => n.id)).toContain(note1.id);
|
||||
expect(response.body.map(n => n.id)).toContain(note2.id);
|
||||
expect(response.body.map((n) => n.id)).toContain(note1.id);
|
||||
expect(response.body.map((n) => n.id)).toContain(note2.id);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
|
|
@ -139,7 +135,7 @@ describe('Notes Routes', () => {
|
|||
title: 'Test Note',
|
||||
content: 'Test content',
|
||||
user_id: user.id,
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -159,16 +155,16 @@ describe('Notes Routes', () => {
|
|||
expect(response.body.error).toBe('Note not found.');
|
||||
});
|
||||
|
||||
it('should not allow access to other user\'s notes', async () => {
|
||||
it("should not allow access to other user's notes", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherNote = await Note.create({
|
||||
title: 'Other Note',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/note/${otherNote.id}`);
|
||||
|
|
@ -192,7 +188,7 @@ describe('Notes Routes', () => {
|
|||
note = await Note.create({
|
||||
title: 'Test Note',
|
||||
content: 'Test content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -200,7 +196,7 @@ describe('Notes Routes', () => {
|
|||
const updateData = {
|
||||
title: 'Updated Note',
|
||||
content: 'Updated content',
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -222,16 +218,16 @@ describe('Notes Routes', () => {
|
|||
expect(response.body.error).toBe('Note not found.');
|
||||
});
|
||||
|
||||
it('should not allow updating other user\'s notes', async () => {
|
||||
it("should not allow updating other user's notes", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherNote = await Note.create({
|
||||
title: 'Other Note',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -258,7 +254,7 @@ describe('Notes Routes', () => {
|
|||
beforeEach(async () => {
|
||||
note = await Note.create({
|
||||
title: 'Test Note',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -280,16 +276,16 @@ describe('Notes Routes', () => {
|
|||
expect(response.body.error).toBe('Note not found.');
|
||||
});
|
||||
|
||||
it('should not allow deleting other user\'s notes', async () => {
|
||||
it("should not allow deleting other user's notes", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherNote = await Note.create({
|
||||
title: 'Other Note',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.delete(`/api/note/${otherNote.id}`);
|
||||
|
|
|
|||
|
|
@ -8,21 +8,19 @@ describe('Projects Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
area = await Area.create({
|
||||
name: 'Work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -34,18 +32,18 @@ describe('Projects Routes', () => {
|
|||
active: true,
|
||||
pin_to_sidebar: false,
|
||||
priority: 1,
|
||||
area_id: area.id
|
||||
area_id: area.id,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/project')
|
||||
.send(projectData);
|
||||
const response = await agent.post('/api/project').send(projectData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe(projectData.name);
|
||||
expect(response.body.description).toBe(projectData.description);
|
||||
expect(response.body.active).toBe(projectData.active);
|
||||
expect(response.body.pin_to_sidebar).toBe(projectData.pin_to_sidebar);
|
||||
expect(response.body.pin_to_sidebar).toBe(
|
||||
projectData.pin_to_sidebar
|
||||
);
|
||||
expect(response.body.priority).toBe(projectData.priority);
|
||||
expect(response.body.area_id).toBe(area.id);
|
||||
expect(response.body.user_id).toBe(user.id);
|
||||
|
|
@ -53,7 +51,7 @@ describe('Projects Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const projectData = {
|
||||
name: 'Test Project'
|
||||
name: 'Test Project',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -66,12 +64,10 @@ describe('Projects Routes', () => {
|
|||
|
||||
it('should require project name', async () => {
|
||||
const projectData = {
|
||||
description: 'Project without name'
|
||||
description: 'Project without name',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/project')
|
||||
.send(projectData);
|
||||
const response = await agent.post('/api/project').send(projectData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
|
@ -85,13 +81,13 @@ describe('Projects Routes', () => {
|
|||
name: 'Project 1',
|
||||
description: 'First project',
|
||||
user_id: user.id,
|
||||
area_id: area.id
|
||||
area_id: area.id,
|
||||
});
|
||||
|
||||
project2 = await Project.create({
|
||||
name: 'Project 2',
|
||||
description: 'Second project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -101,15 +97,21 @@ describe('Projects Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(response.body.projects).toBeDefined();
|
||||
expect(response.body.projects.length).toBe(2);
|
||||
expect(response.body.projects.map(p => p.id)).toContain(project1.id);
|
||||
expect(response.body.projects.map(p => p.id)).toContain(project2.id);
|
||||
expect(response.body.projects.map((p) => p.id)).toContain(
|
||||
project1.id
|
||||
);
|
||||
expect(response.body.projects.map((p) => p.id)).toContain(
|
||||
project2.id
|
||||
);
|
||||
});
|
||||
|
||||
it('should include area information', async () => {
|
||||
const response = await agent.get('/api/projects');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const projectWithArea = response.body.projects.find(p => p.id === project1.id);
|
||||
const projectWithArea = response.body.projects.find(
|
||||
(p) => p.id === project1.id
|
||||
);
|
||||
expect(projectWithArea.Area).toBeDefined();
|
||||
expect(projectWithArea.Area.name).toBe(area.name);
|
||||
});
|
||||
|
|
@ -130,7 +132,7 @@ describe('Projects Routes', () => {
|
|||
name: 'Test Project',
|
||||
description: 'Test Description',
|
||||
user_id: user.id,
|
||||
area_id: area.id
|
||||
area_id: area.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -150,16 +152,16 @@ describe('Projects Routes', () => {
|
|||
expect(response.body.error).toBe('Project not found');
|
||||
});
|
||||
|
||||
it('should not allow access to other user\'s projects', async () => {
|
||||
it("should not allow access to other user's projects", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherProject = await Project.create({
|
||||
name: 'Other Project',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/project/${otherProject.id}`);
|
||||
|
|
@ -169,7 +171,9 @@ describe('Projects Routes', () => {
|
|||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).get(`/api/project/${project.id}`);
|
||||
const response = await request(app).get(
|
||||
`/api/project/${project.id}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -185,7 +189,7 @@ describe('Projects Routes', () => {
|
|||
description: 'Test Description',
|
||||
active: false,
|
||||
priority: 0,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -194,7 +198,7 @@ describe('Projects Routes', () => {
|
|||
name: 'Updated Project',
|
||||
description: 'Updated Description',
|
||||
active: true,
|
||||
priority: 2
|
||||
priority: 2,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -217,16 +221,16 @@ describe('Projects Routes', () => {
|
|||
expect(response.body.error).toBe('Project not found.');
|
||||
});
|
||||
|
||||
it('should not allow updating other user\'s projects', async () => {
|
||||
it("should not allow updating other user's projects", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherProject = await Project.create({
|
||||
name: 'Other Project',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -253,7 +257,7 @@ describe('Projects Routes', () => {
|
|||
beforeEach(async () => {
|
||||
project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -275,26 +279,30 @@ describe('Projects Routes', () => {
|
|||
expect(response.body.error).toBe('Project not found.');
|
||||
});
|
||||
|
||||
it('should not allow deleting other user\'s projects', async () => {
|
||||
it("should not allow deleting other user's projects", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherProject = await Project.create({
|
||||
name: 'Other Project',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.delete(`/api/project/${otherProject.id}`);
|
||||
const response = await agent.delete(
|
||||
`/api/project/${otherProject.id}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.body.error).toBe('Project not found.');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).delete(`/api/project/${project.id}`);
|
||||
const response = await request(app).delete(
|
||||
`/api/project/${project.id}`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
|
|||
|
|
@ -7,23 +7,20 @@ describe('Quotes Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/quotes/random', () => {
|
||||
it('should return a random quote', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes/random');
|
||||
const response = await agent.get('/api/quotes/random');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('quote');
|
||||
|
|
@ -37,11 +34,11 @@ describe('Quotes Routes', () => {
|
|||
agent.get('/api/quotes/random'),
|
||||
agent.get('/api/quotes/random'),
|
||||
agent.get('/api/quotes/random'),
|
||||
agent.get('/api/quotes/random')
|
||||
agent.get('/api/quotes/random'),
|
||||
]);
|
||||
|
||||
// All responses should be successful
|
||||
responses.forEach(response => {
|
||||
responses.forEach((response) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('quote');
|
||||
expect(typeof response.body.quote).toBe('string');
|
||||
|
|
@ -49,7 +46,7 @@ describe('Quotes Routes', () => {
|
|||
|
||||
// With multiple requests, we should get at least some variety
|
||||
// (though it's possible to get the same quote multiple times due to randomness)
|
||||
const quotes = responses.map(r => r.body.quote);
|
||||
const quotes = responses.map((r) => r.body.quote);
|
||||
const uniqueQuotes = new Set(quotes);
|
||||
|
||||
// We expect at least 1 unique quote, but likely more
|
||||
|
|
@ -57,8 +54,7 @@ describe('Quotes Routes', () => {
|
|||
});
|
||||
|
||||
it('should return valid quote structure', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes/random');
|
||||
const response = await agent.get('/api/quotes/random');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Object.keys(response.body)).toEqual(['quote']);
|
||||
|
|
@ -69,8 +65,7 @@ describe('Quotes Routes', () => {
|
|||
|
||||
describe('GET /api/quotes', () => {
|
||||
it('should return all quotes with count', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes');
|
||||
const response = await agent.get('/api/quotes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('quotes');
|
||||
|
|
@ -82,13 +77,12 @@ describe('Quotes Routes', () => {
|
|||
});
|
||||
|
||||
it('should return valid quote array', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes');
|
||||
const response = await agent.get('/api/quotes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
// All quotes should be non-empty strings
|
||||
response.body.quotes.forEach(quote => {
|
||||
response.body.quotes.forEach((quote) => {
|
||||
expect(typeof quote).toBe('string');
|
||||
expect(quote.length).toBeGreaterThan(0);
|
||||
expect(quote.trim()).toBe(quote);
|
||||
|
|
@ -103,7 +97,9 @@ describe('Quotes Routes', () => {
|
|||
expect(response2.status).toBe(200);
|
||||
|
||||
// The quotes array should be the same across requests
|
||||
expect(response1.body.quotes.length).toBe(response2.body.quotes.length);
|
||||
expect(response1.body.quotes.length).toBe(
|
||||
response2.body.quotes.length
|
||||
);
|
||||
expect(response1.body.count).toBe(response2.body.count);
|
||||
|
||||
// Verify the actual content is the same
|
||||
|
|
@ -111,8 +107,7 @@ describe('Quotes Routes', () => {
|
|||
});
|
||||
|
||||
it('should return expected quote count', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes');
|
||||
const response = await agent.get('/api/quotes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
|
|
@ -122,8 +117,7 @@ describe('Quotes Routes', () => {
|
|||
});
|
||||
|
||||
it('should contain productivity-focused quotes', async () => {
|
||||
const response = await agent
|
||||
.get('/api/quotes');
|
||||
const response = await agent.get('/api/quotes');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
|
|
@ -132,13 +126,21 @@ describe('Quotes Routes', () => {
|
|||
|
||||
// These are common themes in productivity quotes
|
||||
const productivityKeywords = [
|
||||
'progress', 'task', 'goal', 'focus', 'accomplish',
|
||||
'success', 'work', 'effort', 'achieve', 'time'
|
||||
'progress',
|
||||
'task',
|
||||
'goal',
|
||||
'focus',
|
||||
'accomplish',
|
||||
'success',
|
||||
'work',
|
||||
'effort',
|
||||
'achieve',
|
||||
'time',
|
||||
];
|
||||
|
||||
// At least some quotes should contain productivity-related terms
|
||||
const hasProductivityContent = productivityKeywords.some(keyword =>
|
||||
allQuotesText.includes(keyword)
|
||||
const hasProductivityContent = productivityKeywords.some(
|
||||
(keyword) => allQuotesText.includes(keyword)
|
||||
);
|
||||
|
||||
expect(hasProductivityContent).toBe(true);
|
||||
|
|
@ -155,11 +157,11 @@ describe('Quotes Routes', () => {
|
|||
const randomQuoteResponses = await Promise.all([
|
||||
agent.get('/api/quotes/random'),
|
||||
agent.get('/api/quotes/random'),
|
||||
agent.get('/api/quotes/random')
|
||||
agent.get('/api/quotes/random'),
|
||||
]);
|
||||
|
||||
// Each random quote should be from the full set
|
||||
randomQuoteResponses.forEach(response => {
|
||||
randomQuoteResponses.forEach((response) => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(allQuotes).toContain(response.body.quote);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Recurring Tasks API', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -28,12 +26,10 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
priority: 1,
|
||||
completion_based: false
|
||||
completion_based: false,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Daily Exercise');
|
||||
|
|
@ -48,12 +44,10 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1, // Monday
|
||||
priority: 2
|
||||
priority: 2,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Weekly Team Meeting');
|
||||
|
|
@ -67,12 +61,10 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_month_day: 1,
|
||||
priority: 2
|
||||
priority: 2,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Pay Rent');
|
||||
|
|
@ -87,12 +79,10 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1, // Monday
|
||||
recurrence_week_of_month: 1, // First week
|
||||
priority: 1
|
||||
priority: 1,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('First Monday Meeting');
|
||||
|
|
@ -106,12 +96,10 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Month-end Report',
|
||||
recurrence_type: 'monthly_last_day',
|
||||
recurrence_interval: 1,
|
||||
priority: 2
|
||||
priority: 2,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Month-end Report');
|
||||
|
|
@ -124,12 +112,10 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 3,
|
||||
completion_based: true,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Car Maintenance');
|
||||
|
|
@ -145,27 +131,25 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 2,
|
||||
recurrence_end_date: endDate.toISOString().split('T')[0],
|
||||
priority: 1
|
||||
priority: 1,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe('Temporary Recurring Task');
|
||||
expect(response.body.recurrence_end_date).toContain(endDate.toISOString().split('T')[0]);
|
||||
expect(response.body.recurrence_end_date).toContain(
|
||||
endDate.toISOString().split('T')[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to none recurrence type if not specified', async () => {
|
||||
const taskData = {
|
||||
name: 'Regular Task',
|
||||
priority: 1
|
||||
priority: 1,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.recurrence_type).toBe('none');
|
||||
|
|
@ -181,7 +165,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -189,7 +173,7 @@ describe('Recurring Tasks API', () => {
|
|||
const updateData = {
|
||||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 2,
|
||||
recurrence_weekday: 5 // Friday
|
||||
recurrence_weekday: 5, // Friday
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -204,7 +188,7 @@ describe('Recurring Tasks API', () => {
|
|||
|
||||
it('should update completion_based setting', async () => {
|
||||
const updateData = {
|
||||
completion_based: true
|
||||
completion_based: true,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -220,7 +204,7 @@ describe('Recurring Tasks API', () => {
|
|||
endDate.setFullYear(endDate.getFullYear() + 1);
|
||||
|
||||
const updateData = {
|
||||
recurrence_end_date: endDate.toISOString().split('T')[0]
|
||||
recurrence_end_date: endDate.toISOString().split('T')[0],
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -228,12 +212,14 @@ describe('Recurring Tasks API', () => {
|
|||
.send(updateData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.recurrence_end_date).toContain(endDate.toISOString().split('T')[0]);
|
||||
expect(response.body.recurrence_end_date).toContain(
|
||||
endDate.toISOString().split('T')[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable recurrence by setting type to none', async () => {
|
||||
const updateData = {
|
||||
recurrence_type: 'none'
|
||||
recurrence_type: 'none',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -254,7 +240,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
childTask = await Task.create({
|
||||
|
|
@ -263,7 +249,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
priority: 1,
|
||||
due_date: new Date()
|
||||
due_date: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -272,7 +258,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 2,
|
||||
recurrence_weekday: 3,
|
||||
update_parent_recurrence: true
|
||||
update_parent_recurrence: true,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -293,7 +279,7 @@ describe('Recurring Tasks API', () => {
|
|||
|
||||
const updateData = {
|
||||
recurrence_type: 'weekly',
|
||||
update_parent_recurrence: false
|
||||
update_parent_recurrence: false,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -312,12 +298,12 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Standalone Task',
|
||||
recurrence_type: 'none',
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
recurrence_type: 'weekly',
|
||||
update_parent_recurrence: true
|
||||
update_parent_recurrence: true,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -337,17 +323,20 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_interval: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id,
|
||||
status: 0 // NOT_STARTED
|
||||
status: 0, // NOT_STARTED
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.patch(`/api/task/${recurringTask.id}/toggle_completion`);
|
||||
const response = await agent.patch(
|
||||
`/api/task/${recurringTask.id}/toggle_completion`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe(2); // DONE
|
||||
expect(response.body.next_task).toBeDefined();
|
||||
expect(response.body.next_task.name).toBe('Completion Based Task');
|
||||
expect(response.body.next_task.recurring_parent_id).toBe(recurringTask.id);
|
||||
expect(response.body.next_task.recurring_parent_id).toBe(
|
||||
recurringTask.id
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create next instance for non-completion-based recurring tasks', async () => {
|
||||
|
|
@ -357,11 +346,12 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_interval: 1,
|
||||
completion_based: false,
|
||||
user_id: user.id,
|
||||
status: 0 // NOT_STARTED
|
||||
status: 0, // NOT_STARTED
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.patch(`/api/task/${recurringTask.id}/toggle_completion`);
|
||||
const response = await agent.patch(
|
||||
`/api/task/${recurringTask.id}/toggle_completion`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe(2); // DONE
|
||||
|
|
@ -373,11 +363,12 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Regular Task',
|
||||
recurrence_type: 'none',
|
||||
user_id: user.id,
|
||||
status: 0 // NOT_STARTED
|
||||
status: 0, // NOT_STARTED
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.patch(`/api/task/${regularTask.id}/toggle_completion`);
|
||||
const response = await agent.patch(
|
||||
`/api/task/${regularTask.id}/toggle_completion`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe(2); // DONE
|
||||
|
|
@ -388,11 +379,12 @@ describe('Recurring Tasks API', () => {
|
|||
const task = await Task.create({
|
||||
name: 'Test Task',
|
||||
user_id: user.id,
|
||||
status: 2 // DONE
|
||||
status: 2, // DONE
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.patch(`/api/task/${task.id}/toggle_completion`);
|
||||
const response = await agent.patch(
|
||||
`/api/task/${task.id}/toggle_completion`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe(0); // NOT_STARTED
|
||||
|
|
@ -403,11 +395,12 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Test Task',
|
||||
note: 'Some notes',
|
||||
user_id: user.id,
|
||||
status: 2 // DONE
|
||||
status: 2, // DONE
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.patch(`/api/task/${task.id}/toggle_completion`);
|
||||
const response = await agent.patch(
|
||||
`/api/task/${task.id}/toggle_completion`
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.status).toBe(1); // IN_PROGRESS
|
||||
|
|
@ -423,7 +416,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-01-01'),
|
||||
last_generated_date: new Date('2025-01-01')
|
||||
last_generated_date: new Date('2025-01-01'),
|
||||
});
|
||||
|
||||
await Task.create({
|
||||
|
|
@ -433,23 +426,25 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_weekday: 1,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-01-06'), // Monday
|
||||
last_generated_date: new Date('2025-01-06')
|
||||
last_generated_date: new Date('2025-01-06'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate recurring task instances', async () => {
|
||||
const response = await agent
|
||||
.post('/api/tasks/generate-recurring');
|
||||
const response = await agent.post('/api/tasks/generate-recurring');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toMatch(/Generated \d+ recurring tasks/);
|
||||
expect(response.body.message).toMatch(
|
||||
/Generated \d+ recurring tasks/
|
||||
);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
expect(Array.isArray(response.body.tasks)).toBe(true);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/tasks/generate-recurring');
|
||||
const response = await request(app).post(
|
||||
'/api/tasks/generate-recurring'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -464,11 +459,10 @@ describe('Recurring Tasks API', () => {
|
|||
await Task.create({
|
||||
name: 'Invalid Task',
|
||||
recurrence_type: 'invalid_type',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.post('/api/tasks/generate-recurring');
|
||||
const response = await agent.post('/api/tasks/generate-recurring');
|
||||
|
||||
// Should still return success even if some tasks fail
|
||||
expect(response.status).toBe(200);
|
||||
|
|
@ -485,7 +479,7 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Regular Task',
|
||||
recurrence_type: 'none',
|
||||
user_id: user.id,
|
||||
status: 0
|
||||
status: 0,
|
||||
});
|
||||
|
||||
const parentTask = await Task.create({
|
||||
|
|
@ -493,7 +487,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
status: 0
|
||||
status: 0,
|
||||
});
|
||||
|
||||
await Task.create({
|
||||
|
|
@ -501,7 +495,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_type: 'none',
|
||||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
status: 0
|
||||
status: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -512,7 +506,7 @@ describe('Recurring Tasks API', () => {
|
|||
expect(response.body.tasks).toBeDefined();
|
||||
expect(response.body.tasks.length).toBe(3);
|
||||
|
||||
const taskNames = response.body.tasks.map(t => t.name);
|
||||
const taskNames = response.body.tasks.map((t) => t.name);
|
||||
expect(taskNames).toContain('Regular Task');
|
||||
expect(taskNames).toContain('Recurring Parent');
|
||||
expect(taskNames).toContain('Recurring Child');
|
||||
|
|
@ -537,7 +531,7 @@ describe('Recurring Tasks API', () => {
|
|||
recurrence_interval: 2,
|
||||
recurrence_weekday: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -561,14 +555,14 @@ describe('Recurring Tasks API', () => {
|
|||
name: 'Parent Recurring Task',
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
childTask = await Task.create({
|
||||
name: 'Child Task Instance',
|
||||
recurrence_type: 'none',
|
||||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -576,7 +570,9 @@ describe('Recurring Tasks API', () => {
|
|||
const response = await agent.delete(`/api/task/${parentTask.id}`);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('There was a problem deleting the task.');
|
||||
expect(response.body.error).toBe(
|
||||
'There was a problem deleting the task.'
|
||||
);
|
||||
|
||||
// Verify task still exists
|
||||
const taskStillExists = await Task.findByPk(parentTask.id);
|
||||
|
|
|
|||
|
|
@ -8,28 +8,24 @@ describe('Tags Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/tag', () => {
|
||||
it('should create a new tag', async () => {
|
||||
const tagData = {
|
||||
name: 'work'
|
||||
name: 'work',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/tag')
|
||||
.send(tagData);
|
||||
const response = await agent.post('/api/tag').send(tagData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.name).toBe(tagData.name);
|
||||
|
|
@ -38,12 +34,10 @@ describe('Tags Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const tagData = {
|
||||
name: 'work'
|
||||
name: 'work',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/tag')
|
||||
.send(tagData);
|
||||
const response = await request(app).post('/api/tag').send(tagData);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -52,9 +46,7 @@ describe('Tags Routes', () => {
|
|||
it('should require tag name', async () => {
|
||||
const tagData = {};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/tag')
|
||||
.send(tagData);
|
||||
const response = await agent.post('/api/tag').send(tagData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Tag name is required');
|
||||
|
|
@ -67,12 +59,12 @@ describe('Tags Routes', () => {
|
|||
beforeEach(async () => {
|
||||
tag1 = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
tag2 = await Tag.create({
|
||||
name: 'personal',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -81,8 +73,8 @@ describe('Tags Routes', () => {
|
|||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveLength(2);
|
||||
expect(response.body.map(t => t.id)).toContain(tag1.id);
|
||||
expect(response.body.map(t => t.id)).toContain(tag2.id);
|
||||
expect(response.body.map((t) => t.id)).toContain(tag1.id);
|
||||
expect(response.body.map((t) => t.id)).toContain(tag2.id);
|
||||
});
|
||||
|
||||
it('should order tags by name', async () => {
|
||||
|
|
@ -107,7 +99,7 @@ describe('Tags Routes', () => {
|
|||
beforeEach(async () => {
|
||||
tag = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -126,16 +118,16 @@ describe('Tags Routes', () => {
|
|||
expect(response.body.error).toBe('Tag not found');
|
||||
});
|
||||
|
||||
it('should not allow access to other user\'s tags', async () => {
|
||||
it("should not allow access to other user's tags", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherTag = await Tag.create({
|
||||
name: 'other-tag',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.get(`/api/tag/${otherTag.id}`);
|
||||
|
|
@ -158,13 +150,13 @@ describe('Tags Routes', () => {
|
|||
beforeEach(async () => {
|
||||
tag = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update tag', async () => {
|
||||
const updateData = {
|
||||
name: 'updated-work'
|
||||
name: 'updated-work',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -184,16 +176,16 @@ describe('Tags Routes', () => {
|
|||
expect(response.body.error).toBe('Tag not found');
|
||||
});
|
||||
|
||||
it('should not allow updating other user\'s tags', async () => {
|
||||
it("should not allow updating other user's tags", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherTag = await Tag.create({
|
||||
name: 'other-tag',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -220,7 +212,7 @@ describe('Tags Routes', () => {
|
|||
beforeEach(async () => {
|
||||
tag = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -242,16 +234,16 @@ describe('Tags Routes', () => {
|
|||
expect(response.body.error).toBe('Tag not found');
|
||||
});
|
||||
|
||||
it('should not allow deleting other user\'s tags', async () => {
|
||||
it("should not allow deleting other user's tags", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherTag = await Tag.create({
|
||||
name: 'other-tag',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.delete(`/api/tag/${otherTag.id}`);
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Tasks Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -27,12 +25,10 @@ describe('Tasks Routes', () => {
|
|||
name: 'Test Task',
|
||||
note: 'Test Note',
|
||||
priority: 1,
|
||||
status: 0
|
||||
status: 0,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.id).toBeDefined();
|
||||
|
|
@ -45,7 +41,7 @@ describe('Tasks Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const taskData = {
|
||||
name: 'Test Task'
|
||||
name: 'Test Task',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -62,12 +58,10 @@ describe('Tasks Routes', () => {
|
|||
console.error = jest.fn();
|
||||
|
||||
const taskData = {
|
||||
description: 'Test Description'
|
||||
description: 'Test Description',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
|
|
@ -84,14 +78,14 @@ describe('Tasks Routes', () => {
|
|||
name: 'Task 1',
|
||||
description: 'Description 1',
|
||||
user_id: user.id,
|
||||
today: true
|
||||
today: true,
|
||||
});
|
||||
|
||||
task2 = await Task.create({
|
||||
name: 'Task 2',
|
||||
description: 'Description 2',
|
||||
user_id: user.id,
|
||||
today: false
|
||||
today: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -101,8 +95,8 @@ describe('Tasks Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(response.body.tasks).toBeDefined();
|
||||
expect(response.body.tasks.length).toBe(2);
|
||||
expect(response.body.tasks.map(t => t.id)).toContain(task1.id);
|
||||
expect(response.body.tasks.map(t => t.id)).toContain(task2.id);
|
||||
expect(response.body.tasks.map((t) => t.id)).toContain(task1.id);
|
||||
expect(response.body.tasks.map((t) => t.id)).toContain(task2.id);
|
||||
});
|
||||
|
||||
it('should filter today tasks (returns all user tasks)', async () => {
|
||||
|
|
@ -133,7 +127,7 @@ describe('Tasks Routes', () => {
|
|||
description: 'Test Description',
|
||||
priority: 0,
|
||||
status: 0,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -142,7 +136,7 @@ describe('Tasks Routes', () => {
|
|||
name: 'Updated Task',
|
||||
note: 'Updated Note',
|
||||
priority: 2,
|
||||
status: 1
|
||||
status: 1,
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -166,16 +160,16 @@ describe('Tasks Routes', () => {
|
|||
expect(response.body.error).toBe('Task not found.');
|
||||
});
|
||||
|
||||
it('should not allow updating other user\'s tasks', async () => {
|
||||
it("should not allow updating other user's tasks", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherTask = await Task.create({
|
||||
name: 'Other Task',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
|
|
@ -202,7 +196,7 @@ describe('Tasks Routes', () => {
|
|||
beforeEach(async () => {
|
||||
task = await Task.create({
|
||||
name: 'Test Task',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -224,16 +218,16 @@ describe('Tasks Routes', () => {
|
|||
expect(response.body.error).toBe('Task not found.');
|
||||
});
|
||||
|
||||
it('should not allow deleting other user\'s tasks', async () => {
|
||||
it("should not allow deleting other user's tasks", async () => {
|
||||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const otherTask = await Task.create({
|
||||
name: 'Other Task',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
const response = await agent.delete(`/api/task/${otherTask.id}`);
|
||||
|
|
@ -254,21 +248,16 @@ describe('Tasks Routes', () => {
|
|||
it('should create task with tags', async () => {
|
||||
const taskData = {
|
||||
name: 'Test Task',
|
||||
tags: [
|
||||
{ name: 'work' },
|
||||
{ name: 'urgent' }
|
||||
]
|
||||
tags: [{ name: 'work' }, { name: 'urgent' }],
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData);
|
||||
const response = await agent.post('/api/task').send(taskData);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.Tags).toBeDefined();
|
||||
expect(response.body.Tags.length).toBe(2);
|
||||
expect(response.body.Tags.map(t => t.name)).toContain('work');
|
||||
expect(response.body.Tags.map(t => t.name)).toContain('urgent');
|
||||
expect(response.body.Tags.map((t) => t.name)).toContain('work');
|
||||
expect(response.body.Tags.map((t) => t.name)).toContain('urgent');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,7 +7,8 @@ jest.mock('https', () => {
|
|||
on: jest.fn((event, callback) => {
|
||||
if (event === 'data') {
|
||||
// Simulate API response with duplicate updates
|
||||
callback(JSON.stringify({
|
||||
callback(
|
||||
JSON.stringify({
|
||||
ok: true,
|
||||
result: [
|
||||
{
|
||||
|
|
@ -16,21 +17,22 @@ jest.mock('https', () => {
|
|||
message_id: 123,
|
||||
text: 'Buy groceries from the store',
|
||||
chat: { id: 987654321 },
|
||||
date: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
} else if (event === 'end') {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
on: jest.fn(),
|
||||
write: jest.fn(),
|
||||
end: jest.fn()
|
||||
end: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -41,7 +43,7 @@ jest.mock('https', () => {
|
|||
request: jest.fn((url, options, callback) => {
|
||||
callback(mockResponse);
|
||||
return mockRequest;
|
||||
})
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -69,7 +71,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
email: 'telegram-user@example.com',
|
||||
password_digest: 'hashedpassword',
|
||||
telegram_bot_token: 'real-bot-token-456',
|
||||
telegram_chat_id: '987654321'
|
||||
telegram_chat_id: '987654321',
|
||||
});
|
||||
|
||||
// Clear inbox
|
||||
|
|
@ -100,11 +102,11 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: messageId }
|
||||
metadata: { telegram_message_id: messageId },
|
||||
});
|
||||
|
||||
// Wait a moment (simulating network delay)
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Simulate duplicate processing attempt (same message, different processing cycle)
|
||||
const recentCutoff = new Date(Date.now() - 30000);
|
||||
|
|
@ -114,9 +116,9 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
user_id: testUser.id,
|
||||
source: 'telegram',
|
||||
created_at: {
|
||||
[require('sequelize').Op.gte]: recentCutoff
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.gte]: recentCutoff,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Should find the existing item
|
||||
|
|
@ -125,7 +127,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
|
||||
// Verify only one item exists
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -135,7 +137,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
{ content: 'First message', messageId: 201, updateId: 2001 },
|
||||
{ content: 'Second message', messageId: 202, updateId: 2002 },
|
||||
{ content: 'First message', messageId: 203, updateId: 2003 }, // Duplicate content
|
||||
{ content: 'Third message', messageId: 204, updateId: 2004 }
|
||||
{ content: 'Third message', messageId: 204, updateId: 2004 },
|
||||
];
|
||||
|
||||
// Process all messages rapidly
|
||||
|
|
@ -149,9 +151,11 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
user_id: testUser.id,
|
||||
source: 'telegram',
|
||||
created_at: {
|
||||
[require('sequelize').Op.gte]: new Date(Date.now() - 30000)
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.gte]: new Date(
|
||||
Date.now() - 30000
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingItem) {
|
||||
|
|
@ -162,7 +166,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
content: msg.content,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: msg.messageId }
|
||||
metadata: { telegram_message_id: msg.messageId },
|
||||
});
|
||||
createdItems.push(newItem);
|
||||
}
|
||||
|
|
@ -173,7 +177,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
|
||||
// Should have 3 unique items (first, second, third) - duplicate "First message" should be prevented
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
|
||||
expect(allItems).toHaveLength(3);
|
||||
|
|
@ -193,10 +197,38 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
const processedUpdates = new Set();
|
||||
|
||||
const updates = [
|
||||
{ update_id: 3001, message: { text: 'Message 1', message_id: 301, chat: { id: 987654321 } } },
|
||||
{ update_id: 3002, message: { text: 'Message 2', message_id: 302, chat: { id: 987654321 } } },
|
||||
{ update_id: 3001, message: { text: 'Message 1', message_id: 301, chat: { id: 987654321 } } }, // Duplicate update
|
||||
{ update_id: 3003, message: { text: 'Message 3', message_id: 303, chat: { id: 987654321 } } }
|
||||
{
|
||||
update_id: 3001,
|
||||
message: {
|
||||
text: 'Message 1',
|
||||
message_id: 301,
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
},
|
||||
{
|
||||
update_id: 3002,
|
||||
message: {
|
||||
text: 'Message 2',
|
||||
message_id: 302,
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
},
|
||||
{
|
||||
update_id: 3001,
|
||||
message: {
|
||||
text: 'Message 1',
|
||||
message_id: 301,
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
}, // Duplicate update
|
||||
{
|
||||
update_id: 3003,
|
||||
message: {
|
||||
text: 'Message 3',
|
||||
message_id: 303,
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const processedCount = { count: 0 };
|
||||
|
|
@ -216,11 +248,13 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
user_id: testUser.id,
|
||||
metadata: {
|
||||
telegram_message_id: update.message.message_id,
|
||||
update_id: update.update_id
|
||||
}
|
||||
update_id: update.update_id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log(`Skipping already processed update: ${update.update_id}`);
|
||||
console.log(
|
||||
`Skipping already processed update: ${update.update_id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +264,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
|
||||
// Verify inbox items
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(3);
|
||||
});
|
||||
|
|
@ -241,7 +275,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
content: 'Message before restart',
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 401, update_id: 4001 }
|
||||
metadata: { telegram_message_id: 401, update_id: 4001 },
|
||||
});
|
||||
|
||||
// Add user to poller
|
||||
|
|
@ -264,7 +298,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
|
||||
// The poller should maintain its state correctly
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(1);
|
||||
expect(allItems[0].id).toBe(initialItem.id);
|
||||
|
|
@ -285,7 +319,9 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
if (processedUpdates.size > 1000) {
|
||||
const allEntries = Array.from(processedUpdates);
|
||||
const oldestEntries = allEntries.slice(0, 200); // Remove oldest 200
|
||||
oldestEntries.forEach(entry => processedUpdates.delete(entry));
|
||||
oldestEntries.forEach((entry) =>
|
||||
processedUpdates.delete(entry)
|
||||
);
|
||||
}
|
||||
|
||||
expect(processedUpdates.size).toBe(1000);
|
||||
|
|
@ -309,11 +345,11 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 501 }
|
||||
metadata: { telegram_message_id: 501 },
|
||||
});
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Try to create with same content but different message ID
|
||||
// This should be prevented by the content-based duplicate check
|
||||
|
|
@ -324,9 +360,9 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
user_id: testUser.id,
|
||||
source: 'telegram',
|
||||
created_at: {
|
||||
[require('sequelize').Op.gte]: recentCutoff
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.gte]: recentCutoff,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(existingItem).toBeTruthy();
|
||||
|
|
@ -344,7 +380,7 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
user_id: testUser.id,
|
||||
created_at: oldTimestamp,
|
||||
updated_at: oldTimestamp,
|
||||
metadata: { telegram_message_id: 601 }
|
||||
metadata: { telegram_message_id: 601 },
|
||||
});
|
||||
|
||||
// Now try to create new item with same content
|
||||
|
|
@ -352,12 +388,12 @@ describe('Telegram Duplicate Message Scenario', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 602 }
|
||||
metadata: { telegram_message_id: 602 },
|
||||
});
|
||||
|
||||
// Should be allowed since the old one is outside the 30-second window
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(2);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
email: 'test-telegram@example.com',
|
||||
password_digest: 'hashedpassword',
|
||||
telegram_bot_token: 'test-bot-token-123',
|
||||
telegram_chat_id: '987654321'
|
||||
telegram_chat_id: '987654321',
|
||||
});
|
||||
|
||||
// Clear any existing inbox items
|
||||
|
|
@ -58,11 +58,11 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 123 }
|
||||
metadata: { telegram_message_id: 123 },
|
||||
});
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Try to create duplicate item (should be prevented)
|
||||
const duplicateCheck = await InboxItem.findOne({
|
||||
|
|
@ -71,9 +71,11 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
user_id: testUser.id,
|
||||
source: 'telegram',
|
||||
created_at: {
|
||||
[require('sequelize').Op.gte]: new Date(Date.now() - 30000)
|
||||
}
|
||||
}
|
||||
[require('sequelize').Op.gte]: new Date(
|
||||
Date.now() - 30000
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(duplicateCheck).toBeTruthy();
|
||||
|
|
@ -81,7 +83,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
|
||||
// Verify only one item exists
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(1);
|
||||
});
|
||||
|
|
@ -95,7 +97,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
created_at: new Date(Date.now() - 35000), // 35 seconds ago
|
||||
metadata: { telegram_message_id: 124 }
|
||||
metadata: { telegram_message_id: 124 },
|
||||
});
|
||||
|
||||
// Create second item (should be allowed)
|
||||
|
|
@ -103,12 +105,12 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 125 }
|
||||
metadata: { telegram_message_id: 125 },
|
||||
});
|
||||
|
||||
// Verify both items exist
|
||||
const allItems = await InboxItem.findAll({
|
||||
where: { user_id: testUser.id }
|
||||
where: { user_id: testUser.id },
|
||||
});
|
||||
expect(allItems).toHaveLength(2);
|
||||
});
|
||||
|
|
@ -119,7 +121,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
email: 'test2-telegram@example.com',
|
||||
password_digest: 'hashedpassword',
|
||||
telegram_bot_token: 'test-bot-token-456',
|
||||
telegram_chat_id: '123456789'
|
||||
telegram_chat_id: '123456789',
|
||||
});
|
||||
|
||||
const messageContent = 'Shared message content';
|
||||
|
|
@ -129,7 +131,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser.id,
|
||||
metadata: { telegram_message_id: 126 }
|
||||
metadata: { telegram_message_id: 126 },
|
||||
});
|
||||
|
||||
// Create item for second user (should be allowed)
|
||||
|
|
@ -137,15 +139,19 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
content: messageContent,
|
||||
source: 'telegram',
|
||||
user_id: testUser2.id,
|
||||
metadata: { telegram_message_id: 127 }
|
||||
metadata: { telegram_message_id: 127 },
|
||||
});
|
||||
|
||||
// Verify both items exist
|
||||
const allItems = await InboxItem.findAll();
|
||||
expect(allItems).toHaveLength(2);
|
||||
|
||||
const user1Items = allItems.filter(item => item.user_id === testUser.id);
|
||||
const user2Items = allItems.filter(item => item.user_id === testUser2.id);
|
||||
const user1Items = allItems.filter(
|
||||
(item) => item.user_id === testUser.id
|
||||
);
|
||||
const user2Items = allItems.filter(
|
||||
(item) => item.user_id === testUser2.id
|
||||
);
|
||||
|
||||
expect(user1Items).toHaveLength(1);
|
||||
expect(user2Items).toHaveLength(1);
|
||||
|
|
@ -178,7 +184,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
test('should not add user without telegram token', async () => {
|
||||
const userWithoutToken = await User.create({
|
||||
email: 'no-token@example.com',
|
||||
password_digest: 'hashedpassword'
|
||||
password_digest: 'hashedpassword',
|
||||
// No telegram_bot_token
|
||||
});
|
||||
|
||||
|
|
@ -213,17 +219,17 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
message: {
|
||||
message_id: 501,
|
||||
text: 'First message',
|
||||
chat: { id: 987654321 }
|
||||
}
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
},
|
||||
{
|
||||
update_id: 1002,
|
||||
message: {
|
||||
message_id: 502,
|
||||
text: 'Second message',
|
||||
chat: { id: 987654321 }
|
||||
}
|
||||
}
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Test highest update ID calculation
|
||||
|
|
@ -231,8 +237,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
expect(highestId).toBe(1002);
|
||||
|
||||
// Test update key generation (simulating internal logic)
|
||||
const updateKeys = mockUpdates.map(update => `${testUser.id}-${update.update_id}`);
|
||||
expect(updateKeys).toEqual([`${testUser.id}-1001`, `${testUser.id}-1002`]);
|
||||
const updateKeys = mockUpdates.map(
|
||||
(update) => `${testUser.id}-${update.update_id}`
|
||||
);
|
||||
expect(updateKeys).toEqual([
|
||||
`${testUser.id}-1001`,
|
||||
`${testUser.id}-1002`,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should properly track processed updates', async () => {
|
||||
|
|
@ -247,8 +258,8 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
const newUpdates = [
|
||||
{ update_id: 1001 }, // Should be filtered out
|
||||
{ update_id: 1002 }, // Should be filtered out
|
||||
{ update_id: 1003 } // Should remain
|
||||
].filter(update => {
|
||||
{ update_id: 1003 }, // Should remain
|
||||
].filter((update) => {
|
||||
const updateKey = `1-${update.update_id}`;
|
||||
return !processedUpdates.has(updateKey);
|
||||
});
|
||||
|
|
@ -270,8 +281,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
|
||||
// Simulate cleanup (remove oldest 100)
|
||||
if (processedUpdates.size > 1000) {
|
||||
const oldestEntries = Array.from(processedUpdates).slice(0, 100);
|
||||
oldestEntries.forEach(entry => processedUpdates.delete(entry));
|
||||
const oldestEntries = Array.from(processedUpdates).slice(
|
||||
0,
|
||||
100
|
||||
);
|
||||
oldestEntries.forEach((entry) =>
|
||||
processedUpdates.delete(entry)
|
||||
);
|
||||
}
|
||||
|
||||
expect(processedUpdates.size).toBe(1000);
|
||||
|
|
@ -284,17 +300,17 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
test('should handle database errors gracefully', async () => {
|
||||
// Mock InboxItem.create to throw an error
|
||||
const originalCreate = InboxItem.create;
|
||||
InboxItem.create = jest.fn().mockRejectedValue(new Error('Database error'));
|
||||
InboxItem.create = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
try {
|
||||
await InboxItem.create({
|
||||
await expect(
|
||||
InboxItem.create({
|
||||
content: 'Test error handling',
|
||||
source: 'telegram',
|
||||
user_id: testUser.id
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.message).toBe('Database error');
|
||||
}
|
||||
user_id: testUser.id,
|
||||
})
|
||||
).rejects.toThrow('Database error');
|
||||
|
||||
// Restore original function
|
||||
InboxItem.create = originalCreate;
|
||||
|
|
@ -313,12 +329,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
|
|||
message: {
|
||||
// Missing text and other properties
|
||||
message_id: 601,
|
||||
chat: { id: 987654321 }
|
||||
}
|
||||
chat: { id: 987654321 },
|
||||
},
|
||||
};
|
||||
|
||||
// The actual processing would skip this message due to missing text
|
||||
const hasText = incompleteUpdate.message && incompleteUpdate.message.text;
|
||||
const hasText =
|
||||
incompleteUpdate.message && incompleteUpdate.message.text;
|
||||
expect(hasText).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Telegram Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -30,7 +28,9 @@ describe('Telegram Routes', () => {
|
|||
.send({ token: botToken });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Telegram bot token updated successfully');
|
||||
expect(response.body.message).toBe(
|
||||
'Telegram bot token updated successfully'
|
||||
);
|
||||
|
||||
// Verify token was saved to user
|
||||
const updatedUser = await User.findByPk(user.id);
|
||||
|
|
@ -40,16 +40,16 @@ describe('Telegram Routes', () => {
|
|||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/telegram/setup')
|
||||
.send({ token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-1234567890' });
|
||||
.send({
|
||||
token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-1234567890',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
});
|
||||
|
||||
it('should require token parameter', async () => {
|
||||
const response = await agent
|
||||
.post('/api/telegram/setup')
|
||||
.send({});
|
||||
const response = await agent.post('/api/telegram/setup').send({});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Telegram bot token is required.');
|
||||
|
|
@ -61,7 +61,9 @@ describe('Telegram Routes', () => {
|
|||
.send({ token: 'invalid-token-format' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid Telegram bot token format.');
|
||||
expect(response.body.error).toBe(
|
||||
'Invalid Telegram bot token format.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate token format with correct pattern', async () => {
|
||||
|
|
@ -71,7 +73,7 @@ describe('Telegram Routes', () => {
|
|||
'notnum:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
|
||||
'123456789-ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
|
||||
'123456789:',
|
||||
':ABCdefGHIjklMNOPQRSTUVwxyz-12345678'
|
||||
':ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
|
||||
];
|
||||
|
||||
for (const token of invalidTokens) {
|
||||
|
|
@ -80,7 +82,9 @@ describe('Telegram Routes', () => {
|
|||
.send({ token });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid Telegram bot token format.');
|
||||
expect(response.body.error).toBe(
|
||||
'Invalid Telegram bot token format.'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -88,7 +92,7 @@ describe('Telegram Routes', () => {
|
|||
const validTokens = [
|
||||
'123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
|
||||
'987654321:XYZabcDEFghiJKLmnoPQRstUVW_09876543',
|
||||
'555555555:abcdefghijklmnopqrstuvwxyzABCDEFGHI'
|
||||
'555555555:abcdefghijklmnopqrstuvwxyzABCDEFGHI',
|
||||
];
|
||||
|
||||
for (const token of validTokens) {
|
||||
|
|
@ -97,7 +101,9 @@ describe('Telegram Routes', () => {
|
|||
.send({ token });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.message).toBe('Telegram bot token updated successfully');
|
||||
expect(response.body.message).toBe(
|
||||
'Telegram bot token updated successfully'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -106,13 +112,15 @@ describe('Telegram Routes', () => {
|
|||
beforeEach(async () => {
|
||||
// Setup bot token first
|
||||
await user.update({
|
||||
telegram_bot_token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678'
|
||||
telegram_bot_token:
|
||||
'123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
|
||||
});
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/telegram/start-polling');
|
||||
const response = await request(app).post(
|
||||
'/api/telegram/start-polling'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -122,8 +130,7 @@ describe('Telegram Routes', () => {
|
|||
// Remove bot token
|
||||
await user.update({ telegram_bot_token: null });
|
||||
|
||||
const response = await agent
|
||||
.post('/api/telegram/start-polling');
|
||||
const response = await agent.post('/api/telegram/start-polling');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Telegram bot token not set.');
|
||||
|
|
@ -132,8 +139,9 @@ describe('Telegram Routes', () => {
|
|||
|
||||
describe('POST /api/telegram/stop-polling', () => {
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/telegram/stop-polling');
|
||||
const response = await request(app).post(
|
||||
'/api/telegram/stop-polling'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -142,8 +150,9 @@ describe('Telegram Routes', () => {
|
|||
|
||||
describe('GET /api/telegram/polling-status', () => {
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/telegram/polling-status');
|
||||
const response = await request(app).get(
|
||||
'/api/telegram/polling-status'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
|
|||
|
|
@ -7,16 +7,14 @@ describe('URL Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -31,8 +29,7 @@ describe('URL Routes', () => {
|
|||
});
|
||||
|
||||
it('should require url parameter', async () => {
|
||||
const response = await agent
|
||||
.get('/api/url/title');
|
||||
const response = await agent.get('/api/url/title');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('URL parameter is required');
|
||||
|
|
@ -48,7 +45,10 @@ describe('URL Routes', () => {
|
|||
expect(response.body).toHaveProperty('title');
|
||||
expect(response.body.url).toBe('https://httpbin.org/html');
|
||||
// Title could be extracted or null depending on network conditions
|
||||
expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
|
||||
expect(
|
||||
typeof response.body.title === 'string' ||
|
||||
response.body.title === null
|
||||
).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('should handle URL without protocol', async () => {
|
||||
|
|
@ -61,7 +61,10 @@ describe('URL Routes', () => {
|
|||
expect(response.body).toHaveProperty('title');
|
||||
expect(response.body.url).toBe('httpbin.org/html');
|
||||
// Title could be extracted or null depending on network conditions
|
||||
expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
|
||||
expect(
|
||||
typeof response.body.title === 'string' ||
|
||||
response.body.title === null
|
||||
).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('should handle invalid URL gracefully', async () => {
|
||||
|
|
@ -74,7 +77,10 @@ describe('URL Routes', () => {
|
|||
expect(response.body).toHaveProperty('title');
|
||||
expect(response.body.url).toBe('not-a-valid-url');
|
||||
// Title could be null or error message
|
||||
expect(response.body.title === null || typeof response.body.title === 'string').toBe(true);
|
||||
expect(
|
||||
response.body.title === null ||
|
||||
typeof response.body.title === 'string'
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle unreachable URL', async () => {
|
||||
|
|
@ -85,7 +91,9 @@ describe('URL Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(response.body).toHaveProperty('url');
|
||||
expect(response.body).toHaveProperty('title');
|
||||
expect(response.body.url).toBe('https://nonexistent-domain-12345.com');
|
||||
expect(response.body.url).toBe(
|
||||
'https://nonexistent-domain-12345.com'
|
||||
);
|
||||
expect(response.body.title).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
@ -110,7 +118,8 @@ describe('URL Routes', () => {
|
|||
});
|
||||
|
||||
it('should extract URL from text and get title', async () => {
|
||||
const testText = 'Check out this interesting site: https://httpbin.org/html';
|
||||
const testText =
|
||||
'Check out this interesting site: https://httpbin.org/html';
|
||||
const response = await agent
|
||||
.post('/api/url/extract-from-text')
|
||||
.send({ text: testText });
|
||||
|
|
@ -121,11 +130,15 @@ describe('URL Routes', () => {
|
|||
expect(response.body.originalText).toBe(testText);
|
||||
expect(response.body).toHaveProperty('title');
|
||||
// Title could be extracted or null depending on network conditions
|
||||
expect(typeof response.body.title === 'string' || response.body.title === null).toBe(true);
|
||||
expect(
|
||||
typeof response.body.title === 'string' ||
|
||||
response.body.title === null
|
||||
).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('should extract first URL when multiple URLs in text', async () => {
|
||||
const testText = 'Check out https://httpbin.org/html and also https://example.com';
|
||||
const testText =
|
||||
'Check out https://httpbin.org/html and also https://example.com';
|
||||
const response = await agent
|
||||
.post('/api/url/extract-from-text')
|
||||
.send({ text: testText });
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('User Create Script', () => {
|
|||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('node', [scriptPath, ...args], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
env: { ...process.env, NODE_ENV: 'test' }
|
||||
env: { ...process.env, NODE_ENV: 'test' },
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
|
|
@ -28,7 +28,7 @@ describe('User Create Script', () => {
|
|||
resolve({
|
||||
code,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim()
|
||||
stderr: stderr.trim(),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -42,8 +42,13 @@ describe('User Create Script', () => {
|
|||
// Clean up any test users created during tests
|
||||
await User.destroy({
|
||||
where: {
|
||||
email: ['testuser@example.com', 'admin@example.com', 'invalid-email', 'existing@example.com']
|
||||
}
|
||||
email: [
|
||||
'testuser@example.com',
|
||||
'admin@example.com',
|
||||
'invalid-email',
|
||||
'existing@example.com',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -108,22 +113,30 @@ describe('User Create Script', () => {
|
|||
const result = await runUserCreateScript([]);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('❌ Usage: npm run user:create <email> <password>');
|
||||
expect(result.stderr).toContain('Example: npm run user:create admin@example.com mypassword123');
|
||||
expect(result.stderr).toContain(
|
||||
'❌ Usage: npm run user:create <email> <password>'
|
||||
);
|
||||
expect(result.stderr).toContain(
|
||||
'Example: npm run user:create admin@example.com mypassword123'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show usage when only email provided', async () => {
|
||||
const result = await runUserCreateScript(['test@example.com']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('❌ Usage: npm run user:create <email> <password>');
|
||||
expect(result.stderr).toContain(
|
||||
'❌ Usage: npm run user:create <email> <password>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should show usage when only password provided', async () => {
|
||||
const result = await runUserCreateScript(['', 'password123']);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('❌ Usage: npm run user:create <email> <password>');
|
||||
expect(result.stderr).toContain(
|
||||
'❌ Usage: npm run user:create <email> <password>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid email format', async () => {
|
||||
|
|
@ -133,11 +146,14 @@ describe('User Create Script', () => {
|
|||
'@missing-local.com',
|
||||
'spaces in@email.com',
|
||||
'double@@domain.com',
|
||||
'trailing.dot.@domain.com'
|
||||
'trailing.dot.@domain.com',
|
||||
];
|
||||
|
||||
for (const email of invalidEmails) {
|
||||
const result = await runUserCreateScript([email, 'password123']);
|
||||
const result = await runUserCreateScript([
|
||||
email,
|
||||
'password123',
|
||||
]);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('❌ Invalid email format');
|
||||
|
|
@ -148,10 +164,15 @@ describe('User Create Script', () => {
|
|||
const shortPasswords = ['', '1', '12', '123', '1234', '12345'];
|
||||
|
||||
for (const password of shortPasswords) {
|
||||
const result = await runUserCreateScript(['test@example.com', password]);
|
||||
const result = await runUserCreateScript([
|
||||
'test@example.com',
|
||||
password,
|
||||
]);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain('❌ Password must be at least 6 characters long');
|
||||
expect(result.stderr).toContain(
|
||||
'❌ Password must be at least 6 characters long'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -162,14 +183,16 @@ describe('User Create Script', () => {
|
|||
// Create user first
|
||||
await User.create({
|
||||
email,
|
||||
password_digest: await require('bcrypt').hash(password, 10)
|
||||
password_digest: await require('bcrypt').hash(password, 10),
|
||||
});
|
||||
|
||||
// Try to create same user again
|
||||
const result = await runUserCreateScript([email, password]);
|
||||
|
||||
expect(result.code).toBe(1);
|
||||
expect(result.stderr).toContain(`❌ User with email ${email} already exists`);
|
||||
expect(result.stderr).toContain(
|
||||
`❌ User with email ${email} already exists`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -178,6 +201,9 @@ describe('User Create Script', () => {
|
|||
const email = 'npmtest@example.com';
|
||||
const password = 'testpassword123';
|
||||
|
||||
// Clean up any existing user first
|
||||
await User.destroy({ where: { email } });
|
||||
|
||||
try {
|
||||
// This simulates running: npm run user:create npmtest@example.com testpassword123
|
||||
const output = execSync(
|
||||
|
|
@ -186,7 +212,7 @@ describe('User Create Script', () => {
|
|||
cwd: path.join(__dirname, '../..'),
|
||||
env: { ...process.env, NODE_ENV: 'test' },
|
||||
encoding: 'utf8',
|
||||
timeout: 10000
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -196,27 +222,6 @@ describe('User Create Script', () => {
|
|||
const createdUser = await User.findOne({ where: { email } });
|
||||
expect(createdUser).toBeTruthy();
|
||||
expect(createdUser.email).toBe(email);
|
||||
|
||||
} catch (error) {
|
||||
// If the command failed, check if it's due to duplicate user (from previous test runs)
|
||||
if (error.stderr?.includes('already exists')) {
|
||||
// Clean up and retry
|
||||
await User.destroy({ where: { email } });
|
||||
|
||||
const output = execSync(
|
||||
`npm run user:create ${email} ${password}`,
|
||||
{
|
||||
cwd: path.join(__dirname, '../..'),
|
||||
env: { ...process.env, NODE_ENV: 'test' },
|
||||
encoding: 'utf8',
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
expect(output).toContain('User created successfully');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
// Clean up
|
||||
await User.destroy({ where: { email } });
|
||||
|
|
@ -243,7 +248,10 @@ describe('User Create Script', () => {
|
|||
|
||||
// Verify the hash is valid
|
||||
const bcrypt = require('bcrypt');
|
||||
const isValid = await bcrypt.compare(password, createdUser.password_digest);
|
||||
const isValid = await bcrypt.compare(
|
||||
password,
|
||||
createdUser.password_digest
|
||||
);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
// Clean up
|
||||
|
|
@ -289,7 +297,10 @@ describe('User Create Script', () => {
|
|||
expect(createdUser).toBeTruthy();
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const isValid = await bcrypt.compare(password, createdUser.password_digest);
|
||||
const isValid = await bcrypt.compare(
|
||||
password,
|
||||
createdUser.password_digest
|
||||
);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
// Clean up
|
||||
|
|
|
|||
|
|
@ -8,16 +8,14 @@ describe('Users Routes', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await createTestUser({
|
||||
email: 'test@example.com'
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
// Create authenticated agent
|
||||
agent = request.agent(app);
|
||||
await agent
|
||||
.post('/api/login')
|
||||
.send({
|
||||
await agent.post('/api/login').send({
|
||||
email: 'test@example.com',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -62,29 +60,27 @@ describe('Users Routes', () => {
|
|||
language: 'es',
|
||||
timezone: 'UTC',
|
||||
avatar_image: 'new-avatar.png',
|
||||
telegram_bot_token: 'new-token'
|
||||
telegram_bot_token: 'new-token',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.patch('/api/profile')
|
||||
.send(updateData);
|
||||
const response = await agent.patch('/api/profile').send(updateData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.appearance).toBe(updateData.appearance);
|
||||
expect(response.body.language).toBe(updateData.language);
|
||||
expect(response.body.timezone).toBe(updateData.timezone);
|
||||
expect(response.body.avatar_image).toBe(updateData.avatar_image);
|
||||
expect(response.body.telegram_bot_token).toBe(updateData.telegram_bot_token);
|
||||
expect(response.body.telegram_bot_token).toBe(
|
||||
updateData.telegram_bot_token
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow partial updates', async () => {
|
||||
const updateData = {
|
||||
appearance: 'dark'
|
||||
appearance: 'dark',
|
||||
};
|
||||
|
||||
const response = await agent
|
||||
.patch('/api/profile')
|
||||
.send(updateData);
|
||||
const response = await agent.patch('/api/profile').send(updateData);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.appearance).toBe(updateData.appearance);
|
||||
|
|
@ -93,7 +89,7 @@ describe('Users Routes', () => {
|
|||
|
||||
it('should require authentication', async () => {
|
||||
const updateData = {
|
||||
appearance: 'dark'
|
||||
appearance: 'dark',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
|
|
@ -122,27 +118,37 @@ describe('Users Routes', () => {
|
|||
});
|
||||
|
||||
it('should toggle task summary on', async () => {
|
||||
const response = await agent.post('/api/profile/task-summary/toggle');
|
||||
const response = await agent.post(
|
||||
'/api/profile/task-summary/toggle'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.enabled).toBe(true);
|
||||
expect(response.body.message).toBe('Task summary notifications have been enabled.');
|
||||
expect(response.body.message).toBe(
|
||||
'Task summary notifications have been enabled.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should toggle task summary off', async () => {
|
||||
await user.update({ task_summary_enabled: true });
|
||||
|
||||
const response = await agent.post('/api/profile/task-summary/toggle');
|
||||
const response = await agent.post(
|
||||
'/api/profile/task-summary/toggle'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.enabled).toBe(false);
|
||||
expect(response.body.message).toBe('Task summary notifications have been disabled.');
|
||||
expect(response.body.message).toBe(
|
||||
'Task summary notifications have been disabled.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).post('/api/profile/task-summary/toggle');
|
||||
const response = await request(app).post(
|
||||
'/api/profile/task-summary/toggle'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -151,7 +157,9 @@ describe('Users Routes', () => {
|
|||
it('should return 401 when session user no longer exists', async () => {
|
||||
await User.destroy({ where: { id: user.id } });
|
||||
|
||||
const response = await agent.post('/api/profile/task-summary/toggle');
|
||||
const response = await agent.post(
|
||||
'/api/profile/task-summary/toggle'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('User not found');
|
||||
|
|
@ -167,7 +175,9 @@ describe('Users Routes', () => {
|
|||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.frequency).toBe('daily');
|
||||
expect(response.body.message).toBe('Task summary frequency has been set to daily.');
|
||||
expect(response.body.message).toBe(
|
||||
'Task summary frequency has been set to daily.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should require frequency parameter', async () => {
|
||||
|
|
@ -189,7 +199,16 @@ describe('Users Routes', () => {
|
|||
});
|
||||
|
||||
it('should accept valid frequencies', async () => {
|
||||
const validFrequencies = ['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h'];
|
||||
const validFrequencies = [
|
||||
'daily',
|
||||
'weekdays',
|
||||
'weekly',
|
||||
'1h',
|
||||
'2h',
|
||||
'4h',
|
||||
'8h',
|
||||
'12h',
|
||||
];
|
||||
|
||||
for (const frequency of validFrequencies) {
|
||||
const response = await agent
|
||||
|
|
@ -224,14 +243,20 @@ describe('Users Routes', () => {
|
|||
|
||||
describe('POST /api/profile/task-summary/send-now', () => {
|
||||
it('should require telegram configuration', async () => {
|
||||
const response = await agent.post('/api/profile/task-summary/send-now');
|
||||
const response = await agent.post(
|
||||
'/api/profile/task-summary/send-now'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Telegram bot is not properly configured.');
|
||||
expect(response.body.error).toBe(
|
||||
'Telegram bot is not properly configured.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).post('/api/profile/task-summary/send-now');
|
||||
const response = await request(app).post(
|
||||
'/api/profile/task-summary/send-now'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -240,7 +265,9 @@ describe('Users Routes', () => {
|
|||
it('should return 401 when session user no longer exists', async () => {
|
||||
await User.destroy({ where: { id: user.id } });
|
||||
|
||||
const response = await agent.post('/api/profile/task-summary/send-now');
|
||||
const response = await agent.post(
|
||||
'/api/profile/task-summary/send-now'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('User not found');
|
||||
|
|
@ -251,10 +278,12 @@ describe('Users Routes', () => {
|
|||
it('should get task summary status', async () => {
|
||||
await user.update({
|
||||
task_summary_enabled: true,
|
||||
task_summary_frequency: 'daily'
|
||||
task_summary_frequency: 'daily',
|
||||
});
|
||||
|
||||
const response = await agent.get('/api/profile/task-summary/status');
|
||||
const response = await agent.get(
|
||||
'/api/profile/task-summary/status'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
|
|
@ -265,7 +294,9 @@ describe('Users Routes', () => {
|
|||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const response = await request(app).get('/api/profile/task-summary/status');
|
||||
const response = await request(app).get(
|
||||
'/api/profile/task-summary/status'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('Authentication required');
|
||||
|
|
@ -274,7 +305,9 @@ describe('Users Routes', () => {
|
|||
it('should return 401 when session user no longer exists', async () => {
|
||||
await User.destroy({ where: { id: user.id } });
|
||||
|
||||
const response = await agent.get('/api/profile/task-summary/status');
|
||||
const response = await agent.get(
|
||||
'/api/profile/task-summary/status'
|
||||
);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(response.body.error).toBe('User not found');
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ describe('Auth Middleware', () => {
|
|||
beforeEach(() => {
|
||||
req = {
|
||||
path: '/api/tasks',
|
||||
session: {}
|
||||
session: {},
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
json: jest.fn(),
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
|
@ -49,7 +49,9 @@ describe('Auth Middleware', () => {
|
|||
await requireAuth(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -59,7 +61,9 @@ describe('Auth Middleware', () => {
|
|||
await requireAuth(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' });
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication required',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -67,12 +71,12 @@ describe('Auth Middleware', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
req.session = {
|
||||
userId: user.id + 1, // Non-existent user ID
|
||||
destroy: jest.fn()
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
|
||||
await requireAuth(req, res, next);
|
||||
|
|
@ -87,11 +91,11 @@ describe('Auth Middleware', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
req.session = {
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
await requireAuth(req, res, next);
|
||||
|
|
@ -110,17 +114,21 @@ describe('Auth Middleware', () => {
|
|||
|
||||
// Mock User.findByPk to throw an error
|
||||
const originalFindByPk = User.findByPk;
|
||||
User.findByPk = jest.fn().mockRejectedValue(new Error('Database connection error'));
|
||||
User.findByPk = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Database connection error'));
|
||||
|
||||
req.session = {
|
||||
userId: 123,
|
||||
destroy: jest.fn()
|
||||
destroy: jest.fn(),
|
||||
};
|
||||
|
||||
await requireAuth(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication error' });
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Authentication error',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
|
||||
// Restore original methods
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('Area Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ describe('Area Model', () => {
|
|||
const areaData = {
|
||||
name: 'Work',
|
||||
description: 'Work related projects',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
const area = await Area.create(areaData);
|
||||
|
|
@ -29,7 +29,7 @@ describe('Area Model', () => {
|
|||
it('should require name', async () => {
|
||||
const areaData = {
|
||||
description: 'Area without name',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await expect(Area.create(areaData)).rejects.toThrow();
|
||||
|
|
@ -37,7 +37,7 @@ describe('Area Model', () => {
|
|||
|
||||
it('should require user_id', async () => {
|
||||
const areaData = {
|
||||
name: 'Test Area'
|
||||
name: 'Test Area',
|
||||
};
|
||||
|
||||
await expect(Area.create(areaData)).rejects.toThrow();
|
||||
|
|
@ -47,7 +47,7 @@ describe('Area Model', () => {
|
|||
const areaData = {
|
||||
name: 'Test Area',
|
||||
user_id: user.id,
|
||||
description: null
|
||||
description: null,
|
||||
};
|
||||
|
||||
const area = await Area.create(areaData);
|
||||
|
|
@ -59,11 +59,11 @@ describe('Area Model', () => {
|
|||
it('should belong to a user', async () => {
|
||||
const area = await Area.create({
|
||||
name: 'Test Area',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const areaWithUser = await Area.findByPk(area.id, {
|
||||
include: [{ model: User }]
|
||||
include: [{ model: User }],
|
||||
});
|
||||
|
||||
expect(areaWithUser.User).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('InboxItem Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ describe('InboxItem Model', () => {
|
|||
content: 'Remember to buy groceries',
|
||||
status: 'added',
|
||||
source: 'web',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
const inboxItem = await InboxItem.create(inboxData);
|
||||
|
|
@ -30,7 +30,7 @@ describe('InboxItem Model', () => {
|
|||
|
||||
it('should require content', async () => {
|
||||
const inboxData = {
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await expect(InboxItem.create(inboxData)).rejects.toThrow();
|
||||
|
|
@ -38,7 +38,7 @@ describe('InboxItem Model', () => {
|
|||
|
||||
it('should require user_id', async () => {
|
||||
const inboxData = {
|
||||
content: 'Test content'
|
||||
content: 'Test content',
|
||||
};
|
||||
|
||||
await expect(InboxItem.create(inboxData)).rejects.toThrow();
|
||||
|
|
@ -48,7 +48,7 @@ describe('InboxItem Model', () => {
|
|||
const inboxData = {
|
||||
content: 'Test content',
|
||||
user_id: user.id,
|
||||
status: null
|
||||
status: null,
|
||||
};
|
||||
|
||||
await expect(InboxItem.create(inboxData)).rejects.toThrow();
|
||||
|
|
@ -58,7 +58,7 @@ describe('InboxItem Model', () => {
|
|||
const inboxData = {
|
||||
content: 'Test content',
|
||||
user_id: user.id,
|
||||
source: null
|
||||
source: null,
|
||||
};
|
||||
|
||||
await expect(InboxItem.create(inboxData)).rejects.toThrow();
|
||||
|
|
@ -69,7 +69,7 @@ describe('InboxItem Model', () => {
|
|||
it('should set correct default values', async () => {
|
||||
const inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(inboxItem.status).toBe('added');
|
||||
|
|
@ -81,11 +81,11 @@ describe('InboxItem Model', () => {
|
|||
it('should belong to a user', async () => {
|
||||
const inboxItem = await InboxItem.create({
|
||||
content: 'Test content',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const inboxItemWithUser = await InboxItem.findByPk(inboxItem.id, {
|
||||
include: [{ model: User }]
|
||||
include: [{ model: User }],
|
||||
});
|
||||
|
||||
expect(inboxItemWithUser.User).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ describe('Note Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ describe('Note Model', () => {
|
|||
title: 'Test Note',
|
||||
content: 'This is a test note content',
|
||||
user_id: user.id,
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
};
|
||||
|
||||
const note = await Note.create(noteData);
|
||||
|
|
@ -36,7 +36,7 @@ describe('Note Model', () => {
|
|||
it('should require user_id', async () => {
|
||||
const noteData = {
|
||||
title: 'Test Note',
|
||||
content: 'Test content'
|
||||
content: 'Test content',
|
||||
};
|
||||
|
||||
await expect(Note.create(noteData)).rejects.toThrow();
|
||||
|
|
@ -46,7 +46,7 @@ describe('Note Model', () => {
|
|||
const noteData = {
|
||||
title: null,
|
||||
content: null,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
const note = await Note.create(noteData);
|
||||
|
|
@ -59,7 +59,7 @@ describe('Note Model', () => {
|
|||
title: 'Test Note',
|
||||
content: 'Test content',
|
||||
user_id: user.id,
|
||||
project_id: null
|
||||
project_id: null,
|
||||
};
|
||||
|
||||
const note = await Note.create(noteData);
|
||||
|
|
@ -71,11 +71,11 @@ describe('Note Model', () => {
|
|||
it('should belong to a user', async () => {
|
||||
const note = await Note.create({
|
||||
title: 'Test Note',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const noteWithUser = await Note.findByPk(note.id, {
|
||||
include: [{ model: User }]
|
||||
include: [{ model: User }],
|
||||
});
|
||||
|
||||
expect(noteWithUser.User).toBeDefined();
|
||||
|
|
@ -87,11 +87,11 @@ describe('Note Model', () => {
|
|||
const note = await Note.create({
|
||||
title: 'Test Note',
|
||||
user_id: user.id,
|
||||
project_id: project.id
|
||||
project_id: project.id,
|
||||
});
|
||||
|
||||
const noteWithProject = await Note.findByPk(note.id, {
|
||||
include: [{ model: Project }]
|
||||
include: [{ model: Project }],
|
||||
});
|
||||
|
||||
expect(noteWithProject.Project).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ describe('Project Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
area = await Area.create({
|
||||
name: 'Work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ describe('Project Model', () => {
|
|||
pin_to_sidebar: false,
|
||||
priority: 1,
|
||||
user_id: user.id,
|
||||
area_id: area.id
|
||||
area_id: area.id,
|
||||
};
|
||||
|
||||
const project = await Project.create(projectData);
|
||||
|
|
@ -42,7 +42,7 @@ describe('Project Model', () => {
|
|||
it('should require name', async () => {
|
||||
const projectData = {
|
||||
description: 'Project without name',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await expect(Project.create(projectData)).rejects.toThrow();
|
||||
|
|
@ -50,7 +50,7 @@ describe('Project Model', () => {
|
|||
|
||||
it('should require user_id', async () => {
|
||||
const projectData = {
|
||||
name: 'Test Project'
|
||||
name: 'Test Project',
|
||||
};
|
||||
|
||||
await expect(Project.create(projectData)).rejects.toThrow();
|
||||
|
|
@ -60,7 +60,7 @@ describe('Project Model', () => {
|
|||
const projectData = {
|
||||
name: 'Test Project',
|
||||
user_id: user.id,
|
||||
priority: 5
|
||||
priority: 5,
|
||||
};
|
||||
|
||||
await expect(Project.create(projectData)).rejects.toThrow();
|
||||
|
|
@ -71,7 +71,7 @@ describe('Project Model', () => {
|
|||
const project = await Project.create({
|
||||
name: `Test Project ${priority}`,
|
||||
user_id: user.id,
|
||||
priority: priority
|
||||
priority: priority,
|
||||
});
|
||||
expect(project.priority).toBe(priority);
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ describe('Project Model', () => {
|
|||
it('should set correct default values', async () => {
|
||||
const project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(project.active).toBe(false);
|
||||
|
|
@ -98,7 +98,7 @@ describe('Project Model', () => {
|
|||
description: null,
|
||||
priority: null,
|
||||
due_date_at: null,
|
||||
area_id: null
|
||||
area_id: null,
|
||||
});
|
||||
|
||||
expect(project.description).toBeNull();
|
||||
|
|
@ -112,11 +112,11 @@ describe('Project Model', () => {
|
|||
it('should belong to a user', async () => {
|
||||
const project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const projectWithUser = await Project.findByPk(project.id, {
|
||||
include: [{ model: User }]
|
||||
include: [{ model: User }],
|
||||
});
|
||||
|
||||
expect(projectWithUser.User).toBeDefined();
|
||||
|
|
@ -127,11 +127,11 @@ describe('Project Model', () => {
|
|||
const project = await Project.create({
|
||||
name: 'Test Project',
|
||||
user_id: user.id,
|
||||
area_id: area.id
|
||||
area_id: area.id,
|
||||
});
|
||||
|
||||
const projectWithArea = await Project.findByPk(project.id, {
|
||||
include: [{ model: Area }]
|
||||
include: [{ model: Area }],
|
||||
});
|
||||
|
||||
expect(projectWithArea.Area).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('Tag Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ describe('Tag Model', () => {
|
|||
it('should create a tag with valid data', async () => {
|
||||
const tagData = {
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
const tag = await Tag.create(tagData);
|
||||
|
|
@ -26,7 +26,7 @@ describe('Tag Model', () => {
|
|||
|
||||
it('should require name', async () => {
|
||||
const tagData = {
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await expect(Tag.create(tagData)).rejects.toThrow();
|
||||
|
|
@ -34,7 +34,7 @@ describe('Tag Model', () => {
|
|||
|
||||
it('should require user_id', async () => {
|
||||
const tagData = {
|
||||
name: 'work'
|
||||
name: 'work',
|
||||
};
|
||||
|
||||
await expect(Tag.create(tagData)).rejects.toThrow();
|
||||
|
|
@ -44,17 +44,17 @@ describe('Tag Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const otherUser = await User.create({
|
||||
email: 'other@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
const tag1 = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const tag2 = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: otherUser.id
|
||||
user_id: otherUser.id,
|
||||
});
|
||||
|
||||
expect(tag1.name).toBe('work');
|
||||
|
|
@ -68,11 +68,11 @@ describe('Tag Model', () => {
|
|||
it('should belong to a user', async () => {
|
||||
const tag = await Tag.create({
|
||||
name: 'work',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const tagWithUser = await Tag.findByPk(tag.id, {
|
||||
include: [{ model: User }]
|
||||
include: [{ model: User }],
|
||||
});
|
||||
|
||||
expect(tagWithUser.User).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ describe('Task Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ describe('Task Model', () => {
|
|||
const taskData = {
|
||||
name: 'Test Task',
|
||||
description: 'Test Description',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
const task = await Task.create(taskData);
|
||||
|
|
@ -32,7 +32,7 @@ describe('Task Model', () => {
|
|||
|
||||
it('should require name', async () => {
|
||||
const taskData = {
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
};
|
||||
|
||||
await expect(Task.create(taskData)).rejects.toThrow();
|
||||
|
|
@ -40,7 +40,7 @@ describe('Task Model', () => {
|
|||
|
||||
it('should require user_id', async () => {
|
||||
const taskData = {
|
||||
name: 'Test Task'
|
||||
name: 'Test Task',
|
||||
};
|
||||
|
||||
await expect(Task.create(taskData)).rejects.toThrow();
|
||||
|
|
@ -50,7 +50,7 @@ describe('Task Model', () => {
|
|||
const taskData = {
|
||||
name: 'Test Task',
|
||||
user_id: user.id,
|
||||
priority: 5
|
||||
priority: 5,
|
||||
};
|
||||
|
||||
await expect(Task.create(taskData)).rejects.toThrow();
|
||||
|
|
@ -60,7 +60,7 @@ describe('Task Model', () => {
|
|||
const taskData = {
|
||||
name: 'Test Task',
|
||||
user_id: user.id,
|
||||
status: 10
|
||||
status: 10,
|
||||
};
|
||||
|
||||
await expect(Task.create(taskData)).rejects.toThrow();
|
||||
|
|
@ -89,7 +89,7 @@ describe('Task Model', () => {
|
|||
beforeEach(async () => {
|
||||
task = await Task.create({
|
||||
name: 'Test Task',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ describe('Task Model', () => {
|
|||
it('should set correct default values', async () => {
|
||||
const task = await Task.create({
|
||||
name: 'Test Task',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(task.today).toBe(false);
|
||||
|
|
@ -147,7 +147,7 @@ describe('Task Model', () => {
|
|||
recurrence_interval: null,
|
||||
recurrence_end_date: null,
|
||||
last_generated_date: null,
|
||||
project_id: null
|
||||
project_id: null,
|
||||
});
|
||||
|
||||
expect(task.description).toBeNull();
|
||||
|
|
@ -169,7 +169,7 @@ describe('Task Model', () => {
|
|||
priority: Task.PRIORITY.HIGH,
|
||||
status: Task.STATUS.IN_PROGRESS,
|
||||
note: 'Test Note',
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(task.description).toBe('Test Description');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ describe('User Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
};
|
||||
|
||||
const user = await User.create(userData);
|
||||
|
|
@ -21,7 +21,7 @@ describe('User Model', () => {
|
|||
|
||||
it('should require email', async () => {
|
||||
const userData = {
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow();
|
||||
|
|
@ -30,7 +30,7 @@ describe('User Model', () => {
|
|||
it('should require valid email format', async () => {
|
||||
const userData = {
|
||||
email: 'invalid-email',
|
||||
password: 'password123'
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow();
|
||||
|
|
@ -40,7 +40,7 @@ describe('User Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
};
|
||||
|
||||
await User.create(userData);
|
||||
|
|
@ -51,7 +51,7 @@ describe('User Model', () => {
|
|||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
appearance: 'invalid'
|
||||
appearance: 'invalid',
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow();
|
||||
|
|
@ -61,7 +61,7 @@ describe('User Model', () => {
|
|||
const userData = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
task_summary_frequency: 'invalid'
|
||||
task_summary_frequency: 'invalid',
|
||||
};
|
||||
|
||||
await expect(User.create(userData)).rejects.toThrow();
|
||||
|
|
@ -75,7 +75,7 @@ describe('User Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -85,10 +85,16 @@ describe('User Model', () => {
|
|||
});
|
||||
|
||||
it('should check password correctly', async () => {
|
||||
const isValid = await User.checkPassword('password123', user.password_digest);
|
||||
const isValid = await User.checkPassword(
|
||||
'password123',
|
||||
user.password_digest
|
||||
);
|
||||
expect(isValid).toBe(true);
|
||||
|
||||
const isInvalid = await User.checkPassword('wrongpassword', user.password_digest);
|
||||
const isInvalid = await User.checkPassword(
|
||||
'wrongpassword',
|
||||
user.password_digest
|
||||
);
|
||||
expect(isInvalid).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -100,10 +106,16 @@ describe('User Model', () => {
|
|||
|
||||
expect(user.password_digest).not.toBe(oldPasswordDigest);
|
||||
|
||||
const isValidNew = await User.checkPassword('newpassword', user.password_digest);
|
||||
const isValidNew = await User.checkPassword(
|
||||
'newpassword',
|
||||
user.password_digest
|
||||
);
|
||||
expect(isValidNew).toBe(true);
|
||||
|
||||
const isValidOld = await User.checkPassword('password123', user.password_digest);
|
||||
const isValidOld = await User.checkPassword(
|
||||
'password123',
|
||||
user.password_digest
|
||||
);
|
||||
expect(isValidOld).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -114,7 +126,10 @@ describe('User Model', () => {
|
|||
|
||||
expect(user.password_digest).not.toBe(oldPasswordDigest);
|
||||
|
||||
const isValidNew = await User.checkPassword('newpassword', user.password_digest);
|
||||
const isValidNew = await User.checkPassword(
|
||||
'newpassword',
|
||||
user.password_digest
|
||||
);
|
||||
expect(isValidNew).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -124,7 +139,7 @@ describe('User Model', () => {
|
|||
const bcrypt = require('bcrypt');
|
||||
const user = await User.create({
|
||||
email: 'test@example.com',
|
||||
password_digest: await bcrypt.hash('password123', 10)
|
||||
password_digest: await bcrypt.hash('password123', 10),
|
||||
});
|
||||
|
||||
expect(user.appearance).toBe('light');
|
||||
|
|
|
|||
|
|
@ -14,13 +14,17 @@ describe('Functional Services', () => {
|
|||
|
||||
it('should have pure helper functions for testing', () => {
|
||||
expect(typeof taskScheduler._createSchedulerState).toBe('function');
|
||||
expect(typeof taskScheduler._shouldDisableScheduler).toBe('function');
|
||||
expect(typeof taskScheduler._shouldDisableScheduler).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof taskScheduler._getCronExpression).toBe('function');
|
||||
});
|
||||
|
||||
it('should return proper cron expressions', () => {
|
||||
expect(taskScheduler._getCronExpression('daily')).toBe('0 7 * * *');
|
||||
expect(taskScheduler._getCronExpression('weekly')).toBe('0 7 * * 1');
|
||||
expect(taskScheduler._getCronExpression('weekly')).toBe(
|
||||
'0 7 * * 1'
|
||||
);
|
||||
expect(taskScheduler._getCronExpression('1h')).toBe('0 * * * *');
|
||||
});
|
||||
});
|
||||
|
|
@ -51,9 +55,12 @@ describe('Functional Services', () => {
|
|||
expect(updatedUsers).toHaveLength(3);
|
||||
expect(users).toHaveLength(2); // Original array unchanged
|
||||
|
||||
const filteredUsers = telegramPoller._removeUserFromList(updatedUsers, 2);
|
||||
const filteredUsers = telegramPoller._removeUserFromList(
|
||||
updatedUsers,
|
||||
2
|
||||
);
|
||||
expect(filteredUsers).toHaveLength(2);
|
||||
expect(filteredUsers.find(u => u.id === 2)).toBeUndefined();
|
||||
expect(filteredUsers.find((u) => u.id === 2)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -86,15 +93,25 @@ describe('Functional Services', () => {
|
|||
|
||||
describe('TaskSummaryService', () => {
|
||||
it('should export functional interface', () => {
|
||||
expect(typeof taskSummaryService.generateSummaryForUser).toBe('function');
|
||||
expect(typeof taskSummaryService.sendSummaryToUser).toBe('function');
|
||||
expect(typeof taskSummaryService.calculateNextRunTime).toBe('function');
|
||||
expect(typeof taskSummaryService.generateSummaryForUser).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof taskSummaryService.sendSummaryToUser).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof taskSummaryService.calculateNextRunTime).toBe(
|
||||
'function'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have pure helper functions for testing', () => {
|
||||
expect(typeof taskSummaryService._escapeMarkdown).toBe('function');
|
||||
expect(typeof taskSummaryService._getPriorityEmoji).toBe('function');
|
||||
expect(typeof taskSummaryService._buildTaskSection).toBe('function');
|
||||
expect(typeof taskSummaryService._getPriorityEmoji).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof taskSummaryService._buildTaskSection).toBe(
|
||||
'function'
|
||||
);
|
||||
});
|
||||
|
||||
it('should escape markdown correctly', () => {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1,
|
||||
note: 'Parent note'
|
||||
note: 'Parent note',
|
||||
});
|
||||
|
||||
const dueDate = new Date('2025-06-20T10:00:00Z');
|
||||
const childTask = await RecurringTaskService.createTaskInstance(parentTask, dueDate);
|
||||
const childTask = await RecurringTaskService.createTaskInstance(
|
||||
parentTask,
|
||||
dueDate
|
||||
);
|
||||
|
||||
expect(childTask.name).toBe(parentTask.name);
|
||||
expect(childTask.description).toBe(parentTask.description);
|
||||
|
|
@ -44,11 +47,14 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
project_id: null, // Changed to null to avoid foreign key issues
|
||||
priority: 2
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
const dueDate = new Date('2025-06-20T10:00:00Z');
|
||||
const childTask = await RecurringTaskService.createTaskInstance(parentTask, dueDate);
|
||||
const childTask = await RecurringTaskService.createTaskInstance(
|
||||
parentTask,
|
||||
dueDate
|
||||
);
|
||||
|
||||
expect(childTask.project_id).toBeNull();
|
||||
expect(childTask.recurring_parent_id).toBe(parentTask.id);
|
||||
|
|
@ -62,11 +68,14 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
user_id: user.id,
|
||||
description: null,
|
||||
note: null,
|
||||
priority: 0
|
||||
priority: 0,
|
||||
});
|
||||
|
||||
const dueDate = new Date('2025-06-20T10:00:00Z');
|
||||
const childTask = await RecurringTaskService.createTaskInstance(parentTask, dueDate);
|
||||
const childTask = await RecurringTaskService.createTaskInstance(
|
||||
parentTask,
|
||||
dueDate
|
||||
);
|
||||
|
||||
expect(childTask.description).toBeNull();
|
||||
expect(childTask.note).toBeNull();
|
||||
|
|
@ -83,7 +92,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
childTask1 = await Task.create({
|
||||
|
|
@ -92,7 +101,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
childTask2 = await Task.create({
|
||||
|
|
@ -101,7 +110,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-21T10:00:00Z'),
|
||||
status: Task.STATUS.DONE
|
||||
status: Task.STATUS.DONE,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -109,9 +118,9 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
const childTasks = await Task.findAll({
|
||||
where: {
|
||||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
},
|
||||
order: [['due_date', 'ASC']]
|
||||
order: [['due_date', 'ASC']],
|
||||
});
|
||||
|
||||
expect(childTasks).toHaveLength(2);
|
||||
|
|
@ -133,11 +142,15 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
it('should distinguish between parent and child tasks', async () => {
|
||||
const allTasks = await Task.findAll({
|
||||
where: { user_id: user.id },
|
||||
order: [['id', 'ASC']]
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
|
||||
const parentTasks = allTasks.filter(t => t.recurrence_type !== 'none');
|
||||
const childTasks = allTasks.filter(t => t.recurring_parent_id !== null);
|
||||
const parentTasks = allTasks.filter(
|
||||
(t) => t.recurrence_type !== 'none'
|
||||
);
|
||||
const childTasks = allTasks.filter(
|
||||
(t) => t.recurring_parent_id !== null
|
||||
);
|
||||
|
||||
expect(parentTasks).toHaveLength(1);
|
||||
expect(childTasks).toHaveLength(2);
|
||||
|
|
@ -149,7 +162,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
name: 'Standalone Task',
|
||||
recurrence_type: 'none',
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
expect(standaloneTask.recurring_parent_id).toBeFalsy(); // Can be null or undefined
|
||||
|
|
@ -165,10 +178,11 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
const nextTask = await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
const nextTask =
|
||||
await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
|
||||
expect(nextTask).not.toBeNull();
|
||||
expect(nextTask.name).toBe(parentTask.name);
|
||||
|
|
@ -189,19 +203,20 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
// Call completion multiple times quickly
|
||||
const firstNextTask = await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
const firstNextTask =
|
||||
await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
expect(firstNextTask).not.toBeNull();
|
||||
|
||||
// Check how many child tasks exist for this parent
|
||||
const childTasks = await Task.findAll({
|
||||
where: {
|
||||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id
|
||||
}
|
||||
user_id: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Should only have one child task despite multiple generations from same parent
|
||||
|
|
@ -215,7 +230,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
const childTask = await Task.create({
|
||||
|
|
@ -224,11 +239,12 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
// Completing child task should not create new instances
|
||||
const nextTask = await RecurringTaskService.handleTaskCompletion(childTask);
|
||||
const nextTask =
|
||||
await RecurringTaskService.handleTaskCompletion(childTask);
|
||||
expect(nextTask).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -244,7 +260,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_weekday: null,
|
||||
completion_based: false,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
childTask = await Task.create({
|
||||
|
|
@ -253,7 +269,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -264,7 +280,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 2,
|
||||
recurrence_weekday: 1, // Monday
|
||||
completion_based: true
|
||||
completion_based: true,
|
||||
});
|
||||
|
||||
const refreshedParent = await Task.findByPk(parentTask.id);
|
||||
|
|
@ -286,13 +302,15 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
const updatedParent = await Task.findByPk(parentTask.id);
|
||||
await updatedParent.update({
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 3
|
||||
recurrence_interval: 3,
|
||||
});
|
||||
|
||||
// Verify child maintains its specific properties
|
||||
const refreshedChild = await Task.findByPk(childTask.id);
|
||||
expect(refreshedChild.status).toBe(Task.STATUS.IN_PROGRESS);
|
||||
expect(refreshedChild.due_date).toEqual(new Date('2025-06-20T10:00:00Z'));
|
||||
expect(refreshedChild.due_date).toEqual(
|
||||
new Date('2025-06-20T10:00:00Z')
|
||||
);
|
||||
expect(refreshedChild.recurring_parent_id).toBe(parentTask.id);
|
||||
});
|
||||
});
|
||||
|
|
@ -306,7 +324,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
childTask1 = await Task.create({
|
||||
|
|
@ -315,7 +333,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
childTask2 = await Task.create({
|
||||
|
|
@ -324,7 +342,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-27T10:00:00Z'),
|
||||
status: Task.STATUS.DONE
|
||||
status: Task.STATUS.DONE,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -343,15 +361,10 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
});
|
||||
|
||||
it('should prevent deleting parent when child tasks exist due to foreign key constraint', async () => {
|
||||
let errorThrown = false;
|
||||
try {
|
||||
await parentTask.destroy();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
expect(error.name).toBe('SequelizeForeignKeyConstraintError');
|
||||
}
|
||||
await expect(parentTask.destroy()).rejects.toThrow();
|
||||
|
||||
expect(errorThrown).toBe(true);
|
||||
const error = await parentTask.destroy().catch((err) => err);
|
||||
expect(error.name).toBe('SequelizeForeignKeyConstraintError');
|
||||
|
||||
// Verify parent and children still exist
|
||||
const existingParent = await Task.findByPk(parentTask.id);
|
||||
|
|
@ -383,7 +396,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const weeklyParent = await Task.create({
|
||||
|
|
@ -392,7 +405,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1,
|
||||
user_id: user.id,
|
||||
priority: 2
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
// Create child tasks for each parent
|
||||
|
|
@ -419,7 +432,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_interval: 1,
|
||||
completion_based: true,
|
||||
user_id: user.id,
|
||||
priority: 2
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
const children = [];
|
||||
|
|
@ -427,7 +440,8 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
// Generate 5 child tasks
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await parentTask.update({ status: Task.STATUS.DONE });
|
||||
const nextTask = await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
const nextTask =
|
||||
await RecurringTaskService.handleTaskCompletion(parentTask);
|
||||
if (nextTask) {
|
||||
children.push(nextTask);
|
||||
}
|
||||
|
|
@ -445,7 +459,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
}
|
||||
|
||||
// Verify no duplicate due dates
|
||||
const dueDates = children.map(c => c.due_date.getTime());
|
||||
const dueDates = children.map((c) => c.due_date.getTime());
|
||||
const uniqueDueDates = [...new Set(dueDates)];
|
||||
expect(uniqueDueDates.length).toBe(dueDates.length);
|
||||
});
|
||||
|
|
@ -456,7 +470,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const childTask = await Task.create({
|
||||
|
|
@ -465,7 +479,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: parentTask.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
// Verify child can be found and has correct parent reference
|
||||
|
|
@ -473,7 +487,9 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
expect(foundChild.recurring_parent_id).toBe(parentTask.id);
|
||||
|
||||
// Try to find parent through child
|
||||
const foundParent = await Task.findByPk(foundChild.recurring_parent_id);
|
||||
const foundParent = await Task.findByPk(
|
||||
foundChild.recurring_parent_id
|
||||
);
|
||||
expect(foundParent).not.toBeNull();
|
||||
expect(foundParent.id).toBe(parentTask.id);
|
||||
});
|
||||
|
|
@ -487,7 +503,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const childTask = await Task.create({
|
||||
|
|
@ -500,7 +516,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_week_of_month: null,
|
||||
completion_based: false,
|
||||
user_id: user.id,
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
expect(childTask.recurrence_type).toBe('none');
|
||||
|
|
@ -519,7 +535,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_weekday: 5, // Friday
|
||||
recurring_parent_id: null,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
expect(parentTask.recurrence_type).toBe('weekly');
|
||||
|
|
@ -529,14 +545,16 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
});
|
||||
|
||||
it('should maintain user isolation for parent-child relationships', async () => {
|
||||
const otherUser = await createTestUser({ email: 'other@example.com' });
|
||||
const otherUser = await createTestUser({
|
||||
email: 'other@example.com',
|
||||
});
|
||||
|
||||
const user1Parent = await Task.create({
|
||||
name: 'User 1 Parent',
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: user.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const user2Parent = await Task.create({
|
||||
|
|
@ -544,7 +562,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1,
|
||||
user_id: otherUser.id,
|
||||
priority: 1
|
||||
priority: 1,
|
||||
});
|
||||
|
||||
const user1Child = await Task.create({
|
||||
|
|
@ -553,7 +571,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
recurring_parent_id: user1Parent.id,
|
||||
user_id: user.id,
|
||||
due_date: new Date('2025-06-20T10:00:00Z'),
|
||||
status: Task.STATUS.NOT_STARTED
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
});
|
||||
|
||||
// Verify child belongs to correct user
|
||||
|
|
@ -561,13 +579,21 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
expect(user1Child.recurring_parent_id).toBe(user1Parent.id);
|
||||
|
||||
// Verify users can't see each other's tasks
|
||||
const user1Tasks = await Task.findAll({ where: { user_id: user.id } });
|
||||
const user2Tasks = await Task.findAll({ where: { user_id: otherUser.id } });
|
||||
const user1Tasks = await Task.findAll({
|
||||
where: { user_id: user.id },
|
||||
});
|
||||
const user2Tasks = await Task.findAll({
|
||||
where: { user_id: otherUser.id },
|
||||
});
|
||||
|
||||
expect(user1Tasks.length).toBe(2); // parent + child
|
||||
expect(user2Tasks.length).toBe(1); // just parent
|
||||
expect(user1Tasks.find(t => t.id === user2Parent.id)).toBeUndefined();
|
||||
expect(user2Tasks.find(t => t.id === user1Parent.id)).toBeUndefined();
|
||||
expect(
|
||||
user1Tasks.find((t) => t.id === user2Parent.id)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
user2Tasks.find((t) => t.id === user1Parent.id)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,10 +9,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should calculate next daily occurrence correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-16T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -20,10 +23,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle custom daily intervals', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 3
|
||||
recurrence_interval: 3,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-18T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -31,10 +37,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle edge case with zero interval', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 0
|
||||
recurrence_interval: 0,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-16T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -45,10 +54,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should calculate next weekly occurrence correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z'); // Wednesday
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-22T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -57,10 +69,13 @@ describe('RecurringTaskService', () => {
|
|||
const task = {
|
||||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1 // Monday
|
||||
recurrence_weekday: 1, // Monday
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z'); // Wednesday
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-20T10:00:00Z')); // Next Monday
|
||||
});
|
||||
|
|
@ -68,10 +83,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle bi-weekly recurrence', () => {
|
||||
const task = {
|
||||
recurrence_type: 'weekly',
|
||||
recurrence_interval: 2
|
||||
recurrence_interval: 2,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-01-29T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -82,10 +100,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should calculate next monthly occurrence correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-02-15T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -93,10 +114,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle month boundaries correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-31T10:00:00Z'); // January 31st
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// February only has 28 days in 2025, should go to Feb 28
|
||||
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
|
||||
|
|
@ -105,10 +129,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle leap year correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2024-01-29T10:00:00Z'); // 2024 is a leap year
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2024-02-29T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -116,10 +143,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle custom monthly intervals', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 3
|
||||
recurrence_interval: 3,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-04-15T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -128,10 +158,13 @@ describe('RecurringTaskService', () => {
|
|||
const task = {
|
||||
recurrence_type: 'monthly',
|
||||
recurrence_interval: 1,
|
||||
recurrence_month_day: 5
|
||||
recurrence_month_day: 5,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toEqual(new Date('2025-02-05T10:00:00Z'));
|
||||
});
|
||||
|
|
@ -144,10 +177,13 @@ describe('RecurringTaskService', () => {
|
|||
recurrence_type: 'monthly_weekday',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 1, // Monday
|
||||
recurrence_week_of_month: 1 // First week
|
||||
recurrence_week_of_month: 1, // First week
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// First Monday of February 2025 is February 3rd
|
||||
expect(nextDate).toEqual(new Date('2025-02-03T10:00:00Z'));
|
||||
|
|
@ -158,10 +194,13 @@ describe('RecurringTaskService', () => {
|
|||
recurrence_type: 'monthly_weekday',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 5, // Friday
|
||||
recurrence_week_of_month: 5 // Last week (represented as 5)
|
||||
recurrence_week_of_month: 5, // Last week (represented as 5)
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// Last Friday of February 2025 is February 28th
|
||||
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
|
||||
|
|
@ -172,10 +211,13 @@ describe('RecurringTaskService', () => {
|
|||
recurrence_type: 'monthly_weekday',
|
||||
recurrence_interval: 1,
|
||||
recurrence_weekday: 3, // Wednesday
|
||||
recurrence_week_of_month: 3 // Third week
|
||||
recurrence_week_of_month: 3, // Third week
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// Third Wednesday of February 2025 is February 19th
|
||||
expect(nextDate).toEqual(new Date('2025-02-19T10:00:00Z'));
|
||||
|
|
@ -187,10 +229,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should calculate last day of month correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly_last_day',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// Last day of February 2025 is February 28th
|
||||
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
|
||||
|
|
@ -199,10 +244,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle leap year last day correctly', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly_last_day',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2024-01-15T10:00:00Z'); // 2024 is a leap year
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// Last day of February 2024 is February 29th
|
||||
expect(nextDate).toEqual(new Date('2024-02-29T10:00:00Z'));
|
||||
|
|
@ -211,10 +259,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle different month lengths', () => {
|
||||
const task = {
|
||||
recurrence_type: 'monthly_last_day',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-04-15T10:00:00Z'); // April has 30 days
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
// Last day of May 2025 is May 31st
|
||||
expect(nextDate).toEqual(new Date('2025-05-31T10:00:00Z'));
|
||||
|
|
@ -226,10 +277,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should return null for unsupported recurrence type', () => {
|
||||
const task = {
|
||||
recurrence_type: 'invalid_type',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toBeNull();
|
||||
});
|
||||
|
|
@ -237,10 +291,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should return null for none recurrence type', () => {
|
||||
const task = {
|
||||
recurrence_type: 'none',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toBeNull();
|
||||
});
|
||||
|
|
@ -248,10 +305,13 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle invalid date inputs gracefully', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_interval: 1
|
||||
recurrence_interval: 1,
|
||||
};
|
||||
const fromDate = new Date('invalid-date');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toBeNull();
|
||||
});
|
||||
|
|
@ -259,7 +319,10 @@ describe('RecurringTaskService', () => {
|
|||
it('should handle missing task properties', () => {
|
||||
const task = {}; // No recurrence properties
|
||||
const fromDate = new Date('2025-01-15T10:00:00Z');
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate);
|
||||
const nextDate = RecurringTaskService.calculateNextDueDate(
|
||||
task,
|
||||
fromDate
|
||||
);
|
||||
|
||||
expect(nextDate).toBeNull();
|
||||
});
|
||||
|
|
@ -269,36 +332,62 @@ describe('RecurringTaskService', () => {
|
|||
describe('Helper Functions', () => {
|
||||
describe('_getFirstWeekdayOfMonth', () => {
|
||||
it('should find first Monday of January 2025', () => {
|
||||
const date = RecurringTaskService._getFirstWeekdayOfMonth(2025, 0, 1); // January, Monday
|
||||
const date = RecurringTaskService._getFirstWeekdayOfMonth(
|
||||
2025,
|
||||
0,
|
||||
1
|
||||
); // January, Monday
|
||||
expect(date.getDate()).toBe(6); // January 6, 2025 is the first Monday
|
||||
});
|
||||
|
||||
it('should find first Sunday of February 2025', () => {
|
||||
const date = RecurringTaskService._getFirstWeekdayOfMonth(2025, 1, 0); // February, Sunday
|
||||
const date = RecurringTaskService._getFirstWeekdayOfMonth(
|
||||
2025,
|
||||
1,
|
||||
0
|
||||
); // February, Sunday
|
||||
expect(date.getDate()).toBe(2); // February 2, 2025 is the first Sunday
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getLastWeekdayOfMonth', () => {
|
||||
it('should find last Friday of January 2025', () => {
|
||||
const date = RecurringTaskService._getLastWeekdayOfMonth(2025, 0, 5); // January, Friday
|
||||
const date = RecurringTaskService._getLastWeekdayOfMonth(
|
||||
2025,
|
||||
0,
|
||||
5
|
||||
); // January, Friday
|
||||
expect(date.getDate()).toBe(31); // January 31, 2025 is the last Friday
|
||||
});
|
||||
|
||||
it('should find last Monday of February 2025', () => {
|
||||
const date = RecurringTaskService._getLastWeekdayOfMonth(2025, 1, 1); // February, Monday
|
||||
const date = RecurringTaskService._getLastWeekdayOfMonth(
|
||||
2025,
|
||||
1,
|
||||
1
|
||||
); // February, Monday
|
||||
expect(date.getDate()).toBe(24); // February 24, 2025 is the last Monday
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getNthWeekdayOfMonth', () => {
|
||||
it('should find second Tuesday of March 2025', () => {
|
||||
const date = RecurringTaskService._getNthWeekdayOfMonth(2025, 2, 2, 2); // March, Tuesday, 2nd
|
||||
const date = RecurringTaskService._getNthWeekdayOfMonth(
|
||||
2025,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
); // March, Tuesday, 2nd
|
||||
expect(date.getDate()).toBe(11); // March 11, 2025 is the second Tuesday
|
||||
});
|
||||
|
||||
it('should find fourth Thursday of April 2025', () => {
|
||||
const date = RecurringTaskService._getNthWeekdayOfMonth(2025, 3, 4, 4); // April, Thursday, 4th
|
||||
const date = RecurringTaskService._getNthWeekdayOfMonth(
|
||||
2025,
|
||||
3,
|
||||
4,
|
||||
4
|
||||
); // April, Thursday, 4th
|
||||
expect(date.getDate()).toBe(24); // April 24, 2025 is the fourth Thursday
|
||||
});
|
||||
});
|
||||
|
|
@ -315,7 +404,7 @@ describe('RecurringTaskService', () => {
|
|||
priority: 1,
|
||||
note: 'Test note',
|
||||
user_id: 1,
|
||||
project_id: 2
|
||||
project_id: 2,
|
||||
};
|
||||
const dueDate = new Date('2025-01-20T10:00:00Z');
|
||||
|
||||
|
|
@ -331,11 +420,14 @@ describe('RecurringTaskService', () => {
|
|||
user_id: template.user_id,
|
||||
project_id: template.project_id,
|
||||
recurrence_type: 'none',
|
||||
recurring_parent_id: template.id
|
||||
recurring_parent_id: template.id,
|
||||
});
|
||||
Task.create = mockCreate;
|
||||
|
||||
const result = await RecurringTaskService.createTaskInstance(template, dueDate);
|
||||
const result = await RecurringTaskService.createTaskInstance(
|
||||
template,
|
||||
dueDate
|
||||
);
|
||||
|
||||
expect(mockCreate).toHaveBeenCalledWith({
|
||||
name: template.name,
|
||||
|
|
@ -348,7 +440,7 @@ describe('RecurringTaskService', () => {
|
|||
user_id: template.user_id,
|
||||
project_id: template.project_id,
|
||||
recurrence_type: 'none',
|
||||
recurring_parent_id: template.id
|
||||
recurring_parent_id: template.id,
|
||||
});
|
||||
|
||||
expect(result.recurring_parent_id).toBe(template.id);
|
||||
|
|
@ -362,33 +454,45 @@ describe('RecurringTaskService', () => {
|
|||
it('should generate task when no end date is set', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_end_date: null
|
||||
recurrence_end_date: null,
|
||||
};
|
||||
const nextDate = new Date('2025-12-31T10:00:00Z');
|
||||
|
||||
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate);
|
||||
const shouldGenerate =
|
||||
RecurringTaskService._shouldGenerateNextTask(
|
||||
task,
|
||||
nextDate
|
||||
);
|
||||
expect(shouldGenerate).toBe(true);
|
||||
});
|
||||
|
||||
it('should generate task when next date is before end date', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_end_date: new Date('2025-12-31T10:00:00Z')
|
||||
recurrence_end_date: new Date('2025-12-31T10:00:00Z'),
|
||||
};
|
||||
const nextDate = new Date('2025-06-15T10:00:00Z');
|
||||
|
||||
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate);
|
||||
const shouldGenerate =
|
||||
RecurringTaskService._shouldGenerateNextTask(
|
||||
task,
|
||||
nextDate
|
||||
);
|
||||
expect(shouldGenerate).toBe(true);
|
||||
});
|
||||
|
||||
it('should not generate task when next date is after end date', () => {
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_end_date: new Date('2025-06-15T10:00:00Z')
|
||||
recurrence_end_date: new Date('2025-06-15T10:00:00Z'),
|
||||
};
|
||||
const nextDate = new Date('2025-12-31T10:00:00Z');
|
||||
|
||||
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate);
|
||||
const shouldGenerate =
|
||||
RecurringTaskService._shouldGenerateNextTask(
|
||||
task,
|
||||
nextDate
|
||||
);
|
||||
expect(shouldGenerate).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -396,11 +500,15 @@ describe('RecurringTaskService', () => {
|
|||
const endDate = new Date('2025-06-15T10:00:00Z');
|
||||
const task = {
|
||||
recurrence_type: 'daily',
|
||||
recurrence_end_date: endDate
|
||||
recurrence_end_date: endDate,
|
||||
};
|
||||
const nextDate = new Date('2025-06-15T10:00:00Z');
|
||||
|
||||
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate);
|
||||
const shouldGenerate =
|
||||
RecurringTaskService._shouldGenerateNextTask(
|
||||
task,
|
||||
nextDate
|
||||
);
|
||||
expect(shouldGenerate).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -408,18 +516,36 @@ describe('RecurringTaskService', () => {
|
|||
|
||||
describe('Service Interface', () => {
|
||||
it('should export all required methods', () => {
|
||||
expect(typeof RecurringTaskService.generateRecurringTasks).toBe('function');
|
||||
expect(typeof RecurringTaskService.processRecurringTask).toBe('function');
|
||||
expect(typeof RecurringTaskService.calculateNextDueDate).toBe('function');
|
||||
expect(typeof RecurringTaskService.createTaskInstance).toBe('function');
|
||||
expect(typeof RecurringTaskService.handleTaskCompletion).toBe('function');
|
||||
expect(typeof RecurringTaskService.generateRecurringTasks).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService.processRecurringTask).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService.calculateNextDueDate).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService.createTaskInstance).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService.handleTaskCompletion).toBe(
|
||||
'function'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have helper functions for testing', () => {
|
||||
expect(typeof RecurringTaskService._getFirstWeekdayOfMonth).toBe('function');
|
||||
expect(typeof RecurringTaskService._getLastWeekdayOfMonth).toBe('function');
|
||||
expect(typeof RecurringTaskService._getNthWeekdayOfMonth).toBe('function');
|
||||
expect(typeof RecurringTaskService._shouldGenerateNextTask).toBe('function');
|
||||
expect(typeof RecurringTaskService._getFirstWeekdayOfMonth).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService._getLastWeekdayOfMonth).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService._getNthWeekdayOfMonth).toBe(
|
||||
'function'
|
||||
);
|
||||
expect(typeof RecurringTaskService._shouldGenerateNextTask).toBe(
|
||||
'function'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,18 +6,18 @@ jest.mock('../../../models', () => ({
|
|||
User: {
|
||||
update: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
findOne: jest.fn()
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
InboxItem: {
|
||||
create: jest.fn(),
|
||||
findOne: jest.fn()
|
||||
}
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock https module
|
||||
jest.mock('https', () => ({
|
||||
get: jest.fn(),
|
||||
request: jest.fn()
|
||||
request: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('TelegramPoller Duplicate Prevention', () => {
|
||||
|
|
@ -29,7 +29,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
mockUser = {
|
||||
id: 1,
|
||||
telegram_bot_token: 'test-token',
|
||||
telegram_chat_id: '123456789'
|
||||
telegram_chat_id: '123456789',
|
||||
};
|
||||
|
||||
// Reset poller state
|
||||
|
|
@ -39,14 +39,35 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
describe('Update ID Tracking', () => {
|
||||
test('should filter out already processed updates', () => {
|
||||
const updates = [
|
||||
{ update_id: 100, message: { text: 'Hello 1', message_id: 1, chat: { id: 123 } } },
|
||||
{ update_id: 101, message: { text: 'Hello 2', message_id: 2, chat: { id: 123 } } },
|
||||
{ update_id: 102, message: { text: 'Hello 3', message_id: 3, chat: { id: 123 } } }
|
||||
{
|
||||
update_id: 100,
|
||||
message: {
|
||||
text: 'Hello 1',
|
||||
message_id: 1,
|
||||
chat: { id: 123 },
|
||||
},
|
||||
},
|
||||
{
|
||||
update_id: 101,
|
||||
message: {
|
||||
text: 'Hello 2',
|
||||
message_id: 2,
|
||||
chat: { id: 123 },
|
||||
},
|
||||
},
|
||||
{
|
||||
update_id: 102,
|
||||
message: {
|
||||
text: 'Hello 3',
|
||||
message_id: 3,
|
||||
chat: { id: 123 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Test internal function for filtering
|
||||
const processedUpdates = new Set(['1-100', '1-101']);
|
||||
const newUpdates = updates.filter(update => {
|
||||
const newUpdates = updates.filter((update) => {
|
||||
const updateKey = `1-${update.update_id}`;
|
||||
return !processedUpdates.has(updateKey);
|
||||
});
|
||||
|
|
@ -59,7 +80,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
const updates = [
|
||||
{ update_id: 98 },
|
||||
{ update_id: 101 },
|
||||
{ update_id: 99 }
|
||||
{ update_id: 99 },
|
||||
];
|
||||
|
||||
const highestUpdateId = telegramPoller._getHighestUpdateId(updates);
|
||||
|
|
@ -101,32 +122,39 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
const users = [
|
||||
{ id: 1, name: 'User 1' },
|
||||
{ id: 2, name: 'User 2' },
|
||||
{ id: 3, name: 'User 3' }
|
||||
{ id: 3, name: 'User 3' },
|
||||
];
|
||||
|
||||
const updatedUsers = telegramPoller._removeUserFromList(users, 2);
|
||||
expect(updatedUsers).toHaveLength(2);
|
||||
expect(updatedUsers.find(u => u.id === 2)).toBeUndefined();
|
||||
expect(updatedUsers.find(u => u.id === 1)).toBeDefined();
|
||||
expect(updatedUsers.find(u => u.id === 3)).toBeDefined();
|
||||
expect(updatedUsers.find((u) => u.id === 2)).toBeUndefined();
|
||||
expect(updatedUsers.find((u) => u.id === 1)).toBeDefined();
|
||||
expect(updatedUsers.find((u) => u.id === 3)).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Parameters', () => {
|
||||
test('should create message parameters without reply', () => {
|
||||
const params = telegramPoller._createMessageParams('123', 'Hello World');
|
||||
const params = telegramPoller._createMessageParams(
|
||||
'123',
|
||||
'Hello World'
|
||||
);
|
||||
expect(params).toEqual({
|
||||
chat_id: '123',
|
||||
text: 'Hello World'
|
||||
text: 'Hello World',
|
||||
});
|
||||
});
|
||||
|
||||
test('should create message parameters with reply', () => {
|
||||
const params = telegramPoller._createMessageParams('123', 'Hello World', 456);
|
||||
const params = telegramPoller._createMessageParams(
|
||||
'123',
|
||||
'Hello World',
|
||||
456
|
||||
);
|
||||
expect(params).toEqual({
|
||||
chat_id: '123',
|
||||
text: 'Hello World',
|
||||
reply_to_message_id: 456
|
||||
reply_to_message_id: 456,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -138,11 +166,17 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
});
|
||||
|
||||
test('should create URL with parameters', () => {
|
||||
const url = telegramPoller._createTelegramUrl('token123', 'getUpdates', {
|
||||
const url = telegramPoller._createTelegramUrl(
|
||||
'token123',
|
||||
'getUpdates',
|
||||
{
|
||||
offset: '100',
|
||||
timeout: '30'
|
||||
});
|
||||
expect(url).toBe('https://api.telegram.org/bottoken123/getUpdates?offset=100&timeout=30');
|
||||
timeout: '30',
|
||||
}
|
||||
);
|
||||
expect(url).toBe(
|
||||
'https://api.telegram.org/bottoken123/getUpdates?offset=100&timeout=30'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -155,7 +189,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
pollInterval: 5000,
|
||||
usersToPool: [],
|
||||
userStatus: {},
|
||||
processedUpdates: expect.any(Set)
|
||||
processedUpdates: expect.any(Set),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +199,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
|
|||
running: false,
|
||||
usersCount: 0,
|
||||
pollInterval: 5000,
|
||||
userStatus: {}
|
||||
userStatus: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
17090
frontend/package-lock.json
generated
17090
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue