Linting cleanup (#99)

* Add eslint and prettier dependencies and configs

* Lint project.
This commit is contained in:
Antonis Anastasiadis 2025-07-01 11:40:09 +03:00 committed by GitHub
parent dd6ec117d0
commit e594d1075b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 26554 additions and 39840 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
.idea
*.sqlite3
*.sqlite3-shm
*.sqlite3-wal

6
backend/.prettierrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
}

View file

@ -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) {

View file

@ -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',
},
},
};

View file

@ -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
View 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,
},
},
},
];

View file

@ -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,
};

View file

@ -27,5 +27,5 @@ const requireAuth = async (req, res, next) => {
};
module.exports = {
requireAuth
requireAuth,
};

View file

@ -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');
}
},
};

View file

@ -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'
);
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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'
);
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -2,15 +2,15 @@
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('users', 'pomodoro_enabled', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
defaultValue: true,
});
},
async down (queryInterface, Sequelize) {
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('users', 'pomodoro_enabled');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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');
}
},
};

View file

@ -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
}
}
},
};

View file

@ -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;
};

View file

@ -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.associate = function (models) {
CalendarToken.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'user'
as: 'user',
});
};

View file

@ -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;
};

View file

@ -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,
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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) {
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;
};

View file

@ -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.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,40 +223,44 @@ module.exports = (sequelize) => {
field_name: fieldName,
old_value: { [fieldName]: oldValue },
new_value: { [fieldName]: newValue },
metadata: metadata
metadata: metadata,
});
};
// Query helpers
TaskEvent.getTaskTimeline = async function(taskId) {
TaskEvent.getTaskTimeline = async function (taskId) {
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'],
},
],
});
};
TaskEvent.getCompletionTime = async function(taskId) {
TaskEvent.getCompletionTime = async function (taskId) {
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),
};
};

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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.',
});
}
});

View file

@ -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);

View file

@ -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 });

View file

@ -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.',
});
}
});

View file

@ -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.',
});
}
});

View file

@ -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.',
});
}
});

View file

@ -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);

View file

@ -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.',
});
}
});

View file

@ -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

View file

@ -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);

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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) {

View file

@ -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') +

View file

@ -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');

View file

@ -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);
});

View file

@ -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 },
];
}

View file

@ -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

View file

@ -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,
};

View file

@ -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) {

View file

@ -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;

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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) {

View file

@ -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,
};

View file

@ -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

View file

@ -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
}

View file

@ -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,
};

View file

@ -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}`);

View file

@ -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

View file

@ -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');

View file

@ -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');

View file

@ -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}`);

View file

@ -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');

View file

@ -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);
});

View file

@ -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);

View file

@ -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}`);

View file

@ -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');
});
});
});

View file

@ -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);
});

View file

@ -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();
});
});

View file

@ -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');

View file

@ -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 });

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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');

View file

@ -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');

View file

@ -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', () => {

View file

@ -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();
});
});
});

View file

@ -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'
);
});
});
});

View file

@ -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

File diff suppressed because it is too large Load diff