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
*.sqlite3-shm *.sqlite3-shm
*.sqlite3-wal *.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 // Middlewares
const sslEnabled = process.env.NODE_ENV === 'production' && process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true'; const sslEnabled =
app.use(helmet({ process.env.NODE_ENV === 'production' &&
process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
app.use(
helmet({
hsts: sslEnabled, // Only enable HSTS when SSL is enabled hsts: sslEnabled, // Only enable HSTS when SSL is enabled
forceHTTPS: sslEnabled, // Only force HTTPS 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(compression());
app.use(morgan('combined')); app.use(morgan('combined'));
// CORS configuration // CORS configuration
const allowedOrigins = process.env.TUDUDI_ALLOWED_ORIGINS const allowedOrigins = process.env.TUDUDI_ALLOWED_ORIGINS
? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map(origin => origin.trim()) ? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map((origin) =>
: ['http://localhost:8080', 'http://localhost:9292', 'http://127.0.0.1:8080', 'http://127.0.0.1:9292']; 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, origin: allowedOrigins,
credentials: true, credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 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'], exposedHeaders: ['Content-Type'],
maxAge: 1728000 maxAge: 1728000,
})); })
);
// Body parsing // Body parsing
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Session configuration // Session configuration
const secureFlag = process.env.NODE_ENV === 'production' && process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true'; const secureFlag =
app.use(session({ process.env.NODE_ENV === 'production' &&
secret: process.env.TUDUDI_SESSION_SECRET || require('crypto').randomBytes(64).toString('hex'), process.env.TUDUDI_INTERNAL_SSL_ENABLED === 'true';
app.use(
session({
secret:
process.env.TUDUDI_SESSION_SECRET ||
require('crypto').randomBytes(64).toString('hex'),
store: sessionStore, store: sessionStore,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
@ -57,9 +80,10 @@ app.use(session({
httpOnly: true, httpOnly: true,
secure: secureFlag, secure: secureFlag,
maxAge: 2592000000, // 30 days maxAge: 2592000000, // 30 days
sameSite: secureFlag ? 'none' : 'lax' sameSite: secureFlag ? 'none' : 'lax',
} },
})); })
);
// Static files // Static files
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
@ -72,7 +96,10 @@ if (process.env.NODE_ENV === 'production') {
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
app.use('/locales', express.static(path.join(__dirname, 'dist/locales'))); app.use('/locales', express.static(path.join(__dirname, 'dist/locales')));
} else { } else {
app.use('/locales', express.static(path.join(__dirname, '../public/locales'))); app.use(
'/locales',
express.static(path.join(__dirname, '../public/locales'))
);
} }
// Serve uploaded files // Serve uploaded files
@ -87,7 +114,7 @@ app.get('/api/health', (req, res) => {
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
uptime: process.uptime(), 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 // SPA fallback
app.get('*', (req, res) => { 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') { if (process.env.NODE_ENV === 'production') {
res.sendFile(path.join(__dirname, 'dist', 'index.html')); res.sendFile(path.join(__dirname, 'dist', 'index.html'));
} else { } else {
res.sendFile(path.join(__dirname, '../public', 'index.html')); res.sendFile(path.join(__dirname, '../public', 'index.html'));
} }
} else { } 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 // Error handling
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(err.stack); 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; const PORT = process.env.PORT || 3002;
@ -145,8 +181,11 @@ async function startServer() {
where: { email: process.env.TUDUDI_USER_EMAIL }, where: { email: process.env.TUDUDI_USER_EMAIL },
defaults: { defaults: {
email: process.env.TUDUDI_USER_EMAIL, 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) { if (created) {

View file

@ -14,8 +14,8 @@ module.exports = {
timestamps: true, timestamps: true,
underscored: true, underscored: true,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at' updatedAt: 'updated_at',
} },
}, },
test: { test: {
dialect: 'sqlite', dialect: 'sqlite',
@ -25,8 +25,8 @@ module.exports = {
timestamps: true, timestamps: true,
underscored: true, underscored: true,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at' updatedAt: 'updated_at',
} },
}, },
production: { production: {
dialect: 'sqlite', dialect: 'sqlite',
@ -36,7 +36,7 @@ module.exports = {
timestamps: true, timestamps: true,
underscored: true, underscored: true,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at' updatedAt: 'updated_at',
} },
} },
}; };

View file

@ -1,22 +1,21 @@
quotes: quotes:
- "Believe you can and you're halfway there." - "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.'
- "Success is not final, failure is not fatal: It is the courage to continue that counts." - 'Success is not final, failure is not fatal: It is the courage to continue that counts.'
- "It always seems impossible until it's done." - "It always seems impossible until it's done."
- "Your time is limited, don't waste it living someone else's life." - "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." - "Don't watch the clock; do what it does. Keep going."
- "Quality is not an act, it is a habit." - 'Quality is not an act, it is a habit.'
- "The only limit to our realization of tomorrow is our doubts of today." - 'The only limit to our realization of tomorrow is our doubts of today.'
- "Act as if what you do makes a difference. It does." - 'Act as if what you do makes a difference. It does.'
- "The best way to predict the future is to create it." - 'The best way to predict the future is to create it.'
- "Success is walking from failure to failure with no loss of enthusiasm." - '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." - 'You are never too old to set another goal or to dream a new dream.'
- "The secret of getting ahead is getting started." - 'The secret of getting ahead is getting started.'
- "Don't let yesterday take up too much of today." - "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." - "You don't have to be great to start, but you have to start to be great."
- "Focus on progress, not perfection." - 'Focus on progress, not perfection.'
- "One task at a time leads to great accomplishments." - 'One task at a time leads to great accomplishments.'
- "Today's effort is tomorrow's success." - "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 = { module.exports = {
testEnvironment: 'node', testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/tests/helpers/setup.js'], setupFilesAfterEnv: ['<rootDir>/tests/helpers/setup.js'],
testMatch: [ testMatch: ['<rootDir>/tests/**/*.test.js', '<rootDir>/tests/**/*.spec.js'],
'<rootDir>/tests/**/*.test.js',
'<rootDir>/tests/**/*.spec.js'
],
maxWorkers: 1, maxWorkers: 1,
collectCoverageFrom: [ collectCoverageFrom: [
'routes/**/*.js', 'routes/**/*.js',
@ -13,7 +10,7 @@ module.exports = {
'services/**/*.js', 'services/**/*.js',
'!models/index.js', '!models/index.js',
'!**/*.test.js', '!**/*.test.js',
'!**/*.spec.js' '!**/*.spec.js',
], ],
coverageDirectory: 'coverage', coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'], coverageReporters: ['text', 'lcov', 'html'],
@ -21,5 +18,5 @@ module.exports = {
forceExit: true, forceExit: true,
clearMocks: true, clearMocks: true,
resetMocks: true, resetMocks: true,
restoreMocks: true restoreMocks: true,
}; };

View file

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

View file

@ -7,55 +7,55 @@ module.exports = {
allowNull: false, allowNull: false,
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
type: Sequelize.INTEGER type: Sequelize.INTEGER,
}, },
email: { email: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: false, allowNull: false,
unique: true unique: true,
}, },
password: { password: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: false allowNull: false,
}, },
telegram_bot_token: { telegram_bot_token: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: true allowNull: true,
}, },
telegram_chat_id: { telegram_chat_id: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: true allowNull: true,
}, },
task_summary_enabled: { task_summary_enabled: {
type: Sequelize.BOOLEAN, type: Sequelize.BOOLEAN,
defaultValue: false defaultValue: false,
}, },
task_summary_frequency: { task_summary_frequency: {
type: Sequelize.STRING, type: Sequelize.STRING,
defaultValue: 'daily' defaultValue: 'daily',
}, },
task_summary_last_run: { task_summary_last_run: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: true allowNull: true,
}, },
task_summary_next_run: { task_summary_next_run: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: true allowNull: true,
}, },
created_at: { created_at: {
allowNull: false, allowNull: false,
type: Sequelize.DATE, type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}, },
updated_at: { updated_at: {
allowNull: false, allowNull: false,
type: Sequelize.DATE, type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
} },
}); });
}, },
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
await queryInterface.dropTable('users'); await queryInterface.dropTable('users');
} },
}; };

View file

@ -6,32 +6,39 @@ module.exports = {
await queryInterface.addColumn('tasks', 'recurrence_weekday', { await queryInterface.addColumn('tasks', 'recurrence_weekday', {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: true, 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', { await queryInterface.addColumn('tasks', 'recurrence_month_day', {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: true, 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', { await queryInterface.addColumn('tasks', 'recurrence_week_of_month', {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: true, 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', { await queryInterface.addColumn('tasks', 'completion_based', {
type: Sequelize.BOOLEAN, type: Sequelize.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: 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 // Add index for efficient recurring task queries
await queryInterface.addIndex('tasks', ['recurrence_type', 'last_generated_date'], { await queryInterface.addIndex(
name: 'idx_tasks_recurrence_lookup' 'tasks',
}); ['recurrence_type', 'last_generated_date'],
{
name: 'idx_tasks_recurrence_lookup',
}
);
}, },
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
@ -42,6 +49,9 @@ module.exports = {
await queryInterface.removeColumn('tasks', 'completion_based'); await queryInterface.removeColumn('tasks', 'completion_based');
// Remove the index // 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, allowNull: true,
references: { references: {
model: 'tasks', model: 'tasks',
key: 'id' key: 'id',
}, },
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
onDelete: 'SET NULL' onDelete: 'SET NULL',
}); });
// Add index for performance // Add index for performance
@ -20,5 +20,5 @@ module.exports = {
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.removeIndex('tasks', ['recurring_parent_id']); await queryInterface.removeIndex('tasks', ['recurring_parent_id']);
await queryInterface.removeColumn('tasks', 'recurring_parent_id'); await queryInterface.removeColumn('tasks', 'recurring_parent_id');
} },
}; };

View file

@ -4,11 +4,11 @@ module.exports = {
up: async (queryInterface, Sequelize) => { up: async (queryInterface, Sequelize) => {
await queryInterface.addColumn('projects', 'image_url', { await queryInterface.addColumn('projects', 'image_url', {
type: Sequelize.TEXT, type: Sequelize.TEXT,
allowNull: true allowNull: true,
}); });
}, },
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('projects', 'image_url'); await queryInterface.removeColumn('projects', 'image_url');
} },
}; };

View file

@ -5,11 +5,11 @@ module.exports = {
await queryInterface.addColumn('users', 'task_intelligence_enabled', { await queryInterface.addColumn('users', 'task_intelligence_enabled', {
type: Sequelize.BOOLEAN, type: Sequelize.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true defaultValue: true,
}); });
}, },
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.removeColumn('users', 'task_intelligence_enabled'); await queryInterface.removeColumn('users', 'task_intelligence_enabled');
} },
}; };

View file

@ -2,14 +2,21 @@
module.exports = { module.exports = {
up: async (queryInterface, Sequelize) => { 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, type: Sequelize.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}); }
);
}, },
down: async (queryInterface, Sequelize) => { 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 // Add completed_at column to tasks table
await queryInterface.addColumn('tasks', 'completed_at', { await queryInterface.addColumn('tasks', 'completed_at', {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: true allowNull: true,
}); });
// Add an index for better query performance // Add an index for better query performance
@ -18,5 +18,5 @@ module.exports = {
// Remove the completed_at column // Remove the completed_at column
await queryInterface.removeColumn('tasks', 'completed_at'); await queryInterface.removeColumn('tasks', 'completed_at');
} },
}; };

View file

@ -7,73 +7,73 @@ module.exports = {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true, autoIncrement: true,
allowNull: false allowNull: false,
}, },
user_id: { user_id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
}, },
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
provider: { provider: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: false, allowNull: false,
defaultValue: 'google' defaultValue: 'google',
}, },
access_token: { access_token: {
type: Sequelize.TEXT, type: Sequelize.TEXT,
allowNull: false allowNull: false,
}, },
refresh_token: { refresh_token: {
type: Sequelize.TEXT, type: Sequelize.TEXT,
allowNull: true allowNull: true,
}, },
token_type: { token_type: {
type: Sequelize.STRING, type: Sequelize.STRING,
defaultValue: 'Bearer' defaultValue: 'Bearer',
}, },
expires_at: { expires_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: true allowNull: true,
}, },
scope: { scope: {
type: Sequelize.TEXT, type: Sequelize.TEXT,
allowNull: true allowNull: true,
}, },
connected_email: { connected_email: {
type: Sequelize.STRING, type: Sequelize.STRING,
allowNull: true allowNull: true,
}, },
created_at: { created_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}, },
updated_at: { updated_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
} },
}); });
// Add unique index for user_id + provider combination // Add unique index for user_id + provider combination
await queryInterface.addIndex('calendar_tokens', { await queryInterface.addIndex('calendar_tokens', {
fields: ['user_id', 'provider'], fields: ['user_id', 'provider'],
unique: true, unique: true,
name: 'calendar_tokens_user_provider_unique' name: 'calendar_tokens_user_provider_unique',
}); });
// Add index for faster lookups by user_id // Add index for faster lookups by user_id
await queryInterface.addIndex('calendar_tokens', { await queryInterface.addIndex('calendar_tokens', {
fields: ['user_id'], fields: ['user_id'],
name: 'calendar_tokens_user_id_index' name: 'calendar_tokens_user_id_index',
}); });
}, },
down: async (queryInterface, Sequelize) => { down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('calendar_tokens'); await queryInterface.dropTable('calendar_tokens');
} },
}; };

View file

@ -8,27 +8,27 @@ module.exports = {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true, autoIncrement: true,
allowNull: false allowNull: false,
}, },
task_id: { task_id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'tasks', model: 'tasks',
key: 'id' key: 'id',
}, },
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
user_id: { user_id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
}, },
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
event_type: { event_type: {
type: Sequelize.STRING, type: Sequelize.STRING,
@ -60,8 +60,8 @@ module.exports = {
created_at: { created_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
} },
}); });
// Add indexes for better query performance // Add indexes for better query performance
@ -75,8 +75,14 @@ module.exports = {
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
// Remove indexes first // Remove indexes first
await queryInterface.removeIndex('task_events', ['task_id', 'created_at']); await queryInterface.removeIndex('task_events', [
await queryInterface.removeIndex('task_events', ['task_id', 'event_type']); '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', ['created_at']);
await queryInterface.removeIndex('task_events', ['event_type']); await queryInterface.removeIndex('task_events', ['event_type']);
await queryInterface.removeIndex('task_events', ['user_id']); await queryInterface.removeIndex('task_events', ['user_id']);
@ -84,5 +90,5 @@ module.exports = {
// Drop the table // Drop the table
await queryInterface.dropTable('task_events'); await queryInterface.dropTable('task_events');
} },
}; };

View file

@ -6,11 +6,11 @@ module.exports = {
await queryInterface.addColumn('users', 'pomodoro_enabled', { await queryInterface.addColumn('users', 'pomodoro_enabled', {
type: Sequelize.BOOLEAN, type: Sequelize.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true defaultValue: true,
}); });
}, },
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('users', 'pomodoro_enabled'); await queryInterface.removeColumn('users', 'pomodoro_enabled');
} },
}; };

View file

@ -7,7 +7,7 @@ module.exports = {
// Add UUID column to tasks table (without unique constraint initially) // Add UUID column to tasks table (without unique constraint initially)
await queryInterface.addColumn('tasks', 'uuid', { await queryInterface.addColumn('tasks', 'uuid', {
type: Sequelize.UUID, type: Sequelize.UUID,
allowNull: true allowNull: true,
}); });
// Backfill existing tasks with UUIDs // Backfill existing tasks with UUIDs
@ -27,7 +27,7 @@ module.exports = {
// Add unique index for UUID // Add unique index for UUID
await queryInterface.addIndex('tasks', ['uuid'], { await queryInterface.addIndex('tasks', ['uuid'], {
unique: true, unique: true,
name: 'tasks_uuid_unique' name: 'tasks_uuid_unique',
}); });
}, },
@ -37,5 +37,5 @@ module.exports = {
// Remove UUID column // Remove UUID column
await queryInterface.removeColumn('tasks', 'uuid'); await queryInterface.removeColumn('tasks', 'uuid');
} },
}; };

View file

@ -10,39 +10,39 @@ module.exports = {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
references: { references: {
model: 'notes', model: 'notes',
key: 'id' key: 'id',
}, },
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
tag_id: { tag_id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
references: { references: {
model: 'tags', model: 'tags',
key: 'id' key: 'id',
}, },
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
created_at: { created_at: {
allowNull: false, allowNull: false,
type: Sequelize.DATE, type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}, },
updated_at: { updated_at: {
allowNull: false, allowNull: false,
type: Sequelize.DATE, type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
} },
}); });
// Add unique index // Add unique index
await queryInterface.addIndex('notes_tags', ['note_id', 'tag_id'], { await queryInterface.addIndex('notes_tags', ['note_id', 'tag_id'], {
unique: true, unique: true,
name: 'notes_tags_unique_idx' name: 'notes_tags_unique_idx',
}); });
} }
}, },
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
await queryInterface.dropTable('notes_tags'); await queryInterface.dropTable('notes_tags');
} },
}; };

View file

@ -7,13 +7,13 @@ module.exports = {
await queryInterface.addColumn('notes_tags', 'created_at', { await queryInterface.addColumn('notes_tags', 'created_at', {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}); });
await queryInterface.addColumn('notes_tags', 'updated_at', { await queryInterface.addColumn('notes_tags', 'updated_at', {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}); });
console.log('Successfully added timestamps to notes_tags table'); console.log('Successfully added timestamps to notes_tags table');
@ -25,5 +25,5 @@ module.exports = {
async down(queryInterface, Sequelize) { async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('notes_tags', 'created_at'); await queryInterface.removeColumn('notes_tags', 'created_at');
await queryInterface.removeColumn('notes_tags', 'updated_at'); await queryInterface.removeColumn('notes_tags', 'updated_at');
} },
}; };

View file

@ -3,8 +3,9 @@
module.exports = { module.exports = {
async up(queryInterface, Sequelize) { async up(queryInterface, Sequelize) {
// Create the projects_tags table if it doesn't exist // Create the projects_tags table if it doesn't exist
const tableExists = await queryInterface.showAllTables() const tableExists = await queryInterface
.then(tables => tables.includes('projects_tags')); .showAllTables()
.then((tables) => tables.includes('projects_tags'));
if (!tableExists) { if (!tableExists) {
await queryInterface.createTable('projects_tags', { await queryInterface.createTable('projects_tags', {
@ -13,38 +14,38 @@ module.exports = {
allowNull: false, allowNull: false,
references: { references: {
model: 'projects', model: 'projects',
key: 'id' key: 'id',
}, },
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
tag_id: { tag_id: {
type: Sequelize.INTEGER, type: Sequelize.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'tags', model: 'tags',
key: 'id' key: 'id',
}, },
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
created_at: { created_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}, },
updated_at: { updated_at: {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
} },
}); });
// Add composite primary key // Add composite primary key
await queryInterface.addConstraint('projects_tags', { await queryInterface.addConstraint('projects_tags', {
fields: ['project_id', 'tag_id'], fields: ['project_id', 'tag_id'],
type: 'primary key', type: 'primary key',
name: 'projects_tags_pkey' name: 'projects_tags_pkey',
}); });
} else { } else {
// Add timestamps if table exists but doesn't have them // Add timestamps if table exists but doesn't have them
@ -52,7 +53,7 @@ module.exports = {
await queryInterface.addColumn('projects_tags', 'created_at', { await queryInterface.addColumn('projects_tags', 'created_at', {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}); });
} catch (error) { } catch (error) {
// Column might already exist // Column might already exist
@ -62,7 +63,7 @@ module.exports = {
await queryInterface.addColumn('projects_tags', 'updated_at', { await queryInterface.addColumn('projects_tags', 'updated_at', {
type: Sequelize.DATE, type: Sequelize.DATE,
allowNull: false, allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP') defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
}); });
} catch (error) { } catch (error) {
// Column might already exist // Column might already exist
@ -78,5 +79,5 @@ module.exports = {
} catch (error) { } catch (error) {
// Columns might not exist // Columns might not exist
} }
} },
}; };

View file

@ -1,36 +1,40 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const Area = sequelize.define('Area', { const Area = sequelize.define(
'Area',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
description: { description: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
} },
}, { },
{
tableName: 'areas', tableName: 'areas',
indexes: [ indexes: [
{ {
fields: ['user_id'] fields: ['user_id'],
},
],
} }
] );
});
return Area; return Area;
}; };

View file

@ -1,59 +1,62 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
const sequelize = require('../config/database'); const sequelize = require('../config/database');
const CalendarToken = sequelize.define('CalendarToken', { const CalendarToken = sequelize.define(
'CalendarToken',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'Users', model: 'Users',
key: 'id' key: 'id',
}, },
onDelete: 'CASCADE' onDelete: 'CASCADE',
}, },
provider: { provider: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'google' defaultValue: 'google',
}, },
access_token: { access_token: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: false allowNull: false,
}, },
refresh_token: { refresh_token: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
token_type: { token_type: {
type: DataTypes.STRING, type: DataTypes.STRING,
defaultValue: 'Bearer' defaultValue: 'Bearer',
}, },
expires_at: { expires_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
scope: { scope: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
connected_email: { connected_email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
created_at: { created_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
defaultValue: DataTypes.NOW defaultValue: DataTypes.NOW,
}, },
updated_at: { updated_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
defaultValue: DataTypes.NOW defaultValue: DataTypes.NOW,
} },
}, { },
{
tableName: 'calendar_tokens', tableName: 'calendar_tokens',
timestamps: true, timestamps: true,
createdAt: 'created_at', createdAt: 'created_at',
@ -61,16 +64,17 @@ const CalendarToken = sequelize.define('CalendarToken', {
indexes: [ indexes: [
{ {
unique: true, unique: true,
fields: ['user_id', 'provider'] fields: ['user_id', 'provider'],
},
],
} }
] );
});
// Associations // Associations
CalendarToken.associate = function (models) { CalendarToken.associate = function (models) {
CalendarToken.belongsTo(models.User, { CalendarToken.belongsTo(models.User, {
foreignKey: 'user_id', foreignKey: 'user_id',
as: 'user' as: 'user',
}); });
}; };

View file

@ -1,42 +1,46 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const InboxItem = sequelize.define('InboxItem', { const InboxItem = sequelize.define(
'InboxItem',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
content: { content: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
status: { status: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'added' defaultValue: 'added',
}, },
source: { source: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'tududi' defaultValue: 'tududi',
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
} },
}, { },
{
tableName: 'inbox_items', tableName: 'inbox_items',
indexes: [ indexes: [
{ {
fields: ['user_id'] fields: ['user_id'],
},
],
} }
] );
});
return InboxItem; return InboxItem;
}; };

View file

@ -15,13 +15,19 @@ if (process.env.NODE_ENV === 'test') {
timestamps: true, timestamps: true,
underscored: true, underscored: true,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: 'updated_at' updatedAt: 'updated_at',
} },
}; };
} else { } else {
const dbPath = process.env.DATABASE_URL const dbPath = process.env.DATABASE_URL
? process.env.DATABASE_URL.replace('sqlite:///', '') ? 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 = { dbConfig = {
dialect: 'sqlite', dialect: 'sqlite',
@ -31,8 +37,8 @@ if (process.env.NODE_ENV === 'test') {
timestamps: true, timestamps: true,
underscored: true, underscored: true,
createdAt: 'created_at', 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' }); TaskEvent.belongsTo(Task, { foreignKey: 'task_id', as: 'Task' });
// Many-to-many associations // Many-to-many associations
Task.belongsToMany(Tag, { through: 'tasks_tags', foreignKey: 'task_id', otherKey: 'tag_id' }); Task.belongsToMany(Tag, {
Tag.belongsToMany(Task, { through: 'tasks_tags', foreignKey: 'tag_id', otherKey: 'task_id' }); 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' }); Note.belongsToMany(Tag, {
Tag.belongsToMany(Note, { through: 'notes_tags', foreignKey: 'tag_id', otherKey: 'note_id' }); 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' }); Project.belongsToMany(Tag, {
Tag.belongsToMany(Project, { through: 'projects_tags', foreignKey: 'tag_id', otherKey: 'project_id' }); through: 'projects_tags',
foreignKey: 'project_id',
otherKey: 'tag_id',
});
Tag.belongsToMany(Project, {
through: 'projects_tags',
foreignKey: 'tag_id',
otherKey: 'project_id',
});
module.exports = { module.exports = {
sequelize, sequelize,
@ -98,5 +128,5 @@ module.exports = {
Tag, Tag,
Note, Note,
InboxItem, InboxItem,
TaskEvent TaskEvent,
}; };

View file

@ -1,47 +1,51 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const Note = sequelize.define('Note', { const Note = sequelize.define(
'Note',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
title: { title: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
content: { content: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
}, },
project_id: { project_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
references: { references: {
model: 'projects', model: 'projects',
key: 'id' key: 'id',
} },
} },
}, { },
{
tableName: 'notes', tableName: 'notes',
indexes: [ indexes: [
{ {
fields: ['user_id'] fields: ['user_id'],
}, },
{ {
fields: ['project_id'] fields: ['project_id'],
},
],
} }
] );
});
return Note; return Note;
}; };

View file

@ -1,73 +1,77 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const Project = sequelize.define('Project', { const Project = sequelize.define(
'Project',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
description: { description: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
active: { active: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
pin_to_sidebar: { pin_to_sidebar: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
priority: { priority: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
validate: { validate: {
min: 0, min: 0,
max: 2 max: 2,
} },
}, },
due_date_at: { due_date_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
}, },
area_id: { area_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
references: { references: {
model: 'areas', model: 'areas',
key: 'id' key: 'id',
} },
}, },
image_url: { image_url: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
} },
}, { },
{
tableName: 'projects', tableName: 'projects',
indexes: [ indexes: [
{ {
fields: ['user_id'] fields: ['user_id'],
}, },
{ {
fields: ['area_id'] fields: ['area_id'],
},
],
} }
] );
});
return Project; return Project;
}; };

View file

@ -1,32 +1,36 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const Tag = sequelize.define('Tag', { const Tag = sequelize.define(
'Tag',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
} },
}, { },
{
tableName: 'tags', tableName: 'tags',
indexes: [ indexes: [
{ {
fields: ['user_id'] fields: ['user_id'],
},
],
} }
] );
});
return Tag; return Tag;
}; };

View file

@ -1,34 +1,36 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const Task = sequelize.define('Task', { const Task = sequelize.define(
'Task',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
uuid: { uuid: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: false, allowNull: false,
unique: true, unique: true,
defaultValue: DataTypes.UUIDV4 defaultValue: DataTypes.UUIDV4,
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false allowNull: false,
}, },
description: { description: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
due_date: { due_date: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
today: { today: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
priority: { priority: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
@ -36,8 +38,8 @@ module.exports = (sequelize) => {
defaultValue: 0, defaultValue: 0,
validate: { validate: {
min: 0, min: 0,
max: 2 max: 2,
} },
}, },
status: { status: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
@ -45,116 +47,118 @@ module.exports = (sequelize) => {
defaultValue: 0, defaultValue: 0,
validate: { validate: {
min: 0, min: 0,
max: 4 max: 4,
} },
}, },
note: { note: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true allowNull: true,
}, },
recurrence_type: { recurrence_type: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'none' defaultValue: 'none',
}, },
recurrence_interval: { recurrence_interval: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true allowNull: true,
}, },
recurrence_end_date: { recurrence_end_date: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
last_generated_date: { last_generated_date: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
recurrence_weekday: { recurrence_weekday: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
validate: { validate: {
min: 0, min: 0,
max: 6 max: 6,
} },
}, },
recurrence_month_day: { recurrence_month_day: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
validate: { validate: {
min: -1, min: -1,
max: 31 max: 31,
} },
}, },
recurrence_week_of_month: { recurrence_week_of_month: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
validate: { validate: {
min: 1, min: 1,
max: 5 max: 5,
} },
}, },
completion_based: { completion_based: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
}, },
project_id: { project_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
references: { references: {
model: 'projects', model: 'projects',
key: 'id' key: 'id',
} },
}, },
recurring_parent_id: { recurring_parent_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: true, allowNull: true,
references: { references: {
model: 'tasks', model: 'tasks',
key: 'id' key: 'id',
} },
}, },
completed_at: { completed_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
} },
}, { },
{
tableName: 'tasks', tableName: 'tasks',
indexes: [ 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 // Define associations
Task.associate = function (models) { Task.associate = function (models) {
// Self-referencing association for recurring tasks // Self-referencing association for recurring tasks
Task.belongsTo(models.Task, { Task.belongsTo(models.Task, {
as: 'RecurringParent', as: 'RecurringParent',
foreignKey: 'recurring_parent_id' foreignKey: 'recurring_parent_id',
}); });
Task.hasMany(models.Task, { Task.hasMany(models.Task, {
as: 'RecurringChildren', as: 'RecurringChildren',
foreignKey: 'recurring_parent_id' foreignKey: 'recurring_parent_id',
}); });
}; };
@ -162,7 +166,7 @@ module.exports = (sequelize) => {
Task.PRIORITY = { Task.PRIORITY = {
LOW: 0, LOW: 0,
MEDIUM: 1, MEDIUM: 1,
HIGH: 2 HIGH: 2,
}; };
Task.STATUS = { Task.STATUS = {
@ -170,7 +174,7 @@ module.exports = (sequelize) => {
IN_PROGRESS: 1, IN_PROGRESS: 1,
DONE: 2, DONE: 2,
ARCHIVED: 3, ARCHIVED: 3,
WAITING: 4 WAITING: 4,
}; };
Task.RECURRENCE_TYPE = { Task.RECURRENCE_TYPE = {
@ -179,7 +183,7 @@ module.exports = (sequelize) => {
WEEKLY: 'weekly', WEEKLY: 'weekly',
MONTHLY: 'monthly', MONTHLY: 'monthly',
MONTHLY_WEEKDAY: 'monthly_weekday', MONTHLY_WEEKDAY: 'monthly_weekday',
MONTHLY_LAST_DAY: 'monthly_last_day' MONTHLY_LAST_DAY: 'monthly_last_day',
}; };
// priority and status // priority and status
@ -189,22 +193,30 @@ module.exports = (sequelize) => {
}; };
const getStatusName = (statusValue) => { 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'; return statuses[statusValue] || 'not_started';
}; };
const getPriorityValue = (priorityName) => { const getPriorityValue = (priorityName) => {
const priorities = { 'low': 0, 'medium': 1, 'high': 2 }; const priorities = { low: 0, medium: 1, high: 2 };
return priorities[priorityName] !== undefined ? priorities[priorityName] : 0; return priorities[priorityName] !== undefined
? priorities[priorityName]
: 0;
}; };
const getStatusValue = (statusName) => { const getStatusValue = (statusName) => {
const statuses = { const statuses = {
'not_started': 0, not_started: 0,
'in_progress': 1, in_progress: 1,
'done': 2, done: 2,
'archived': 3, archived: 3,
'waiting': 4 waiting: 4,
}; };
return statuses[statusName] !== undefined ? statuses[statusName] : 0; return statuses[statusName] !== undefined ? statuses[statusName] : 0;
}; };

View file

@ -1,38 +1,57 @@
const { DataTypes } = require('sequelize'); const { DataTypes } = require('sequelize');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const TaskEvent = sequelize.define('TaskEvent', { const TaskEvent = sequelize.define(
'TaskEvent',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
task_id: { task_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'tasks', model: 'tasks',
key: 'id' key: 'id',
} },
}, },
user_id: { user_id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false, allowNull: false,
references: { references: {
model: 'users', model: 'users',
key: 'id' key: 'id',
} },
}, },
event_type: { event_type: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
validate: { validate: {
isIn: [['created', 'status_changed', 'priority_changed', 'due_date_changed', isIn: [
'project_changed', 'name_changed', 'description_changed', 'note_changed', [
'completed', 'archived', 'deleted', 'restored', 'today_changed', 'created',
'tags_changed', 'recurrence_changed', 'recurrence_type_changed', 'status_changed',
'completion_based_changed', 'recurrence_end_date_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: { old_value: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
@ -42,8 +61,11 @@ module.exports = (sequelize) => {
return rawValue ? JSON.parse(rawValue) : null; return rawValue ? JSON.parse(rawValue) : null;
}, },
set(value) { set(value) {
this.setDataValue('old_value', value ? JSON.stringify(value) : null); this.setDataValue(
} 'old_value',
value ? JSON.stringify(value) : null
);
},
}, },
new_value: { new_value: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
@ -53,18 +75,37 @@ module.exports = (sequelize) => {
return rawValue ? JSON.parse(rawValue) : null; return rawValue ? JSON.parse(rawValue) : null;
}, },
set(value) { set(value) {
this.setDataValue('new_value', value ? JSON.stringify(value) : null); this.setDataValue(
} 'new_value',
value ? JSON.stringify(value) : null
);
},
}, },
field_name: { field_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
validate: { validate: {
isIn: [['status', 'priority', 'due_date', 'project_id', 'name', 'description', isIn: [
'note', 'today', 'tags', 'recurrence_type', 'recurrence_interval', [
'recurrence_end_date', 'recurrence_weekday', 'recurrence_month_day', 'status',
'recurrence_week_of_month', 'completion_based']] '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: { metadata: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
@ -74,53 +115,64 @@ module.exports = (sequelize) => {
return rawValue ? JSON.parse(rawValue) : null; return rawValue ? JSON.parse(rawValue) : null;
}, },
set(value) { set(value) {
this.setDataValue('metadata', value ? JSON.stringify(value) : null); this.setDataValue(
} 'metadata',
} value ? JSON.stringify(value) : null
}, { );
},
},
},
{
tableName: 'task_events', tableName: 'task_events',
timestamps: true, timestamps: true,
createdAt: 'created_at', createdAt: 'created_at',
updatedAt: false, // We don't need updated_at for events (they're immutable) updatedAt: false, // We don't need updated_at for events (they're immutable)
indexes: [ 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 // Define associations
TaskEvent.associate = function (models) { TaskEvent.associate = function (models) {
// TaskEvent belongs to Task // TaskEvent belongs to Task
TaskEvent.belongsTo(models.Task, { TaskEvent.belongsTo(models.Task, {
foreignKey: 'task_id', foreignKey: 'task_id',
as: 'Task' as: 'Task',
}); });
// TaskEvent belongs to User // TaskEvent belongs to User
TaskEvent.belongsTo(models.User, { TaskEvent.belongsTo(models.User, {
foreignKey: 'user_id', foreignKey: 'user_id',
as: 'User' as: 'User',
}); });
}; };
// Helper methods for common event types // 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({ return await TaskEvent.create({
task_id: taskId, task_id: taskId,
user_id: userId, user_id: userId,
@ -128,11 +180,16 @@ module.exports = (sequelize) => {
field_name: 'status', field_name: 'status',
old_value: { status: oldStatus }, old_value: { status: oldStatus },
new_value: { status: newStatus }, 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({ return await TaskEvent.create({
task_id: taskId, task_id: taskId,
user_id: userId, user_id: userId,
@ -140,14 +197,24 @@ module.exports = (sequelize) => {
field_name: null, field_name: null,
old_value: null, old_value: null,
new_value: taskData, new_value: taskData,
metadata: metadata metadata: metadata,
}); });
}; };
TaskEvent.createFieldChangeEvent = async function(taskId, userId, fieldName, oldValue, newValue, metadata = {}) { TaskEvent.createFieldChangeEvent = async function (
const eventType = fieldName === 'status' && newValue === 2 ? 'completed' : taskId,
fieldName === 'status' && newValue === 3 ? 'archived' : userId,
`${fieldName}_changed`; fieldName,
oldValue,
newValue,
metadata = {}
) {
const eventType =
fieldName === 'status' && newValue === 2
? 'completed'
: fieldName === 'status' && newValue === 3
? 'archived'
: `${fieldName}_changed`;
return await TaskEvent.create({ return await TaskEvent.create({
task_id: taskId, task_id: taskId,
@ -156,7 +223,7 @@ module.exports = (sequelize) => {
field_name: fieldName, field_name: fieldName,
old_value: { [fieldName]: oldValue }, old_value: { [fieldName]: oldValue },
new_value: { [fieldName]: newValue }, new_value: { [fieldName]: newValue },
metadata: metadata metadata: metadata,
}); });
}; };
@ -165,11 +232,13 @@ module.exports = (sequelize) => {
return await TaskEvent.findAll({ return await TaskEvent.findAll({
where: { task_id: taskId }, where: { task_id: taskId },
order: [['created_at', 'ASC']], order: [['created_at', 'ASC']],
include: [{ include: [
{
model: sequelize.models.User, model: sequelize.models.User,
as: 'User', as: 'User',
attributes: ['id', 'name', 'email'] attributes: ['id', 'name', 'email'],
}] },
],
}); });
}; };
@ -177,19 +246,21 @@ module.exports = (sequelize) => {
const events = await TaskEvent.findAll({ const events = await TaskEvent.findAll({
where: { where: {
task_id: taskId, 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; if (events.length === 0) return null;
const startEvent = events.find(e => const startEvent = events.find(
(e) =>
e.event_type === 'created' || e.event_type === 'created' ||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress (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 === 'completed' ||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done (e.event_type === 'status_changed' && e.new_value?.status === 2) // done
); );
@ -203,7 +274,7 @@ module.exports = (sequelize) => {
started_at: startTime, started_at: startTime,
completed_at: endTime, completed_at: endTime,
duration_ms: endTime - startTime, 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'); const bcrypt = require('bcrypt');
module.exports = (sequelize) => { module.exports = (sequelize) => {
const User = sequelize.define('User', { const User = sequelize.define(
'User',
{
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
autoIncrement: true autoIncrement: true,
}, },
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
email: { email: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
unique: true, unique: true,
validate: { validate: {
isEmail: true isEmail: true,
} },
}, },
password: { password: {
type: DataTypes.VIRTUAL, type: DataTypes.VIRTUAL,
allowNull: true allowNull: true,
}, },
password_digest: { password_digest: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
field: 'password_digest' field: 'password_digest',
}, },
appearance: { appearance: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'light', defaultValue: 'light',
validate: { validate: {
isIn: [['light', 'dark']] isIn: [['light', 'dark']],
} },
}, },
language: { language: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'en' defaultValue: 'en',
}, },
timezone: { timezone: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
defaultValue: 'UTC' defaultValue: 'UTC',
}, },
avatar_image: { avatar_image: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
telegram_bot_token: { telegram_bot_token: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
telegram_chat_id: { telegram_chat_id: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true allowNull: true,
}, },
task_summary_enabled: { task_summary_enabled: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
task_summary_frequency: { task_summary_frequency: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: 'daily', defaultValue: 'daily',
validate: { validate: {
isIn: [['daily', 'weekdays', 'weekly', '1h', '2h', '4h', '8h', '12h']] isIn: [
} [
'daily',
'weekdays',
'weekly',
'1h',
'2h',
'4h',
'8h',
'12h',
],
],
},
}, },
task_summary_last_run: { task_summary_last_run: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
task_summary_next_run: { task_summary_next_run: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true allowNull: true,
}, },
task_intelligence_enabled: { task_intelligence_enabled: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true defaultValue: true,
}, },
auto_suggest_next_actions_enabled: { auto_suggest_next_actions_enabled: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: false defaultValue: false,
}, },
pomodoro_enabled: { pomodoro_enabled: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
allowNull: false, allowNull: false,
defaultValue: true defaultValue: true,
}, },
today_settings: { today_settings: {
type: DataTypes.JSON, type: DataTypes.JSON,
@ -105,19 +118,24 @@ module.exports = (sequelize) => {
showDueToday: true, showDueToday: true,
showCompleted: true, showCompleted: true,
showProgressBar: true, showProgressBar: true,
showDailyQuote: true showDailyQuote: true,
} },
} },
}, { },
{
tableName: 'users', tableName: 'users',
hooks: { hooks: {
beforeValidate: async (user) => { beforeValidate: async (user) => {
if (user.password) { if (user.password) {
user.password_digest = await bcrypt.hash(user.password, 10); user.password_digest = await bcrypt.hash(
user.password,
10
);
} }
},
},
} }
} );
});
// password operations // password operations
const hashPassword = async (password) => { 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": "npx sequelize-cli db:migrate:undo",
"migration:undo:all": "npx sequelize-cli db:migrate:undo:all", "migration:undo:all": "npx sequelize-cli db:migrate:undo:all",
"migration:status": "npx sequelize-cli db:migrate:status", "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": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"bcrypt": "^6.0.0", "bcrypt": "~6.0.0",
"cheerio": "^1.1.0", "compression": "~1.8.0",
"compression": "^1.8.0", "connect-session-sequelize": "~7.1.7",
"connect-session-sequelize": "^7.1.7", "cors": "~2.8.5",
"cors": "^2.8.5", "dotenv": "~16.5.0",
"dotenv": "^16.5.0", "eslint": "^8.0.0",
"express": "^4.18.2", "express": "~4.18.2",
"express-session": "^1.18.1", "express-session": "~1.18.1",
"googleapis": "^144.0.0", "googleapis": "~144.0.0",
"helmet": "^8.1.0", "helmet": "~8.1.0",
"js-yaml": "^4.1.0", "js-yaml": "~4.1.0",
"moment-timezone": "^0.6.0", "moment-timezone": "~0.6.0",
"morgan": "^1.10.0", "morgan": "~1.10.0",
"multer": "^2.0.1", "multer": "~2.0.1",
"node-cron": "^4.1.0", "node-cron": "~4.1.0",
"recharts": "^2.15.4", "recharts": "~2.15.4",
"sequelize": "^6.37.7", "sequelize": "~6.37.7",
"sqlite3": "^5.1.7", "sqlite3": "~5.1.7",
"uuid": "^11.1.0" "uuid": "~11.1.0"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "^7.0.3", "cross-env": "~7.0.3",
"jest": "^30.0.0", "eslint-plugin-jest": "^29.0.1",
"nodemon": "^3.0.1", "eslint-plugin-prettier": "^5.5.1",
"sequelize-cli": "^6.6.2", "jest": "~30.0.0",
"supertest": "^7.1.1" "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({ const areas = await Area.findAll({
where: { user_id: req.session.userId }, where: { user_id: req.session.userId },
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
res.json(areas); res.json(areas);
@ -29,11 +29,13 @@ router.get('/areas/:id', async (req, res) => {
} }
const area = await Area.findOne({ 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) { 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); res.json(area);
@ -59,7 +61,7 @@ router.post('/areas', async (req, res) => {
const area = await Area.create({ const area = await Area.create({
name: name.trim(), name: name.trim(),
description: description || '', description: description || '',
user_id: req.session.userId user_id: req.session.userId,
}); });
res.status(201).json(area); res.status(201).json(area);
@ -67,7 +69,9 @@ router.post('/areas', async (req, res) => {
console.error('Error creating area:', error); console.error('Error creating area:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem creating the area.', 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({ 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) { if (!area) {
@ -99,7 +103,9 @@ router.patch('/areas/:id', async (req, res) => {
console.error('Error updating area:', error); console.error('Error updating area:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem updating the area.', 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({ 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) { if (!area) {
@ -123,7 +129,9 @@ router.delete('/areas/:id', async (req, res) => {
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
console.error('Error deleting area:', 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, email: user.email,
language: user.language, language: user.language,
appearance: user.appearance, 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'] }); 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) { if (!isValidPassword) {
return res.status(401).json({ errors: ['Invalid credentials'] }); return res.status(401).json({ errors: ['Invalid credentials'] });
} }
@ -54,8 +57,8 @@ router.post('/login', async (req, res) => {
email: user.email, email: user.email,
language: user.language, language: user.language,
appearance: user.appearance, appearance: user.appearance,
timezone: user.timezone timezone: user.timezone,
} },
}); });
} catch (error) { } catch (error) {
console.error('Login error:', error); console.error('Login error:', error);

View file

@ -11,7 +11,8 @@ const getOAuth2Client = () => {
return new google.auth.OAuth2( return new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET, 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) => { router.get('/auth', requireAuth, (req, res) => {
try { try {
// Check if Google credentials are configured // 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 // 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 // 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({ return res.json({
authUrl: `${frontendUrl}/calendar?demo=true&connected=true`, authUrl: `${frontendUrl}/calendar?demo=true&connected=true`,
demo: 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({ const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline', access_type: 'offline',
scope: SCOPES, scope: SCOPES,
state: JSON.stringify({ userId: req.currentUser.id }) state: JSON.stringify({ userId: req.currentUser.id }),
}); });
res.json({ authUrl }); res.json({ authUrl });
@ -54,7 +62,9 @@ router.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query; const { code, state } = req.query;
if (!code) { 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(); const oauth2Client = getOAuth2Client();
@ -72,10 +82,14 @@ router.get('/oauth/callback', async (req, res) => {
// await saveGoogleTokensForUser(userId, tokens); // await saveGoogleTokensForUser(userId, tokens);
// Redirect to frontend with success // 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) { } catch (error) {
console.error('Error handling OAuth callback:', 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) => { router.get('/status', requireAuth, async (req, res) => {
try { try {
// Check if we're in demo mode or have real Google integration // 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 // Demo mode - check if user has been "connected" in this session
// For demo purposes, we'll simulate connection status // For demo purposes, we'll simulate connection status
res.json({ res.json({
connected: false, // Will be set to true after demo connection connected: false, // Will be set to true after demo connection
email: null, email: null,
demo: true demo: true,
}); });
return; return;
} }
@ -99,7 +116,7 @@ router.get('/status', requireAuth, async (req, res) => {
res.json({ res.json({
connected: false, // Change to true when tokens exist and are valid 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) { } catch (error) {
console.error('Error checking calendar status:', error); console.error('Error checking calendar status:', error);
@ -126,8 +143,8 @@ router.get('/events', requireAuth, async (req, res) => {
start: new Date().toISOString(), start: new Date().toISOString(),
end: new Date(Date.now() + 60 * 60 * 1000).toISOString(), end: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
type: 'google', type: 'google',
color: '#ea4335' color: '#ea4335',
} },
]; ];
res.json({ events: sampleEvents }); res.json({ events: sampleEvents });

View file

@ -12,9 +12,9 @@ router.get('/inbox', async (req, res) => {
const items = await InboxItem.findAll({ const items = await InboxItem.findAll({
where: { where: {
user_id: req.session.userId, user_id: req.session.userId,
status: 'added' status: 'added',
}, },
order: [['created_at', 'DESC']] order: [['created_at', 'DESC']],
}); });
res.json(items); res.json(items);
@ -40,7 +40,7 @@ router.post('/inbox', async (req, res) => {
const item = await InboxItem.create({ const item = await InboxItem.create({
content: content.trim(), content: content.trim(),
source: source || 'tududi', source: source || 'tududi',
user_id: req.session.userId user_id: req.session.userId,
}); });
res.status(201).json(item); res.status(201).json(item);
@ -48,7 +48,9 @@ router.post('/inbox', async (req, res) => {
console.error('Error creating inbox item:', error); console.error('Error creating inbox item:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem creating the inbox item.', 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({ 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) { if (!item) {
@ -83,7 +85,7 @@ router.patch('/inbox/:id', async (req, res) => {
} }
const item = await InboxItem.findOne({ 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) { if (!item) {
@ -102,7 +104,9 @@ router.patch('/inbox/:id', async (req, res) => {
console.error('Error updating inbox item:', error); console.error('Error updating inbox item:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem updating the inbox item.', 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({ 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) { if (!item) {
@ -127,7 +131,9 @@ router.delete('/inbox/:id', async (req, res) => {
res.json({ message: 'Inbox item successfully deleted' }); res.json({ message: 'Inbox item successfully deleted' });
} catch (error) { } catch (error) {
console.error('Error deleting inbox item:', 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({ 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) { if (!item) {
@ -150,7 +156,9 @@ router.patch('/inbox/:id/process', async (req, res) => {
res.json(item); res.json(item);
} catch (error) { } catch (error) {
console.error('Error processing inbox item:', 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 { 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( const tags = await Promise.all(
tagNames.map(async (name) => { tagNames.map(async (name) => {
const [tag] = await Tag.findOrCreate({ const [tag] = await Tag.findOrCreate({
where: { name, user_id: userId }, where: { name, user_id: userId },
defaults: { name, user_id: userId } defaults: { name, user_id: userId },
}); });
return tag; return tag;
}) })
@ -40,7 +42,7 @@ router.get('/notes', async (req, res) => {
let whereClause = { user_id: req.session.userId }; let whereClause = { user_id: req.session.userId };
let includeClause = [ let includeClause = [
{ model: Tag, through: { attributes: [] } }, { model: Tag, through: { attributes: [] } },
{ model: Project, required: false, attributes: ['id', 'name'] } { model: Project, required: false, attributes: ['id', 'name'] },
]; ];
// Filter by tag // Filter by tag
@ -53,7 +55,7 @@ router.get('/notes', async (req, res) => {
where: whereClause, where: whereClause,
include: includeClause, include: includeClause,
order: [[orderColumn, orderDirection.toUpperCase()]], order: [[orderColumn, orderDirection.toUpperCase()]],
distinct: true distinct: true,
}); });
res.json(notes); res.json(notes);
@ -74,8 +76,8 @@ router.get('/note/:id', async (req, res) => {
where: { id: req.params.id, user_id: req.session.userId }, where: { id: req.params.id, user_id: req.session.userId },
include: [ include: [
{ model: Tag, through: { attributes: [] } }, { model: Tag, through: { attributes: [] } },
{ model: Project, required: false, attributes: ['id', 'name'] } { model: Project, required: false, attributes: ['id', 'name'] },
] ],
}); });
if (!note) { if (!note) {
@ -101,13 +103,13 @@ router.post('/note', async (req, res) => {
const noteAttributes = { const noteAttributes = {
title, title,
content, content,
user_id: req.session.userId user_id: req.session.userId,
}; };
// Handle project assignment // Handle project assignment
if (project_id && project_id.toString().trim()) { if (project_id && project_id.toString().trim()) {
const project = await Project.findOne({ 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) { if (!project) {
return res.status(400).json({ error: 'Invalid 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 // Handle tags - can be array of strings or array of objects with name property
let tagNames = []; let tagNames = [];
if (Array.isArray(tags)) { if (Array.isArray(tags)) {
if (tags.every(t => typeof t === 'string')) { if (tags.every((t) => typeof t === 'string')) {
tagNames = tags; tagNames = tags;
} else if (tags.every(t => typeof t === 'object' && t.name)) { } else if (tags.every((t) => typeof t === 'object' && t.name)) {
tagNames = tags.map(t => 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, { const noteWithAssociations = await Note.findByPk(note.id, {
include: [ include: [
{ model: Tag, through: { attributes: [] } }, { model: Tag, through: { attributes: [] } },
{ model: Project, required: false, attributes: ['id', 'name'] } { model: Project, required: false, attributes: ['id', 'name'] },
] ],
}); });
res.status(201).json(noteWithAssociations); res.status(201).json(noteWithAssociations);
@ -142,7 +144,9 @@ router.post('/note', async (req, res) => {
console.error('Error creating note:', error); console.error('Error creating note:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem creating the note.', 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({ 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) { if (!note) {
@ -172,7 +176,7 @@ router.patch('/note/:id', async (req, res) => {
if (project_id !== undefined) { if (project_id !== undefined) {
if (project_id && project_id.toString().trim()) { if (project_id && project_id.toString().trim()) {
const project = await Project.findOne({ 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) { if (!project) {
return res.status(400).json({ error: 'Invalid project.' }); return res.status(400).json({ error: 'Invalid project.' });
@ -189,10 +193,10 @@ router.patch('/note/:id', async (req, res) => {
if (tags !== undefined) { if (tags !== undefined) {
let tagNames = []; let tagNames = [];
if (Array.isArray(tags)) { if (Array.isArray(tags)) {
if (tags.every(t => typeof t === 'string')) { if (tags.every((t) => typeof t === 'string')) {
tagNames = tags; tagNames = tags;
} else if (tags.every(t => typeof t === 'object' && t.name)) { } else if (tags.every((t) => typeof t === 'object' && t.name)) {
tagNames = tags.map(t => t.name); tagNames = tags.map((t) => t.name);
} }
} }
await updateNoteTags(note, tagNames, req.session.userId); 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, { const noteWithAssociations = await Note.findByPk(note.id, {
include: [ include: [
{ model: Tag, through: { attributes: [] } }, { model: Tag, through: { attributes: [] } },
{ model: Project, required: false, attributes: ['id', 'name'] } { model: Project, required: false, attributes: ['id', 'name'] },
] ],
}); });
res.json(noteWithAssociations); res.json(noteWithAssociations);
@ -211,7 +215,9 @@ router.patch('/note/:id', async (req, res) => {
console.error('Error updating note:', error); console.error('Error updating note:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem updating the note.', 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({ 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) { if (!note) {
@ -235,7 +241,9 @@ router.delete('/note/:id', async (req, res) => {
res.json({ message: 'Note deleted successfully.' }); res.json({ message: 'Note deleted successfully.' });
} catch (error) { } catch (error) {
console.error('Error deleting note:', 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); cb(null, uploadDir);
}, },
filename: function (req, file, cb) { 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)); cb(null, 'project-' + uniqueSuffix + path.extname(file.originalname));
} },
}); });
const upload = multer({ const upload = multer({
@ -40,7 +40,9 @@ const upload = multer({
}, },
fileFilter: function (req, file, cb) { fileFilter: function (req, file, cb) {
const allowedTypes = /jpeg|jpg|png|gif|webp/; 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); const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) { if (mimetype && extname) {
@ -48,7 +50,7 @@ const upload = multer({
} else { } else {
cb(new Error('Only image files are allowed!')); cb(new Error('Only image files are allowed!'));
} }
} },
}); });
// Helper function to update project tags // Helper function to update project tags
@ -56,8 +58,8 @@ async function updateProjectTags(project, tagsData, userId) {
if (!tagsData) return; if (!tagsData) return;
const tagNames = tagsData const tagNames = tagsData
.map(tag => tag.name) .map((tag) => tag.name)
.filter(name => name && name.trim()) .filter((name) => name && name.trim())
.filter((name, index, arr) => arr.indexOf(name) === index); // unique .filter((name, index, arr) => arr.indexOf(name) === index); // unique
if (tagNames.length === 0) { if (tagNames.length === 0) {
@ -67,15 +69,17 @@ async function updateProjectTags(project, tagsData, userId) {
// Find existing tags // Find existing tags
const existingTags = await Tag.findAll({ const existingTags = await Tag.findAll({
where: { user_id: userId, name: tagNames } where: { user_id: userId, name: tagNames },
}); });
// Create new tags // Create new tags
const existingTagNames = existingTags.map(tag => tag.name); const existingTagNames = existingTags.map((tag) => tag.name);
const newTagNames = tagNames.filter(name => !existingTagNames.includes(name)); const newTagNames = tagNames.filter(
(name) => !existingTagNames.includes(name)
);
const createdTags = await Promise.all( 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 // Set all tags to project
@ -139,33 +143,33 @@ router.get('/projects', async (req, res) => {
{ {
model: Task, model: Task,
required: false, required: false,
attributes: ['id', 'status'] attributes: ['id', 'status'],
}, },
{ {
model: Area, model: Area,
required: false, required: false,
attributes: ['name'] attributes: ['name'],
}, },
{ {
model: Tag, model: Tag,
attributes: ['id', 'name'], attributes: ['id', 'name'],
through: { attributes: [] } through: { attributes: [] },
} },
], ],
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
const { grouped } = req.query; const { grouped } = req.query;
// Calculate task status counts for each project // Calculate task status counts for each project
const taskStatusCounts = {}; const taskStatusCounts = {};
const enhancedProjects = projects.map(project => { const enhancedProjects = projects.map((project) => {
const tasks = project.Tasks || []; const tasks = project.Tasks || [];
const taskStatus = { const taskStatus = {
total: tasks.length, total: tasks.length,
done: tasks.filter(t => t.status === 2).length, done: tasks.filter((t) => t.status === 2).length,
in_progress: tasks.filter(t => t.status === 1).length, in_progress: tasks.filter((t) => t.status === 1).length,
not_started: tasks.filter(t => t.status === 0).length not_started: tasks.filter((t) => t.status === 0).length,
}; };
taskStatusCounts[project.id] = taskStatus; taskStatusCounts[project.id] = taskStatus;
@ -176,14 +180,17 @@ router.get('/projects', async (req, res) => {
tags: projectJson.Tags || [], // Normalize Tags to tags tags: projectJson.Tags || [], // Normalize Tags to tags
due_date_at: formatDate(project.due_date_at), due_date_at: formatDate(project.due_date_at),
task_status: taskStatus, 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, return grouped format
if (grouped === 'true') { if (grouped === 'true') {
const groupedProjects = {}; const groupedProjects = {};
enhancedProjects.forEach(project => { enhancedProjects.forEach((project) => {
const areaName = project.Area ? project.Area.name : 'No Area'; const areaName = project.Area ? project.Area.name : 'No Area';
if (!groupedProjects[areaName]) { if (!groupedProjects[areaName]) {
groupedProjects[areaName] = []; groupedProjects[areaName] = [];
@ -193,7 +200,7 @@ router.get('/projects', async (req, res) => {
res.json(groupedProjects); res.json(groupedProjects);
} else { } else {
res.json({ res.json({
projects: enhancedProjects projects: enhancedProjects,
}); });
} }
} catch (error) { } catch (error) {
@ -220,13 +227,17 @@ router.get('/project/:id', async (req, res) => {
model: Tag, model: Tag,
attributes: ['id', 'name'], attributes: ['id', 'name'],
through: { attributes: [] }, through: { attributes: [] },
required: false required: false,
} },
] ],
}, },
{ model: Area, required: false, attributes: ['id', 'name'] }, { model: Area, required: false, attributes: ['id', 'name'] },
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } } {
] model: Tag,
attributes: ['id', 'name'],
through: { attributes: [] },
},
],
}); });
if (!project) { if (!project) {
@ -237,7 +248,7 @@ router.get('/project/:id', async (req, res) => {
const result = { const result = {
...projectJson, ...projectJson,
tags: projectJson.Tags || [], // Normalize Tags to tags 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); res.json(result);
@ -254,7 +265,16 @@ router.post('/project', async (req, res) => {
return res.status(401).json({ error: 'Authentication required' }); 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) // Handle both tags and Tags (Sequelize association format)
const tagsData = tags || Tags; const tagsData = tags || Tags;
@ -272,7 +292,7 @@ router.post('/project', async (req, res) => {
priority: priority || null, priority: priority || null,
due_date_at: due_date_at || null, due_date_at: due_date_at || null,
image_url: image_url || null, image_url: image_url || null,
user_id: req.session.userId user_id: req.session.userId,
}; };
const project = await Project.create(projectData); const project = await Project.create(projectData);
@ -281,8 +301,12 @@ router.post('/project', async (req, res) => {
// Reload project with associations // Reload project with associations
const projectWithAssociations = await Project.findByPk(project.id, { const projectWithAssociations = await Project.findByPk(project.id, {
include: [ include: [
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } } {
] model: Tag,
attributes: ['id', 'name'],
through: { attributes: [] },
},
],
}); });
const projectJson = projectWithAssociations.toJSON(); const projectJson = projectWithAssociations.toJSON();
@ -290,13 +314,15 @@ router.post('/project', async (req, res) => {
res.status(201).json({ res.status(201).json({
...projectJson, ...projectJson,
tags: projectJson.Tags || [], // Normalize Tags to tags 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) { } catch (error) {
console.error('Error creating project:', error); console.error('Error creating project:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem creating the project.', 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({ 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) { if (!project) {
return res.status(404).json({ error: 'Project not found.' }); 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) // Handle both tags and Tags (Sequelize association format)
const tagsData = tags || Tags; const tagsData = tags || Tags;
@ -326,7 +363,8 @@ router.patch('/project/:id', async (req, res) => {
if (description !== undefined) updateData.description = description; if (description !== undefined) updateData.description = description;
if (area_id !== undefined) updateData.area_id = area_id; if (area_id !== undefined) updateData.area_id = area_id;
if (active !== undefined) updateData.active = active; 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 (priority !== undefined) updateData.priority = priority;
if (due_date_at !== undefined) updateData.due_date_at = due_date_at; if (due_date_at !== undefined) updateData.due_date_at = due_date_at;
if (image_url !== undefined) updateData.image_url = image_url; if (image_url !== undefined) updateData.image_url = image_url;
@ -337,8 +375,12 @@ router.patch('/project/:id', async (req, res) => {
// Reload project with associations // Reload project with associations
const projectWithAssociations = await Project.findByPk(project.id, { const projectWithAssociations = await Project.findByPk(project.id, {
include: [ include: [
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } } {
] model: Tag,
attributes: ['id', 'name'],
through: { attributes: [] },
},
],
}); });
const projectJson = projectWithAssociations.toJSON(); const projectJson = projectWithAssociations.toJSON();
@ -346,13 +388,15 @@ router.patch('/project/:id', async (req, res) => {
res.json({ res.json({
...projectJson, ...projectJson,
tags: projectJson.Tags || [], // Normalize Tags to tags 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) { } catch (error) {
console.error('Error updating project:', error); console.error('Error updating project:', error);
res.status(400).json({ res.status(400).json({
error: 'There was a problem updating the project.', 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({ 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) { if (!project) {
@ -376,7 +420,9 @@ router.delete('/project/:id', async (req, res) => {
res.json({ message: 'Project successfully deleted' }); res.json({ message: 'Project successfully deleted' });
} catch (error) { } catch (error) {
console.error('Error deleting project:', 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(); const quotes = quotesService.getAllQuotes();
res.json({ res.json({
quotes, quotes,
count: quotesService.getQuotesCount() count: quotesService.getQuotesCount(),
}); });
} catch (error) { } catch (error) {
console.error('Error getting quotes:', error); console.error('Error getting quotes:', error);

View file

@ -8,7 +8,7 @@ router.get('/tags', async (req, res) => {
const tags = await Tag.findAll({ const tags = await Tag.findAll({
where: { user_id: req.currentUser.id }, where: { user_id: req.currentUser.id },
attributes: ['id', 'name'], attributes: ['id', 'name'],
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
res.json(tags); res.json(tags);
@ -23,7 +23,7 @@ router.get('/tag/:id', async (req, res) => {
try { try {
const tag = await Tag.findOne({ 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 },
attributes: ['id', 'name'] attributes: ['id', 'name'],
}); });
if (!tag) { if (!tag) {
@ -48,16 +48,18 @@ router.post('/tag', async (req, res) => {
const tag = await Tag.create({ const tag = await Tag.create({
name: name.trim(), name: name.trim(),
user_id: req.currentUser.id user_id: req.currentUser.id,
}); });
res.status(201).json({ res.status(201).json({
id: tag.id, id: tag.id,
name: tag.name name: tag.name,
}); });
} catch (error) { } catch (error) {
console.error('Error creating tag:', 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) => { router.patch('/tag/:id', async (req, res) => {
try { try {
const tag = await Tag.findOne({ 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) { if (!tag) {
@ -82,11 +84,13 @@ router.patch('/tag/:id', async (req, res) => {
res.json({ res.json({
id: tag.id, id: tag.id,
name: tag.name name: tag.name,
}); });
} catch (error) { } catch (error) {
console.error('Error updating tag:', 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 { try {
const tag = await Tag.findOne({ 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) { if (!tag) {
@ -111,7 +115,7 @@ router.delete('/tag/:id', async (req, res) => {
await sequelize.query('DELETE FROM tasks_tags WHERE tag_id = ?', { await sequelize.query('DELETE FROM tasks_tags WHERE tag_id = ?', {
replacements: [tag.id], replacements: [tag.id],
type: sequelize.QueryTypes.DELETE, type: sequelize.QueryTypes.DELETE,
transaction transaction,
}); });
} catch (error) { } catch (error) {
// Ignore if table doesn't exist // 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 = ?', { await sequelize.query('DELETE FROM notes_tags WHERE tag_id = ?', {
replacements: [tag.id], replacements: [tag.id],
type: sequelize.QueryTypes.DELETE, type: sequelize.QueryTypes.DELETE,
transaction transaction,
}); });
} catch (error) { } catch (error) {
// Ignore if table doesn't exist // Ignore if table doesn't exist
@ -130,11 +134,14 @@ router.delete('/tag/:id', async (req, res) => {
} }
try { try {
await sequelize.query('DELETE FROM projects_tags WHERE tag_id = ?', { await sequelize.query(
'DELETE FROM projects_tags WHERE tag_id = ?',
{
replacements: [tag.id], replacements: [tag.id],
type: sequelize.QueryTypes.DELETE, type: sequelize.QueryTypes.DELETE,
transaction transaction,
}); }
);
} catch (error) { } catch (error) {
// Ignore if table doesn't exist // Ignore if table doesn't exist
console.log('projects_tags table not found, skipping'); console.log('projects_tags table not found, skipping');
@ -148,7 +155,9 @@ router.delete('/tag/:id', async (req, res) => {
} catch (error) { } catch (error) {
await transaction.rollback(); await transaction.rollback();
console.error('Error deleting tag:', error); 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); const timeline = await TaskEventService.getTaskTimeline(req.params.id);
// Filter to only show events for tasks owned by the current user // 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); res.json(userTimeline);
} catch (error) { } catch (error) {
@ -21,10 +23,14 @@ router.get('/task/:id/timeline', async (req, res) => {
// GET /api/task/:id/completion-time - Get task completion analytics // GET /api/task/:id/completion-time - Get task completion analytics
router.get('/task/:id/completion-time', async (req, res) => { router.get('/task/:id/completion-time', async (req, res) => {
try { try {
const completionTime = await TaskEventService.getTaskCompletionTime(req.params.id); const completionTime = await TaskEventService.getTaskCompletionTime(
req.params.id
);
if (!completionTime) { 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); res.json(completionTime);
@ -58,7 +64,9 @@ router.get('/user/activity-summary', async (req, res) => {
const { startDate, endDate } = req.query; const { startDate, endDate } = req.query;
if (!startDate || !endDate) { 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( const activitySummary = await TaskEventService.getTaskActivitySummary(
@ -85,7 +93,7 @@ router.get('/tasks/completion-analytics', async (req, res) => {
const whereClause = { const whereClause = {
user_id: req.currentUser.id, user_id: req.currentUser.id,
status: 2 // completed status: 2, // completed
}; };
if (projectId) { if (projectId) {
@ -95,23 +103,25 @@ router.get('/tasks/completion-analytics', async (req, res) => {
const completedTasks = await Task.findAll({ const completedTasks = await Task.findAll({
where: whereClause, where: whereClause,
include: [ include: [
{ model: Project, attributes: ['name'], required: false } { model: Project, attributes: ['name'], required: false },
], ],
order: [['completed_at', 'DESC']], order: [['completed_at', 'DESC']],
limit: parseInt(limit), limit: parseInt(limit),
offset: parseInt(offset) offset: parseInt(offset),
}); });
// Get completion time analytics for each task // Get completion time analytics for each task
const analytics = []; const analytics = [];
for (const task of completedTasks) { for (const task of completedTasks) {
const completionTime = await TaskEventService.getTaskCompletionTime(task.id); const completionTime = await TaskEventService.getTaskCompletionTime(
task.id
);
if (completionTime) { if (completionTime) {
analytics.push({ analytics.push({
task_id: task.id, task_id: task.id,
task_name: task.name, task_name: task.name,
project_name: task.Project?.name || null, project_name: task.Project?.name || null,
...completionTime ...completionTime,
}); });
} }
} }
@ -119,30 +129,37 @@ router.get('/tasks/completion-analytics', async (req, res) => {
// Calculate summary statistics // Calculate summary statistics
const summary = { const summary = {
total_tasks: analytics.length, total_tasks: analytics.length,
average_completion_hours: analytics.length > 0 average_completion_hours:
? analytics.reduce((sum, a) => sum + a.duration_hours, 0) / analytics.length analytics.length > 0
? analytics.reduce((sum, a) => sum + a.duration_hours, 0) /
analytics.length
: 0, : 0,
median_completion_hours: 0, median_completion_hours: 0,
fastest_completion: analytics.length > 0 fastest_completion:
? Math.min(...analytics.map(a => a.duration_hours)) 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, : 0,
slowest_completion: analytics.length > 0
? Math.max(...analytics.map(a => a.duration_hours))
: 0
}; };
// Calculate median // Calculate median
if (analytics.length > 0) { 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); 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 - 1] + sorted[middle]) / 2
: sorted[middle]; : sorted[middle];
} }
res.json({ res.json({
tasks: analytics, tasks: analytics,
summary summary,
}); });
} catch (error) { } catch (error) {
console.error('Error fetching completion analytics:', 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); const user = await User.findByPk(req.session.userId);
if (!user || !user.telegram_bot_token) { 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); const success = await telegramPoller.addUser(user);
@ -21,10 +23,12 @@ router.post('/telegram/start-polling', async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: 'Telegram polling started', message: 'Telegram polling started',
status: telegramPoller.getStatus() status: telegramPoller.getStatus(),
}); });
} else { } else {
res.status(500).json({ error: 'Failed to start Telegram polling.' }); res.status(500).json({
error: 'Failed to start Telegram polling.',
});
} }
} catch (error) { } catch (error) {
console.error('Error starting Telegram polling:', error); console.error('Error starting Telegram polling:', error);
@ -44,7 +48,7 @@ router.post('/telegram/stop-polling', async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: 'Telegram polling stopped', message: 'Telegram polling stopped',
status: telegramPoller.getStatus() status: telegramPoller.getStatus(),
}); });
} catch (error) { } catch (error) {
console.error('Error stopping Telegram polling:', error); console.error('Error stopping Telegram polling:', error);
@ -61,7 +65,7 @@ router.get('/telegram/polling-status', async (req, res) => {
res.json({ res.json({
success: true, success: true,
status: telegramPoller.getStatus() status: telegramPoller.getStatus(),
}); });
} catch (error) { } catch (error) {
console.error('Error getting Telegram polling status:', error); console.error('Error getting Telegram polling status:', error);
@ -79,7 +83,9 @@ router.post('/telegram/setup', async (req, res) => {
const { token } = req.body; const { token } = req.body;
if (!token) { 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); 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 // Basic token validation - check if it looks like a Telegram bot token
if (!/^\d+:[A-Za-z0-9_-]{35}$/.test(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 // Update user's telegram bot token
@ -98,7 +106,7 @@ router.post('/telegram/setup', async (req, res) => {
res.json({ res.json({
success: true, success: true,
message: 'Telegram bot token updated successfully', message: 'Telegram bot token updated successfully',
token: token token: token,
}); });
} catch (error) { } catch (error) {
console.error('Error setting up Telegram:', error); console.error('Error setting up Telegram:', error);

View file

@ -11,12 +11,16 @@ function extractMetadataFromHtml(html) {
let title = null; let title = null;
// Try og:title first // 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) { if (ogTitleMatch) {
title = ogTitleMatch[1]; title = ogTitleMatch[1];
} else { } else {
// Try twitter:title // 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) { if (twitterTitleMatch) {
title = twitterTitleMatch[1]; title = twitterTitleMatch[1];
} else { } else {
@ -46,11 +50,15 @@ function extractMetadataFromHtml(html) {
// Extract image with priority: og:image > twitter:image // Extract image with priority: og:image > twitter:image
let image = null; 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) { if (ogImageMatch) {
image = ogImageMatch[1]; image = ogImageMatch[1];
} else { } else {
const twitterImageMatch = html.match(/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i); const twitterImageMatch = html.match(
/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i
);
if (twitterImageMatch) { if (twitterImageMatch) {
image = twitterImageMatch[1]; image = twitterImageMatch[1];
} }
@ -58,15 +66,21 @@ function extractMetadataFromHtml(html) {
// Extract description // Extract description
let description = null; 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) { if (ogDescMatch) {
description = ogDescMatch[1]; description = ogDescMatch[1];
} else { } else {
const twitterDescMatch = html.match(/<meta[^>]+name=["']twitter:description["'][^>]+content=["']([^"']+)["']/i); const twitterDescMatch = html.match(
/<meta[^>]+name=["']twitter:description["'][^>]+content=["']([^"']+)["']/i
);
if (twitterDescMatch) { if (twitterDescMatch) {
description = twitterDescMatch[1]; description = twitterDescMatch[1];
} else { } else {
const metaDescMatch = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i); const metaDescMatch = html.match(
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i
);
if (metaDescMatch) { if (metaDescMatch) {
description = metaDescMatch[1]; description = metaDescMatch[1];
} }
@ -80,7 +94,7 @@ function extractMetadataFromHtml(html) {
return { return {
title, title,
image, image,
description description,
}; };
} catch (error) { } catch (error) {
console.error('Error parsing HTML:', error); console.error('Error parsing HTML:', error);
@ -90,7 +104,8 @@ function extractMetadataFromHtml(html) {
// Helper function to check if text is a URL // Helper function to check if text is a URL
function isUrl(text) { 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()); return urlRegex.test(text.trim());
} }
@ -105,7 +120,8 @@ function resolveUrl(baseUrl, relativeUrl) {
// Helper function to handle YouTube URLs specially // Helper function to handle YouTube URLs specially
function handleYouTubeUrl(url) { 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); const match = url.match(youtubeRegex);
if (match) { if (match) {
@ -115,7 +131,7 @@ function handleYouTubeUrl(url) {
return { return {
title: 'YouTube Video', title: 'YouTube Video',
image: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`, 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', method: 'GET',
timeout: 2000, // Reduced from 5000ms to 2000ms timeout: 2000, // Reduced from 5000ms to 2000ms
headers: { 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) => { const req = client.request(options, (res) => {
// Handle redirects (301, 302, 303, 307, 308) // Handle redirects (301, 302, 303, 307, 308)
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) { if (
const redirectUrl = new URL(res.headers.location, currentUrl).href; [301, 302, 303, 307, 308].includes(res.statusCode) &&
res.headers.location
) {
const redirectUrl = new URL(
res.headers.location,
currentUrl
).href;
makeRequest(redirectUrl, redirectCount + 1); makeRequest(redirectUrl, redirectCount + 1);
return; return;
} }
@ -199,7 +222,12 @@ async function fetchUrlMetadata(url, maxRedirects = 5) {
data += chunk; data += chunk;
// Early termination if we've found essential meta tags and closed head // 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; foundMeta = true;
} }
@ -216,8 +244,14 @@ async function fetchUrlMetadata(url, maxRedirects = 5) {
const metadata = extractMetadataFromHtml(data); const metadata = extractMetadataFromHtml(data);
// Resolve relative image URLs to absolute // Resolve relative image URLs to absolute
if (metadata.image && !metadata.image.startsWith('http')) { if (
metadata.image = resolveUrl(currentUrl, metadata.image); metadata.image &&
!metadata.image.startsWith('http')
) {
metadata.image = resolveUrl(
currentUrl,
metadata.image
);
} }
resolve(metadata); resolve(metadata);
@ -266,10 +300,16 @@ router.get('/url/title', async (req, res) => {
url, url,
title: metadata.title, title: metadata.title,
image: metadata.image, image: metadata.image,
description: metadata.description description: metadata.description,
}); });
} else { } 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) { } catch (error) {
console.error('Error extracting URL title:', 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; const { text } = req.body;
if (!text) { 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 // Enhanced URL extraction - look for URLs with or without protocol
const urlWithProtocolRegex = /(https?:\/\/[^\s]+)/gi; 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); let urls = text.match(urlWithProtocolRegex);
@ -301,7 +344,7 @@ router.post('/url/extract-from-text', async (req, res) => {
const matches = text.match(urlWithoutProtocolRegex); const matches = text.match(urlWithoutProtocolRegex);
if (matches) { if (matches) {
// Clean up the matches (remove leading whitespace) // 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, title: metadata.title,
image: metadata.image, image: metadata.image,
description: metadata.description, description: metadata.description,
originalText: text originalText: text,
}); });
} else { } else {
res.json({ res.json({
@ -325,7 +368,7 @@ router.post('/url/extract-from-text', async (req, res) => {
title: null, title: null,
image: null, image: null,
description: null, description: null,
originalText: text originalText: text,
}); });
} }
} else { } else {

View file

@ -3,7 +3,16 @@ const { User } = require('../models');
const taskSummaryService = require('../services/taskSummaryService'); const taskSummaryService = require('../services/taskSummaryService');
const router = express.Router(); 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 // GET /api/profile
router.get('/profile', async (req, res) => { router.get('/profile', async (req, res) => {
@ -14,11 +23,21 @@ router.get('/profile', async (req, res) => {
const user = await User.findByPk(req.session.userId, { const user = await User.findByPk(req.session.userId, {
attributes: [ attributes: [
'id', 'email', 'appearance', 'language', 'timezone', 'id',
'avatar_image', 'telegram_bot_token', 'telegram_chat_id', 'email',
'task_summary_enabled', 'task_summary_frequency', 'task_intelligence_enabled', 'appearance',
'auto_suggest_next_actions_enabled', 'pomodoro_enabled', 'today_settings' '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) { if (!user) {
@ -54,35 +73,60 @@ router.patch('/profile', async (req, res) => {
return res.status(404).json({ error: 'Profile not found.' }); 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 = {}; const allowedUpdates = {};
if (appearance !== undefined) allowedUpdates.appearance = appearance; if (appearance !== undefined) allowedUpdates.appearance = appearance;
if (language !== undefined) allowedUpdates.language = language; if (language !== undefined) allowedUpdates.language = language;
if (timezone !== undefined) allowedUpdates.timezone = timezone; if (timezone !== undefined) allowedUpdates.timezone = timezone;
if (avatar_image !== undefined) allowedUpdates.avatar_image = avatar_image; if (avatar_image !== undefined)
if (telegram_bot_token !== undefined) allowedUpdates.telegram_bot_token = telegram_bot_token; allowedUpdates.avatar_image = avatar_image;
if (task_intelligence_enabled !== undefined) allowedUpdates.task_intelligence_enabled = task_intelligence_enabled; if (telegram_bot_token !== undefined)
if (task_summary_enabled !== undefined) allowedUpdates.task_summary_enabled = task_summary_enabled; allowedUpdates.telegram_bot_token = telegram_bot_token;
if (task_summary_frequency !== undefined) allowedUpdates.task_summary_frequency = task_summary_frequency; if (task_intelligence_enabled !== undefined)
if (auto_suggest_next_actions_enabled !== undefined) allowedUpdates.auto_suggest_next_actions_enabled = auto_suggest_next_actions_enabled; allowedUpdates.task_intelligence_enabled =
if (pomodoro_enabled !== undefined) allowedUpdates.pomodoro_enabled = pomodoro_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 // Handle password change if provided
if (currentPassword && newPassword) { if (currentPassword && newPassword) {
if (newPassword.length < 6) { if (newPassword.length < 6) {
return res.status(400).json({ return res.status(400).json({
field: 'newPassword', field: 'newPassword',
error: 'Password must be at least 6 characters' error: 'Password must be at least 6 characters',
}); });
} }
// Verify current password // Verify current password
const isValidPassword = await User.checkPassword(currentPassword, user.password_digest); const isValidPassword = await User.checkPassword(
currentPassword,
user.password_digest
);
if (!isValidPassword) { if (!isValidPassword) {
return res.status(400).json({ return res.status(400).json({
field: 'currentPassword', 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 // Return updated user with limited fields
const updatedUser = await User.findByPk(user.id, { 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); res.json(updatedUser);
@ -103,7 +161,9 @@ router.patch('/profile', async (req, res) => {
console.error('Error updating profile:', error); console.error('Error updating profile:', error);
res.status(400).json({ res.status(400).json({
error: 'Failed to update profile.', 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; const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) { 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) { if (newPassword.length < 6) {
return res.status(400).json({ return res.status(400).json({
field: 'newPassword', 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 // Verify current password
const isValidPassword = await User.checkPassword(currentPassword, user.password_digest); const isValidPassword = await User.checkPassword(
currentPassword,
user.password_digest
);
if (!isValidPassword) { if (!isValidPassword) {
return res.status(400).json({ return res.status(400).json({
field: 'currentPassword', 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({ res.json({
success: true, success: true,
enabled: enabled, enabled: enabled,
message: message message: message,
}); });
} catch (error) { } catch (error) {
console.error('Error toggling task summary:', error); console.error('Error toggling task summary:', error);
res.status(400).json({ res.status(400).json({
error: 'Failed to update task summary settings.', 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({ res.json({
success: true, success: true,
frequency: frequency, frequency: frequency,
message: `Task summary frequency has been set to ${frequency}.` message: `Task summary frequency has been set to ${frequency}.`,
}); });
} catch (error) { } catch (error) {
console.error('Error updating task summary frequency:', error); console.error('Error updating task summary frequency:', error);
res.status(400).json({ res.status(400).json({
error: 'Failed to update task summary frequency.', 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) { 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 // Send the task summary
@ -248,16 +319,18 @@ router.post('/profile/task-summary/send-now', async (req, res) => {
if (success) { if (success) {
res.json({ res.json({
success: true, success: true,
message: 'Task summary was sent to your Telegram.' message: 'Task summary was sent to your Telegram.',
}); });
} else { } 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) { } catch (error) {
console.error('Error sending task summary:', error); console.error('Error sending task summary:', error);
res.status(400).json({ res.status(400).json({
error: 'Error sending message to Telegram.', 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, enabled: user.task_summary_enabled,
frequency: user.task_summary_frequency, frequency: user.task_summary_frequency,
last_run: user.task_summary_last_run, last_run: user.task_summary_last_run,
next_run: user.task_summary_next_run next_run: user.task_summary_next_run,
}); });
} catch (error) { } catch (error) {
console.error('Error fetching task summary status:', error); console.error('Error fetching task summary status:', error);
@ -306,24 +379,42 @@ router.put('/profile/today-settings', async (req, res) => {
showDueToday, showDueToday,
showCompleted, showCompleted,
showProgressBar, showProgressBar,
showDailyQuote showDailyQuote,
} = req.body; } = req.body;
const todaySettings = { const todaySettings = {
showMetrics: showMetrics !== undefined ? showMetrics : user.today_settings?.showMetrics || false, showMetrics:
showProductivity: showProductivity !== undefined ? showProductivity : user.today_settings?.showProductivity || false, showMetrics !== undefined
showIntelligence: showIntelligence !== undefined ? showIntelligence : user.today_settings?.showIntelligence || false, ? showMetrics
showDueToday: showDueToday !== undefined ? showDueToday : user.today_settings?.showDueToday || true, : user.today_settings?.showMetrics || false,
showCompleted: showCompleted !== undefined ? showCompleted : user.today_settings?.showCompleted || true, 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 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 }); await user.update({ today_settings: todaySettings });
res.json({ res.json({
success: true, success: true,
today_settings: todaySettings today_settings: todaySettings,
}); });
} catch (error) { } catch (error) {
console.error('Error updating today settings:', error); console.error('Error updating today settings:', error);

View file

@ -16,7 +16,9 @@ async function initDatabase() {
await sequelize.sync({ force: true }); await sequelize.sync({ force: true });
console.log('✅ Database initialized successfully'); 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); process.exit(0);
} catch (error) { } catch (error) {
console.error('❌ Error initializing database:', error.message); console.error('❌ Error initializing database:', error.message);

View file

@ -6,7 +6,16 @@
*/ */
require('dotenv').config(); 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 fs = require('fs');
const path = require('path'); const path = require('path');
@ -20,7 +29,9 @@ async function checkDatabaseStatus() {
console.log('📂 Database Configuration:'); console.log('📂 Database Configuration:');
console.log(` Storage: ${dbPath}`); 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'}`); console.log(` Environment: ${process.env.NODE_ENV || 'development'}`);
// Check if database file exists // Check if database file exists
@ -45,7 +56,7 @@ async function checkDatabaseStatus() {
{ name: 'Tasks', model: Task }, { name: 'Tasks', model: Task },
{ name: 'Notes', model: Note }, { name: 'Notes', model: Note },
{ name: 'Tags', model: Tag }, { name: 'Tags', model: Tag },
{ name: 'Inbox Items', model: InboxItem } { name: 'Inbox Items', model: InboxItem },
]; ];
for (const { name, model } of models) { for (const { name, model } of models) {

View file

@ -14,13 +14,16 @@ function createMigration() {
if (!migrationName) { if (!migrationName) {
console.error('❌ Usage: npm run migration:create <migration-name>'); 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); process.exit(1);
} }
// Generate timestamp (YYYYMMDDHHMMSS format) // Generate timestamp (YYYYMMDDHHMMSS format)
const now = new Date(); const now = new Date();
const timestamp = now.getFullYear().toString() + const timestamp =
now.getFullYear().toString() +
(now.getMonth() + 1).toString().padStart(2, '0') + (now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') + now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') + now.getHours().toString().padStart(2, '0') +

View file

@ -15,7 +15,9 @@ async function createUser() {
if (!email || password === undefined) { if (!email || password === undefined) {
console.error('❌ Usage: npm run user:create <email> <password>'); 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); process.exit(1);
} }
@ -29,7 +31,8 @@ async function createUser() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
// Check for common invalid patterns // Check for common invalid patterns
if (!email.includes('@') || if (
!email.includes('@') ||
!email.includes('.') || !email.includes('.') ||
email.includes('@@') || email.includes('@@') ||
email.includes(' ') || email.includes(' ') ||
@ -38,7 +41,8 @@ async function createUser() {
email.endsWith('.') || email.endsWith('.') ||
email.includes('@.') || email.includes('@.') ||
email.includes('.@') || email.includes('.@') ||
!emailRegex.test(email)) { !emailRegex.test(email)
) {
console.error('❌ Invalid email format'); console.error('❌ Invalid email format');
process.exit(1); process.exit(1);
} }
@ -59,7 +63,7 @@ async function createUser() {
// Create the user // Create the user
const user = await User.create({ const user = await User.create({
email, email,
password_digest: hashedPassword password_digest: hashedPassword,
}); });
console.log('✅ User created successfully'); 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 bcrypt = require('bcrypt');
const { createMassiveTaskData } = require('./massive-tasks'); const { createMassiveTaskData } = require('./massive-tasks');
@ -19,7 +27,7 @@ async function seedDatabase() {
password_digest: await bcrypt.hash('password123', 10), password_digest: await bcrypt.hash('password123', 10),
appearance: 'light', appearance: 'light',
language: 'en', language: 'en',
timezone: 'Europe/Athens' timezone: 'Europe/Athens',
}); });
console.log('✅ Created new test user with ID:', testUser.id); console.log('✅ Created new test user with ID:', testUser.id);
} else { } else {
@ -46,7 +54,7 @@ async function seedDatabase() {
{ name: 'Travel', user_id: testUser.id }, { name: 'Travel', user_id: testUser.id },
{ name: 'Hobbies', user_id: testUser.id }, { name: 'Hobbies', user_id: testUser.id },
{ name: 'Social', user_id: testUser.id }, { name: 'Social', user_id: testUser.id },
{ name: 'Career', user_id: testUser.id } { name: 'Career', user_id: testUser.id },
]); ]);
// Create projects // Create projects
@ -58,14 +66,14 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[1].id, area_id: areas[1].id,
active: true, 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', name: 'Learn React Native',
description: 'Master mobile app development', description: 'Master mobile app development',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[3].id, area_id: areas[3].id,
active: true active: true,
}, },
{ {
name: 'Home Renovation', name: 'Home Renovation',
@ -73,7 +81,7 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[4].id, area_id: areas[4].id,
active: true, 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', name: 'Fitness Challenge',
@ -81,14 +89,14 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[2].id, area_id: areas[2].id,
active: true, 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', name: 'Side Business',
description: 'Launch online consulting service', description: 'Launch online consulting service',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[1].id, area_id: areas[1].id,
active: true active: true,
}, },
{ {
name: 'Investment Portfolio', name: 'Investment Portfolio',
@ -96,7 +104,7 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[5].id, area_id: areas[5].id,
active: true, 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', name: 'Europe Trip 2024',
@ -104,14 +112,14 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[6].id, area_id: areas[6].id,
active: true, 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', name: 'Photography Mastery',
description: 'Learn advanced photography techniques', description: 'Learn advanced photography techniques',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[7].id, area_id: areas[7].id,
active: true active: true,
}, },
{ {
name: 'Professional Certification', name: 'Professional Certification',
@ -119,7 +127,7 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[9].id, area_id: areas[9].id,
active: true, 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', name: 'Garden Makeover',
@ -127,21 +135,21 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[4].id, area_id: areas[4].id,
active: true, 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', name: 'Blog Launch',
description: 'Start personal tech blog', description: 'Start personal tech blog',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[0].id, area_id: areas[0].id,
active: true active: true,
}, },
{ {
name: 'Language Learning Spanish', name: 'Language Learning Spanish',
description: 'Become conversational in Spanish', description: 'Become conversational in Spanish',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[3].id, area_id: areas[3].id,
active: false // Paused project active: false, // Paused project
}, },
{ {
name: 'Wedding Planning', name: 'Wedding Planning',
@ -149,14 +157,14 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[8].id, area_id: areas[8].id,
active: true, 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', name: 'Meal Prep System',
description: 'Establish weekly meal preparation routine', description: 'Establish weekly meal preparation routine',
user_id: testUser.id, user_id: testUser.id,
area_id: areas[2].id, area_id: areas[2].id,
active: true active: true,
}, },
{ {
name: 'Smart Home Setup', name: 'Smart Home Setup',
@ -164,8 +172,8 @@ async function seedDatabase() {
user_id: testUser.id, user_id: testUser.id,
area_id: areas[4].id, area_id: areas[4].id,
active: true, 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 // Create tags
@ -195,7 +203,7 @@ async function seedDatabase() {
{ name: 'review', user_id: testUser.id }, { name: 'review', user_id: testUser.id },
{ name: 'automation', user_id: testUser.id }, { name: 'automation', user_id: testUser.id },
{ name: 'documentation', 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 // Helper function to get random date
@ -211,14 +219,18 @@ async function seedDatabase() {
// Create tasks // Create tasks
console.log('✅ Creating massive task dataset...'); console.log('✅ Creating massive task dataset...');
const taskData = createMassiveTaskData(projects, getRandomDate, getPastDate); const taskData = createMassiveTaskData(
projects,
getRandomDate,
getPastDate
);
const tasks = []; const tasks = [];
for (const taskInfo of taskData) { for (const taskInfo of taskData) {
const task = await Task.create({ const task = await Task.create({
...taskInfo, ...taskInfo,
user_id: testUser.id, user_id: testUser.id,
note: taskInfo.note || null note: taskInfo.note || null,
}); });
tasks.push(task); tasks.push(task);
} }
@ -255,22 +267,28 @@ async function seedDatabase() {
'Plan workshop or shed organization', 'Plan workshop or shed organization',
'Research travel planning tools', 'Research travel planning tools',
'Update subscription management', 'Update subscription management',
'Plan digital decluttering project' 'Plan digital decluttering project',
]; ];
for (let i = 0; i < backlogTaskNames.length; i++) { for (let i = 0; i < backlogTaskNames.length; i++) {
const daysAgo = Math.floor(Math.random() * 120) + 31; // 31-150 days ago 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({ const backlogTask = await Task.create({
name: backlogTaskNames[i], name: backlogTaskNames[i],
priority: Math.floor(Math.random() * 3), priority: Math.floor(Math.random() * 3),
status: Math.random() < 0.9 ? 0 : 1, // 90% not started, 10% in progress status: Math.random() < 0.9 ? 0 : 1, // 90% not started, 10% in progress
user_id: testUser.id, 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, due_date: Math.random() < 0.2 ? getRandomDate(30) : null,
created_at: oldDate, created_at: oldDate,
updated_at: oldDate updated_at: oldDate,
}); });
tasks.push(backlogTask); tasks.push(backlogTask);
} }
@ -287,7 +305,7 @@ async function seedDatabase() {
'Complete expense report submission', 'Complete expense report submission',
'Follow up on pending client emails', 'Follow up on pending client emails',
'Review contract terms and conditions', 'Review contract terms and conditions',
'Update project timeline document' 'Update project timeline document',
]; ];
const today = new Date(); const today = new Date();
@ -299,10 +317,14 @@ async function seedDatabase() {
priority: Math.floor(Math.random() * 3), priority: Math.floor(Math.random() * 3),
status: Math.random() < 0.8 ? 0 : 1, // 80% not started, 20% in progress status: Math.random() < 0.8 ? 0 : 1, // 80% not started, 20% in progress
user_id: testUser.id, 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, due_date: today,
created_at: getPastDate(7), // Created within last week created_at: getPastDate(7), // Created within last week
updated_at: getPastDate(7) updated_at: getPastDate(7),
}); });
tasks.push(todayTask); tasks.push(todayTask);
} }
@ -318,7 +340,11 @@ async function seedDatabase() {
const taskTags = []; const taskTags = [];
// Pattern-based tagging for AI trigger recognition // 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 taskTags.push(tags[0]); // urgent
} }
@ -326,56 +352,113 @@ async function seedDatabase() {
taskTags.push(tags[5]); // phone-call 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 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[2]); // research
taskTags.push(tags[15]); // learning 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 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 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 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 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 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 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 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 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 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 taskTags.push(tags[6]); // online
} }
if (task.status === 4) { // waiting status if (task.status === 4) {
// waiting status
taskTags.push(tags[10]); // waiting-for taskTags.push(tags[10]); // waiting-for
} }
@ -383,31 +466,59 @@ async function seedDatabase() {
taskTags.push(tags[11]); // someday-maybe 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 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 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 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 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 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 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 taskTags.push(tags[24]); // bug-fix
} }
@ -425,43 +536,77 @@ async function seedDatabase() {
const TaskEventService = require('../services/taskEventService'); const TaskEventService = require('../services/taskEventService');
// Create events for completed tasks to show user patterns // Create events for completed tasks to show user patterns
const completedTasks = tasks.filter(t => t.status === 2); const completedTasks = tasks.filter((t) => t.status === 2);
for (const task of completedTasks.slice(0, 20)) { // Just first 20 to avoid too much data for (const task of completedTasks.slice(0, 20)) {
// Just first 20 to avoid too much data
try { try {
// Create task creation event // Create task creation event
await TaskEventService.logTaskCreated(task.id, testUser.id, { await TaskEventService.logTaskCreated(
task.id,
testUser.id,
{
name: task.name, name: task.name,
status: 0, status: 0,
priority: task.priority, priority: task.priority,
project_id: task.project_id project_id: task.project_id,
}, { source: 'web' }); },
{ source: 'web' }
);
// Create status change to in_progress // Create status change to in_progress
if (Math.random() < 0.7) { // 70% had in_progress phase if (Math.random() < 0.7) {
await TaskEventService.logStatusChange(task.id, testUser.id, 0, 1, { source: 'web' }); // 70% had in_progress phase
await TaskEventService.logStatusChange(
task.id,
testUser.id,
0,
1,
{ source: 'web' }
);
} }
// Create completion event // 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) { } 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 // 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)) { for (const task of inProgressTasks.slice(0, 10)) {
try { try {
await TaskEventService.logTaskCreated(task.id, testUser.id, { await TaskEventService.logTaskCreated(
task.id,
testUser.id,
{
name: task.name, name: task.name,
status: 0, status: 0,
priority: task.priority, priority: task.priority,
project_id: task.project_id project_id: task.project_id,
}, { source: 'web' }); },
{ 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) { } 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([ await Note.bulkCreate([
{ {
title: 'Meeting Notes - Website Redesign', 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, user_id: testUser.id,
project_id: projects[0].id project_id: projects[0].id,
}, },
{ {
title: 'React Native Learning Resources', 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, user_id: testUser.id,
project_id: projects[1].id project_id: projects[1].id,
}, },
{ {
title: 'Home Renovation Budget', 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, user_id: testUser.id,
project_id: projects[2].id project_id: projects[2].id,
}, },
{ {
title: 'Investment Strategy Notes', 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, user_id: testUser.id,
project_id: projects[5].id project_id: projects[5].id,
}, },
{ {
title: 'Europe Trip Planning', 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, user_id: testUser.id,
project_id: projects[6].id project_id: projects[6].id,
}, },
{ {
title: 'Photography Equipment Wishlist', 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, user_id: testUser.id,
project_id: projects[7].id project_id: projects[7].id,
}, },
{ {
title: 'Book Recommendations', 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', content:
user_id: testUser.id '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', 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', content:
user_id: testUser.id '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', 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, user_id: testUser.id,
project_id: projects[4].id project_id: projects[4].id,
}, },
{ {
title: 'Fitness Goals & Progress', 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, user_id: testUser.id,
project_id: projects[3].id project_id: projects[3].id,
}, },
{ {
title: 'Weekly Meal Prep Ideas', 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, user_id: testUser.id,
project_id: projects[13].id project_id: projects[13].id,
}, },
{ {
title: 'Smart Home Device List', 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, user_id: testUser.id,
project_id: projects[14].id project_id: projects[14].id,
} },
]); ]);
// Create inbox items // Create inbox items
@ -546,103 +703,103 @@ async function seedDatabase() {
{ {
content: 'Research new project management tools', content: 'Research new project management tools',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Plan team building activity for Q4', content: 'Plan team building activity for Q4',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Look into cloud storage solutions', content: 'Look into cloud storage solutions',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider learning TypeScript', content: 'Consider learning TypeScript',
user_id: testUser.id, user_id: testUser.id,
processed: true processed: true,
}, },
{ {
content: 'Update emergency contact information', content: 'Update emergency contact information',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Research sustainable investing options', content: 'Research sustainable investing options',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Look into ergonomic desk setup', content: 'Look into ergonomic desk setup',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider getting a pet', content: 'Consider getting a pet',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Research meditation retreats', content: 'Research meditation retreats',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Look into renewable energy for home', content: 'Look into renewable energy for home',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider starting a podcast', content: 'Consider starting a podcast',
user_id: testUser.id, user_id: testUser.id,
processed: true processed: true,
}, },
{ {
content: 'Research local volunteer opportunities', content: 'Research local volunteer opportunities',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Look into professional coaching', content: 'Look into professional coaching',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider learning a musical instrument', content: 'Consider learning a musical instrument',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Research minimalism lifestyle', content: 'Research minimalism lifestyle',
user_id: testUser.id, user_id: testUser.id,
processed: true processed: true,
}, },
{ {
content: 'Look into starting a garden', content: 'Look into starting a garden',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider learning sign language', content: 'Consider learning sign language',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Research passive income strategies', content: 'Research passive income strategies',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Look into digital nomad lifestyle', content: 'Look into digital nomad lifestyle',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
}, },
{ {
content: 'Consider getting professional headshots', content: 'Consider getting professional headshots',
user_id: testUser.id, user_id: testUser.id,
processed: false processed: false,
} },
]); ]);
console.log('✨ Database seeding completed successfully!'); console.log('✨ Database seeding completed successfully!');
@ -656,13 +813,14 @@ async function seedDatabase() {
- 20 inbox items`); - 20 inbox items`);
console.log('\n🚀 You can now:'); 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('- Your original account data is preserved and untouched');
console.log('- Explore the Today view with various task statuses'); console.log('- Explore the Today view with various task statuses');
console.log('- Test task editing, priority changes, etc.'); console.log('- Test task editing, priority changes, etc.');
console.log('- View projects with different completion states'); console.log('- View projects with different completion states');
console.log('- Test the task timeline feature'); console.log('- Test the task timeline feature');
} catch (error) { } catch (error) {
console.error('❌ Error seeding database:', error); console.error('❌ Error seeding database:', error);
} }
@ -672,10 +830,12 @@ module.exports = { seedDatabase };
// Allow running directly // Allow running directly
if (require.main === module) { if (require.main === module) {
seedDatabase().then(() => { seedDatabase()
.then(() => {
console.log('🏁 Seeding finished'); console.log('🏁 Seeding finished');
process.exit(0); process.exit(0);
}).catch(error => { })
.catch((error) => {
console.error('💥 Seeding failed:', error); console.error('💥 Seeding failed:', error);
process.exit(1); process.exit(1);
}); });

View file

@ -2,128 +2,600 @@
function createExpandedTaskData(projects, getRandomDate, getPastDate) { function createExpandedTaskData(projects, getRandomDate, getPastDate) {
return [ return [
// Website Redesign Project Tasks (Project 0) // 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: 'Research competitor websites',
{ name: 'Design new color palette', project_id: projects[0].id, priority: 1, status: 0 }, project_id: projects[0].id,
{ name: 'Write content for About page', project_id: projects[0].id, priority: 1, status: 0 }, priority: 1,
{ name: 'Set up staging environment', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(7) }, status: 2,
{ name: 'Optimize images for web', project_id: projects[0].id, priority: 1, status: 0 }, completed_at: getPastDate(5),
{ 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 wireframes for homepage',
{ name: 'Create contact form', project_id: projects[0].id, priority: 1, status: 0 }, project_id: projects[0].id,
{ name: 'Write SEO meta descriptions', project_id: projects[0].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Design mobile navigation', project_id: projects[0].id, priority: 2, status: 0 }, status: 1,
{ 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: '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) // 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: 'Complete React Native tutorial',
{ name: 'Learn about navigation', project_id: projects[1].id, priority: 1, status: 0 }, project_id: projects[1].id,
{ name: 'Study state management', project_id: projects[1].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Practice with APIs', project_id: projects[1].id, priority: 1, status: 0 }, status: 1,
{ 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: 'Build first mobile app',
{ name: 'Learn about app deployment', project_id: projects[1].id, priority: 1, status: 0 }, project_id: projects[1].id,
{ name: 'Practice with AsyncStorage', project_id: projects[1].id, priority: 1, status: 0 }, 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) // 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: 'Get quotes from contractors',
{ name: 'Order new appliances', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(21) }, project_id: projects[2].id,
{ name: 'Plan bathroom layout', project_id: projects[2].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Select paint colors', project_id: projects[2].id, priority: 1, status: 1 }, status: 0,
{ name: 'Research flooring options', project_id: projects[2].id, priority: 1, status: 2, completed_at: getPastDate(3) }, due_date: getRandomDate(14),
{ 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 kitchen tiles',
{ name: 'Choose lighting fixtures', project_id: projects[2].id, priority: 1, status: 0 }, 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) // 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: 'Create workout schedule',
{ name: 'Join gym membership', project_id: projects[3].id, priority: 2, status: 2, completed_at: getPastDate(15) }, project_id: projects[3].id,
{ name: 'Buy workout equipment', project_id: projects[3].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Plan meal prep schedule', project_id: projects[3].id, priority: 1, status: 1 }, status: 2,
{ name: 'Find workout buddy', project_id: projects[3].id, priority: 0, status: 0 }, completed_at: getPastDate(10),
{ 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: 'Track daily calories',
{ name: 'Plan recovery routine', project_id: projects[3].id, priority: 1, status: 0 }, 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) // 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: 'Define service offerings',
{ name: 'Set up payment processing', project_id: projects[4].id, priority: 1, status: 0 }, project_id: projects[4].id,
{ name: 'Network with potential clients', project_id: projects[4].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Register business name', project_id: projects[4].id, priority: 2, status: 2, completed_at: getPastDate(12) }, status: 1,
{ 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: 'Create business website',
{ name: 'Research competitors', project_id: projects[4].id, priority: 1, status: 1 }, project_id: projects[4].id,
{ name: 'Create pricing strategy', project_id: projects[4].id, priority: 2, status: 0 }, 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) // 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: 'Research investment platforms',
{ name: 'Study different asset classes', project_id: projects[5].id, priority: 1, status: 0 }, project_id: projects[5].id,
{ name: 'Set investment goals', project_id: projects[5].id, priority: 2, status: 2, completed_at: getPastDate(7) }, priority: 2,
{ name: 'Create risk assessment', project_id: projects[5].id, priority: 1, status: 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: 'Open brokerage account',
{ name: 'Create emergency fund', project_id: projects[5].id, priority: 2, status: 1 }, project_id: projects[5].id,
{ name: 'Review portfolio monthly', project_id: projects[5].id, priority: 1, status: 0 }, 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) // 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: 'Research destinations',
{ name: 'Reserve accommodations', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(45) }, project_id: projects[6].id,
{ name: 'Apply for passport renewal', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(60) }, priority: 2,
{ name: 'Plan itinerary', project_id: projects[6].id, priority: 1, status: 0 }, status: 1,
{ 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: 'Book flights',
{ name: 'Get travel insurance', project_id: projects[6].id, priority: 1, status: 0, due_date: getRandomDate(21) }, project_id: projects[6].id,
{ name: 'Plan budget', project_id: projects[6].id, priority: 1, status: 1 }, 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) // 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: 'Learn camera basics',
{ name: 'Study lighting techniques', project_id: projects[7].id, priority: 1, status: 0 }, project_id: projects[7].id,
{ name: 'Learn photo editing', project_id: projects[7].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Build portfolio', project_id: projects[7].id, priority: 1, status: 0 }, status: 2,
{ name: 'Join photography community', project_id: projects[7].id, priority: 0, status: 0 }, completed_at: getPastDate(14),
{ 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: 'Practice composition rules',
{ name: 'Study famous photographers', project_id: projects[7].id, priority: 0, status: 0 }, 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 // 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: 'Buy groceries for the week', priority: 0, status: 0 },
{ name: 'Clean garage', priority: 0, status: 0 }, { name: 'Clean garage', priority: 0, status: 0 },
{ name: 'Update resume', priority: 1, status: 0 }, { name: 'Update resume', priority: 1, status: 0 },
{ name: 'Read "Atomic Habits" book', priority: 0, status: 0 }, { name: 'Read "Atomic Habits" book', priority: 0, status: 0 },
{ name: 'Organize digital photos', 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: 'Plan weekend trip', priority: 0, status: 0 },
{ name: 'Learn basic Spanish', priority: 0, status: 0 }, { name: 'Learn basic Spanish', priority: 0, status: 0 },
{ name: 'Backup computer files', priority: 1, status: 0 }, { name: 'Backup computer files', priority: 1, status: 0 },
{ name: 'Donate old clothes', priority: 0, status: 0 }, { name: 'Donate old clothes', priority: 0, status: 0 },
{ name: 'Research investment options', priority: 1, 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: 'Fix leaky faucet', priority: 0, status: 0 },
{ name: 'Try new restaurant', priority: 0, status: 0 }, { name: 'Try new restaurant', priority: 0, status: 0 },
{ name: 'Update LinkedIn profile', priority: 1, 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: 'Organize desk workspace', priority: 0, status: 0 },
{ name: 'Plan birthday party', priority: 1, status: 0 }, { name: 'Plan birthday party', priority: 1, status: 0 },
{ name: 'Research new phone', priority: 0, 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: 'Update emergency contacts', priority: 1, status: 0 },
{ name: 'Clean out email inbox', priority: 0, status: 0 }, { name: 'Clean out email inbox', priority: 0, status: 0 },
{ name: 'Research vacation destinations', 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: 'Update password manager', priority: 1, status: 0 },
{ name: 'Organize physical documents', priority: 0, status: 0 }, { name: 'Organize physical documents', priority: 0, status: 0 },
{ name: 'Research new coffee maker', 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: 'Plan gift for anniversary', priority: 1, status: 0 },
{ name: 'Research home security system', priority: 0, 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: 'Learn keyboard shortcuts', priority: 0, status: 0 },
{ name: 'Research meditation apps', priority: 0, status: 0 }, { name: 'Research meditation apps', priority: 0, status: 0 },
{ name: 'Plan date night', priority: 1, status: 0 }, { name: 'Plan date night', priority: 1, status: 0 },
{ name: 'Research side income ideas', priority: 0, 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: 'Learn new cooking recipe', priority: 0, status: 0 },
{ name: 'Research productivity tools', priority: 0, status: 0 }, { name: 'Research productivity tools', priority: 0, status: 0 },
{ name: 'Plan garden for spring', 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: 'Update social media profiles', priority: 0, status: 0 },
{ name: 'Plan weekend activities', priority: 0, status: 0 }, { name: 'Plan weekend activities', priority: 0, status: 0 },
{ name: 'Research new podcast', 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: 'Learn new Excel functions', priority: 0, status: 0 },
{ name: 'Research retirement planning', priority: 1, status: 0 }, { name: 'Research retirement planning', priority: 1, status: 0 },
{ name: 'Plan family reunion', 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 }, { name: 'Plan workout routine', priority: 1, status: 0 },
// Completed tasks for metrics - spread across different dates // 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: 'Pay monthly bills',
{ name: 'Weekly team meeting', priority: 1, status: 2, completed_at: getPastDate(2) }, priority: 1,
{ name: 'Review project proposal', priority: 2, status: 2, completed_at: getPastDate(3) }, status: 2,
{ name: 'Update LinkedIn profile', priority: 0, status: 2, completed_at: getPastDate(4) }, completed_at: getPastDate(1),
{ 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: 'Submit expense reports',
{ name: 'Call insurance company', priority: 1, status: 2, completed_at: getPastDate(2) }, priority: 1,
{ name: 'Send birthday card', priority: 0, status: 2, completed_at: getPastDate(3) }, status: 2,
{ name: 'Fix printer issue', priority: 1, status: 2, completed_at: getPastDate(1) }, 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: 'Weekly team meeting',
{ name: 'Schedule vet appointment', priority: 1, status: 2, completed_at: getPastDate(2) }, priority: 1,
{ name: 'Buy gift for colleague', priority: 0, status: 2, completed_at: getPastDate(3) }, status: 2,
{ name: 'Update calendar', priority: 0, status: 2, completed_at: getPastDate(1) }, completed_at: getPastDate(2),
{ 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: '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 // Recurring tasks
{ {
@ -187,7 +779,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
due_date: new Date(), due_date: new Date(),
project_id: projects[0].id project_id: projects[0].id,
}, },
{ {
name: 'Weekly grocery shopping', name: 'Weekly grocery shopping',
@ -196,7 +788,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 6, // Saturday recurrence_weekday: 6, // Saturday
due_date: getRandomDate(7) due_date: getRandomDate(7),
}, },
{ {
name: 'Monthly budget review', name: 'Monthly budget review',
@ -205,7 +797,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_month_day: 1, recurrence_month_day: 1,
due_date: getRandomDate(30) due_date: getRandomDate(30),
}, },
{ {
name: 'Weekly meal prep', name: 'Weekly meal prep',
@ -214,7 +806,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 0, // Sunday recurrence_weekday: 0, // Sunday
due_date: getRandomDate(7) due_date: getRandomDate(7),
}, },
{ {
name: 'Daily workout', name: 'Daily workout',
@ -223,7 +815,7 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
due_date: new Date(), due_date: new Date(),
project_id: projects[3].id project_id: projects[3].id,
}, },
{ {
name: 'Weekly house cleaning', name: 'Weekly house cleaning',
@ -232,11 +824,16 @@ function createExpandedTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 6, // Saturday recurrence_weekday: 6, // Saturday
due_date: getRandomDate(7) due_date: getRandomDate(7),
}, },
// Waiting and someday tasks // 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: 'Learn advanced photography', priority: 0, status: 0 },
{ name: 'Write a book', priority: 0, status: 0 }, { name: 'Write a book', priority: 0, status: 0 },
{ name: 'Learn to play guitar', 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: 'Learn rock climbing', priority: 0, status: 0 },
{ name: 'Start a podcast', priority: 0, status: 0 }, { name: 'Start a podcast', priority: 0, status: 0 },
{ name: 'Learn wine tasting', 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', 'Implement data validation',
'Create audit logging', 'Create audit logging',
'Setup health checks', 'Setup health checks',
'Implement graceful shutdowns' 'Implement graceful shutdowns',
]; ];
// Personal development and learning tasks // Personal development and learning tasks
@ -95,7 +95,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Learn about data visualization', 'Learn about data visualization',
'Study cybersecurity fundamentals', 'Study cybersecurity fundamentals',
'Learn about scalability patterns', 'Learn about scalability patterns',
'Study database design principles' 'Study database design principles',
]; ];
// Health and fitness tasks // Health and fitness tasks
@ -139,7 +139,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Research healthy recipes', 'Research healthy recipes',
'Update meal planning app', 'Update meal planning app',
'Schedule workout with trainer', 'Schedule workout with trainer',
'Join new fitness class' 'Join new fitness class',
]; ];
// Home and family tasks // Home and family tasks
@ -183,7 +183,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Clean dryer vent', 'Clean dryer vent',
'Organize medicine cabinet', 'Organize medicine cabinet',
'Check expiration dates on medications', 'Check expiration dates on medications',
'Update emergency contact list' 'Update emergency contact list',
]; ];
// Financial and administrative tasks // Financial and administrative tasks
@ -217,7 +217,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Review online banking security', 'Review online banking security',
'Setup automatic bill pay', 'Setup automatic bill pay',
'Research high-yield savings', 'Research high-yield savings',
'Update direct deposit info' 'Update direct deposit info',
]; ];
// Social and relationship tasks // Social and relationship tasks
@ -244,14 +244,14 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Schedule catch-up with old friend', 'Schedule catch-up with old friend',
'Write recommendation letter', 'Write recommendation letter',
'Plan anniversary celebration', 'Plan anniversary celebration',
'Organize children\'s playdate', "Organize children's playdate",
'Schedule babysitter', 'Schedule babysitter',
'Plan family photo session', 'Plan family photo session',
'Organize neighborhood BBQ', 'Organize neighborhood BBQ',
'Plan holiday gathering', 'Plan holiday gathering',
'Schedule couple\'s therapy', "Schedule couple's therapy",
'Plan birthday celebration', 'Plan birthday celebration',
'Organize team building activity' 'Organize team building activity',
]; ];
// Creative and hobby tasks // Creative and hobby tasks
@ -285,7 +285,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Practice stand-up comedy', 'Practice stand-up comedy',
'Work on graphic design', 'Work on graphic design',
'Learn new language phrases', 'Learn new language phrases',
'Practice mindful writing' 'Practice mindful writing',
]; ];
// Travel and adventure tasks // Travel and adventure tasks
@ -314,7 +314,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
'Check weather forecast', 'Check weather forecast',
'Pack travel documents', 'Pack travel documents',
'Arrange airport transportation', 'Arrange airport transportation',
'Update travel blog' 'Update travel blog',
]; ];
// All task categories combined // All task categories combined
@ -326,119 +326,553 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
...financialTasks, ...financialTasks,
...socialTasks, ...socialTasks,
...creativeeTasks, ...creativeeTasks,
...travelTasks ...travelTasks,
]; ];
// Create base task data with existing project tasks // Create base task data with existing project tasks
const baseTaskData = [ const baseTaskData = [
// Website Redesign Project (triggers collaboration, urgent deadlines) // 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: 'Research competitor websites',
{ name: 'Design new color palette', project_id: projects[0].id, priority: 1, status: 0 }, project_id: projects[0].id,
{ name: 'Write content for About page', project_id: projects[0].id, priority: 1, status: 0 }, priority: 1,
{ name: 'Set up staging environment', project_id: projects[0].id, priority: 2, status: 0, due_date: getRandomDate(3) }, // Urgent deadline status: 2,
{ name: 'Optimize images for web', project_id: projects[0].id, priority: 1, status: 0 }, completed_at: getPastDate(5),
{ 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 wireframes for homepage',
{ name: 'Create contact form', project_id: projects[0].id, priority: 1, status: 0 }, project_id: projects[0].id,
{ name: 'Write SEO meta descriptions', project_id: projects[0].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Design mobile navigation', project_id: projects[0].id, priority: 2, status: 0 }, status: 1,
{ 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: '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 // 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: 'Research flight options to Paris',
{ name: 'Apply for European travel insurance', project_id: projects[6].id, priority: 2, status: 0, due_date: getRandomDate(30) }, project_id: projects[6].id,
{ name: 'Learn basic Italian phrases', project_id: projects[6].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Research train routes between cities', project_id: projects[6].id, priority: 1, status: 0 }, status: 1,
{ 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: '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 // 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: 'Track daily protein intake',
{ name: 'Plan weekly meal prep', project_id: projects[3].id, priority: 1, status: 0 }, project_id: projects[3].id,
{ name: 'Schedule body composition scan', project_id: projects[3].id, priority: 1, status: 0, due_date: getRandomDate(7) }, priority: 1,
{ name: 'Research new workout routines', project_id: projects[3].id, priority: 0, status: 0 }, status: 1,
{ name: 'Update fitness tracker goals', project_id: projects[3].id, priority: 1, status: 0 }, },
{
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 // 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: 'Research ESG investment options',
{ name: 'Review quarterly performance', project_id: projects[5].id, priority: 1, status: 1 }, project_id: projects[5].id,
{ name: 'Set up automatic dividend reinvestment', project_id: projects[5].id, priority: 1, status: 0 }, priority: 1,
{ name: 'Research international market exposure', project_id: projects[5].id, priority: 0, status: 0 }, 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 // 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: 'Create business plan document',
{ name: 'Design logo and branding', project_id: projects[4].id, priority: 1, status: 0 }, project_id: projects[4].id,
{ name: 'Setup business social media accounts', project_id: projects[4].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Register domain name', project_id: projects[4].id, priority: 2, status: 2, completed_at: getPastDate(3) }, status: 1,
{ 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: '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 // 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: 'Get electrical work permit',
{ name: 'Schedule plumbing inspection', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(14) }, project_id: projects[2].id,
{ name: 'Order kitchen countertops', project_id: projects[2].id, priority: 2, status: 0, due_date: getRandomDate(21) }, priority: 2,
{ name: 'Research energy-efficient appliances', project_id: projects[2].id, priority: 1, status: 0 }, status: 0,
{ name: 'Plan kitchen lighting layout', project_id: projects[2].id, priority: 1, 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 // 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: 'Practice portrait lighting techniques',
{ name: 'Research local photography groups', project_id: projects[7].id, priority: 0, status: 0 }, project_id: projects[7].id,
{ name: 'Plan golden hour photo session', project_id: projects[7].id, priority: 1, status: 0 }, priority: 1,
{ name: 'Learn advanced Lightroom techniques', project_id: projects[7].id, priority: 1, status: 0 }, 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 // 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: 'Install smart thermostat',
{ name: 'Setup voice assistant routines', project_id: projects[14].id, priority: 1, status: 0 }, project_id: projects[14].id,
{ name: 'Install smart door locks', project_id: projects[14].id, priority: 2, status: 0 }, priority: 2,
{ name: 'Configure automated lighting', project_id: projects[14].id, priority: 1, status: 0 }, 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 // 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: 'Write first blog post about productivity',
{ name: 'Setup email newsletter signup', project_id: projects[10].id, priority: 1, status: 0 }, project_id: projects[10].id,
{ name: 'Research SEO keywords for niche', project_id: projects[10].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Create content calendar for 3 months', project_id: projects[10].id, priority: 2, status: 0 }, 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 // 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: 'Complete AWS practice exams',
{ name: 'Review cloud architecture patterns', project_id: projects[8].id, priority: 1, status: 0 }, project_id: projects[8].id,
{ name: 'Practice hands-on labs', project_id: projects[8].id, priority: 1, status: 1 }, priority: 2,
{ name: 'Join AWS study group', project_id: projects[8].id, priority: 0, status: 0 }, 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 // 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: 'Plan balanced weekly menu',
{ name: 'Cook batch of protein sources', project_id: projects[13].id, priority: 1, status: 0 }, project_id: projects[13].id,
{ name: 'Calculate macronutrient ratios', project_id: projects[13].id, priority: 1, status: 0 }, priority: 1,
{ name: 'Research meal prep containers', project_id: projects[13].id, priority: 0, status: 0 }, 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 // 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 venue',
{ name: 'Book wedding photographer', project_id: projects[12].id, priority: 2, status: 0, due_date: getRandomDate(45) }, project_id: projects[12].id,
{ name: 'Choose wedding cake flavors', project_id: projects[12].id, priority: 1, status: 0 }, priority: 2,
{ name: 'Plan seating arrangement', project_id: projects[12].id, priority: 1, status: 0 }, status: 2,
{ name: 'Book honeymoon flights', project_id: projects[12].id, priority: 1, status: 0 }, 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 // 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: 'Plan vegetable garden layout',
{ name: 'Install drip irrigation system', project_id: projects[9].id, priority: 1, status: 0 }, project_id: projects[9].id,
{ name: 'Build raised garden beds', project_id: projects[9].id, priority: 2, status: 0 }, priority: 1,
{ name: 'Research companion planting', project_id: projects[9].id, priority: 0, status: 0 } 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 // Generate massive additional tasks
@ -446,7 +880,10 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
// Add random tasks from all categories (including old tasks for backlog) // Add random tasks from all categories (including old tasks for backlog)
for (let i = 0; i < 150; i++) { 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 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 hasDueDate = Math.random() < 0.3; // 30% chance of having a due date
const isCompleted = Math.random() < 0.08; // 8% chance of being completed const isCompleted = Math.random() < 0.08; // 8% chance of being completed
@ -455,11 +892,15 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
name: taskName, name: taskName,
priority: getRandomPriority(), priority: getRandomPriority(),
status: isCompleted ? 2 : getRandomStatus(), 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) { 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) { if (hasDueDate) {
@ -468,7 +909,9 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
task.due_date = getPastDate(Math.floor(Math.random() * 30) + 1); task.due_date = getPastDate(Math.floor(Math.random() * 30) + 1);
} else { } else {
// Future due date // 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) // Add specific AI trigger tasks (tasks that should trigger intelligent suggestions)
const aiTriggerTasks = [ const aiTriggerTasks = [
// Overdue tasks (AI should suggest prioritizing) // 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: 'Submit tax documents',
{ name: 'Pay property taxes', priority: 2, status: 0, due_date: getPastDate(10) }, priority: 2,
{ name: 'Submit insurance claim', priority: 2, status: 0, due_date: getPastDate(7) }, 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) // 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: 'Prepare presentation for CEO',
{ name: 'Complete performance review', priority: 2, status: 0, due_date: getRandomDate(3) }, 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) // Health-related tasks (AI should suggest wellness patterns)
{ name: 'Schedule annual checkup', priority: 1, status: 0 }, { name: 'Schedule annual checkup', priority: 1, status: 0 },
@ -501,7 +979,11 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
// Financial tasks (AI should suggest money management) // Financial tasks (AI should suggest money management)
{ name: 'Review investment portfolio', priority: 1, status: 0 }, { name: 'Review investment portfolio', priority: 1, status: 0 },
{ name: 'Update budget spreadsheet', 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 }, { name: 'Review insurance coverage', priority: 1, status: 0 },
// Learning tasks (AI should suggest skill development) // 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: 'Change air filter in HVAC', priority: 0, status: 0 },
{ name: 'Test smoke detector batteries', priority: 1, status: 0 }, { name: 'Test smoke detector batteries', priority: 1, status: 0 },
{ name: 'Backup computer files', 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) // Social/relationship tasks (AI should suggest work-life balance)
{ name: 'Plan anniversary dinner', priority: 1, status: 0 }, { name: 'Plan anniversary dinner', priority: 1, status: 0 },
@ -535,7 +1021,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
status: 0, status: 0,
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
due_date: new Date() due_date: new Date(),
}, },
{ {
name: 'Review daily priorities', name: 'Review daily priorities',
@ -543,7 +1029,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
status: 0, status: 0,
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
due_date: new Date() due_date: new Date(),
}, },
{ {
name: 'Log daily expenses', name: 'Log daily expenses',
@ -551,7 +1037,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
status: 0, status: 0,
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
due_date: new Date() due_date: new Date(),
}, },
// Weekly recurring tasks // Weekly recurring tasks
@ -562,7 +1048,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 0, // Sunday recurrence_weekday: 0, // Sunday
due_date: getRandomDate(7) due_date: getRandomDate(7),
}, },
{ {
name: 'Weekly house cleaning', name: 'Weekly house cleaning',
@ -571,7 +1057,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 6, // Saturday recurrence_weekday: 6, // Saturday
due_date: getRandomDate(7) due_date: getRandomDate(7),
}, },
{ {
name: 'Weekly team standup', name: 'Weekly team standup',
@ -581,7 +1067,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1, // Monday recurrence_weekday: 1, // Monday
due_date: getRandomDate(7), due_date: getRandomDate(7),
project_id: projects[0].id project_id: projects[0].id,
}, },
// Monthly recurring tasks // Monthly recurring tasks
@ -592,7 +1078,7 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_month_day: 1, recurrence_month_day: 1,
due_date: getRandomDate(30) due_date: getRandomDate(30),
}, },
{ {
name: 'Monthly backup verification', name: 'Monthly backup verification',
@ -601,27 +1087,93 @@ function createMassiveTaskData(projects, getRandomDate, getPastDate) {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_month_day: 15, recurrence_month_day: 15,
due_date: getRandomDate(30) due_date: getRandomDate(30),
}, },
// Waiting status tasks (AI should suggest follow-up actions) // 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 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 medical test results', priority: 1, status: 4 },
{ name: 'Wait for loan approval', priority: 2, status: 4 }, { name: 'Wait for loan approval', priority: 2, status: 4 },
// Recently completed tasks for learning patterns // 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 weekly workout goal',
{ name: 'Complete online course module', priority: 1, status: 2, completed_at: getPastDate(1) }, priority: 1,
{ name: 'Submit weekly report', priority: 1, status: 2, completed_at: getPastDate(1), project_id: projects[0].id }, status: 2,
{ name: 'Complete meal prep for week', priority: 1, status: 2, completed_at: getPastDate(1), project_id: projects[13].id }, completed_at: getPastDate(1),
{ name: 'Finish monthly budget', priority: 1, status: 2, completed_at: getPastDate(3) }, project_id: projects[3].id,
{ 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 reading productivity book',
{ name: 'Finish blog post draft', priority: 1, status: 2, completed_at: getPastDate(2), project_id: projects[10].id } 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 // Combine all tasks

View file

@ -5,23 +5,20 @@ const yaml = require('js-yaml');
// create default quotes // create default quotes
const createDefaultQuotes = () => [ const createDefaultQuotes = () => [
"Believe you can and you're halfway there.", "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.", "It always seems impossible until it's done.",
"Focus on progress, not perfection.", 'Focus on progress, not perfection.',
"One task at a time leads to great accomplishments." 'One task at a time leads to great accomplishments.',
]; ];
// get quotes file path // get quotes file path
const getQuotesFilePath = () => const getQuotesFilePath = () => path.join(__dirname, '../config/quotes.yml');
path.join(__dirname, '../config/quotes.yml');
// Side effect function to check if file exists // Side effect function to check if file exists
const fileExists = (filePath) => const fileExists = (filePath) => fs.existsSync(filePath);
fs.existsSync(filePath);
// Side effect function to read file contents // Side effect function to read file contents
const readFileContents = (filePath) => const readFileContents = (filePath) => fs.readFileSync(filePath, 'utf8');
fs.readFileSync(filePath, 'utf8');
// parse YAML content // parse YAML content
const parseYamlContent = (content) => { const parseYamlContent = (content) => {
@ -72,13 +69,12 @@ const loadQuotesFromFile = () => {
}; };
// get random index // get random index
const getRandomIndex = (arrayLength) => const getRandomIndex = (arrayLength) => Math.floor(Math.random() * arrayLength);
Math.floor(Math.random() * arrayLength);
// get random quote from array // get random quote from array
const getRandomQuoteFromArray = (quotes) => { const getRandomQuoteFromArray = (quotes) => {
if (quotes.length === 0) { if (quotes.length === 0) {
return "Stay focused and keep going!"; return 'Stay focused and keep going!';
} }
const randomIndex = getRandomIndex(quotes.length); const randomIndex = getRandomIndex(quotes.length);
@ -86,12 +82,10 @@ const getRandomQuoteFromArray = (quotes) => {
}; };
// get all quotes // get all quotes
const getAllQuotesFromArray = (quotes) => const getAllQuotesFromArray = (quotes) => [...quotes]; // Return copy to maintain immutability
[...quotes]; // Return copy to maintain immutability
// get quotes count // get quotes count
const getQuotesCount = (quotes) => const getQuotesCount = (quotes) => quotes.length;
quotes.length;
// Initialize quotes on module load // Initialize quotes on module load
let quotes = loadQuotesFromFile(); let quotes = loadQuotesFromFile();
@ -103,16 +97,13 @@ const reloadQuotes = () => {
}; };
// get random quote // get random quote
const getRandomQuote = () => const getRandomQuote = () => getRandomQuoteFromArray(quotes);
getRandomQuoteFromArray(quotes);
// get all quotes // get all quotes
const getAllQuotes = () => const getAllQuotes = () => getAllQuotesFromArray(quotes);
getAllQuotesFromArray(quotes);
// get count // get count
const getCount = () => const getCount = () => getQuotesCount(quotes);
getQuotesCount(quotes);
// Export functional interface // Export functional interface
module.exports = { module.exports = {
@ -127,5 +118,5 @@ module.exports = {
_validateQuotesData: validateQuotesData, _validateQuotesData: validateQuotesData,
_extractQuotes: extractQuotes, _extractQuotes: extractQuotes,
_getRandomIndex: getRandomIndex, _getRandomIndex: getRandomIndex,
_getRandomQuoteFromArray: getRandomQuoteFromArray _getRandomQuoteFromArray: getRandomQuoteFromArray,
}; };

View file

@ -5,7 +5,6 @@ const { Op } = require('sequelize');
* Service for managing recurring tasks * Service for managing recurring tasks
*/ */
class RecurringTaskService { class RecurringTaskService {
/** /**
* Generate new tasks from recurring task templates * Generate new tasks from recurring task templates
* @param {number} userId - Optional user ID to limit processing * @param {number} userId - Optional user ID to limit processing
@ -15,7 +14,7 @@ class RecurringTaskService {
try { try {
const whereClause = { const whereClause = {
recurrence_type: { [Op.ne]: 'none' }, recurrence_type: { [Op.ne]: 'none' },
status: { [Op.ne]: Task.STATUS.ARCHIVED } status: { [Op.ne]: Task.STATUS.ARCHIVED },
}; };
if (userId) { if (userId) {
@ -25,14 +24,17 @@ class RecurringTaskService {
// Find all recurring tasks that need processing // Find all recurring tasks that need processing
const recurringTasks = await Task.findAll({ const recurringTasks = await Task.findAll({
where: whereClause, where: whereClause,
order: [['last_generated_date', 'ASC']] order: [['last_generated_date', 'ASC']],
}); });
const newTasks = []; const newTasks = [];
const now = new Date(); const now = new Date();
for (const task of recurringTasks) { for (const task of recurringTasks) {
const generatedTasks = await this.processRecurringTask(task, now); const generatedTasks = await this.processRecurringTask(
task,
now
);
newTasks.push(...generatedTasks); newTasks.push(...generatedTasks);
} }
@ -67,12 +69,15 @@ class RecurringTaskService {
user_id: task.user_id, user_id: task.user_id,
name: task.name, name: task.name,
due_date: nextDueDate, due_date: nextDueDate,
project_id: task.project_id project_id: task.project_id,
} },
}); });
if (!existingTask) { if (!existingTask) {
const newTask = await this.createTaskInstance(task, nextDueDate); const newTask = await this.createTaskInstance(
task,
nextDueDate
);
newTasks.push(newTask); newTasks.push(newTask);
} }
@ -85,7 +90,9 @@ class RecurringTaskService {
// Safety check to prevent infinite loops // Safety check to prevent infinite loops
if (newTasks.length > 100) { 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; break;
} }
} }
@ -111,7 +118,7 @@ class RecurringTaskService {
user_id: template.user_id, user_id: template.user_id,
project_id: template.project_id, project_id: template.project_id,
recurrence_type: 'none', // Instances are not recurring themselves 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); return await Task.create(taskData);
@ -125,28 +132,44 @@ class RecurringTaskService {
*/ */
static calculateNextDueDate(task, fromDate) { static calculateNextDueDate(task, fromDate) {
// Handle invalid inputs // Handle invalid inputs
if (!task || !task.recurrence_type || !fromDate || isNaN(fromDate.getTime())) { if (
!task ||
!task.recurrence_type ||
!fromDate ||
isNaN(fromDate.getTime())
) {
return null; return null;
} }
const baseDate = task.completion_based ? const baseDate = task.completion_based
(task.last_generated_date || task.created_at) : ? task.last_generated_date || task.created_at
(task.due_date || task.created_at); : task.due_date || task.created_at;
// If no base date is available, use fromDate // If no base date is available, use fromDate
const startDate = baseDate ? const startDate = baseDate
new Date(Math.max(fromDate.getTime(), baseDate.getTime())) : ? new Date(Math.max(fromDate.getTime(), baseDate.getTime()))
new Date(fromDate.getTime()); : new Date(fromDate.getTime());
switch (task.recurrence_type) { switch (task.recurrence_type) {
case 'daily': case 'daily':
return this.calculateDailyRecurrence(startDate, task.recurrence_interval || 1); return this.calculateDailyRecurrence(
startDate,
task.recurrence_interval || 1
);
case 'weekly': 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': 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': case 'monthly_weekday':
return this.calculateMonthlyWeekdayRecurrence( return this.calculateMonthlyWeekdayRecurrence(
@ -157,7 +180,10 @@ class RecurringTaskService {
); );
case 'monthly_last_day': case 'monthly_last_day':
return this.calculateMonthlyLastDayRecurrence(startDate, task.recurrence_interval || 1); return this.calculateMonthlyLastDayRecurrence(
startDate,
task.recurrence_interval || 1
);
default: default:
return null; return null;
@ -191,18 +217,21 @@ class RecurringTaskService {
const currentWeekday = nextDate.getDay(); const currentWeekday = nextDate.getDay();
const daysUntilTarget = (weekday - currentWeekday + 7) % 7; 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 // 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 { } else {
nextDate.setDate(nextDate.getDate() + daysUntilTarget); nextDate.setDate(nextDate.getDate() + daysUntilTarget);
if (nextDate <= fromDate) { if (nextDate <= fromDate) {
nextDate.setDate(nextDate.getDate() + (interval * 7)); nextDate.setDate(nextDate.getDate() + interval * 7);
} }
} }
} else { } else {
// No specific weekday, just add interval weeks // No specific weekday, just add interval weeks
nextDate.setDate(nextDate.getDate() + (interval * 7)); nextDate.setDate(nextDate.getDate() + interval * 7);
} }
return nextDate; return nextDate;
@ -221,15 +250,19 @@ class RecurringTaskService {
// Move to target month // Move to target month
const targetMonth = nextDate.getUTCMonth() + interval; 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; const finalMonth = targetMonth % 12;
// Get the max day for the target month // 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); const finalDay = Math.min(targetDay, maxDay);
// Create the new date // Create the new date
const result = new Date(Date.UTC( const result = new Date(
Date.UTC(
targetYear, targetYear,
finalMonth, finalMonth,
finalDay, finalDay,
@ -237,7 +270,8 @@ class RecurringTaskService {
fromDate.getUTCMinutes(), fromDate.getUTCMinutes(),
fromDate.getUTCSeconds(), fromDate.getUTCSeconds(),
fromDate.getUTCMilliseconds() fromDate.getUTCMilliseconds()
)); )
);
return result; return result;
} }
@ -250,12 +284,19 @@ class RecurringTaskService {
* @param {number} weekOfMonth - Which occurrence in month (1-5) * @param {number} weekOfMonth - Which occurrence in month (1-5)
* @returns {Date} Next due date * @returns {Date} Next due date
*/ */
static calculateMonthlyWeekdayRecurrence(fromDate, interval, weekday, weekOfMonth) { static calculateMonthlyWeekdayRecurrence(
fromDate,
interval,
weekday,
weekOfMonth
) {
const nextDate = new Date(fromDate); const nextDate = new Date(fromDate);
nextDate.setUTCMonth(nextDate.getUTCMonth() + interval); nextDate.setUTCMonth(nextDate.getUTCMonth() + interval);
// Find the first day of the month // 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(); const firstWeekday = firstOfMonth.getUTCDay();
// Calculate the first occurrence of the target weekday // Calculate the first occurrence of the target weekday
@ -265,7 +306,9 @@ class RecurringTaskService {
// Add weeks to get to the target week of month // Add weeks to get to the target week of month
const targetDate = new Date(firstOccurrence); 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 // Make sure we're still in the same month
if (targetDate.getUTCMonth() !== nextDate.getUTCMonth()) { if (targetDate.getUTCMonth() !== nextDate.getUTCMonth()) {
@ -274,7 +317,12 @@ class RecurringTaskService {
} }
// Preserve the original time // 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; return targetDate;
} }
@ -332,9 +380,13 @@ class RecurringTaskService {
* @returns {Date} Nth occurrence of weekday in month * @returns {Date} Nth occurrence of weekday in month
*/ */
static _getNthWeekdayOfMonth(year, month, weekday, n) { 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); 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 target date is in next month, return null
if (targetDate.getMonth() !== month) { if (targetDate.getMonth() !== month) {
@ -388,7 +440,7 @@ class RecurringTaskService {
const whereClause = { const whereClause = {
user_id: task.user_id, user_id: task.user_id,
name: task.name, name: task.name,
due_date: nextDueDate due_date: nextDueDate,
}; };
// Only add project_id to where clause if it's not null/undefined // Only add project_id to where clause if it's not null/undefined
@ -399,7 +451,7 @@ class RecurringTaskService {
} }
const existingTask = await Task.findOne({ const existingTask = await Task.findOne({
where: whereClause where: whereClause,
}); });
if (existingTask) { if (existingTask) {

View file

@ -12,7 +12,15 @@ class TaskEventService {
* @param {any} eventData.newValue - New value (optional) * @param {any} eventData.newValue - New value (optional)
* @param {Object} eventData.metadata - Additional metadata (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 { try {
// Add source to metadata if not provided // Add source to metadata if not provided
if (!metadata.source) { if (!metadata.source) {
@ -24,9 +32,13 @@ class TaskEventService {
user_id: userId, user_id: userId,
event_type: eventType, event_type: eventType,
field_name: fieldName, field_name: fieldName,
old_value: oldValue ? { [fieldName || 'value']: oldValue } : null, old_value: oldValue
new_value: newValue ? { [fieldName || 'value']: newValue } : null, ? { [fieldName || 'value']: oldValue }
metadata: metadata : null,
new_value: newValue
? { [fieldName || 'value']: newValue }
: null,
metadata: metadata,
}); });
return event; return event;
@ -45,17 +57,26 @@ class TaskEventService {
userId, userId,
eventType: 'created', eventType: 'created',
newValue: taskData, newValue: taskData,
metadata: { ...metadata, action: 'task_created' } metadata: { ...metadata, action: 'task_created' },
}); });
} }
/** /**
* Log status change event * Log status change event
*/ */
static async logStatusChange(taskId, userId, oldStatus, newStatus, metadata = {}) { static async logStatusChange(
const eventType = newStatus === 2 ? 'completed' : taskId,
newStatus === 3 ? 'archived' : userId,
'status_changed'; oldStatus,
newStatus,
metadata = {}
) {
const eventType =
newStatus === 2
? 'completed'
: newStatus === 3
? 'archived'
: 'status_changed';
return await this.logEvent({ return await this.logEvent({
taskId, taskId,
@ -64,14 +85,20 @@ class TaskEventService {
fieldName: 'status', fieldName: 'status',
oldValue: oldStatus, oldValue: oldStatus,
newValue: newStatus, newValue: newStatus,
metadata: { ...metadata, action: 'status_change' } metadata: { ...metadata, action: 'status_change' },
}); });
} }
/** /**
* Log priority change event * Log priority change event
*/ */
static async logPriorityChange(taskId, userId, oldPriority, newPriority, metadata = {}) { static async logPriorityChange(
taskId,
userId,
oldPriority,
newPriority,
metadata = {}
) {
return await this.logEvent({ return await this.logEvent({
taskId, taskId,
userId, userId,
@ -79,14 +106,20 @@ class TaskEventService {
fieldName: 'priority', fieldName: 'priority',
oldValue: oldPriority, oldValue: oldPriority,
newValue: newPriority, newValue: newPriority,
metadata: { ...metadata, action: 'priority_change' } metadata: { ...metadata, action: 'priority_change' },
}); });
} }
/** /**
* Log due date change event * 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({ return await this.logEvent({
taskId, taskId,
userId, userId,
@ -94,14 +127,20 @@ class TaskEventService {
fieldName: 'due_date', fieldName: 'due_date',
oldValue: oldDueDate, oldValue: oldDueDate,
newValue: newDueDate, newValue: newDueDate,
metadata: { ...metadata, action: 'due_date_change' } metadata: { ...metadata, action: 'due_date_change' },
}); });
} }
/** /**
* Log project change event * Log project change event
*/ */
static async logProjectChange(taskId, userId, oldProjectId, newProjectId, metadata = {}) { static async logProjectChange(
taskId,
userId,
oldProjectId,
newProjectId,
metadata = {}
) {
return await this.logEvent({ return await this.logEvent({
taskId, taskId,
userId, userId,
@ -109,14 +148,20 @@ class TaskEventService {
fieldName: 'project_id', fieldName: 'project_id',
oldValue: oldProjectId, oldValue: oldProjectId,
newValue: newProjectId, newValue: newProjectId,
metadata: { ...metadata, action: 'project_change' } metadata: { ...metadata, action: 'project_change' },
}); });
} }
/** /**
* Log task name change event * 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({ return await this.logEvent({
taskId, taskId,
userId, userId,
@ -124,14 +169,20 @@ class TaskEventService {
fieldName: 'name', fieldName: 'name',
oldValue: oldName, oldValue: oldName,
newValue: newName, newValue: newName,
metadata: { ...metadata, action: 'name_change' } metadata: { ...metadata, action: 'name_change' },
}); });
} }
/** /**
* Log description change event * Log description change event
*/ */
static async logDescriptionChange(taskId, userId, oldDescription, newDescription, metadata = {}) { static async logDescriptionChange(
taskId,
userId,
oldDescription,
newDescription,
metadata = {}
) {
return await this.logEvent({ return await this.logEvent({
taskId, taskId,
userId, userId,
@ -139,7 +190,7 @@ class TaskEventService {
fieldName: 'description', fieldName: 'description',
oldValue: oldDescription, oldValue: oldDescription,
newValue: newDescription, 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 = {}) { static async logTaskUpdate(taskId, userId, changes, metadata = {}) {
const events = []; 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 // Skip if values are the same
if (oldValue === newValue) continue; if (oldValue === newValue) continue;
let eventType; let eventType;
switch (fieldName) { switch (fieldName) {
case 'status': case 'status':
eventType = newValue === 2 ? 'completed' : eventType =
newValue === 3 ? 'archived' : newValue === 2
'status_changed'; ? 'completed'
: newValue === 3
? 'archived'
: 'status_changed';
break; break;
default: default:
eventType = `${fieldName}_changed`; eventType = `${fieldName}_changed`;
@ -171,7 +227,7 @@ class TaskEventService {
fieldName, fieldName,
oldValue, oldValue,
newValue, newValue,
metadata: { ...metadata, action: 'bulk_update' } metadata: { ...metadata, action: 'bulk_update' },
}); });
events.push(event); events.push(event);
@ -187,11 +243,13 @@ class TaskEventService {
return await TaskEvent.findAll({ return await TaskEvent.findAll({
where: { task_id: taskId }, where: { task_id: taskId },
order: [['created_at', 'ASC']], order: [['created_at', 'ASC']],
include: [{ include: [
{
model: require('../models').User, model: require('../models').User,
as: 'User', as: 'User',
attributes: ['id', 'name', 'email'] attributes: ['id', 'name', 'email'],
}] },
],
}); });
} }
@ -202,21 +260,23 @@ class TaskEventService {
const events = await TaskEvent.findAll({ const events = await TaskEvent.findAll({
where: { where: {
task_id: taskId, 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; if (events.length === 0) return null;
// Find when task was started (moved to in_progress or created) // 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 === 'created' ||
(e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress (e.event_type === 'status_changed' && e.new_value?.status === 1) // in_progress
); );
// Find when task was completed // Find when task was completed
const completedEvent = events.find(e => const completedEvent = events.find(
(e) =>
e.event_type === 'completed' || e.event_type === 'completed' ||
(e.event_type === 'status_changed' && e.new_value?.status === 2) // done (e.event_type === 'status_changed' && e.new_value?.status === 2) // done
); );
@ -232,51 +292,67 @@ class TaskEventService {
completed_at: endTime, completed_at: endTime,
duration_ms: endTime - startTime, duration_ms: endTime - startTime,
duration_hours: (endTime - startTime) / (1000 * 60 * 60), 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 * 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 }; const whereClause = { user_id: userId };
if (startDate && endDate) { if (startDate && endDate) {
whereClause.created_at = { whereClause.created_at = {
[require('sequelize').Op.between]: [startDate, endDate] [require('sequelize').Op.between]: [startDate, endDate],
}; };
} }
const events = await TaskEvent.findAll({ const events = await TaskEvent.findAll({
where: whereClause, where: whereClause,
order: [['created_at', 'ASC']] order: [['created_at', 'ASC']],
}); });
// Calculate metrics // Calculate metrics
const metrics = { const metrics = {
total_events: events.length, total_events: events.length,
tasks_created: events.filter(e => e.event_type === 'created').length, tasks_created: events.filter((e) => e.event_type === 'created')
tasks_completed: events.filter(e => e.event_type === 'completed').length, .length,
status_changes: events.filter(e => e.event_type === 'status_changed').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, average_completion_time: null,
completion_times: [] completion_times: [],
}; };
// Calculate completion times for all completed tasks // 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 = []; const completionTimes = [];
for (const completedEvent of completedTasks) { for (const completedEvent of completedTasks) {
const taskCompletion = await this.getTaskCompletionTime(completedEvent.task_id); const taskCompletion = await this.getTaskCompletionTime(
completedEvent.task_id
);
if (taskCompletion) { if (taskCompletion) {
completionTimes.push(taskCompletion); completionTimes.push(taskCompletion);
} }
} }
if (completionTimes.length > 0) { if (completionTimes.length > 0) {
const totalHours = completionTimes.reduce((sum, ct) => sum + ct.duration_hours, 0); const totalHours = completionTimes.reduce(
metrics.average_completion_time = totalHours / completionTimes.length; (sum, ct) => sum + ct.duration_hours,
0
);
metrics.average_completion_time =
totalHours / completionTimes.length;
metrics.completion_times = completionTimes; metrics.completion_times = completionTimes;
} }
@ -291,16 +367,28 @@ class TaskEventService {
where: { where: {
user_id: userId, user_id: userId,
created_at: { created_at: {
[require('sequelize').Op.between]: [startDate, endDate] [require('sequelize').Op.between]: [startDate, endDate],
} },
}, },
attributes: [ attributes: [
'event_type', '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'], group: ['event_type', 'date'],
order: [['date', 'ASC']] order: [['date', 'ASC']],
}); });
return events; return events;
@ -317,9 +405,9 @@ class TaskEventService {
task_id: taskId, task_id: taskId,
event_type: 'today_changed', event_type: 'today_changed',
new_value: { new_value: {
[Op.like]: '%"today":true%' [Op.like]: '%"today":true%',
} },
} },
}); });
return count; return count;

View file

@ -6,7 +6,7 @@ const RecurringTaskService = require('./recurringTaskService');
// Create scheduler state // Create scheduler state
const createSchedulerState = () => ({ const createSchedulerState = () => ({
jobs: new Map(), jobs: new Map(),
isInitialized: false isInitialized: false,
}); });
// Global mutable state (will be managed functionally) // Global mutable state (will be managed functionally)
@ -19,7 +19,7 @@ const shouldDisableScheduler = () =>
// Create job configuration // Create job configuration
const createJobConfig = () => ({ const createJobConfig = () => ({
scheduled: false, scheduled: false,
timezone: 'UTC' timezone: 'UTC',
}); });
// Create cron expressions // Create cron expressions
@ -33,7 +33,7 @@ const getCronExpression = (frequency) => {
'4h': '0 */4 * * *', '4h': '0 */4 * * *',
'8h': '0 */8 * * *', '8h': '0 */8 * * *',
'12h': '0 */12 * * *', '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]; return expressions[frequency];
}; };
@ -49,9 +49,19 @@ const createJobHandler = (frequency) => async () => {
// Create job entries // Create job entries
const createJobEntries = () => { 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 cronExpression = getCronExpression(frequency);
const jobHandler = createJobHandler(frequency); const jobHandler = createJobHandler(frequency);
const jobConfig = createJobConfig(); const jobConfig = createJobConfig();
@ -82,8 +92,8 @@ const fetchUsersForFrequency = async (frequency) => {
telegram_bot_token: { [require('sequelize').Op.ne]: null }, telegram_bot_token: { [require('sequelize').Op.ne]: null },
telegram_chat_id: { [require('sequelize').Op.ne]: null }, telegram_chat_id: { [require('sequelize').Op.ne]: null },
task_summary_enabled: true, 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 users = await fetchUsersForFrequency(frequency);
const results = await Promise.allSettled( const results = await Promise.allSettled(
users.map(user => sendSummaryToUser(user.id, frequency)) users.map((user) => sendSummaryToUser(user.id, frequency))
); );
return results; return results;
@ -142,7 +152,7 @@ const initialize = async () => {
// Update state immutably // Update state immutably
schedulerState = { schedulerState = {
jobs, jobs,
isInitialized: true isInitialized: true,
}; };
return schedulerState; return schedulerState;
@ -173,7 +183,7 @@ const restart = async () => {
const getStatus = () => ({ const getStatus = () => ({
initialized: schedulerState.isInitialized, initialized: schedulerState.isInitialized,
jobCount: schedulerState.jobs.size, jobCount: schedulerState.jobs.size,
jobs: Array.from(schedulerState.jobs.keys()) jobs: Array.from(schedulerState.jobs.keys()),
}); });
// Export functional interface // Export functional interface
@ -187,5 +197,5 @@ module.exports = {
// For testing // For testing
_createSchedulerState: createSchedulerState, _createSchedulerState: createSchedulerState,
_shouldDisableScheduler: shouldDisableScheduler, _shouldDisableScheduler: shouldDisableScheduler,
_getCronExpression: getCronExpression _getCronExpression: getCronExpression,
}; };

View file

@ -14,7 +14,7 @@ const getPriorityEmoji = (priority) => {
const emojiMap = { const emojiMap = {
2: '🔴', // high 2: '🔴', // high
1: '🟠', // medium 1: '🟠', // medium
0: '🟢' // low 0: '🟢', // low
}; };
return emojiMap[priority] || '⚪'; return emojiMap[priority] || '⚪';
}; };
@ -33,7 +33,9 @@ const formatTaskForDisplay = (task, index, includeStatus = false) => {
const priorityEmoji = getPriorityEmoji(task.priority); const priorityEmoji = getPriorityEmoji(task.priority);
const statusEmoji = includeStatus ? '✅ ' : ''; const statusEmoji = includeStatus ? '✅ ' : '';
const taskName = escapeMarkdown(task.name); 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`; return `${index + 1}\\. ${statusEmoji}${priorityEmoji} ${taskName}${projectInfo}\n`;
}; };
@ -42,9 +44,9 @@ const buildTaskSection = (tasks, title, includeStatus = false) => {
if (tasks.length === 0) return ''; if (tasks.length === 0) return '';
let section = `${title}\n`; let section = `${title}\n`;
section += tasks.map((task, index) => section += tasks
formatTaskForDisplay(task, index, includeStatus) .map((task, index) => formatTaskForDisplay(task, index, includeStatus))
).join(''); .join('');
section += '\n'; section += '\n';
return section; return section;
@ -53,7 +55,7 @@ const buildTaskSection = (tasks, title, includeStatus = false) => {
// build summary message // build summary message
const buildSummaryMessage = (taskSections) => { const buildSummaryMessage = (taskSections) => {
let message = "📋 *Today's Task Summary*\n\n"; let message = "📋 *Today's Task Summary*\n\n";
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n\n"; message += '━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
message += "✏️ *Today's Plan*\n\n"; message += "✏️ *Today's Plan*\n\n";
message += taskSections.dueToday; message += taskSections.dueToday;
@ -61,8 +63,8 @@ const buildSummaryMessage = (taskSections) => {
message += taskSections.suggested; message += taskSections.suggested;
message += taskSections.completed; message += taskSections.completed;
message += "━━━━━━━━━━━━━━━━━━━━━━━━\n"; message += '━━━━━━━━━━━━━━━━━━━━━━━━\n';
message += "🎯 *Stay focused and make it happen\\!*"; message += '🎯 *Stay focused and make it happen\\!*';
return message; return message;
}; };
@ -83,9 +85,11 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
weekdays: () => { weekdays: () => {
const currentDay = from.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday const currentDay = from.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
let daysToAdd = 1; let daysToAdd = 1;
if (currentDay === 5) { // Friday if (currentDay === 5) {
// Friday
daysToAdd = 3; // Skip to Monday daysToAdd = 3; // Skip to Monday
} else if (currentDay === 6) { // Saturday } else if (currentDay === 6) {
// Saturday
daysToAdd = 2; // Skip to Monday daysToAdd = 2; // Skip to Monday
} }
const nextWeekday = new Date(from); const nextWeekday = new Date(from);
@ -129,7 +133,7 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
const next = new Date(from); const next = new Date(from);
next.setHours(next.getHours() + 12); next.setHours(next.getHours() + 12);
return next; return next;
} },
}; };
const calculator = calculations[frequency]; const calculator = calculations[frequency];
@ -137,8 +141,7 @@ const calculateNextRunTime = (user, fromTime = new Date()) => {
}; };
// Side effect function to fetch user by ID // Side effect function to fetch user by ID
const fetchUser = async (userId) => const fetchUser = async (userId) => await User.findByPk(userId);
await User.findByPk(userId);
// Side effect function to fetch due today tasks // Side effect function to fetch due today tasks
const fetchDueTodayTasks = async (userId, today, tomorrow) => const fetchDueTodayTasks = async (userId, today, tomorrow) =>
@ -147,12 +150,12 @@ const fetchDueTodayTasks = async (userId, today, tomorrow) =>
user_id: userId, user_id: userId,
due_date: { due_date: {
[Op.gte]: today, [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'] }], include: [{ model: Project, attributes: ['name'] }],
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
// Side effect function to fetch in progress tasks // Side effect function to fetch in progress tasks
@ -160,10 +163,10 @@ const fetchInProgressTasks = async (userId) =>
await Task.findAll({ await Task.findAll({
where: { where: {
user_id: userId, user_id: userId,
status: 1 // in_progress status: 1, // in_progress
}, },
include: [{ model: Project, attributes: ['name'] }], include: [{ model: Project, attributes: ['name'] }],
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
// Side effect function to fetch completed today tasks // Side effect function to fetch completed today tasks
@ -174,11 +177,11 @@ const fetchCompletedTodayTasks = async (userId, today, tomorrow) =>
status: 2, // done status: 2, // done
updated_at: { updated_at: {
[Op.gte]: today, [Op.gte]: today,
[Op.lt]: tomorrow [Op.lt]: tomorrow,
} },
}, },
include: [{ model: Project, attributes: ['name'] }], include: [{ model: Project, attributes: ['name'] }],
order: [['name', 'ASC']] order: [['name', 'ASC']],
}); });
// Side effect function to fetch suggested tasks // Side effect function to fetch suggested tasks
@ -187,11 +190,14 @@ const fetchSuggestedTasks = async (userId, excludedIds) =>
where: { where: {
user_id: userId, user_id: userId,
status: { [Op.ne]: 2 }, // not done status: { [Op.ne]: 2 }, // not done
id: { [Op.notIn]: excludedIds } id: { [Op.notIn]: excludedIds },
}, },
include: [{ model: Project, attributes: ['name'] }], include: [{ model: Project, attributes: ['name'] }],
order: [['priority', 'DESC'], ['name', 'ASC']], order: [
limit: 5 ['priority', 'DESC'],
['name', 'ASC'],
],
limit: 5,
}); });
// Side effect function to send telegram message // Side effect function to send telegram message
@ -204,7 +210,7 @@ const sendTelegramMessage = async (token, chatId, message) => {
const updateUserTracking = async (user, lastRun, nextRun) => const updateUserTracking = async (user, lastRun, nextRun) =>
await user.update({ await user.update({
task_summary_last_run: lastRun, task_summary_last_run: lastRun,
task_summary_next_run: nextRun task_summary_next_run: nextRun,
}); });
// Function to generate summary for user (contains side effects) // Function to generate summary for user (contains side effects)
@ -219,19 +225,29 @@ const generateSummaryForUser = async (userId) => {
const [dueToday, inProgress, completedToday] = await Promise.all([ const [dueToday, inProgress, completedToday] = await Promise.all([
fetchDueTodayTasks(userId, today, tomorrow), fetchDueTodayTasks(userId, today, tomorrow),
fetchInProgressTasks(userId), fetchInProgressTasks(userId),
fetchCompletedTodayTasks(userId, today, tomorrow) fetchCompletedTodayTasks(userId, today, tomorrow),
]); ]);
// Get suggested tasks (excluding already fetched ones) // 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); const suggestedTasks = await fetchSuggestedTasks(userId, excludedIds);
// Build task sections // Build task sections
const taskSections = { const taskSections = {
dueToday: buildTaskSection(dueToday, "🚀 *Tasks Due Today:*"), dueToday: buildTaskSection(dueToday, '🚀 *Tasks Due Today:*'),
inProgress: buildTaskSection(inProgress, "⚙️ *In Progress Tasks:*"), inProgress: buildTaskSection(inProgress, '⚙️ *In Progress Tasks:*'),
suggested: buildTaskSection(suggestedTasks, "💡 *Suggested Tasks:*"), suggested: buildTaskSection(
completed: buildTaskSection(completedToday, "✅ *Completed Today:*", true) suggestedTasks,
'💡 *Suggested Tasks:*'
),
completed: buildTaskSection(
completedToday,
'✅ *Completed Today:*',
true
),
}; };
return buildSummaryMessage(taskSections); return buildSummaryMessage(taskSections);
@ -266,7 +282,10 @@ const sendSummaryToUser = async (userId) => {
return true; return true;
} catch (error) { } 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; return false;
} }
}; };
@ -282,5 +301,5 @@ module.exports = {
_createTodayDateRange: createTodayDateRange, _createTodayDateRange: createTodayDateRange,
_formatTaskForDisplay: formatTaskForDisplay, _formatTaskForDisplay: formatTaskForDisplay,
_buildTaskSection: buildTaskSection, _buildTaskSection: buildTaskSection,
_buildSummaryMessage: buildSummaryMessage _buildSummaryMessage: buildSummaryMessage,
}; };

View file

@ -2,7 +2,10 @@ const telegramPoller = require('./telegramPoller');
const { User } = require('../models'); const { User } = require('../models');
async function initializeTelegramPolling() { 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; return;
} }
@ -11,9 +14,9 @@ async function initializeTelegramPolling() {
const usersWithTelegram = await User.findAll({ const usersWithTelegram = await User.findAll({
where: { where: {
telegram_bot_token: { telegram_bot_token: {
[require('sequelize').Op.ne]: null [require('sequelize').Op.ne]: null,
} },
} },
}); });
if (usersWithTelegram.length > 0) { if (usersWithTelegram.length > 0) {

View file

@ -8,15 +8,14 @@ const createPollerState = () => ({
pollInterval: 5000, // 5 seconds pollInterval: 5000, // 5 seconds
usersToPool: [], usersToPool: [],
userStatus: {}, 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) // Global mutable state (managed functionally)
let pollerState = createPollerState(); let pollerState = createPollerState();
// Check if user exists in list // Check if user exists in list
const userExistsInList = (users, userId) => const userExistsInList = (users, userId) => users.some((u) => u.id === userId);
users.some(u => u.id === userId);
// Add user to list // Add user to list
const addUserToList = (users, user) => { const addUserToList = (users, user) => {
@ -28,7 +27,7 @@ const addUserToList = (users, user) => {
// Remove user from list // Remove user from list
const removeUserFromList = (users, userId) => const removeUserFromList = (users, userId) =>
users.filter(u => u.id !== userId); users.filter((u) => u.id !== userId);
// Remove user status // Remove user status
const removeUserStatus = (userStatus, userId) => { const removeUserStatus = (userStatus, userId) => {
@ -41,14 +40,14 @@ const updateUserStatus = (userStatus, userId, updates) => ({
...userStatus, ...userStatus,
[userId]: { [userId]: {
...userStatus[userId], ...userStatus[userId],
...updates ...updates,
} },
}); });
// Get highest update ID from updates // Get highest update ID from updates
const getHighestUpdateId = (updates) => { const getHighestUpdateId = (updates) => {
if (!updates.length) return 0; 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 // Create message parameters
@ -72,7 +71,8 @@ const createTelegramUrl = (token, endpoint, params = {}) => {
// Side effect function to make HTTP GET request // Side effect function to make HTTP GET request
const makeHttpGetRequest = (url, timeout = 5000) => { const makeHttpGetRequest = (url, timeout = 5000) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
https.get(url, { timeout }, (res) => { https
.get(url, { timeout }, (res) => {
let data = ''; let data = '';
res.on('data', (chunk) => { res.on('data', (chunk) => {
@ -87,9 +87,11 @@ const makeHttpGetRequest = (url, timeout = 5000) => {
reject(error); reject(error);
} }
}); });
}).on('error', (error) => { })
.on('error', (error) => {
reject(error); reject(error);
}).on('timeout', () => { })
.on('timeout', () => {
reject(new Error('Request timeout')); reject(new Error('Request timeout'));
}); });
}); });
@ -100,7 +102,7 @@ const makeHttpPostRequest = (url, postData, options) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const req = https.request(url, options, (res) => { const req = https.request(url, options, (res) => {
let data = ''; let data = '';
res.on('data', (chunk) => data += chunk); res.on('data', (chunk) => (data += chunk));
res.on('end', () => { res.on('end', () => {
try { try {
const response = JSON.parse(data); const response = JSON.parse(data);
@ -122,7 +124,7 @@ const getTelegramUpdates = async (token, offset) => {
try { try {
const url = createTelegramUrl(token, 'getUpdates', { const url = createTelegramUrl(token, 'getUpdates', {
offset: offset.toString(), offset: offset.toString(),
timeout: '1' timeout: '1',
}); });
const response = await makeHttpGetRequest(url, 5000); const response = await makeHttpGetRequest(url, 5000);
@ -138,9 +140,18 @@ const getTelegramUpdates = async (token, offset) => {
}; };
// Side effect function to send Telegram message // Side effect function to send Telegram message
const sendTelegramMessage = async (token, chatId, text, replyToMessageId = null) => { const sendTelegramMessage = async (
token,
chatId,
text,
replyToMessageId = null
) => {
try { try {
const messageParams = createMessageParams(chatId, text, replyToMessageId); const messageParams = createMessageParams(
chatId,
text,
replyToMessageId
);
const postData = JSON.stringify(messageParams); const postData = JSON.stringify(messageParams);
const url = createTelegramUrl(token, 'sendMessage'); const url = createTelegramUrl(token, 'sendMessage');
@ -148,8 +159,8 @@ const sendTelegramMessage = async (token, chatId, text, replyToMessageId = null)
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData) 'Content-Length': Buffer.byteLength(postData),
} },
}; };
return await makeHttpPostRequest(url, postData, options); 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 // Side effect function to update user chat ID
const updateUserChatId = async (userId, chatId) => { const updateUserChatId = async (userId, chatId) => {
await User.update( await User.update({ telegram_chat_id: chatId }, { where: { id: userId } });
{ telegram_chat_id: chatId },
{ where: { id: userId } }
);
}; };
// Side effect function to create inbox item // Side effect function to create inbox item
@ -178,13 +186,15 @@ const createInboxItem = async (content, userId, messageId) => {
user_id: userId, user_id: userId,
source: 'telegram', source: 'telegram',
created_at: { created_at: {
[require('sequelize').Op.gte]: recentCutoff [require('sequelize').Op.gte]: recentCutoff,
} },
} },
}); });
if (existingItem) { 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; return existingItem;
} }
@ -192,7 +202,7 @@ const createInboxItem = async (content, userId, messageId) => {
content: content, content: content,
source: 'telegram', source: 'telegram',
user_id: userId, 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 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) { } catch (error) {
// Send error message // Send error message
await sendTelegramMessage( await sendTelegramMessage(
@ -238,7 +250,7 @@ const processUpdates = async (user, updates) => {
if (!updates.length) return; if (!updates.length) return;
// Filter out already processed updates // Filter out already processed updates
const newUpdates = updates.filter(update => { const newUpdates = updates.filter((update) => {
const updateKey = `${user.id}-${update.update_id}`; const updateKey = `${user.id}-${update.update_id}`;
return !pollerState.processedUpdates.has(updateKey); return !pollerState.processedUpdates.has(updateKey);
}); });
@ -252,8 +264,8 @@ const processUpdates = async (user, updates) => {
pollerState = { pollerState = {
...pollerState, ...pollerState,
userStatus: updateUserStatus(pollerState.userStatus, user.id, { userStatus: updateUserStatus(pollerState.userStatus, user.id, {
lastUpdateId: highestUpdateId lastUpdateId: highestUpdateId,
}) }),
}; };
// Process each new update // 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) // Clean up old processed updates (keep only last 1000 to prevent memory leak)
if (pollerState.processedUpdates.size > 1000) { if (pollerState.processedUpdates.size > 1000) {
const oldestEntries = Array.from(pollerState.processedUpdates).slice(0, 100); const oldestEntries = Array.from(
oldestEntries.forEach(entry => pollerState.processedUpdates.delete(entry)); pollerState.processedUpdates
).slice(0, 100);
oldestEntries.forEach((entry) =>
pollerState.processedUpdates.delete(entry)
);
} }
} }
} catch (error) { } 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; if (!token) continue;
try { try {
const lastUpdateId = pollerState.userStatus[user.id]?.lastUpdateId || 0; const lastUpdateId =
pollerState.userStatus[user.id]?.lastUpdateId || 0;
const updates = await getTelegramUpdates(token, lastUpdateId + 1); const updates = await getTelegramUpdates(token, lastUpdateId + 1);
if (updates && updates.length > 0) { 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); await processUpdates(user, updates);
} }
} catch (error) { } catch (error) {
@ -314,7 +336,7 @@ const startPolling = () => {
pollerState = { pollerState = {
...pollerState, ...pollerState,
running: true, running: true,
interval interval,
}; };
}; };
@ -329,7 +351,7 @@ const stopPolling = () => {
pollerState = { pollerState = {
...pollerState, ...pollerState,
running: false, running: false,
interval: null interval: null,
}; };
}; };
@ -344,7 +366,7 @@ const addUser = async (user) => {
pollerState = { pollerState = {
...pollerState, ...pollerState,
usersToPool: newUsersList usersToPool: newUsersList,
}; };
// Start polling if not already running and we have users // Start polling if not already running and we have users
@ -364,7 +386,7 @@ const removeUser = (userId) => {
pollerState = { pollerState = {
...pollerState, ...pollerState,
usersToPool: newUsersList, usersToPool: newUsersList,
userStatus: newUserStatus userStatus: newUserStatus,
}; };
// Stop polling if no users left // Stop polling if no users left
@ -380,7 +402,7 @@ const getStatus = () => ({
running: pollerState.running, running: pollerState.running,
usersCount: pollerState.usersToPool.length, usersCount: pollerState.usersToPool.length,
pollInterval: pollerState.pollInterval, pollInterval: pollerState.pollInterval,
userStatus: pollerState.userStatus userStatus: pollerState.userStatus,
}); });
// Export functional interface // Export functional interface
@ -398,5 +420,5 @@ module.exports = {
_removeUserFromList: removeUserFromList, _removeUserFromList: removeUserFromList,
_getHighestUpdateId: getHighestUpdateId, _getHighestUpdateId: getHighestUpdateId,
_createMessageParams: createMessageParams, _createMessageParams: createMessageParams,
_createTelegramUrl: createTelegramUrl _createTelegramUrl: createTelegramUrl,
}; };

View file

@ -18,26 +18,31 @@ tests/
## Running Tests ## Running Tests
### All Tests ### All Tests
```bash ```bash
npm test npm test
``` ```
### Unit Tests Only ### Unit Tests Only
```bash ```bash
npm run test:unit npm run test:unit
``` ```
### Integration Tests Only ### Integration Tests Only
```bash ```bash
npm run test:integration npm run test:integration
``` ```
### Watch Mode (for development) ### Watch Mode (for development)
```bash ```bash
npm run test:watch npm run test:watch
``` ```
### Coverage Report ### Coverage Report
```bash ```bash
npm run test:coverage npm run test:coverage
``` ```
@ -45,6 +50,7 @@ npm run test:coverage
## Test Environment ## Test Environment
Tests run in a separate test environment with: Tests run in a separate test environment with:
- In-memory SQLite database (isolated from development data) - In-memory SQLite database (isolated from development data)
- Test-specific configuration from `.env.test` - Test-specific configuration from `.env.test`
- Automatic database cleanup between tests - Automatic database cleanup between tests
@ -52,17 +58,20 @@ Tests run in a separate test environment with:
## Writing Tests ## Writing Tests
### Unit Tests ### Unit Tests
- Test individual functions, models, or middleware in isolation - Test individual functions, models, or middleware in isolation
- Mock external dependencies - Mock external dependencies
- Focus on business logic and edge cases - Focus on business logic and edge cases
### Integration Tests ### Integration Tests
- Test complete API endpoints - Test complete API endpoints
- Use authenticated requests where needed - Use authenticated requests where needed
- Test real database interactions - Test real database interactions
- Verify response formats and status codes - Verify response formats and status codes
### Test Utilities ### Test Utilities
- `tests/helpers/testUtils.js` provides utilities for creating test data - `tests/helpers/testUtils.js` provides utilities for creating test data
- `tests/helpers/setup.js` handles database setup and cleanup - `tests/helpers/setup.js` handles database setup and cleanup
- Use `createTestUser()` for creating authenticated test users - Use `createTestUser()` for creating authenticated test users

View file

@ -14,8 +14,14 @@ beforeEach(async () => {
// Clean all tables except Sessions to avoid conflicts // Clean all tables except Sessions to avoid conflicts
try { try {
const models = Object.values(sequelize.models); const models = Object.values(sequelize.models);
const nonSessionModels = models.filter(model => model.name !== 'Session'); const nonSessionModels = models.filter(
await Promise.all(nonSessionModels.map(model => model.destroy({ truncate: true, cascade: true }))); (model) => model.name !== 'Session'
);
await Promise.all(
nonSessionModels.map((model) =>
model.destroy({ truncate: true, cascade: true })
)
);
} catch (error) { } catch (error) {
// Ignore errors during cleanup // Ignore errors during cleanup
} }

View file

@ -5,18 +5,16 @@ const createTestUser = async (userData = {}) => {
const defaultUser = { const defaultUser = {
email: 'test@example.com', email: 'test@example.com',
password: 'password123', // Use password field to trigger model hook password: 'password123', // Use password field to trigger model hook
...userData ...userData,
}; };
return await User.create(defaultUser); return await User.create(defaultUser);
}; };
const authenticateUser = async (request, user) => { const authenticateUser = async (request, user) => {
const response = await request const response = await request.post('/api/login').send({
.post('/api/login')
.send({
email: user.email, email: user.email,
password: 'password123' password: 'password123',
}); });
return response.headers['set-cookie']; return response.headers['set-cookie'];
@ -24,5 +22,5 @@ const authenticateUser = async (request, user) => {
module.exports = { module.exports = {
createTestUser, createTestUser,
authenticateUser authenticateUser,
}; };

View file

@ -8,16 +8,14 @@ describe('Areas Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -25,12 +23,10 @@ describe('Areas Routes', () => {
it('should create a new area', async () => { it('should create a new area', async () => {
const areaData = { const areaData = {
name: 'Work', name: 'Work',
description: 'Work related projects' description: 'Work related projects',
}; };
const response = await agent const response = await agent.post('/api/areas').send(areaData);
.post('/api/areas')
.send(areaData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe(areaData.name); expect(response.body.name).toBe(areaData.name);
@ -40,7 +36,7 @@ describe('Areas Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const areaData = { const areaData = {
name: 'Work' name: 'Work',
}; };
const response = await request(app) const response = await request(app)
@ -53,12 +49,10 @@ describe('Areas Routes', () => {
it('should require area name', async () => { it('should require area name', async () => {
const areaData = { const areaData = {
description: 'Area without name' description: 'Area without name',
}; };
const response = await agent const response = await agent.post('/api/areas').send(areaData);
.post('/api/areas')
.send(areaData);
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Area name is required.'); expect(response.body.error).toBe('Area name is required.');
@ -72,13 +66,13 @@ describe('Areas Routes', () => {
area1 = await Area.create({ area1 = await Area.create({
name: 'Work', name: 'Work',
description: 'Work projects', description: 'Work projects',
user_id: user.id user_id: user.id,
}); });
area2 = await Area.create({ area2 = await Area.create({
name: 'Personal', name: 'Personal',
description: 'Personal projects', 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.status).toBe(200);
expect(response.body).toHaveLength(2); expect(response.body).toHaveLength(2);
expect(response.body.map(a => a.id)).toContain(area1.id); 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(area2.id);
}); });
it('should order areas by name', async () => { it('should order areas by name', async () => {
@ -114,7 +108,7 @@ describe('Areas Routes', () => {
area = await Area.create({ area = await Area.create({
name: 'Work', name: 'Work',
description: 'Work projects', 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'); const response = await agent.get('/api/areas/999999');
expect(response.status).toBe(404); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherArea = await Area.create({ const otherArea = await Area.create({
name: 'Other Area', name: 'Other Area',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.get(`/api/areas/${otherArea.id}`); const response = await agent.get(`/api/areas/${otherArea.id}`);
expect(response.status).toBe(404); 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 () => { it('should require authentication', async () => {
@ -167,14 +165,14 @@ describe('Areas Routes', () => {
area = await Area.create({ area = await Area.create({
name: 'Work', name: 'Work',
description: 'Work projects', description: 'Work projects',
user_id: user.id user_id: user.id,
}); });
}); });
it('should update area', async () => { it('should update area', async () => {
const updateData = { const updateData = {
name: 'Updated Work', name: 'Updated Work',
description: 'Updated description' description: 'Updated description',
}; };
const response = await agent const response = await agent
@ -195,16 +193,16 @@ describe('Areas Routes', () => {
expect(response.body.error).toBe('Area not found.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherArea = await Area.create({ const otherArea = await Area.create({
name: 'Other Area', name: 'Other Area',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent const response = await agent
@ -231,7 +229,7 @@ describe('Areas Routes', () => {
beforeEach(async () => { beforeEach(async () => {
area = await Area.create({ area = await Area.create({
name: 'Work', 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherArea = await Area.create({ const otherArea = await Area.create({
name: 'Other Area', name: 'Other Area',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.delete(`/api/areas/${otherArea.id}`); const response = await agent.delete(`/api/areas/${otherArea.id}`);

View file

@ -9,16 +9,14 @@ describe('Auth Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
}); });
it('should login with valid credentials', async () => { it('should login with valid credentials', async () => {
const response = await request(app) const response = await request(app).post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -31,10 +29,8 @@ describe('Auth Routes', () => {
}); });
it('should return 400 for missing email', async () => { it('should return 400 for missing email', async () => {
const response = await request(app) const response = await request(app).post('/api/login').send({
.post('/api/login') password: 'password123',
.send({
password: 'password123'
}); });
expect(response.status).toBe(400); expect(response.status).toBe(400);
@ -42,10 +38,8 @@ describe('Auth Routes', () => {
}); });
it('should return 400 for missing password', async () => { it('should return 400 for missing password', async () => {
const response = await request(app) const response = await request(app).post('/api/login').send({
.post('/api/login') email: 'test@example.com',
.send({
email: 'test@example.com'
}); });
expect(response.status).toBe(400); expect(response.status).toBe(400);
@ -53,11 +47,9 @@ describe('Auth Routes', () => {
}); });
it('should return 401 for non-existent user', async () => { it('should return 401 for non-existent user', async () => {
const response = await request(app) const response = await request(app).post('/api/login').send({
.post('/api/login')
.send({
email: 'nonexistent@example.com', email: 'nonexistent@example.com',
password: 'password123' password: 'password123',
}); });
expect(response.status).toBe(401); expect(response.status).toBe(401);
@ -65,11 +57,9 @@ describe('Auth Routes', () => {
}); });
it('should return 401 for invalid password', async () => { it('should return 401 for invalid password', async () => {
const response = await request(app) const response = await request(app).post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'wrongpassword' password: 'wrongpassword',
}); });
expect(response.status).toBe(401); expect(response.status).toBe(401);
@ -82,7 +72,7 @@ describe('Auth Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
}); });
@ -90,11 +80,9 @@ describe('Auth Routes', () => {
const agent = request.agent(app); const agent = request.agent(app);
// Login first // Login first
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
// Check current user // Check current user
@ -119,7 +107,7 @@ describe('Auth Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
}); });
@ -127,11 +115,9 @@ describe('Auth Routes', () => {
const agent = request.agent(app); const agent = request.agent(app);
// Login first // Login first
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
// Logout // Logout

View file

@ -8,16 +8,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
// Ensure no tags exist for this user (clean slate) // Ensure no tags exist for this user (clean slate)
@ -38,13 +36,13 @@ describe('Inbox Routes - No Tags Scenario', () => {
const inboxItem1 = await InboxItem.create({ const inboxItem1 = await InboxItem.create({
content: 'Test item without tags', content: 'Test item without tags',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
const inboxItem2 = await InboxItem.create({ const inboxItem2 = await InboxItem.create({
content: 'Another item without tags', content: 'Another item without tags',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
const response = await agent.get('/api/inbox'); const response = await agent.get('/api/inbox');
@ -52,8 +50,12 @@ describe('Inbox Routes - No Tags Scenario', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true); expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBe(2); expect(response.body.length).toBe(2);
expect(response.body.map(item => item.id)).toContain(inboxItem1.id); expect(response.body.map((item) => item.id)).toContain(
expect(response.body.map(item => item.id)).toContain(inboxItem2.id); inboxItem1.id
);
expect(response.body.map((item) => item.id)).toContain(
inboxItem2.id
);
expect(response.body[0].content).toBeDefined(); expect(response.body[0].content).toBeDefined();
expect(response.body[0].status).toBe('added'); expect(response.body[0].status).toBe('added');
expect(response.body[0].user_id).toBe(user.id); expect(response.body[0].user_id).toBe(user.id);
@ -64,19 +66,19 @@ describe('Inbox Routes - No Tags Scenario', () => {
const addedItem = await InboxItem.create({ const addedItem = await InboxItem.create({
content: 'Added item', content: 'Added item',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
await InboxItem.create({ await InboxItem.create({
content: 'Processed item', content: 'Processed item',
status: 'processed', status: 'processed',
user_id: user.id user_id: user.id,
}); });
await InboxItem.create({ await InboxItem.create({
content: 'Deleted item', content: 'Deleted item',
status: 'deleted', status: 'deleted',
user_id: user.id user_id: user.id,
}); });
const response = await agent.get('/api/inbox'); const response = await agent.get('/api/inbox');
@ -102,7 +104,7 @@ describe('Inbox Routes - No Tags Scenario', () => {
await InboxItem.create({ await InboxItem.create({
content: 'Test item', content: 'Test item',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
// Verify tags endpoint returns empty // 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 () => { it('should create inbox items successfully when no tags exist', async () => {
const inboxData = { const inboxData = {
content: 'New inbox item without tags', content: 'New inbox item without tags',
source: 'web' source: 'web',
}; };
const response = await agent const response = await agent.post('/api/inbox').send(inboxData);
.post('/api/inbox')
.send(inboxData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.content).toBe(inboxData.content); expect(response.body.content).toBe(inboxData.content);
@ -140,13 +140,11 @@ describe('Inbox Routes - No Tags Scenario', () => {
const items = [ const items = [
{ content: 'First item', source: 'web' }, { content: 'First item', source: 'web' },
{ content: 'Second item', source: 'telegram' }, { content: 'Second item', source: 'telegram' },
{ content: 'Third item', source: 'api' } { content: 'Third item', source: 'api' },
]; ];
for (const item of items) { for (const item of items) {
const response = await agent const response = await agent.post('/api/inbox').send(item);
.post('/api/inbox')
.send(item);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.content).toBe(item.content); expect(response.body.content).toBe(item.content);
@ -167,14 +165,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Original content', content: 'Original content',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
}); });
it('should update inbox items when no tags exist', async () => { it('should update inbox items when no tags exist', async () => {
const updateData = { const updateData = {
content: 'Updated content without tags', content: 'Updated content without tags',
status: 'processed' status: 'processed',
}; };
const response = await agent const response = await agent
@ -194,12 +192,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Item to process', content: 'Item to process',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
}); });
it('should process inbox items when no tags exist', async () => { 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.status).toBe(200);
expect(response.body.status).toBe('processed'); expect(response.body.status).toBe('processed');
@ -213,7 +213,7 @@ describe('Inbox Routes - No Tags Scenario', () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Item to delete', content: 'Item to delete',
status: 'added', 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}`); const response = await agent.delete(`/api/inbox/${inboxItem.id}`);
expect(response.status).toBe(200); 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 // Verify item status is updated to deleted
const deletedItem = await InboxItem.findByPk(inboxItem.id); 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'); expect(updateResponse.body.content).toBe('Updated workflow test');
// Step 5: Process inbox item // 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.status).toBe(200);
expect(processResponse.body.status).toBe('processed'); expect(processResponse.body.status).toBe('processed');
@ -273,14 +277,14 @@ describe('Inbox Routes - No Tags Scenario', () => {
const createPromises = Array.from({ length: 5 }, (_, i) => const createPromises = Array.from({ length: 5 }, (_, i) =>
agent.post('/api/inbox').send({ agent.post('/api/inbox').send({
content: `Concurrent item ${i + 1}`, content: `Concurrent item ${i + 1}`,
source: 'test' source: 'test',
}) })
); );
const createResponses = await Promise.all(createPromises); const createResponses = await Promise.all(createPromises);
// All should succeed // All should succeed
createResponses.forEach(response => { createResponses.forEach((response) => {
expect(response.status).toBe(201); expect(response.status).toBe(201);
}); });
@ -290,15 +294,15 @@ describe('Inbox Routes - No Tags Scenario', () => {
expect(getResponse.body.length).toBe(5); expect(getResponse.body.length).toBe(5);
// Process all items concurrently // Process all items concurrently
const itemIds = createResponses.map(response => response.body.id); const itemIds = createResponses.map((response) => response.body.id);
const processPromises = itemIds.map(id => const processPromises = itemIds.map((id) =>
agent.patch(`/api/inbox/${id}/process`) agent.patch(`/api/inbox/${id}/process`)
); );
const processResponses = await Promise.all(processPromises); const processResponses = await Promise.all(processPromises);
// All should succeed // All should succeed
processResponses.forEach(response => { processResponses.forEach((response) => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe('processed'); 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.'); expect(updateResponse.body.error).toBe('Inbox item not found.');
// Try to process non-existent item // 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.status).toBe(404);
expect(processResponse.body.error).toBe('Inbox item not found.'); 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 () => { it('should validate required fields when creating inbox items (no tags scenario)', async () => {
// Try to create item without content // Try to create item without content
const response = await agent const response = await agent.post('/api/inbox').send({});
.post('/api/inbox')
.send({});
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Content is required'); expect(response.body.error).toBe('Content is required');

View file

@ -8,16 +8,14 @@ describe('Inbox Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -25,12 +23,10 @@ describe('Inbox Routes', () => {
it('should create a new inbox item', async () => { it('should create a new inbox item', async () => {
const inboxData = { const inboxData = {
content: 'Remember to buy groceries', content: 'Remember to buy groceries',
source: 'web' source: 'web',
}; };
const response = await agent const response = await agent.post('/api/inbox').send(inboxData);
.post('/api/inbox')
.send(inboxData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.content).toBe(inboxData.content); expect(response.body.content).toBe(inboxData.content);
@ -41,7 +37,7 @@ describe('Inbox Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const inboxData = { const inboxData = {
content: 'Test content' content: 'Test content',
}; };
const response = await request(app) const response = await request(app)
@ -55,9 +51,7 @@ describe('Inbox Routes', () => {
it('should require content', async () => { it('should require content', async () => {
const inboxData = {}; const inboxData = {};
const response = await agent const response = await agent.post('/api/inbox').send(inboxData);
.post('/api/inbox')
.send(inboxData);
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Content is required'); expect(response.body.error).toBe('Content is required');
@ -71,13 +65,13 @@ describe('Inbox Routes', () => {
inboxItem1 = await InboxItem.create({ inboxItem1 = await InboxItem.create({
content: 'First item', content: 'First item',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
inboxItem2 = await InboxItem.create({ inboxItem2 = await InboxItem.create({
content: 'Second item', content: 'Second item',
status: 'processed', status: 'processed',
user_id: user.id user_id: user.id,
}); });
}); });
@ -87,7 +81,7 @@ describe('Inbox Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true); expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBe(1); // Only items with status 'added' are returned 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 () => { it('should only return items with added status', async () => {
@ -113,7 +107,7 @@ describe('Inbox Routes', () => {
beforeEach(async () => { beforeEach(async () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Test content', 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherInboxItem = await InboxItem.create({ const otherInboxItem = await InboxItem.create({
content: 'Other content', content: 'Other content',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.get(`/api/inbox/${otherInboxItem.id}`); const response = await agent.get(`/api/inbox/${otherInboxItem.id}`);
@ -151,7 +145,9 @@ describe('Inbox Routes', () => {
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -165,14 +161,14 @@ describe('Inbox Routes', () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Test content', content: 'Test content',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
}); });
it('should update inbox item', async () => { it('should update inbox item', async () => {
const updateData = { const updateData = {
content: 'Updated content', content: 'Updated content',
status: 'processed' status: 'processed',
}; };
const response = await agent const response = await agent
@ -209,7 +205,7 @@ describe('Inbox Routes', () => {
beforeEach(async () => { beforeEach(async () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Test content', 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}`); const response = await agent.delete(`/api/inbox/${inboxItem.id}`);
expect(response.status).toBe(200); 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 // Verify inbox item status is updated to deleted
const deletedItem = await InboxItem.findByPk(inboxItem.id); const deletedItem = await InboxItem.findByPk(inboxItem.id);
@ -233,7 +231,9 @@ describe('Inbox Routes', () => {
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -247,12 +247,14 @@ describe('Inbox Routes', () => {
inboxItem = await InboxItem.create({ inboxItem = await InboxItem.create({
content: 'Test content', content: 'Test content',
status: 'added', status: 'added',
user_id: user.id user_id: user.id,
}); });
}); });
it('should process inbox item', async () => { 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.status).toBe(200);
expect(response.body.status).toBe('processed'); expect(response.body.status).toBe('processed');
@ -266,7 +268,9 @@ describe('Inbox Routes', () => {
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');

View file

@ -8,21 +8,19 @@ describe('Notes Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
project = await Project.create({ project = await Project.create({
name: 'Test Project', name: 'Test Project',
user_id: user.id user_id: user.id,
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -31,12 +29,10 @@ describe('Notes Routes', () => {
const noteData = { const noteData = {
title: 'Test Note', title: 'Test Note',
content: 'This is a test note content', content: 'This is a test note content',
project_id: project.id project_id: project.id,
}; };
const response = await agent const response = await agent.post('/api/note').send(noteData);
.post('/api/note')
.send(noteData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.title).toBe(noteData.title); expect(response.body.title).toBe(noteData.title);
@ -48,12 +44,10 @@ describe('Notes Routes', () => {
it('should create note without project', async () => { it('should create note without project', async () => {
const noteData = { const noteData = {
title: 'Test Note', title: 'Test Note',
content: 'This is a test note content' content: 'This is a test note content',
}; };
const response = await agent const response = await agent.post('/api/note').send(noteData);
.post('/api/note')
.send(noteData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.title).toBe(noteData.title); expect(response.body.title).toBe(noteData.title);
@ -65,7 +59,7 @@ describe('Notes Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const noteData = { const noteData = {
title: 'Test Note', title: 'Test Note',
content: 'Test content' content: 'Test content',
}; };
const response = await request(app) const response = await request(app)
@ -85,13 +79,13 @@ describe('Notes Routes', () => {
title: 'Note 1', title: 'Note 1',
content: 'First note content', content: 'First note content',
user_id: user.id, user_id: user.id,
project_id: project.id project_id: project.id,
}); });
note2 = await Note.create({ note2 = await Note.create({
title: 'Note 2', title: 'Note 2',
content: 'Second note content', 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(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true); expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBe(2); expect(response.body.length).toBe(2);
expect(response.body.map(n => n.id)).toContain(note1.id); 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(note2.id);
}); });
it('should include project information', async () => { it('should include project information', async () => {
const response = await agent.get('/api/notes'); const response = await agent.get('/api/notes');
expect(response.status).toBe(200); 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).toBeDefined();
expect(noteWithProject.Project.name).toBe(project.name); expect(noteWithProject.Project.name).toBe(project.name);
}); });
@ -119,8 +115,8 @@ describe('Notes Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.length).toBe(2); expect(response.body.length).toBe(2);
expect(response.body.map(n => n.id)).toContain(note1.id); 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(note2.id);
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
@ -139,7 +135,7 @@ describe('Notes Routes', () => {
title: 'Test Note', title: 'Test Note',
content: 'Test content', content: 'Test content',
user_id: user.id, 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherNote = await Note.create({ const otherNote = await Note.create({
title: 'Other Note', title: 'Other Note',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.get(`/api/note/${otherNote.id}`); const response = await agent.get(`/api/note/${otherNote.id}`);
@ -192,7 +188,7 @@ describe('Notes Routes', () => {
note = await Note.create({ note = await Note.create({
title: 'Test Note', title: 'Test Note',
content: 'Test content', content: 'Test content',
user_id: user.id user_id: user.id,
}); });
}); });
@ -200,7 +196,7 @@ describe('Notes Routes', () => {
const updateData = { const updateData = {
title: 'Updated Note', title: 'Updated Note',
content: 'Updated content', content: 'Updated content',
project_id: project.id project_id: project.id,
}; };
const response = await agent const response = await agent
@ -222,16 +218,16 @@ describe('Notes Routes', () => {
expect(response.body.error).toBe('Note not found.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherNote = await Note.create({ const otherNote = await Note.create({
title: 'Other Note', title: 'Other Note',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent const response = await agent
@ -258,7 +254,7 @@ describe('Notes Routes', () => {
beforeEach(async () => { beforeEach(async () => {
note = await Note.create({ note = await Note.create({
title: 'Test Note', 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherNote = await Note.create({ const otherNote = await Note.create({
title: 'Other Note', title: 'Other Note',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.delete(`/api/note/${otherNote.id}`); const response = await agent.delete(`/api/note/${otherNote.id}`);

View file

@ -8,21 +8,19 @@ describe('Projects Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
area = await Area.create({ area = await Area.create({
name: 'Work', name: 'Work',
user_id: user.id user_id: user.id,
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -34,18 +32,18 @@ describe('Projects Routes', () => {
active: true, active: true,
pin_to_sidebar: false, pin_to_sidebar: false,
priority: 1, priority: 1,
area_id: area.id area_id: area.id,
}; };
const response = await agent const response = await agent.post('/api/project').send(projectData);
.post('/api/project')
.send(projectData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe(projectData.name); expect(response.body.name).toBe(projectData.name);
expect(response.body.description).toBe(projectData.description); expect(response.body.description).toBe(projectData.description);
expect(response.body.active).toBe(projectData.active); 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.priority).toBe(projectData.priority);
expect(response.body.area_id).toBe(area.id); expect(response.body.area_id).toBe(area.id);
expect(response.body.user_id).toBe(user.id); expect(response.body.user_id).toBe(user.id);
@ -53,7 +51,7 @@ describe('Projects Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const projectData = { const projectData = {
name: 'Test Project' name: 'Test Project',
}; };
const response = await request(app) const response = await request(app)
@ -66,12 +64,10 @@ describe('Projects Routes', () => {
it('should require project name', async () => { it('should require project name', async () => {
const projectData = { const projectData = {
description: 'Project without name' description: 'Project without name',
}; };
const response = await agent const response = await agent.post('/api/project').send(projectData);
.post('/api/project')
.send(projectData);
expect(response.status).toBe(400); expect(response.status).toBe(400);
}); });
@ -85,13 +81,13 @@ describe('Projects Routes', () => {
name: 'Project 1', name: 'Project 1',
description: 'First project', description: 'First project',
user_id: user.id, user_id: user.id,
area_id: area.id area_id: area.id,
}); });
project2 = await Project.create({ project2 = await Project.create({
name: 'Project 2', name: 'Project 2',
description: 'Second project', 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.status).toBe(200);
expect(response.body.projects).toBeDefined(); expect(response.body.projects).toBeDefined();
expect(response.body.projects.length).toBe(2); 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(
expect(response.body.projects.map(p => p.id)).toContain(project2.id); project1.id
);
expect(response.body.projects.map((p) => p.id)).toContain(
project2.id
);
}); });
it('should include area information', async () => { it('should include area information', async () => {
const response = await agent.get('/api/projects'); const response = await agent.get('/api/projects');
expect(response.status).toBe(200); 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).toBeDefined();
expect(projectWithArea.Area.name).toBe(area.name); expect(projectWithArea.Area.name).toBe(area.name);
}); });
@ -130,7 +132,7 @@ describe('Projects Routes', () => {
name: 'Test Project', name: 'Test Project',
description: 'Test Description', description: 'Test Description',
user_id: user.id, 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'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherProject = await Project.create({ const otherProject = await Project.create({
name: 'Other Project', name: 'Other Project',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.get(`/api/project/${otherProject.id}`); const response = await agent.get(`/api/project/${otherProject.id}`);
@ -169,7 +171,9 @@ describe('Projects Routes', () => {
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -185,7 +189,7 @@ describe('Projects Routes', () => {
description: 'Test Description', description: 'Test Description',
active: false, active: false,
priority: 0, priority: 0,
user_id: user.id user_id: user.id,
}); });
}); });
@ -194,7 +198,7 @@ describe('Projects Routes', () => {
name: 'Updated Project', name: 'Updated Project',
description: 'Updated Description', description: 'Updated Description',
active: true, active: true,
priority: 2 priority: 2,
}; };
const response = await agent const response = await agent
@ -217,16 +221,16 @@ describe('Projects Routes', () => {
expect(response.body.error).toBe('Project not found.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherProject = await Project.create({ const otherProject = await Project.create({
name: 'Other Project', name: 'Other Project',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent const response = await agent
@ -253,7 +257,7 @@ describe('Projects Routes', () => {
beforeEach(async () => { beforeEach(async () => {
project = await Project.create({ project = await Project.create({
name: 'Test Project', 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherProject = await Project.create({ const otherProject = await Project.create({
name: 'Other Project', 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.status).toBe(404);
expect(response.body.error).toBe('Project not found.'); expect(response.body.error).toBe('Project not found.');
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');

View file

@ -7,23 +7,20 @@ describe('Quotes Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
describe('GET /api/quotes/random', () => { describe('GET /api/quotes/random', () => {
it('should return a random quote', async () => { it('should return a random quote', async () => {
const response = await agent const response = await agent.get('/api/quotes/random');
.get('/api/quotes/random');
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toHaveProperty('quote'); 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'), agent.get('/api/quotes/random'),
agent.get('/api/quotes/random') agent.get('/api/quotes/random'),
]); ]);
// All responses should be successful // All responses should be successful
responses.forEach(response => { responses.forEach((response) => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toHaveProperty('quote'); expect(response.body).toHaveProperty('quote');
expect(typeof response.body.quote).toBe('string'); expect(typeof response.body.quote).toBe('string');
@ -49,7 +46,7 @@ describe('Quotes Routes', () => {
// With multiple requests, we should get at least some variety // With multiple requests, we should get at least some variety
// (though it's possible to get the same quote multiple times due to randomness) // (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); const uniqueQuotes = new Set(quotes);
// We expect at least 1 unique quote, but likely more // We expect at least 1 unique quote, but likely more
@ -57,8 +54,7 @@ describe('Quotes Routes', () => {
}); });
it('should return valid quote structure', async () => { it('should return valid quote structure', async () => {
const response = await agent const response = await agent.get('/api/quotes/random');
.get('/api/quotes/random');
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(Object.keys(response.body)).toEqual(['quote']); expect(Object.keys(response.body)).toEqual(['quote']);
@ -69,8 +65,7 @@ describe('Quotes Routes', () => {
describe('GET /api/quotes', () => { describe('GET /api/quotes', () => {
it('should return all quotes with count', async () => { it('should return all quotes with count', async () => {
const response = await agent const response = await agent.get('/api/quotes');
.get('/api/quotes');
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toHaveProperty('quotes'); expect(response.body).toHaveProperty('quotes');
@ -82,13 +77,12 @@ describe('Quotes Routes', () => {
}); });
it('should return valid quote array', async () => { it('should return valid quote array', async () => {
const response = await agent const response = await agent.get('/api/quotes');
.get('/api/quotes');
expect(response.status).toBe(200); expect(response.status).toBe(200);
// All quotes should be non-empty strings // All quotes should be non-empty strings
response.body.quotes.forEach(quote => { response.body.quotes.forEach((quote) => {
expect(typeof quote).toBe('string'); expect(typeof quote).toBe('string');
expect(quote.length).toBeGreaterThan(0); expect(quote.length).toBeGreaterThan(0);
expect(quote.trim()).toBe(quote); expect(quote.trim()).toBe(quote);
@ -103,7 +97,9 @@ describe('Quotes Routes', () => {
expect(response2.status).toBe(200); expect(response2.status).toBe(200);
// The quotes array should be the same across requests // 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); expect(response1.body.count).toBe(response2.body.count);
// Verify the actual content is the same // Verify the actual content is the same
@ -111,8 +107,7 @@ describe('Quotes Routes', () => {
}); });
it('should return expected quote count', async () => { it('should return expected quote count', async () => {
const response = await agent const response = await agent.get('/api/quotes');
.get('/api/quotes');
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -122,8 +117,7 @@ describe('Quotes Routes', () => {
}); });
it('should contain productivity-focused quotes', async () => { it('should contain productivity-focused quotes', async () => {
const response = await agent const response = await agent.get('/api/quotes');
.get('/api/quotes');
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -132,13 +126,21 @@ describe('Quotes Routes', () => {
// These are common themes in productivity quotes // These are common themes in productivity quotes
const productivityKeywords = [ const productivityKeywords = [
'progress', 'task', 'goal', 'focus', 'accomplish', 'progress',
'success', 'work', 'effort', 'achieve', 'time' 'task',
'goal',
'focus',
'accomplish',
'success',
'work',
'effort',
'achieve',
'time',
]; ];
// At least some quotes should contain productivity-related terms // At least some quotes should contain productivity-related terms
const hasProductivityContent = productivityKeywords.some(keyword => const hasProductivityContent = productivityKeywords.some(
allQuotesText.includes(keyword) (keyword) => allQuotesText.includes(keyword)
); );
expect(hasProductivityContent).toBe(true); expect(hasProductivityContent).toBe(true);
@ -155,11 +157,11 @@ describe('Quotes Routes', () => {
const randomQuoteResponses = await Promise.all([ 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'),
agent.get('/api/quotes/random') agent.get('/api/quotes/random'),
]); ]);
// Each random quote should be from the full set // Each random quote should be from the full set
randomQuoteResponses.forEach(response => { randomQuoteResponses.forEach((response) => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(allQuotes).toContain(response.body.quote); expect(allQuotes).toContain(response.body.quote);
}); });

View file

@ -8,16 +8,14 @@ describe('Recurring Tasks API', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -28,12 +26,10 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
priority: 1, priority: 1,
completion_based: false completion_based: false,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Daily Exercise'); expect(response.body.name).toBe('Daily Exercise');
@ -48,12 +44,10 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1, // Monday recurrence_weekday: 1, // Monday
priority: 2 priority: 2,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Weekly Team Meeting'); expect(response.body.name).toBe('Weekly Team Meeting');
@ -67,12 +61,10 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_month_day: 1, recurrence_month_day: 1,
priority: 2 priority: 2,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Pay Rent'); expect(response.body.name).toBe('Pay Rent');
@ -87,12 +79,10 @@ describe('Recurring Tasks API', () => {
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1, // Monday recurrence_weekday: 1, // Monday
recurrence_week_of_month: 1, // First week recurrence_week_of_month: 1, // First week
priority: 1 priority: 1,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('First Monday Meeting'); expect(response.body.name).toBe('First Monday Meeting');
@ -106,12 +96,10 @@ describe('Recurring Tasks API', () => {
name: 'Month-end Report', name: 'Month-end Report',
recurrence_type: 'monthly_last_day', recurrence_type: 'monthly_last_day',
recurrence_interval: 1, recurrence_interval: 1,
priority: 2 priority: 2,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Month-end Report'); expect(response.body.name).toBe('Month-end Report');
@ -124,12 +112,10 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 3, recurrence_interval: 3,
completion_based: true, completion_based: true,
priority: 1 priority: 1,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Car Maintenance'); expect(response.body.name).toBe('Car Maintenance');
@ -145,27 +131,25 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 2, recurrence_interval: 2,
recurrence_end_date: endDate.toISOString().split('T')[0], recurrence_end_date: endDate.toISOString().split('T')[0],
priority: 1 priority: 1,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe('Temporary Recurring Task'); 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 () => { it('should default to none recurrence type if not specified', async () => {
const taskData = { const taskData = {
name: 'Regular Task', name: 'Regular Task',
priority: 1 priority: 1,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.recurrence_type).toBe('none'); expect(response.body.recurrence_type).toBe('none');
@ -181,7 +165,7 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
}); });
@ -189,7 +173,7 @@ describe('Recurring Tasks API', () => {
const updateData = { const updateData = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 2, recurrence_interval: 2,
recurrence_weekday: 5 // Friday recurrence_weekday: 5, // Friday
}; };
const response = await agent const response = await agent
@ -204,7 +188,7 @@ describe('Recurring Tasks API', () => {
it('should update completion_based setting', async () => { it('should update completion_based setting', async () => {
const updateData = { const updateData = {
completion_based: true completion_based: true,
}; };
const response = await agent const response = await agent
@ -220,7 +204,7 @@ describe('Recurring Tasks API', () => {
endDate.setFullYear(endDate.getFullYear() + 1); endDate.setFullYear(endDate.getFullYear() + 1);
const updateData = { const updateData = {
recurrence_end_date: endDate.toISOString().split('T')[0] recurrence_end_date: endDate.toISOString().split('T')[0],
}; };
const response = await agent const response = await agent
@ -228,12 +212,14 @@ describe('Recurring Tasks API', () => {
.send(updateData); .send(updateData);
expect(response.status).toBe(200); 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 () => { it('should disable recurrence by setting type to none', async () => {
const updateData = { const updateData = {
recurrence_type: 'none' recurrence_type: 'none',
}; };
const response = await agent const response = await agent
@ -254,7 +240,7 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
childTask = await Task.create({ childTask = await Task.create({
@ -263,7 +249,7 @@ describe('Recurring Tasks API', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
priority: 1, priority: 1,
due_date: new Date() due_date: new Date(),
}); });
}); });
@ -272,7 +258,7 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 2, recurrence_interval: 2,
recurrence_weekday: 3, recurrence_weekday: 3,
update_parent_recurrence: true update_parent_recurrence: true,
}; };
const response = await agent const response = await agent
@ -293,7 +279,7 @@ describe('Recurring Tasks API', () => {
const updateData = { const updateData = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
update_parent_recurrence: false update_parent_recurrence: false,
}; };
const response = await agent const response = await agent
@ -312,12 +298,12 @@ describe('Recurring Tasks API', () => {
name: 'Standalone Task', name: 'Standalone Task',
recurrence_type: 'none', recurrence_type: 'none',
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
const updateData = { const updateData = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
update_parent_recurrence: true update_parent_recurrence: true,
}; };
const response = await agent const response = await agent
@ -337,17 +323,20 @@ describe('Recurring Tasks API', () => {
recurrence_interval: 1, recurrence_interval: 1,
completion_based: true, completion_based: true,
user_id: user.id, user_id: user.id,
status: 0 // NOT_STARTED status: 0, // NOT_STARTED
}); });
const response = await agent const response = await agent.patch(
.patch(`/api/task/${recurringTask.id}/toggle_completion`); `/api/task/${recurringTask.id}/toggle_completion`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe(2); // DONE expect(response.body.status).toBe(2); // DONE
expect(response.body.next_task).toBeDefined(); expect(response.body.next_task).toBeDefined();
expect(response.body.next_task.name).toBe('Completion Based Task'); 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 () => { it('should not create next instance for non-completion-based recurring tasks', async () => {
@ -357,11 +346,12 @@ describe('Recurring Tasks API', () => {
recurrence_interval: 1, recurrence_interval: 1,
completion_based: false, completion_based: false,
user_id: user.id, user_id: user.id,
status: 0 // NOT_STARTED status: 0, // NOT_STARTED
}); });
const response = await agent const response = await agent.patch(
.patch(`/api/task/${recurringTask.id}/toggle_completion`); `/api/task/${recurringTask.id}/toggle_completion`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe(2); // DONE expect(response.body.status).toBe(2); // DONE
@ -373,11 +363,12 @@ describe('Recurring Tasks API', () => {
name: 'Regular Task', name: 'Regular Task',
recurrence_type: 'none', recurrence_type: 'none',
user_id: user.id, user_id: user.id,
status: 0 // NOT_STARTED status: 0, // NOT_STARTED
}); });
const response = await agent const response = await agent.patch(
.patch(`/api/task/${regularTask.id}/toggle_completion`); `/api/task/${regularTask.id}/toggle_completion`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe(2); // DONE expect(response.body.status).toBe(2); // DONE
@ -388,11 +379,12 @@ describe('Recurring Tasks API', () => {
const task = await Task.create({ const task = await Task.create({
name: 'Test Task', name: 'Test Task',
user_id: user.id, user_id: user.id,
status: 2 // DONE status: 2, // DONE
}); });
const response = await agent const response = await agent.patch(
.patch(`/api/task/${task.id}/toggle_completion`); `/api/task/${task.id}/toggle_completion`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe(0); // NOT_STARTED expect(response.body.status).toBe(0); // NOT_STARTED
@ -403,11 +395,12 @@ describe('Recurring Tasks API', () => {
name: 'Test Task', name: 'Test Task',
note: 'Some notes', note: 'Some notes',
user_id: user.id, user_id: user.id,
status: 2 // DONE status: 2, // DONE
}); });
const response = await agent const response = await agent.patch(
.patch(`/api/task/${task.id}/toggle_completion`); `/api/task/${task.id}/toggle_completion`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.status).toBe(1); // IN_PROGRESS expect(response.body.status).toBe(1); // IN_PROGRESS
@ -423,7 +416,7 @@ describe('Recurring Tasks API', () => {
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-01-01'), 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({ await Task.create({
@ -433,23 +426,25 @@ describe('Recurring Tasks API', () => {
recurrence_weekday: 1, recurrence_weekday: 1,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-01-06'), // Monday 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 () => { it('should generate recurring task instances', async () => {
const response = await agent const response = await agent.post('/api/tasks/generate-recurring');
.post('/api/tasks/generate-recurring');
expect(response.status).toBe(200); 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(response.body.tasks).toBeDefined();
expect(Array.isArray(response.body.tasks)).toBe(true); expect(Array.isArray(response.body.tasks)).toBe(true);
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const response = await request(app) const response = await request(app).post(
.post('/api/tasks/generate-recurring'); '/api/tasks/generate-recurring'
);
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -464,11 +459,10 @@ describe('Recurring Tasks API', () => {
await Task.create({ await Task.create({
name: 'Invalid Task', name: 'Invalid Task',
recurrence_type: 'invalid_type', recurrence_type: 'invalid_type',
user_id: user.id user_id: user.id,
}); });
const response = await agent const response = await agent.post('/api/tasks/generate-recurring');
.post('/api/tasks/generate-recurring');
// Should still return success even if some tasks fail // Should still return success even if some tasks fail
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -485,7 +479,7 @@ describe('Recurring Tasks API', () => {
name: 'Regular Task', name: 'Regular Task',
recurrence_type: 'none', recurrence_type: 'none',
user_id: user.id, user_id: user.id,
status: 0 status: 0,
}); });
const parentTask = await Task.create({ const parentTask = await Task.create({
@ -493,7 +487,7 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
status: 0 status: 0,
}); });
await Task.create({ await Task.create({
@ -501,7 +495,7 @@ describe('Recurring Tasks API', () => {
recurrence_type: 'none', recurrence_type: 'none',
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.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).toBeDefined();
expect(response.body.tasks.length).toBe(3); 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('Regular Task');
expect(taskNames).toContain('Recurring Parent'); expect(taskNames).toContain('Recurring Parent');
expect(taskNames).toContain('Recurring Child'); expect(taskNames).toContain('Recurring Child');
@ -537,7 +531,7 @@ describe('Recurring Tasks API', () => {
recurrence_interval: 2, recurrence_interval: 2,
recurrence_weekday: 1, recurrence_weekday: 1,
completion_based: true, completion_based: true,
user_id: user.id user_id: user.id,
}); });
}); });
@ -561,14 +555,14 @@ describe('Recurring Tasks API', () => {
name: 'Parent Recurring Task', name: 'Parent Recurring Task',
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id user_id: user.id,
}); });
childTask = await Task.create({ childTask = await Task.create({
name: 'Child Task Instance', name: 'Child Task Instance',
recurrence_type: 'none', recurrence_type: 'none',
recurring_parent_id: parentTask.id, 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}`); const response = await agent.delete(`/api/task/${parentTask.id}`);
expect(response.status).toBe(400); 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 // Verify task still exists
const taskStillExists = await Task.findByPk(parentTask.id); const taskStillExists = await Task.findByPk(parentTask.id);

View file

@ -8,28 +8,24 @@ describe('Tags Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
describe('POST /api/tag', () => { describe('POST /api/tag', () => {
it('should create a new tag', async () => { it('should create a new tag', async () => {
const tagData = { const tagData = {
name: 'work' name: 'work',
}; };
const response = await agent const response = await agent.post('/api/tag').send(tagData);
.post('/api/tag')
.send(tagData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.name).toBe(tagData.name); expect(response.body.name).toBe(tagData.name);
@ -38,12 +34,10 @@ describe('Tags Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const tagData = { const tagData = {
name: 'work' name: 'work',
}; };
const response = await request(app) const response = await request(app).post('/api/tag').send(tagData);
.post('/api/tag')
.send(tagData);
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -52,9 +46,7 @@ describe('Tags Routes', () => {
it('should require tag name', async () => { it('should require tag name', async () => {
const tagData = {}; const tagData = {};
const response = await agent const response = await agent.post('/api/tag').send(tagData);
.post('/api/tag')
.send(tagData);
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Tag name is required'); expect(response.body.error).toBe('Tag name is required');
@ -67,12 +59,12 @@ describe('Tags Routes', () => {
beforeEach(async () => { beforeEach(async () => {
tag1 = await Tag.create({ tag1 = await Tag.create({
name: 'work', name: 'work',
user_id: user.id user_id: user.id,
}); });
tag2 = await Tag.create({ tag2 = await Tag.create({
name: 'personal', name: 'personal',
user_id: user.id user_id: user.id,
}); });
}); });
@ -81,8 +73,8 @@ describe('Tags Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toHaveLength(2); expect(response.body).toHaveLength(2);
expect(response.body.map(t => t.id)).toContain(tag1.id); 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(tag2.id);
}); });
it('should order tags by name', async () => { it('should order tags by name', async () => {
@ -107,7 +99,7 @@ describe('Tags Routes', () => {
beforeEach(async () => { beforeEach(async () => {
tag = await Tag.create({ tag = await Tag.create({
name: 'work', 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'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherTag = await Tag.create({ const otherTag = await Tag.create({
name: 'other-tag', name: 'other-tag',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.get(`/api/tag/${otherTag.id}`); const response = await agent.get(`/api/tag/${otherTag.id}`);
@ -158,13 +150,13 @@ describe('Tags Routes', () => {
beforeEach(async () => { beforeEach(async () => {
tag = await Tag.create({ tag = await Tag.create({
name: 'work', name: 'work',
user_id: user.id user_id: user.id,
}); });
}); });
it('should update tag', async () => { it('should update tag', async () => {
const updateData = { const updateData = {
name: 'updated-work' name: 'updated-work',
}; };
const response = await agent const response = await agent
@ -184,16 +176,16 @@ describe('Tags Routes', () => {
expect(response.body.error).toBe('Tag not found'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherTag = await Tag.create({ const otherTag = await Tag.create({
name: 'other-tag', name: 'other-tag',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent const response = await agent
@ -220,7 +212,7 @@ describe('Tags Routes', () => {
beforeEach(async () => { beforeEach(async () => {
tag = await Tag.create({ tag = await Tag.create({
name: 'work', 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'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherTag = await Tag.create({ const otherTag = await Tag.create({
name: 'other-tag', name: 'other-tag',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.delete(`/api/tag/${otherTag.id}`); const response = await agent.delete(`/api/tag/${otherTag.id}`);

View file

@ -8,16 +8,14 @@ describe('Tasks Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -27,12 +25,10 @@ describe('Tasks Routes', () => {
name: 'Test Task', name: 'Test Task',
note: 'Test Note', note: 'Test Note',
priority: 1, priority: 1,
status: 0 status: 0,
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.id).toBeDefined(); expect(response.body.id).toBeDefined();
@ -45,7 +41,7 @@ describe('Tasks Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const taskData = { const taskData = {
name: 'Test Task' name: 'Test Task',
}; };
const response = await request(app) const response = await request(app)
@ -62,12 +58,10 @@ describe('Tasks Routes', () => {
console.error = jest.fn(); console.error = jest.fn();
const taskData = { const taskData = {
description: 'Test Description' description: 'Test Description',
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(400); expect(response.status).toBe(400);
@ -84,14 +78,14 @@ describe('Tasks Routes', () => {
name: 'Task 1', name: 'Task 1',
description: 'Description 1', description: 'Description 1',
user_id: user.id, user_id: user.id,
today: true today: true,
}); });
task2 = await Task.create({ task2 = await Task.create({
name: 'Task 2', name: 'Task 2',
description: 'Description 2', description: 'Description 2',
user_id: user.id, user_id: user.id,
today: false today: false,
}); });
}); });
@ -101,8 +95,8 @@ describe('Tasks Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined(); expect(response.body.tasks).toBeDefined();
expect(response.body.tasks.length).toBe(2); 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(task1.id);
expect(response.body.tasks.map(t => t.id)).toContain(task2.id); expect(response.body.tasks.map((t) => t.id)).toContain(task2.id);
}); });
it('should filter today tasks (returns all user tasks)', async () => { it('should filter today tasks (returns all user tasks)', async () => {
@ -133,7 +127,7 @@ describe('Tasks Routes', () => {
description: 'Test Description', description: 'Test Description',
priority: 0, priority: 0,
status: 0, status: 0,
user_id: user.id user_id: user.id,
}); });
}); });
@ -142,7 +136,7 @@ describe('Tasks Routes', () => {
name: 'Updated Task', name: 'Updated Task',
note: 'Updated Note', note: 'Updated Note',
priority: 2, priority: 2,
status: 1 status: 1,
}; };
const response = await agent const response = await agent
@ -166,16 +160,16 @@ describe('Tasks Routes', () => {
expect(response.body.error).toBe('Task not found.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherTask = await Task.create({ const otherTask = await Task.create({
name: 'Other Task', name: 'Other Task',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent const response = await agent
@ -202,7 +196,7 @@ describe('Tasks Routes', () => {
beforeEach(async () => { beforeEach(async () => {
task = await Task.create({ task = await Task.create({
name: 'Test Task', 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.'); 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 bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const otherTask = await Task.create({ const otherTask = await Task.create({
name: 'Other Task', name: 'Other Task',
user_id: otherUser.id user_id: otherUser.id,
}); });
const response = await agent.delete(`/api/task/${otherTask.id}`); const response = await agent.delete(`/api/task/${otherTask.id}`);
@ -254,21 +248,16 @@ describe('Tasks Routes', () => {
it('should create task with tags', async () => { it('should create task with tags', async () => {
const taskData = { const taskData = {
name: 'Test Task', name: 'Test Task',
tags: [ tags: [{ name: 'work' }, { name: 'urgent' }],
{ name: 'work' },
{ name: 'urgent' }
]
}; };
const response = await agent const response = await agent.post('/api/task').send(taskData);
.post('/api/task')
.send(taskData);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body.Tags).toBeDefined(); expect(response.body.Tags).toBeDefined();
expect(response.body.Tags.length).toBe(2); 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('work');
expect(response.body.Tags.map(t => t.name)).toContain('urgent'); expect(response.body.Tags.map((t) => t.name)).toContain('urgent');
}); });
}); });
}); });

View file

@ -7,7 +7,8 @@ jest.mock('https', () => {
on: jest.fn((event, callback) => { on: jest.fn((event, callback) => {
if (event === 'data') { if (event === 'data') {
// Simulate API response with duplicate updates // Simulate API response with duplicate updates
callback(JSON.stringify({ callback(
JSON.stringify({
ok: true, ok: true,
result: [ result: [
{ {
@ -16,21 +17,22 @@ jest.mock('https', () => {
message_id: 123, message_id: 123,
text: 'Buy groceries from the store', text: 'Buy groceries from the store',
chat: { id: 987654321 }, chat: { id: 987654321 },
date: Math.floor(Date.now() / 1000) date: Math.floor(Date.now() / 1000),
} },
} },
] ],
})); })
);
} else if (event === 'end') { } else if (event === 'end') {
callback(); callback();
} }
}) }),
}; };
const mockRequest = { const mockRequest = {
on: jest.fn(), on: jest.fn(),
write: jest.fn(), write: jest.fn(),
end: jest.fn() end: jest.fn(),
}; };
return { return {
@ -41,7 +43,7 @@ jest.mock('https', () => {
request: jest.fn((url, options, callback) => { request: jest.fn((url, options, callback) => {
callback(mockResponse); callback(mockResponse);
return mockRequest; return mockRequest;
}) }),
}; };
}); });
@ -69,7 +71,7 @@ describe('Telegram Duplicate Message Scenario', () => {
email: 'telegram-user@example.com', email: 'telegram-user@example.com',
password_digest: 'hashedpassword', password_digest: 'hashedpassword',
telegram_bot_token: 'real-bot-token-456', telegram_bot_token: 'real-bot-token-456',
telegram_chat_id: '987654321' telegram_chat_id: '987654321',
}); });
// Clear inbox // Clear inbox
@ -100,11 +102,11 @@ describe('Telegram Duplicate Message Scenario', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: messageId } metadata: { telegram_message_id: messageId },
}); });
// Wait a moment (simulating network delay) // 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) // Simulate duplicate processing attempt (same message, different processing cycle)
const recentCutoff = new Date(Date.now() - 30000); const recentCutoff = new Date(Date.now() - 30000);
@ -114,9 +116,9 @@ describe('Telegram Duplicate Message Scenario', () => {
user_id: testUser.id, user_id: testUser.id,
source: 'telegram', source: 'telegram',
created_at: { created_at: {
[require('sequelize').Op.gte]: recentCutoff [require('sequelize').Op.gte]: recentCutoff,
} },
} },
}); });
// Should find the existing item // Should find the existing item
@ -125,7 +127,7 @@ describe('Telegram Duplicate Message Scenario', () => {
// Verify only one item exists // Verify only one item exists
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(1); expect(allItems).toHaveLength(1);
}); });
@ -135,7 +137,7 @@ describe('Telegram Duplicate Message Scenario', () => {
{ content: 'First message', messageId: 201, updateId: 2001 }, { content: 'First message', messageId: 201, updateId: 2001 },
{ content: 'Second message', messageId: 202, updateId: 2002 }, { content: 'Second message', messageId: 202, updateId: 2002 },
{ content: 'First message', messageId: 203, updateId: 2003 }, // Duplicate content { 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 // Process all messages rapidly
@ -149,9 +151,11 @@ describe('Telegram Duplicate Message Scenario', () => {
user_id: testUser.id, user_id: testUser.id,
source: 'telegram', source: 'telegram',
created_at: { created_at: {
[require('sequelize').Op.gte]: new Date(Date.now() - 30000) [require('sequelize').Op.gte]: new Date(
} Date.now() - 30000
} ),
},
},
}); });
if (existingItem) { if (existingItem) {
@ -162,7 +166,7 @@ describe('Telegram Duplicate Message Scenario', () => {
content: msg.content, content: msg.content,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: msg.messageId } metadata: { telegram_message_id: msg.messageId },
}); });
createdItems.push(newItem); 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 // Should have 3 unique items (first, second, third) - duplicate "First message" should be prevented
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(3); expect(allItems).toHaveLength(3);
@ -193,10 +197,38 @@ describe('Telegram Duplicate Message Scenario', () => {
const processedUpdates = new Set(); const processedUpdates = new Set();
const updates = [ 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,
{ update_id: 3001, message: { text: 'Message 1', message_id: 301, chat: { id: 987654321 } } }, // Duplicate update message: {
{ update_id: 3003, message: { text: 'Message 3', message_id: 303, chat: { id: 987654321 } } } 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 }; const processedCount = { count: 0 };
@ -216,11 +248,13 @@ describe('Telegram Duplicate Message Scenario', () => {
user_id: testUser.id, user_id: testUser.id,
metadata: { metadata: {
telegram_message_id: update.message.message_id, telegram_message_id: update.message.message_id,
update_id: update.update_id update_id: update.update_id,
} },
}); });
} else { } 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 // Verify inbox items
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(3); expect(allItems).toHaveLength(3);
}); });
@ -241,7 +275,7 @@ describe('Telegram Duplicate Message Scenario', () => {
content: 'Message before restart', content: 'Message before restart',
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: 401, update_id: 4001 } metadata: { telegram_message_id: 401, update_id: 4001 },
}); });
// Add user to poller // Add user to poller
@ -264,7 +298,7 @@ describe('Telegram Duplicate Message Scenario', () => {
// The poller should maintain its state correctly // The poller should maintain its state correctly
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(1); expect(allItems).toHaveLength(1);
expect(allItems[0].id).toBe(initialItem.id); expect(allItems[0].id).toBe(initialItem.id);
@ -285,7 +319,9 @@ describe('Telegram Duplicate Message Scenario', () => {
if (processedUpdates.size > 1000) { if (processedUpdates.size > 1000) {
const allEntries = Array.from(processedUpdates); const allEntries = Array.from(processedUpdates);
const oldestEntries = allEntries.slice(0, 200); // Remove oldest 200 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); expect(processedUpdates.size).toBe(1000);
@ -309,11 +345,11 @@ describe('Telegram Duplicate Message Scenario', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: 501 } metadata: { telegram_message_id: 501 },
}); });
// Wait a moment // 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 // Try to create with same content but different message ID
// This should be prevented by the content-based duplicate check // This should be prevented by the content-based duplicate check
@ -324,9 +360,9 @@ describe('Telegram Duplicate Message Scenario', () => {
user_id: testUser.id, user_id: testUser.id,
source: 'telegram', source: 'telegram',
created_at: { created_at: {
[require('sequelize').Op.gte]: recentCutoff [require('sequelize').Op.gte]: recentCutoff,
} },
} },
}); });
expect(existingItem).toBeTruthy(); expect(existingItem).toBeTruthy();
@ -344,7 +380,7 @@ describe('Telegram Duplicate Message Scenario', () => {
user_id: testUser.id, user_id: testUser.id,
created_at: oldTimestamp, created_at: oldTimestamp,
updated_at: oldTimestamp, updated_at: oldTimestamp,
metadata: { telegram_message_id: 601 } metadata: { telegram_message_id: 601 },
}); });
// Now try to create new item with same content // Now try to create new item with same content
@ -352,12 +388,12 @@ describe('Telegram Duplicate Message Scenario', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, 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 // Should be allowed since the old one is outside the 30-second window
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(2); expect(allItems).toHaveLength(2);
}); });

View file

@ -28,7 +28,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
email: 'test-telegram@example.com', email: 'test-telegram@example.com',
password_digest: 'hashedpassword', password_digest: 'hashedpassword',
telegram_bot_token: 'test-bot-token-123', telegram_bot_token: 'test-bot-token-123',
telegram_chat_id: '987654321' telegram_chat_id: '987654321',
}); });
// Clear any existing inbox items // Clear any existing inbox items
@ -58,11 +58,11 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: 123 } metadata: { telegram_message_id: 123 },
}); });
// Wait a moment // 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) // Try to create duplicate item (should be prevented)
const duplicateCheck = await InboxItem.findOne({ const duplicateCheck = await InboxItem.findOne({
@ -71,9 +71,11 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
user_id: testUser.id, user_id: testUser.id,
source: 'telegram', source: 'telegram',
created_at: { created_at: {
[require('sequelize').Op.gte]: new Date(Date.now() - 30000) [require('sequelize').Op.gte]: new Date(
} Date.now() - 30000
} ),
},
},
}); });
expect(duplicateCheck).toBeTruthy(); expect(duplicateCheck).toBeTruthy();
@ -81,7 +83,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
// Verify only one item exists // Verify only one item exists
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(1); expect(allItems).toHaveLength(1);
}); });
@ -95,7 +97,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
created_at: new Date(Date.now() - 35000), // 35 seconds ago 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) // Create second item (should be allowed)
@ -103,12 +105,12 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: 125 } metadata: { telegram_message_id: 125 },
}); });
// Verify both items exist // Verify both items exist
const allItems = await InboxItem.findAll({ const allItems = await InboxItem.findAll({
where: { user_id: testUser.id } where: { user_id: testUser.id },
}); });
expect(allItems).toHaveLength(2); expect(allItems).toHaveLength(2);
}); });
@ -119,7 +121,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
email: 'test2-telegram@example.com', email: 'test2-telegram@example.com',
password_digest: 'hashedpassword', password_digest: 'hashedpassword',
telegram_bot_token: 'test-bot-token-456', telegram_bot_token: 'test-bot-token-456',
telegram_chat_id: '123456789' telegram_chat_id: '123456789',
}); });
const messageContent = 'Shared message content'; const messageContent = 'Shared message content';
@ -129,7 +131,7 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser.id, user_id: testUser.id,
metadata: { telegram_message_id: 126 } metadata: { telegram_message_id: 126 },
}); });
// Create item for second user (should be allowed) // Create item for second user (should be allowed)
@ -137,15 +139,19 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
content: messageContent, content: messageContent,
source: 'telegram', source: 'telegram',
user_id: testUser2.id, user_id: testUser2.id,
metadata: { telegram_message_id: 127 } metadata: { telegram_message_id: 127 },
}); });
// Verify both items exist // Verify both items exist
const allItems = await InboxItem.findAll(); const allItems = await InboxItem.findAll();
expect(allItems).toHaveLength(2); expect(allItems).toHaveLength(2);
const user1Items = allItems.filter(item => item.user_id === testUser.id); const user1Items = allItems.filter(
const user2Items = allItems.filter(item => item.user_id === testUser2.id); (item) => item.user_id === testUser.id
);
const user2Items = allItems.filter(
(item) => item.user_id === testUser2.id
);
expect(user1Items).toHaveLength(1); expect(user1Items).toHaveLength(1);
expect(user2Items).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 () => { test('should not add user without telegram token', async () => {
const userWithoutToken = await User.create({ const userWithoutToken = await User.create({
email: 'no-token@example.com', email: 'no-token@example.com',
password_digest: 'hashedpassword' password_digest: 'hashedpassword',
// No telegram_bot_token // No telegram_bot_token
}); });
@ -213,17 +219,17 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
message: { message: {
message_id: 501, message_id: 501,
text: 'First message', text: 'First message',
chat: { id: 987654321 } chat: { id: 987654321 },
} },
}, },
{ {
update_id: 1002, update_id: 1002,
message: { message: {
message_id: 502, message_id: 502,
text: 'Second message', text: 'Second message',
chat: { id: 987654321 } chat: { id: 987654321 },
} },
} },
]; ];
// Test highest update ID calculation // Test highest update ID calculation
@ -231,8 +237,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
expect(highestId).toBe(1002); expect(highestId).toBe(1002);
// Test update key generation (simulating internal logic) // Test update key generation (simulating internal logic)
const updateKeys = mockUpdates.map(update => `${testUser.id}-${update.update_id}`); const updateKeys = mockUpdates.map(
expect(updateKeys).toEqual([`${testUser.id}-1001`, `${testUser.id}-1002`]); (update) => `${testUser.id}-${update.update_id}`
);
expect(updateKeys).toEqual([
`${testUser.id}-1001`,
`${testUser.id}-1002`,
]);
}); });
test('should properly track processed updates', async () => { test('should properly track processed updates', async () => {
@ -247,8 +258,8 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
const newUpdates = [ const newUpdates = [
{ update_id: 1001 }, // Should be filtered out { update_id: 1001 }, // Should be filtered out
{ update_id: 1002 }, // Should be filtered out { update_id: 1002 }, // Should be filtered out
{ update_id: 1003 } // Should remain { update_id: 1003 }, // Should remain
].filter(update => { ].filter((update) => {
const updateKey = `1-${update.update_id}`; const updateKey = `1-${update.update_id}`;
return !processedUpdates.has(updateKey); return !processedUpdates.has(updateKey);
}); });
@ -270,8 +281,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
// Simulate cleanup (remove oldest 100) // Simulate cleanup (remove oldest 100)
if (processedUpdates.size > 1000) { if (processedUpdates.size > 1000) {
const oldestEntries = Array.from(processedUpdates).slice(0, 100); const oldestEntries = Array.from(processedUpdates).slice(
oldestEntries.forEach(entry => processedUpdates.delete(entry)); 0,
100
);
oldestEntries.forEach((entry) =>
processedUpdates.delete(entry)
);
} }
expect(processedUpdates.size).toBe(1000); expect(processedUpdates.size).toBe(1000);
@ -284,17 +300,17 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
test('should handle database errors gracefully', async () => { test('should handle database errors gracefully', async () => {
// Mock InboxItem.create to throw an error // Mock InboxItem.create to throw an error
const originalCreate = InboxItem.create; const originalCreate = InboxItem.create;
InboxItem.create = jest.fn().mockRejectedValue(new Error('Database error')); InboxItem.create = jest
.fn()
.mockRejectedValue(new Error('Database error'));
try { await expect(
await InboxItem.create({ InboxItem.create({
content: 'Test error handling', content: 'Test error handling',
source: 'telegram', source: 'telegram',
user_id: testUser.id user_id: testUser.id,
}); })
} catch (error) { ).rejects.toThrow('Database error');
expect(error.message).toBe('Database error');
}
// Restore original function // Restore original function
InboxItem.create = originalCreate; InboxItem.create = originalCreate;
@ -313,12 +329,13 @@ describe('Telegram Duplicate Prevention Integration Tests', () => {
message: { message: {
// Missing text and other properties // Missing text and other properties
message_id: 601, message_id: 601,
chat: { id: 987654321 } chat: { id: 987654321 },
} },
}; };
// The actual processing would skip this message due to missing text // 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(); expect(hasText).toBeFalsy();
}); });
}); });

View file

@ -8,16 +8,14 @@ describe('Telegram Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -30,7 +28,9 @@ describe('Telegram Routes', () => {
.send({ token: botToken }); .send({ token: botToken });
expect(response.status).toBe(200); 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 // Verify token was saved to user
const updatedUser = await User.findByPk(user.id); const updatedUser = await User.findByPk(user.id);
@ -40,16 +40,16 @@ describe('Telegram Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const response = await request(app) const response = await request(app)
.post('/api/telegram/setup') .post('/api/telegram/setup')
.send({ token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-1234567890' }); .send({
token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-1234567890',
});
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
}); });
it('should require token parameter', async () => { it('should require token parameter', async () => {
const response = await agent const response = await agent.post('/api/telegram/setup').send({});
.post('/api/telegram/setup')
.send({});
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Telegram bot token is required.'); expect(response.body.error).toBe('Telegram bot token is required.');
@ -61,7 +61,9 @@ describe('Telegram Routes', () => {
.send({ token: 'invalid-token-format' }); .send({ token: 'invalid-token-format' });
expect(response.status).toBe(400); 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 () => { it('should validate token format with correct pattern', async () => {
@ -71,7 +73,7 @@ describe('Telegram Routes', () => {
'notnum:ABCdefGHIjklMNOPQRSTUVwxyz-12345678', 'notnum:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
'123456789-ABCdefGHIjklMNOPQRSTUVwxyz-12345678', '123456789-ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
'123456789:', '123456789:',
':ABCdefGHIjklMNOPQRSTUVwxyz-12345678' ':ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
]; ];
for (const token of invalidTokens) { for (const token of invalidTokens) {
@ -80,7 +82,9 @@ describe('Telegram Routes', () => {
.send({ token }); .send({ token });
expect(response.status).toBe(400); 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 = [ const validTokens = [
'123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678', '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
'987654321:XYZabcDEFghiJKLmnoPQRstUVW_09876543', '987654321:XYZabcDEFghiJKLmnoPQRstUVW_09876543',
'555555555:abcdefghijklmnopqrstuvwxyzABCDEFGHI' '555555555:abcdefghijklmnopqrstuvwxyzABCDEFGHI',
]; ];
for (const token of validTokens) { for (const token of validTokens) {
@ -97,7 +101,9 @@ describe('Telegram Routes', () => {
.send({ token }); .send({ token });
expect(response.status).toBe(200); 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 () => { beforeEach(async () => {
// Setup bot token first // Setup bot token first
await user.update({ await user.update({
telegram_bot_token: '123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678' telegram_bot_token:
'123456789:ABCdefGHIjklMNOPQRSTUVwxyz-12345678',
}); });
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const response = await request(app) const response = await request(app).post(
.post('/api/telegram/start-polling'); '/api/telegram/start-polling'
);
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -122,8 +130,7 @@ describe('Telegram Routes', () => {
// Remove bot token // Remove bot token
await user.update({ telegram_bot_token: null }); await user.update({ telegram_bot_token: null });
const response = await agent const response = await agent.post('/api/telegram/start-polling');
.post('/api/telegram/start-polling');
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('Telegram bot token not set.'); expect(response.body.error).toBe('Telegram bot token not set.');
@ -132,8 +139,9 @@ describe('Telegram Routes', () => {
describe('POST /api/telegram/stop-polling', () => { describe('POST /api/telegram/stop-polling', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const response = await request(app) const response = await request(app).post(
.post('/api/telegram/stop-polling'); '/api/telegram/stop-polling'
);
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');
@ -142,8 +150,9 @@ describe('Telegram Routes', () => {
describe('GET /api/telegram/polling-status', () => { describe('GET /api/telegram/polling-status', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const response = await request(app) const response = await request(app).get(
.get('/api/telegram/polling-status'); '/api/telegram/polling-status'
);
expect(response.status).toBe(401); expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); expect(response.body.error).toBe('Authentication required');

View file

@ -7,16 +7,14 @@ describe('URL Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -31,8 +29,7 @@ describe('URL Routes', () => {
}); });
it('should require url parameter', async () => { it('should require url parameter', async () => {
const response = await agent const response = await agent.get('/api/url/title');
.get('/api/url/title');
expect(response.status).toBe(400); expect(response.status).toBe(400);
expect(response.body.error).toBe('URL parameter is required'); expect(response.body.error).toBe('URL parameter is required');
@ -48,7 +45,10 @@ describe('URL Routes', () => {
expect(response.body).toHaveProperty('title'); expect(response.body).toHaveProperty('title');
expect(response.body.url).toBe('https://httpbin.org/html'); expect(response.body.url).toBe('https://httpbin.org/html');
// Title could be extracted or null depending on network conditions // 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); }, 10000);
it('should handle URL without protocol', async () => { it('should handle URL without protocol', async () => {
@ -61,7 +61,10 @@ describe('URL Routes', () => {
expect(response.body).toHaveProperty('title'); expect(response.body).toHaveProperty('title');
expect(response.body.url).toBe('httpbin.org/html'); expect(response.body.url).toBe('httpbin.org/html');
// Title could be extracted or null depending on network conditions // 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); }, 10000);
it('should handle invalid URL gracefully', async () => { it('should handle invalid URL gracefully', async () => {
@ -74,7 +77,10 @@ describe('URL Routes', () => {
expect(response.body).toHaveProperty('title'); expect(response.body).toHaveProperty('title');
expect(response.body.url).toBe('not-a-valid-url'); expect(response.body.url).toBe('not-a-valid-url');
// Title could be null or error message // 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 () => { it('should handle unreachable URL', async () => {
@ -85,7 +91,9 @@ describe('URL Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toHaveProperty('url'); expect(response.body).toHaveProperty('url');
expect(response.body).toHaveProperty('title'); 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); expect(response.body.title).toBe(null);
}); });
}); });
@ -110,7 +118,8 @@ describe('URL Routes', () => {
}); });
it('should extract URL from text and get title', async () => { 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 const response = await agent
.post('/api/url/extract-from-text') .post('/api/url/extract-from-text')
.send({ text: testText }); .send({ text: testText });
@ -121,11 +130,15 @@ describe('URL Routes', () => {
expect(response.body.originalText).toBe(testText); expect(response.body.originalText).toBe(testText);
expect(response.body).toHaveProperty('title'); expect(response.body).toHaveProperty('title');
// Title could be extracted or null depending on network conditions // 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); }, 10000);
it('should extract first URL when multiple URLs in text', async () => { 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 const response = await agent
.post('/api/url/extract-from-text') .post('/api/url/extract-from-text')
.send({ text: testText }); .send({ text: testText });

View file

@ -10,7 +10,7 @@ describe('User Create Script', () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn('node', [scriptPath, ...args], { const child = spawn('node', [scriptPath, ...args], {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, NODE_ENV: 'test' } env: { ...process.env, NODE_ENV: 'test' },
}); });
let stdout = ''; let stdout = '';
@ -28,7 +28,7 @@ describe('User Create Script', () => {
resolve({ resolve({
code, code,
stdout: stdout.trim(), 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 // Clean up any test users created during tests
await User.destroy({ await User.destroy({
where: { 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([]); const result = await runUserCreateScript([]);
expect(result.code).toBe(1); expect(result.code).toBe(1);
expect(result.stderr).toContain('❌ Usage: npm run user:create <email> <password>'); expect(result.stderr).toContain(
expect(result.stderr).toContain('Example: npm run user:create admin@example.com mypassword123'); '❌ 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 () => { it('should show usage when only email provided', async () => {
const result = await runUserCreateScript(['test@example.com']); const result = await runUserCreateScript(['test@example.com']);
expect(result.code).toBe(1); 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 () => { it('should show usage when only password provided', async () => {
const result = await runUserCreateScript(['', 'password123']); const result = await runUserCreateScript(['', 'password123']);
expect(result.code).toBe(1); 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 () => { it('should reject invalid email format', async () => {
@ -133,11 +146,14 @@ describe('User Create Script', () => {
'@missing-local.com', '@missing-local.com',
'spaces in@email.com', 'spaces in@email.com',
'double@@domain.com', 'double@@domain.com',
'trailing.dot.@domain.com' 'trailing.dot.@domain.com',
]; ];
for (const email of invalidEmails) { for (const email of invalidEmails) {
const result = await runUserCreateScript([email, 'password123']); const result = await runUserCreateScript([
email,
'password123',
]);
expect(result.code).toBe(1); expect(result.code).toBe(1);
expect(result.stderr).toContain('❌ Invalid email format'); expect(result.stderr).toContain('❌ Invalid email format');
@ -148,10 +164,15 @@ describe('User Create Script', () => {
const shortPasswords = ['', '1', '12', '123', '1234', '12345']; const shortPasswords = ['', '1', '12', '123', '1234', '12345'];
for (const password of shortPasswords) { 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.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 // Create user first
await User.create({ await User.create({
email, email,
password_digest: await require('bcrypt').hash(password, 10) password_digest: await require('bcrypt').hash(password, 10),
}); });
// Try to create same user again // Try to create same user again
const result = await runUserCreateScript([email, password]); const result = await runUserCreateScript([email, password]);
expect(result.code).toBe(1); 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 email = 'npmtest@example.com';
const password = 'testpassword123'; const password = 'testpassword123';
// Clean up any existing user first
await User.destroy({ where: { email } });
try { try {
// This simulates running: npm run user:create npmtest@example.com testpassword123 // This simulates running: npm run user:create npmtest@example.com testpassword123
const output = execSync( const output = execSync(
@ -186,7 +212,7 @@ describe('User Create Script', () => {
cwd: path.join(__dirname, '../..'), cwd: path.join(__dirname, '../..'),
env: { ...process.env, NODE_ENV: 'test' }, env: { ...process.env, NODE_ENV: 'test' },
encoding: 'utf8', encoding: 'utf8',
timeout: 10000 timeout: 10000,
} }
); );
@ -196,27 +222,6 @@ describe('User Create Script', () => {
const createdUser = await User.findOne({ where: { email } }); const createdUser = await User.findOne({ where: { email } });
expect(createdUser).toBeTruthy(); expect(createdUser).toBeTruthy();
expect(createdUser.email).toBe(email); 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 { } finally {
// Clean up // Clean up
await User.destroy({ where: { email } }); await User.destroy({ where: { email } });
@ -243,7 +248,10 @@ describe('User Create Script', () => {
// Verify the hash is valid // Verify the hash is valid
const bcrypt = require('bcrypt'); 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); expect(isValid).toBe(true);
// Clean up // Clean up
@ -289,7 +297,10 @@ describe('User Create Script', () => {
expect(createdUser).toBeTruthy(); expect(createdUser).toBeTruthy();
const bcrypt = require('bcrypt'); 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); expect(isValid).toBe(true);
// Clean up // Clean up

View file

@ -8,16 +8,14 @@ describe('Users Routes', () => {
beforeEach(async () => { beforeEach(async () => {
user = await createTestUser({ user = await createTestUser({
email: 'test@example.com' email: 'test@example.com',
}); });
// Create authenticated agent // Create authenticated agent
agent = request.agent(app); agent = request.agent(app);
await agent await agent.post('/api/login').send({
.post('/api/login')
.send({
email: 'test@example.com', email: 'test@example.com',
password: 'password123' password: 'password123',
}); });
}); });
@ -62,29 +60,27 @@ describe('Users Routes', () => {
language: 'es', language: 'es',
timezone: 'UTC', timezone: 'UTC',
avatar_image: 'new-avatar.png', avatar_image: 'new-avatar.png',
telegram_bot_token: 'new-token' telegram_bot_token: 'new-token',
}; };
const response = await agent const response = await agent.patch('/api/profile').send(updateData);
.patch('/api/profile')
.send(updateData);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.appearance).toBe(updateData.appearance); expect(response.body.appearance).toBe(updateData.appearance);
expect(response.body.language).toBe(updateData.language); expect(response.body.language).toBe(updateData.language);
expect(response.body.timezone).toBe(updateData.timezone); expect(response.body.timezone).toBe(updateData.timezone);
expect(response.body.avatar_image).toBe(updateData.avatar_image); 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 () => { it('should allow partial updates', async () => {
const updateData = { const updateData = {
appearance: 'dark' appearance: 'dark',
}; };
const response = await agent const response = await agent.patch('/api/profile').send(updateData);
.patch('/api/profile')
.send(updateData);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.appearance).toBe(updateData.appearance); expect(response.body.appearance).toBe(updateData.appearance);
@ -93,7 +89,7 @@ describe('Users Routes', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const updateData = { const updateData = {
appearance: 'dark' appearance: 'dark',
}; };
const response = await request(app) const response = await request(app)
@ -122,27 +118,37 @@ describe('Users Routes', () => {
}); });
it('should toggle task summary on', async () => { 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.status).toBe(200);
expect(response.body.success).toBe(true); expect(response.body.success).toBe(true);
expect(response.body.enabled).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 () => { it('should toggle task summary off', async () => {
await user.update({ task_summary_enabled: true }); 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.status).toBe(200);
expect(response.body.success).toBe(true); expect(response.body.success).toBe(true);
expect(response.body.enabled).toBe(false); 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 () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); 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 () => { it('should return 401 when session user no longer exists', async () => {
await User.destroy({ where: { id: user.id } }); 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.status).toBe(401);
expect(response.body.error).toBe('User not found'); expect(response.body.error).toBe('User not found');
@ -167,7 +175,9 @@ describe('Users Routes', () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body.success).toBe(true); expect(response.body.success).toBe(true);
expect(response.body.frequency).toBe('daily'); 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 () => { it('should require frequency parameter', async () => {
@ -189,7 +199,16 @@ describe('Users Routes', () => {
}); });
it('should accept valid frequencies', async () => { 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) { for (const frequency of validFrequencies) {
const response = await agent const response = await agent
@ -224,14 +243,20 @@ describe('Users Routes', () => {
describe('POST /api/profile/task-summary/send-now', () => { describe('POST /api/profile/task-summary/send-now', () => {
it('should require telegram configuration', async () => { 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.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 () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); 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 () => { it('should return 401 when session user no longer exists', async () => {
await User.destroy({ where: { id: user.id } }); 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.status).toBe(401);
expect(response.body.error).toBe('User not found'); expect(response.body.error).toBe('User not found');
@ -251,10 +278,12 @@ describe('Users Routes', () => {
it('should get task summary status', async () => { it('should get task summary status', async () => {
await user.update({ await user.update({
task_summary_enabled: true, 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.status).toBe(200);
expect(response.body.success).toBe(true); expect(response.body.success).toBe(true);
@ -265,7 +294,9 @@ describe('Users Routes', () => {
}); });
it('should require authentication', async () => { 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.status).toBe(401);
expect(response.body.error).toBe('Authentication required'); 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 () => { it('should return 401 when session user no longer exists', async () => {
await User.destroy({ where: { id: user.id } }); 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.status).toBe(401);
expect(response.body.error).toBe('User not found'); expect(response.body.error).toBe('User not found');

View file

@ -7,11 +7,11 @@ describe('Auth Middleware', () => {
beforeEach(() => { beforeEach(() => {
req = { req = {
path: '/api/tasks', path: '/api/tasks',
session: {} session: {},
}; };
res = { res = {
status: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis(),
json: jest.fn() json: jest.fn(),
}; };
next = jest.fn(); next = jest.fn();
}); });
@ -49,7 +49,9 @@ describe('Auth Middleware', () => {
await requireAuth(req, res, next); await requireAuth(req, res, next);
expect(res.status).toHaveBeenCalledWith(401); expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' }); expect(res.json).toHaveBeenCalledWith({
error: 'Authentication required',
});
expect(next).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled();
}); });
@ -59,7 +61,9 @@ describe('Auth Middleware', () => {
await requireAuth(req, res, next); await requireAuth(req, res, next);
expect(res.status).toHaveBeenCalledWith(401); expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication required' }); expect(res.json).toHaveBeenCalledWith({
error: 'Authentication required',
});
expect(next).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled();
}); });
@ -67,12 +71,12 @@ describe('Auth Middleware', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const user = await User.create({ const user = await User.create({
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
req.session = { req.session = {
userId: user.id + 1, // Non-existent user ID userId: user.id + 1, // Non-existent user ID
destroy: jest.fn() destroy: jest.fn(),
}; };
await requireAuth(req, res, next); await requireAuth(req, res, next);
@ -87,11 +91,11 @@ describe('Auth Middleware', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const user = await User.create({ const user = await User.create({
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
req.session = { req.session = {
userId: user.id userId: user.id,
}; };
await requireAuth(req, res, next); await requireAuth(req, res, next);
@ -110,17 +114,21 @@ describe('Auth Middleware', () => {
// Mock User.findByPk to throw an error // Mock User.findByPk to throw an error
const originalFindByPk = User.findByPk; 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 = { req.session = {
userId: 123, userId: 123,
destroy: jest.fn() destroy: jest.fn(),
}; };
await requireAuth(req, res, next); await requireAuth(req, res, next);
expect(res.status).toHaveBeenCalledWith(500); expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'Authentication error' }); expect(res.json).toHaveBeenCalledWith({
error: 'Authentication error',
});
expect(next).not.toHaveBeenCalled(); expect(next).not.toHaveBeenCalled();
// Restore original methods // Restore original methods

View file

@ -7,7 +7,7 @@ describe('Area Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', 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 = { const areaData = {
name: 'Work', name: 'Work',
description: 'Work related projects', description: 'Work related projects',
user_id: user.id user_id: user.id,
}; };
const area = await Area.create(areaData); const area = await Area.create(areaData);
@ -29,7 +29,7 @@ describe('Area Model', () => {
it('should require name', async () => { it('should require name', async () => {
const areaData = { const areaData = {
description: 'Area without name', description: 'Area without name',
user_id: user.id user_id: user.id,
}; };
await expect(Area.create(areaData)).rejects.toThrow(); await expect(Area.create(areaData)).rejects.toThrow();
@ -37,7 +37,7 @@ describe('Area Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const areaData = { const areaData = {
name: 'Test Area' name: 'Test Area',
}; };
await expect(Area.create(areaData)).rejects.toThrow(); await expect(Area.create(areaData)).rejects.toThrow();
@ -47,7 +47,7 @@ describe('Area Model', () => {
const areaData = { const areaData = {
name: 'Test Area', name: 'Test Area',
user_id: user.id, user_id: user.id,
description: null description: null,
}; };
const area = await Area.create(areaData); const area = await Area.create(areaData);
@ -59,11 +59,11 @@ describe('Area Model', () => {
it('should belong to a user', async () => { it('should belong to a user', async () => {
const area = await Area.create({ const area = await Area.create({
name: 'Test Area', name: 'Test Area',
user_id: user.id user_id: user.id,
}); });
const areaWithUser = await Area.findByPk(area.id, { const areaWithUser = await Area.findByPk(area.id, {
include: [{ model: User }] include: [{ model: User }],
}); });
expect(areaWithUser.User).toBeDefined(); expect(areaWithUser.User).toBeDefined();

View file

@ -7,7 +7,7 @@ describe('InboxItem Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', 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', content: 'Remember to buy groceries',
status: 'added', status: 'added',
source: 'web', source: 'web',
user_id: user.id user_id: user.id,
}; };
const inboxItem = await InboxItem.create(inboxData); const inboxItem = await InboxItem.create(inboxData);
@ -30,7 +30,7 @@ describe('InboxItem Model', () => {
it('should require content', async () => { it('should require content', async () => {
const inboxData = { const inboxData = {
user_id: user.id user_id: user.id,
}; };
await expect(InboxItem.create(inboxData)).rejects.toThrow(); await expect(InboxItem.create(inboxData)).rejects.toThrow();
@ -38,7 +38,7 @@ describe('InboxItem Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const inboxData = { const inboxData = {
content: 'Test content' content: 'Test content',
}; };
await expect(InboxItem.create(inboxData)).rejects.toThrow(); await expect(InboxItem.create(inboxData)).rejects.toThrow();
@ -48,7 +48,7 @@ describe('InboxItem Model', () => {
const inboxData = { const inboxData = {
content: 'Test content', content: 'Test content',
user_id: user.id, user_id: user.id,
status: null status: null,
}; };
await expect(InboxItem.create(inboxData)).rejects.toThrow(); await expect(InboxItem.create(inboxData)).rejects.toThrow();
@ -58,7 +58,7 @@ describe('InboxItem Model', () => {
const inboxData = { const inboxData = {
content: 'Test content', content: 'Test content',
user_id: user.id, user_id: user.id,
source: null source: null,
}; };
await expect(InboxItem.create(inboxData)).rejects.toThrow(); await expect(InboxItem.create(inboxData)).rejects.toThrow();
@ -69,7 +69,7 @@ describe('InboxItem Model', () => {
it('should set correct default values', async () => { it('should set correct default values', async () => {
const inboxItem = await InboxItem.create({ const inboxItem = await InboxItem.create({
content: 'Test content', content: 'Test content',
user_id: user.id user_id: user.id,
}); });
expect(inboxItem.status).toBe('added'); expect(inboxItem.status).toBe('added');
@ -81,11 +81,11 @@ describe('InboxItem Model', () => {
it('should belong to a user', async () => { it('should belong to a user', async () => {
const inboxItem = await InboxItem.create({ const inboxItem = await InboxItem.create({
content: 'Test content', content: 'Test content',
user_id: user.id user_id: user.id,
}); });
const inboxItemWithUser = await InboxItem.findByPk(inboxItem.id, { const inboxItemWithUser = await InboxItem.findByPk(inboxItem.id, {
include: [{ model: User }] include: [{ model: User }],
}); });
expect(inboxItemWithUser.User).toBeDefined(); expect(inboxItemWithUser.User).toBeDefined();

View file

@ -7,12 +7,12 @@ describe('Note Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
project = await Project.create({ project = await Project.create({
name: 'Test Project', name: 'Test Project',
user_id: user.id user_id: user.id,
}); });
}); });
@ -22,7 +22,7 @@ describe('Note Model', () => {
title: 'Test Note', title: 'Test Note',
content: 'This is a test note content', content: 'This is a test note content',
user_id: user.id, user_id: user.id,
project_id: project.id project_id: project.id,
}; };
const note = await Note.create(noteData); const note = await Note.create(noteData);
@ -36,7 +36,7 @@ describe('Note Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const noteData = { const noteData = {
title: 'Test Note', title: 'Test Note',
content: 'Test content' content: 'Test content',
}; };
await expect(Note.create(noteData)).rejects.toThrow(); await expect(Note.create(noteData)).rejects.toThrow();
@ -46,7 +46,7 @@ describe('Note Model', () => {
const noteData = { const noteData = {
title: null, title: null,
content: null, content: null,
user_id: user.id user_id: user.id,
}; };
const note = await Note.create(noteData); const note = await Note.create(noteData);
@ -59,7 +59,7 @@ describe('Note Model', () => {
title: 'Test Note', title: 'Test Note',
content: 'Test content', content: 'Test content',
user_id: user.id, user_id: user.id,
project_id: null project_id: null,
}; };
const note = await Note.create(noteData); const note = await Note.create(noteData);
@ -71,11 +71,11 @@ describe('Note Model', () => {
it('should belong to a user', async () => { it('should belong to a user', async () => {
const note = await Note.create({ const note = await Note.create({
title: 'Test Note', title: 'Test Note',
user_id: user.id user_id: user.id,
}); });
const noteWithUser = await Note.findByPk(note.id, { const noteWithUser = await Note.findByPk(note.id, {
include: [{ model: User }] include: [{ model: User }],
}); });
expect(noteWithUser.User).toBeDefined(); expect(noteWithUser.User).toBeDefined();
@ -87,11 +87,11 @@ describe('Note Model', () => {
const note = await Note.create({ const note = await Note.create({
title: 'Test Note', title: 'Test Note',
user_id: user.id, user_id: user.id,
project_id: project.id project_id: project.id,
}); });
const noteWithProject = await Note.findByPk(note.id, { const noteWithProject = await Note.findByPk(note.id, {
include: [{ model: Project }] include: [{ model: Project }],
}); });
expect(noteWithProject.Project).toBeDefined(); expect(noteWithProject.Project).toBeDefined();

View file

@ -7,12 +7,12 @@ describe('Project Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
area = await Area.create({ area = await Area.create({
name: 'Work', name: 'Work',
user_id: user.id user_id: user.id,
}); });
}); });
@ -25,7 +25,7 @@ describe('Project Model', () => {
pin_to_sidebar: false, pin_to_sidebar: false,
priority: 1, priority: 1,
user_id: user.id, user_id: user.id,
area_id: area.id area_id: area.id,
}; };
const project = await Project.create(projectData); const project = await Project.create(projectData);
@ -42,7 +42,7 @@ describe('Project Model', () => {
it('should require name', async () => { it('should require name', async () => {
const projectData = { const projectData = {
description: 'Project without name', description: 'Project without name',
user_id: user.id user_id: user.id,
}; };
await expect(Project.create(projectData)).rejects.toThrow(); await expect(Project.create(projectData)).rejects.toThrow();
@ -50,7 +50,7 @@ describe('Project Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const projectData = { const projectData = {
name: 'Test Project' name: 'Test Project',
}; };
await expect(Project.create(projectData)).rejects.toThrow(); await expect(Project.create(projectData)).rejects.toThrow();
@ -60,7 +60,7 @@ describe('Project Model', () => {
const projectData = { const projectData = {
name: 'Test Project', name: 'Test Project',
user_id: user.id, user_id: user.id,
priority: 5 priority: 5,
}; };
await expect(Project.create(projectData)).rejects.toThrow(); await expect(Project.create(projectData)).rejects.toThrow();
@ -71,7 +71,7 @@ describe('Project Model', () => {
const project = await Project.create({ const project = await Project.create({
name: `Test Project ${priority}`, name: `Test Project ${priority}`,
user_id: user.id, user_id: user.id,
priority: priority priority: priority,
}); });
expect(project.priority).toBe(priority); expect(project.priority).toBe(priority);
} }
@ -82,7 +82,7 @@ describe('Project Model', () => {
it('should set correct default values', async () => { it('should set correct default values', async () => {
const project = await Project.create({ const project = await Project.create({
name: 'Test Project', name: 'Test Project',
user_id: user.id user_id: user.id,
}); });
expect(project.active).toBe(false); expect(project.active).toBe(false);
@ -98,7 +98,7 @@ describe('Project Model', () => {
description: null, description: null,
priority: null, priority: null,
due_date_at: null, due_date_at: null,
area_id: null area_id: null,
}); });
expect(project.description).toBeNull(); expect(project.description).toBeNull();
@ -112,11 +112,11 @@ describe('Project Model', () => {
it('should belong to a user', async () => { it('should belong to a user', async () => {
const project = await Project.create({ const project = await Project.create({
name: 'Test Project', name: 'Test Project',
user_id: user.id user_id: user.id,
}); });
const projectWithUser = await Project.findByPk(project.id, { const projectWithUser = await Project.findByPk(project.id, {
include: [{ model: User }] include: [{ model: User }],
}); });
expect(projectWithUser.User).toBeDefined(); expect(projectWithUser.User).toBeDefined();
@ -127,11 +127,11 @@ describe('Project Model', () => {
const project = await Project.create({ const project = await Project.create({
name: 'Test Project', name: 'Test Project',
user_id: user.id, user_id: user.id,
area_id: area.id area_id: area.id,
}); });
const projectWithArea = await Project.findByPk(project.id, { const projectWithArea = await Project.findByPk(project.id, {
include: [{ model: Area }] include: [{ model: Area }],
}); });
expect(projectWithArea.Area).toBeDefined(); expect(projectWithArea.Area).toBeDefined();

View file

@ -7,7 +7,7 @@ describe('Tag Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', 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 () => { it('should create a tag with valid data', async () => {
const tagData = { const tagData = {
name: 'work', name: 'work',
user_id: user.id user_id: user.id,
}; };
const tag = await Tag.create(tagData); const tag = await Tag.create(tagData);
@ -26,7 +26,7 @@ describe('Tag Model', () => {
it('should require name', async () => { it('should require name', async () => {
const tagData = { const tagData = {
user_id: user.id user_id: user.id,
}; };
await expect(Tag.create(tagData)).rejects.toThrow(); await expect(Tag.create(tagData)).rejects.toThrow();
@ -34,7 +34,7 @@ describe('Tag Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const tagData = { const tagData = {
name: 'work' name: 'work',
}; };
await expect(Tag.create(tagData)).rejects.toThrow(); await expect(Tag.create(tagData)).rejects.toThrow();
@ -44,17 +44,17 @@ describe('Tag Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const otherUser = await User.create({ const otherUser = await User.create({
email: 'other@example.com', email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
const tag1 = await Tag.create({ const tag1 = await Tag.create({
name: 'work', name: 'work',
user_id: user.id user_id: user.id,
}); });
const tag2 = await Tag.create({ const tag2 = await Tag.create({
name: 'work', name: 'work',
user_id: otherUser.id user_id: otherUser.id,
}); });
expect(tag1.name).toBe('work'); expect(tag1.name).toBe('work');
@ -68,11 +68,11 @@ describe('Tag Model', () => {
it('should belong to a user', async () => { it('should belong to a user', async () => {
const tag = await Tag.create({ const tag = await Tag.create({
name: 'work', name: 'work',
user_id: user.id user_id: user.id,
}); });
const tagWithUser = await Tag.findByPk(tag.id, { const tagWithUser = await Tag.findByPk(tag.id, {
include: [{ model: User }] include: [{ model: User }],
}); });
expect(tagWithUser.User).toBeDefined(); expect(tagWithUser.User).toBeDefined();

View file

@ -7,7 +7,7 @@ describe('Task Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', 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 = { const taskData = {
name: 'Test Task', name: 'Test Task',
description: 'Test Description', description: 'Test Description',
user_id: user.id user_id: user.id,
}; };
const task = await Task.create(taskData); const task = await Task.create(taskData);
@ -32,7 +32,7 @@ describe('Task Model', () => {
it('should require name', async () => { it('should require name', async () => {
const taskData = { const taskData = {
user_id: user.id user_id: user.id,
}; };
await expect(Task.create(taskData)).rejects.toThrow(); await expect(Task.create(taskData)).rejects.toThrow();
@ -40,7 +40,7 @@ describe('Task Model', () => {
it('should require user_id', async () => { it('should require user_id', async () => {
const taskData = { const taskData = {
name: 'Test Task' name: 'Test Task',
}; };
await expect(Task.create(taskData)).rejects.toThrow(); await expect(Task.create(taskData)).rejects.toThrow();
@ -50,7 +50,7 @@ describe('Task Model', () => {
const taskData = { const taskData = {
name: 'Test Task', name: 'Test Task',
user_id: user.id, user_id: user.id,
priority: 5 priority: 5,
}; };
await expect(Task.create(taskData)).rejects.toThrow(); await expect(Task.create(taskData)).rejects.toThrow();
@ -60,7 +60,7 @@ describe('Task Model', () => {
const taskData = { const taskData = {
name: 'Test Task', name: 'Test Task',
user_id: user.id, user_id: user.id,
status: 10 status: 10,
}; };
await expect(Task.create(taskData)).rejects.toThrow(); await expect(Task.create(taskData)).rejects.toThrow();
@ -89,7 +89,7 @@ describe('Task Model', () => {
beforeEach(async () => { beforeEach(async () => {
task = await Task.create({ task = await Task.create({
name: 'Test Task', 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 () => { it('should set correct default values', async () => {
const task = await Task.create({ const task = await Task.create({
name: 'Test Task', name: 'Test Task',
user_id: user.id user_id: user.id,
}); });
expect(task.today).toBe(false); expect(task.today).toBe(false);
@ -147,7 +147,7 @@ describe('Task Model', () => {
recurrence_interval: null, recurrence_interval: null,
recurrence_end_date: null, recurrence_end_date: null,
last_generated_date: null, last_generated_date: null,
project_id: null project_id: null,
}); });
expect(task.description).toBeNull(); expect(task.description).toBeNull();
@ -169,7 +169,7 @@ describe('Task Model', () => {
priority: Task.PRIORITY.HIGH, priority: Task.PRIORITY.HIGH,
status: Task.STATUS.IN_PROGRESS, status: Task.STATUS.IN_PROGRESS,
note: 'Test Note', note: 'Test Note',
user_id: user.id user_id: user.id,
}); });
expect(task.description).toBe('Test Description'); expect(task.description).toBe('Test Description');

View file

@ -6,7 +6,7 @@ describe('User Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const userData = { const userData = {
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}; };
const user = await User.create(userData); const user = await User.create(userData);
@ -21,7 +21,7 @@ describe('User Model', () => {
it('should require email', async () => { it('should require email', async () => {
const userData = { const userData = {
password: 'password123' password: 'password123',
}; };
await expect(User.create(userData)).rejects.toThrow(); await expect(User.create(userData)).rejects.toThrow();
@ -30,7 +30,7 @@ describe('User Model', () => {
it('should require valid email format', async () => { it('should require valid email format', async () => {
const userData = { const userData = {
email: 'invalid-email', email: 'invalid-email',
password: 'password123' password: 'password123',
}; };
await expect(User.create(userData)).rejects.toThrow(); await expect(User.create(userData)).rejects.toThrow();
@ -40,7 +40,7 @@ describe('User Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const userData = { const userData = {
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}; };
await User.create(userData); await User.create(userData);
@ -51,7 +51,7 @@ describe('User Model', () => {
const userData = { const userData = {
email: 'test@example.com', email: 'test@example.com',
password: 'password123', password: 'password123',
appearance: 'invalid' appearance: 'invalid',
}; };
await expect(User.create(userData)).rejects.toThrow(); await expect(User.create(userData)).rejects.toThrow();
@ -61,7 +61,7 @@ describe('User Model', () => {
const userData = { const userData = {
email: 'test@example.com', email: 'test@example.com',
password: 'password123', password: 'password123',
task_summary_frequency: 'invalid' task_summary_frequency: 'invalid',
}; };
await expect(User.create(userData)).rejects.toThrow(); await expect(User.create(userData)).rejects.toThrow();
@ -75,7 +75,7 @@ describe('User Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
user = await User.create({ user = await User.create({
email: 'test@example.com', 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 () => { 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); 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); expect(isInvalid).toBe(false);
}); });
@ -100,10 +106,16 @@ describe('User Model', () => {
expect(user.password_digest).not.toBe(oldPasswordDigest); 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); 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); expect(isValidOld).toBe(false);
}); });
@ -114,7 +126,10 @@ describe('User Model', () => {
expect(user.password_digest).not.toBe(oldPasswordDigest); 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); expect(isValidNew).toBe(true);
}); });
}); });
@ -124,7 +139,7 @@ describe('User Model', () => {
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
const user = await User.create({ const user = await User.create({
email: 'test@example.com', email: 'test@example.com',
password_digest: await bcrypt.hash('password123', 10) password_digest: await bcrypt.hash('password123', 10),
}); });
expect(user.appearance).toBe('light'); expect(user.appearance).toBe('light');

View file

@ -14,13 +14,17 @@ describe('Functional Services', () => {
it('should have pure helper functions for testing', () => { it('should have pure helper functions for testing', () => {
expect(typeof taskScheduler._createSchedulerState).toBe('function'); expect(typeof taskScheduler._createSchedulerState).toBe('function');
expect(typeof taskScheduler._shouldDisableScheduler).toBe('function'); expect(typeof taskScheduler._shouldDisableScheduler).toBe(
'function'
);
expect(typeof taskScheduler._getCronExpression).toBe('function'); expect(typeof taskScheduler._getCronExpression).toBe('function');
}); });
it('should return proper cron expressions', () => { it('should return proper cron expressions', () => {
expect(taskScheduler._getCronExpression('daily')).toBe('0 7 * * *'); 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 * * * *'); expect(taskScheduler._getCronExpression('1h')).toBe('0 * * * *');
}); });
}); });
@ -51,9 +55,12 @@ describe('Functional Services', () => {
expect(updatedUsers).toHaveLength(3); expect(updatedUsers).toHaveLength(3);
expect(users).toHaveLength(2); // Original array unchanged 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).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', () => { describe('TaskSummaryService', () => {
it('should export functional interface', () => { it('should export functional interface', () => {
expect(typeof taskSummaryService.generateSummaryForUser).toBe('function'); expect(typeof taskSummaryService.generateSummaryForUser).toBe(
expect(typeof taskSummaryService.sendSummaryToUser).toBe('function'); 'function'
expect(typeof taskSummaryService.calculateNextRunTime).toBe('function'); );
expect(typeof taskSummaryService.sendSummaryToUser).toBe(
'function'
);
expect(typeof taskSummaryService.calculateNextRunTime).toBe(
'function'
);
}); });
it('should have pure helper functions for testing', () => { it('should have pure helper functions for testing', () => {
expect(typeof taskSummaryService._escapeMarkdown).toBe('function'); expect(typeof taskSummaryService._escapeMarkdown).toBe('function');
expect(typeof taskSummaryService._getPriorityEmoji).toBe('function'); expect(typeof taskSummaryService._getPriorityEmoji).toBe(
expect(typeof taskSummaryService._buildTaskSection).toBe('function'); 'function'
);
expect(typeof taskSummaryService._buildTaskSection).toBe(
'function'
);
}); });
it('should escape markdown correctly', () => { it('should escape markdown correctly', () => {

View file

@ -17,11 +17,14 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1, priority: 1,
note: 'Parent note' note: 'Parent note',
}); });
const dueDate = new Date('2025-06-20T10:00:00Z'); 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.name).toBe(parentTask.name);
expect(childTask.description).toBe(parentTask.description); expect(childTask.description).toBe(parentTask.description);
@ -44,11 +47,14 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
project_id: null, // Changed to null to avoid foreign key issues 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 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.project_id).toBeNull();
expect(childTask.recurring_parent_id).toBe(parentTask.id); expect(childTask.recurring_parent_id).toBe(parentTask.id);
@ -62,11 +68,14 @@ describe('Parent-Child Relationship Functionality', () => {
user_id: user.id, user_id: user.id,
description: null, description: null,
note: null, note: null,
priority: 0 priority: 0,
}); });
const dueDate = new Date('2025-06-20T10:00:00Z'); 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.description).toBeNull();
expect(childTask.note).toBeNull(); expect(childTask.note).toBeNull();
@ -83,7 +92,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
childTask1 = await Task.create({ childTask1 = await Task.create({
@ -92,7 +101,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED status: Task.STATUS.NOT_STARTED,
}); });
childTask2 = await Task.create({ childTask2 = await Task.create({
@ -101,7 +110,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-21T10:00:00Z'), 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({ const childTasks = await Task.findAll({
where: { where: {
recurring_parent_id: parentTask.id, 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); expect(childTasks).toHaveLength(2);
@ -133,11 +142,15 @@ describe('Parent-Child Relationship Functionality', () => {
it('should distinguish between parent and child tasks', async () => { it('should distinguish between parent and child tasks', async () => {
const allTasks = await Task.findAll({ const allTasks = await Task.findAll({
where: { user_id: user.id }, where: { user_id: user.id },
order: [['id', 'ASC']] order: [['id', 'ASC']],
}); });
const parentTasks = allTasks.filter(t => t.recurrence_type !== 'none'); const parentTasks = allTasks.filter(
const childTasks = allTasks.filter(t => t.recurring_parent_id !== null); (t) => t.recurrence_type !== 'none'
);
const childTasks = allTasks.filter(
(t) => t.recurring_parent_id !== null
);
expect(parentTasks).toHaveLength(1); expect(parentTasks).toHaveLength(1);
expect(childTasks).toHaveLength(2); expect(childTasks).toHaveLength(2);
@ -149,7 +162,7 @@ describe('Parent-Child Relationship Functionality', () => {
name: 'Standalone Task', name: 'Standalone Task',
recurrence_type: 'none', recurrence_type: 'none',
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
expect(standaloneTask.recurring_parent_id).toBeFalsy(); // Can be null or undefined expect(standaloneTask.recurring_parent_id).toBeFalsy(); // Can be null or undefined
@ -165,10 +178,11 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
completion_based: true, completion_based: true,
user_id: user.id, 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).not.toBeNull();
expect(nextTask.name).toBe(parentTask.name); expect(nextTask.name).toBe(parentTask.name);
@ -189,19 +203,20 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
completion_based: true, completion_based: true,
user_id: user.id, user_id: user.id,
status: Task.STATUS.NOT_STARTED status: Task.STATUS.NOT_STARTED,
}); });
// Call completion multiple times quickly // Call completion multiple times quickly
const firstNextTask = await RecurringTaskService.handleTaskCompletion(parentTask); const firstNextTask =
await RecurringTaskService.handleTaskCompletion(parentTask);
expect(firstNextTask).not.toBeNull(); expect(firstNextTask).not.toBeNull();
// Check how many child tasks exist for this parent // Check how many child tasks exist for this parent
const childTasks = await Task.findAll({ const childTasks = await Task.findAll({
where: { where: {
recurring_parent_id: parentTask.id, 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 // 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_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
completion_based: true, completion_based: true,
user_id: user.id user_id: user.id,
}); });
const childTask = await Task.create({ const childTask = await Task.create({
@ -224,11 +239,12 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), 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 // Completing child task should not create new instances
const nextTask = await RecurringTaskService.handleTaskCompletion(childTask); const nextTask =
await RecurringTaskService.handleTaskCompletion(childTask);
expect(nextTask).toBeNull(); expect(nextTask).toBeNull();
}); });
}); });
@ -244,7 +260,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_weekday: null, recurrence_weekday: null,
completion_based: false, completion_based: false,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
childTask = await Task.create({ childTask = await Task.create({
@ -253,7 +269,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), 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_type: 'weekly',
recurrence_interval: 2, recurrence_interval: 2,
recurrence_weekday: 1, // Monday recurrence_weekday: 1, // Monday
completion_based: true completion_based: true,
}); });
const refreshedParent = await Task.findByPk(parentTask.id); const refreshedParent = await Task.findByPk(parentTask.id);
@ -286,13 +302,15 @@ describe('Parent-Child Relationship Functionality', () => {
const updatedParent = await Task.findByPk(parentTask.id); const updatedParent = await Task.findByPk(parentTask.id);
await updatedParent.update({ await updatedParent.update({
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 3 recurrence_interval: 3,
}); });
// Verify child maintains its specific properties // Verify child maintains its specific properties
const refreshedChild = await Task.findByPk(childTask.id); const refreshedChild = await Task.findByPk(childTask.id);
expect(refreshedChild.status).toBe(Task.STATUS.IN_PROGRESS); 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); expect(refreshedChild.recurring_parent_id).toBe(parentTask.id);
}); });
}); });
@ -306,7 +324,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
childTask1 = await Task.create({ childTask1 = await Task.create({
@ -315,7 +333,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED status: Task.STATUS.NOT_STARTED,
}); });
childTask2 = await Task.create({ childTask2 = await Task.create({
@ -324,7 +342,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-27T10:00:00Z'), 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 () => { it('should prevent deleting parent when child tasks exist due to foreign key constraint', async () => {
let errorThrown = false; await expect(parentTask.destroy()).rejects.toThrow();
try {
await parentTask.destroy();
} catch (error) {
errorThrown = true;
expect(error.name).toBe('SequelizeForeignKeyConstraintError');
}
expect(errorThrown).toBe(true); const error = await parentTask.destroy().catch((err) => err);
expect(error.name).toBe('SequelizeForeignKeyConstraintError');
// Verify parent and children still exist // Verify parent and children still exist
const existingParent = await Task.findByPk(parentTask.id); const existingParent = await Task.findByPk(parentTask.id);
@ -383,7 +396,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
const weeklyParent = await Task.create({ const weeklyParent = await Task.create({
@ -392,7 +405,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1, recurrence_weekday: 1,
user_id: user.id, user_id: user.id,
priority: 2 priority: 2,
}); });
// Create child tasks for each parent // Create child tasks for each parent
@ -419,7 +432,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_interval: 1, recurrence_interval: 1,
completion_based: true, completion_based: true,
user_id: user.id, user_id: user.id,
priority: 2 priority: 2,
}); });
const children = []; const children = [];
@ -427,7 +440,8 @@ describe('Parent-Child Relationship Functionality', () => {
// Generate 5 child tasks // Generate 5 child tasks
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
await parentTask.update({ status: Task.STATUS.DONE }); await parentTask.update({ status: Task.STATUS.DONE });
const nextTask = await RecurringTaskService.handleTaskCompletion(parentTask); const nextTask =
await RecurringTaskService.handleTaskCompletion(parentTask);
if (nextTask) { if (nextTask) {
children.push(nextTask); children.push(nextTask);
} }
@ -445,7 +459,7 @@ describe('Parent-Child Relationship Functionality', () => {
} }
// Verify no duplicate due dates // 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)]; const uniqueDueDates = [...new Set(dueDates)];
expect(uniqueDueDates.length).toBe(dueDates.length); expect(uniqueDueDates.length).toBe(dueDates.length);
}); });
@ -456,7 +470,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
const childTask = await Task.create({ const childTask = await Task.create({
@ -465,7 +479,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: parentTask.id, recurring_parent_id: parentTask.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), 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 // 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); expect(foundChild.recurring_parent_id).toBe(parentTask.id);
// Try to find parent through child // 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).not.toBeNull();
expect(foundParent.id).toBe(parentTask.id); expect(foundParent.id).toBe(parentTask.id);
}); });
@ -487,7 +503,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
const childTask = await Task.create({ const childTask = await Task.create({
@ -500,7 +516,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_week_of_month: null, recurrence_week_of_month: null,
completion_based: false, completion_based: false,
user_id: user.id, user_id: user.id,
status: Task.STATUS.NOT_STARTED status: Task.STATUS.NOT_STARTED,
}); });
expect(childTask.recurrence_type).toBe('none'); expect(childTask.recurrence_type).toBe('none');
@ -519,7 +535,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_weekday: 5, // Friday recurrence_weekday: 5, // Friday
recurring_parent_id: null, recurring_parent_id: null,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
expect(parentTask.recurrence_type).toBe('weekly'); 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 () => { 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({ const user1Parent = await Task.create({
name: 'User 1 Parent', name: 'User 1 Parent',
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: user.id, user_id: user.id,
priority: 1 priority: 1,
}); });
const user2Parent = await Task.create({ const user2Parent = await Task.create({
@ -544,7 +562,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1, recurrence_interval: 1,
user_id: otherUser.id, user_id: otherUser.id,
priority: 1 priority: 1,
}); });
const user1Child = await Task.create({ const user1Child = await Task.create({
@ -553,7 +571,7 @@ describe('Parent-Child Relationship Functionality', () => {
recurring_parent_id: user1Parent.id, recurring_parent_id: user1Parent.id,
user_id: user.id, user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'), 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 // Verify child belongs to correct user
@ -561,13 +579,21 @@ describe('Parent-Child Relationship Functionality', () => {
expect(user1Child.recurring_parent_id).toBe(user1Parent.id); expect(user1Child.recurring_parent_id).toBe(user1Parent.id);
// Verify users can't see each other's tasks // Verify users can't see each other's tasks
const user1Tasks = await Task.findAll({ where: { user_id: user.id } }); const user1Tasks = await Task.findAll({
const user2Tasks = await Task.findAll({ where: { user_id: otherUser.id } }); where: { user_id: user.id },
});
const user2Tasks = await Task.findAll({
where: { user_id: otherUser.id },
});
expect(user1Tasks.length).toBe(2); // parent + child expect(user1Tasks.length).toBe(2); // parent + child
expect(user2Tasks.length).toBe(1); // just parent expect(user2Tasks.length).toBe(1); // just parent
expect(user1Tasks.find(t => t.id === user2Parent.id)).toBeUndefined(); expect(
expect(user2Tasks.find(t => t.id === user1Parent.id)).toBeUndefined(); 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', () => { it('should calculate next daily occurrence correctly', () => {
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-01-16T10:00:00Z'));
}); });
@ -20,10 +23,13 @@ describe('RecurringTaskService', () => {
it('should handle custom daily intervals', () => { it('should handle custom daily intervals', () => {
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 3 recurrence_interval: 3,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-01-18T10:00:00Z'));
}); });
@ -31,10 +37,13 @@ describe('RecurringTaskService', () => {
it('should handle edge case with zero interval', () => { it('should handle edge case with zero interval', () => {
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 0 recurrence_interval: 0,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-01-16T10:00:00Z'));
}); });
@ -45,10 +54,13 @@ describe('RecurringTaskService', () => {
it('should calculate next weekly occurrence correctly', () => { it('should calculate next weekly occurrence correctly', () => {
const task = { const task = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); // Wednesday 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')); expect(nextDate).toEqual(new Date('2025-01-22T10:00:00Z'));
}); });
@ -57,10 +69,13 @@ describe('RecurringTaskService', () => {
const task = { const task = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1 // Monday recurrence_weekday: 1, // Monday
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); // Wednesday 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 expect(nextDate).toEqual(new Date('2025-01-20T10:00:00Z')); // Next Monday
}); });
@ -68,10 +83,13 @@ describe('RecurringTaskService', () => {
it('should handle bi-weekly recurrence', () => { it('should handle bi-weekly recurrence', () => {
const task = { const task = {
recurrence_type: 'weekly', recurrence_type: 'weekly',
recurrence_interval: 2 recurrence_interval: 2,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-01-29T10:00:00Z'));
}); });
@ -82,10 +100,13 @@ describe('RecurringTaskService', () => {
it('should calculate next monthly occurrence correctly', () => { it('should calculate next monthly occurrence correctly', () => {
const task = { const task = {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-02-15T10:00:00Z'));
}); });
@ -93,10 +114,13 @@ describe('RecurringTaskService', () => {
it('should handle month boundaries correctly', () => { it('should handle month boundaries correctly', () => {
const task = { const task = {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-31T10:00:00Z'); // January 31st 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 // February only has 28 days in 2025, should go to Feb 28
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
@ -105,10 +129,13 @@ describe('RecurringTaskService', () => {
it('should handle leap year correctly', () => { it('should handle leap year correctly', () => {
const task = { const task = {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2024-01-29T10:00:00Z'); // 2024 is a leap year 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')); expect(nextDate).toEqual(new Date('2024-02-29T10:00:00Z'));
}); });
@ -116,10 +143,13 @@ describe('RecurringTaskService', () => {
it('should handle custom monthly intervals', () => { it('should handle custom monthly intervals', () => {
const task = { const task = {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 3 recurrence_interval: 3,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-04-15T10:00:00Z'));
}); });
@ -128,10 +158,13 @@ describe('RecurringTaskService', () => {
const task = { const task = {
recurrence_type: 'monthly', recurrence_type: 'monthly',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_month_day: 5 recurrence_month_day: 5,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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')); expect(nextDate).toEqual(new Date('2025-02-05T10:00:00Z'));
}); });
@ -144,10 +177,13 @@ describe('RecurringTaskService', () => {
recurrence_type: 'monthly_weekday', recurrence_type: 'monthly_weekday',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 1, // Monday 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 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 // First Monday of February 2025 is February 3rd
expect(nextDate).toEqual(new Date('2025-02-03T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-02-03T10:00:00Z'));
@ -158,10 +194,13 @@ describe('RecurringTaskService', () => {
recurrence_type: 'monthly_weekday', recurrence_type: 'monthly_weekday',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 5, // Friday 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 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 // Last Friday of February 2025 is February 28th
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
@ -172,10 +211,13 @@ describe('RecurringTaskService', () => {
recurrence_type: 'monthly_weekday', recurrence_type: 'monthly_weekday',
recurrence_interval: 1, recurrence_interval: 1,
recurrence_weekday: 3, // Wednesday 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 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 // Third Wednesday of February 2025 is February 19th
expect(nextDate).toEqual(new Date('2025-02-19T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-02-19T10:00:00Z'));
@ -187,10 +229,13 @@ describe('RecurringTaskService', () => {
it('should calculate last day of month correctly', () => { it('should calculate last day of month correctly', () => {
const task = { const task = {
recurrence_type: 'monthly_last_day', recurrence_type: 'monthly_last_day',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); 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 // Last day of February 2025 is February 28th
expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-02-28T10:00:00Z'));
@ -199,10 +244,13 @@ describe('RecurringTaskService', () => {
it('should handle leap year last day correctly', () => { it('should handle leap year last day correctly', () => {
const task = { const task = {
recurrence_type: 'monthly_last_day', 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 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 // Last day of February 2024 is February 29th
expect(nextDate).toEqual(new Date('2024-02-29T10:00:00Z')); expect(nextDate).toEqual(new Date('2024-02-29T10:00:00Z'));
@ -211,10 +259,13 @@ describe('RecurringTaskService', () => {
it('should handle different month lengths', () => { it('should handle different month lengths', () => {
const task = { const task = {
recurrence_type: 'monthly_last_day', 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 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 // Last day of May 2025 is May 31st
expect(nextDate).toEqual(new Date('2025-05-31T10:00:00Z')); expect(nextDate).toEqual(new Date('2025-05-31T10:00:00Z'));
@ -226,10 +277,13 @@ describe('RecurringTaskService', () => {
it('should return null for unsupported recurrence type', () => { it('should return null for unsupported recurrence type', () => {
const task = { const task = {
recurrence_type: 'invalid_type', recurrence_type: 'invalid_type',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); const fromDate = new Date('2025-01-15T10:00:00Z');
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate); const nextDate = RecurringTaskService.calculateNextDueDate(
task,
fromDate
);
expect(nextDate).toBeNull(); expect(nextDate).toBeNull();
}); });
@ -237,10 +291,13 @@ describe('RecurringTaskService', () => {
it('should return null for none recurrence type', () => { it('should return null for none recurrence type', () => {
const task = { const task = {
recurrence_type: 'none', recurrence_type: 'none',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('2025-01-15T10:00:00Z'); const fromDate = new Date('2025-01-15T10:00:00Z');
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate); const nextDate = RecurringTaskService.calculateNextDueDate(
task,
fromDate
);
expect(nextDate).toBeNull(); expect(nextDate).toBeNull();
}); });
@ -248,10 +305,13 @@ describe('RecurringTaskService', () => {
it('should handle invalid date inputs gracefully', () => { it('should handle invalid date inputs gracefully', () => {
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_interval: 1 recurrence_interval: 1,
}; };
const fromDate = new Date('invalid-date'); const fromDate = new Date('invalid-date');
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate); const nextDate = RecurringTaskService.calculateNextDueDate(
task,
fromDate
);
expect(nextDate).toBeNull(); expect(nextDate).toBeNull();
}); });
@ -259,7 +319,10 @@ describe('RecurringTaskService', () => {
it('should handle missing task properties', () => { it('should handle missing task properties', () => {
const task = {}; // No recurrence properties const task = {}; // No recurrence properties
const fromDate = new Date('2025-01-15T10:00:00Z'); const fromDate = new Date('2025-01-15T10:00:00Z');
const nextDate = RecurringTaskService.calculateNextDueDate(task, fromDate); const nextDate = RecurringTaskService.calculateNextDueDate(
task,
fromDate
);
expect(nextDate).toBeNull(); expect(nextDate).toBeNull();
}); });
@ -269,36 +332,62 @@ describe('RecurringTaskService', () => {
describe('Helper Functions', () => { describe('Helper Functions', () => {
describe('_getFirstWeekdayOfMonth', () => { describe('_getFirstWeekdayOfMonth', () => {
it('should find first Monday of January 2025', () => { 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 expect(date.getDate()).toBe(6); // January 6, 2025 is the first Monday
}); });
it('should find first Sunday of February 2025', () => { 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 expect(date.getDate()).toBe(2); // February 2, 2025 is the first Sunday
}); });
}); });
describe('_getLastWeekdayOfMonth', () => { describe('_getLastWeekdayOfMonth', () => {
it('should find last Friday of January 2025', () => { 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 expect(date.getDate()).toBe(31); // January 31, 2025 is the last Friday
}); });
it('should find last Monday of February 2025', () => { 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 expect(date.getDate()).toBe(24); // February 24, 2025 is the last Monday
}); });
}); });
describe('_getNthWeekdayOfMonth', () => { describe('_getNthWeekdayOfMonth', () => {
it('should find second Tuesday of March 2025', () => { 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 expect(date.getDate()).toBe(11); // March 11, 2025 is the second Tuesday
}); });
it('should find fourth Thursday of April 2025', () => { 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 expect(date.getDate()).toBe(24); // April 24, 2025 is the fourth Thursday
}); });
}); });
@ -315,7 +404,7 @@ describe('RecurringTaskService', () => {
priority: 1, priority: 1,
note: 'Test note', note: 'Test note',
user_id: 1, user_id: 1,
project_id: 2 project_id: 2,
}; };
const dueDate = new Date('2025-01-20T10:00:00Z'); const dueDate = new Date('2025-01-20T10:00:00Z');
@ -331,11 +420,14 @@ describe('RecurringTaskService', () => {
user_id: template.user_id, user_id: template.user_id,
project_id: template.project_id, project_id: template.project_id,
recurrence_type: 'none', recurrence_type: 'none',
recurring_parent_id: template.id recurring_parent_id: template.id,
}); });
Task.create = mockCreate; Task.create = mockCreate;
const result = await RecurringTaskService.createTaskInstance(template, dueDate); const result = await RecurringTaskService.createTaskInstance(
template,
dueDate
);
expect(mockCreate).toHaveBeenCalledWith({ expect(mockCreate).toHaveBeenCalledWith({
name: template.name, name: template.name,
@ -348,7 +440,7 @@ describe('RecurringTaskService', () => {
user_id: template.user_id, user_id: template.user_id,
project_id: template.project_id, project_id: template.project_id,
recurrence_type: 'none', recurrence_type: 'none',
recurring_parent_id: template.id recurring_parent_id: template.id,
}); });
expect(result.recurring_parent_id).toBe(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', () => { it('should generate task when no end date is set', () => {
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_end_date: null recurrence_end_date: null,
}; };
const nextDate = new Date('2025-12-31T10: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(true); expect(shouldGenerate).toBe(true);
}); });
it('should generate task when next date is before end date', () => { it('should generate task when next date is before end date', () => {
const task = { const task = {
recurrence_type: 'daily', 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 nextDate = new Date('2025-06-15T10:00:00Z');
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate); const shouldGenerate =
RecurringTaskService._shouldGenerateNextTask(
task,
nextDate
);
expect(shouldGenerate).toBe(true); expect(shouldGenerate).toBe(true);
}); });
it('should not generate task when next date is after end date', () => { it('should not generate task when next date is after end date', () => {
const task = { const task = {
recurrence_type: 'daily', 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 nextDate = new Date('2025-12-31T10:00:00Z');
const shouldGenerate = RecurringTaskService._shouldGenerateNextTask(task, nextDate); const shouldGenerate =
RecurringTaskService._shouldGenerateNextTask(
task,
nextDate
);
expect(shouldGenerate).toBe(false); expect(shouldGenerate).toBe(false);
}); });
@ -396,11 +500,15 @@ describe('RecurringTaskService', () => {
const endDate = new Date('2025-06-15T10:00:00Z'); const endDate = new Date('2025-06-15T10:00:00Z');
const task = { const task = {
recurrence_type: 'daily', recurrence_type: 'daily',
recurrence_end_date: endDate recurrence_end_date: endDate,
}; };
const nextDate = new Date('2025-06-15T10: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(false); expect(shouldGenerate).toBe(false);
}); });
}); });
@ -408,18 +516,36 @@ describe('RecurringTaskService', () => {
describe('Service Interface', () => { describe('Service Interface', () => {
it('should export all required methods', () => { it('should export all required methods', () => {
expect(typeof RecurringTaskService.generateRecurringTasks).toBe('function'); expect(typeof RecurringTaskService.generateRecurringTasks).toBe(
expect(typeof RecurringTaskService.processRecurringTask).toBe('function'); 'function'
expect(typeof RecurringTaskService.calculateNextDueDate).toBe('function'); );
expect(typeof RecurringTaskService.createTaskInstance).toBe('function'); expect(typeof RecurringTaskService.processRecurringTask).toBe(
expect(typeof RecurringTaskService.handleTaskCompletion).toBe('function'); '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', () => { it('should have helper functions for testing', () => {
expect(typeof RecurringTaskService._getFirstWeekdayOfMonth).toBe('function'); expect(typeof RecurringTaskService._getFirstWeekdayOfMonth).toBe(
expect(typeof RecurringTaskService._getLastWeekdayOfMonth).toBe('function'); 'function'
expect(typeof RecurringTaskService._getNthWeekdayOfMonth).toBe('function'); );
expect(typeof RecurringTaskService._shouldGenerateNextTask).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: { User: {
update: jest.fn(), update: jest.fn(),
findAll: jest.fn(), findAll: jest.fn(),
findOne: jest.fn() findOne: jest.fn(),
}, },
InboxItem: { InboxItem: {
create: jest.fn(), create: jest.fn(),
findOne: jest.fn() findOne: jest.fn(),
} },
})); }));
// Mock https module // Mock https module
jest.mock('https', () => ({ jest.mock('https', () => ({
get: jest.fn(), get: jest.fn(),
request: jest.fn() request: jest.fn(),
})); }));
describe('TelegramPoller Duplicate Prevention', () => { describe('TelegramPoller Duplicate Prevention', () => {
@ -29,7 +29,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
mockUser = { mockUser = {
id: 1, id: 1,
telegram_bot_token: 'test-token', telegram_bot_token: 'test-token',
telegram_chat_id: '123456789' telegram_chat_id: '123456789',
}; };
// Reset poller state // Reset poller state
@ -39,14 +39,35 @@ describe('TelegramPoller Duplicate Prevention', () => {
describe('Update ID Tracking', () => { describe('Update ID Tracking', () => {
test('should filter out already processed updates', () => { test('should filter out already processed updates', () => {
const 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: 100,
{ update_id: 102, message: { text: 'Hello 3', message_id: 3, chat: { id: 123 } } } 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 // Test internal function for filtering
const processedUpdates = new Set(['1-100', '1-101']); const processedUpdates = new Set(['1-100', '1-101']);
const newUpdates = updates.filter(update => { const newUpdates = updates.filter((update) => {
const updateKey = `1-${update.update_id}`; const updateKey = `1-${update.update_id}`;
return !processedUpdates.has(updateKey); return !processedUpdates.has(updateKey);
}); });
@ -59,7 +80,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
const updates = [ const updates = [
{ update_id: 98 }, { update_id: 98 },
{ update_id: 101 }, { update_id: 101 },
{ update_id: 99 } { update_id: 99 },
]; ];
const highestUpdateId = telegramPoller._getHighestUpdateId(updates); const highestUpdateId = telegramPoller._getHighestUpdateId(updates);
@ -101,32 +122,39 @@ describe('TelegramPoller Duplicate Prevention', () => {
const users = [ const users = [
{ id: 1, name: 'User 1' }, { id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' }, { id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' } { id: 3, name: 'User 3' },
]; ];
const updatedUsers = telegramPoller._removeUserFromList(users, 2); const updatedUsers = telegramPoller._removeUserFromList(users, 2);
expect(updatedUsers).toHaveLength(2); expect(updatedUsers).toHaveLength(2);
expect(updatedUsers.find(u => u.id === 2)).toBeUndefined(); expect(updatedUsers.find((u) => u.id === 2)).toBeUndefined();
expect(updatedUsers.find(u => u.id === 1)).toBeDefined(); expect(updatedUsers.find((u) => u.id === 1)).toBeDefined();
expect(updatedUsers.find(u => u.id === 3)).toBeDefined(); expect(updatedUsers.find((u) => u.id === 3)).toBeDefined();
}); });
}); });
describe('Message Parameters', () => { describe('Message Parameters', () => {
test('should create message parameters without reply', () => { test('should create message parameters without reply', () => {
const params = telegramPoller._createMessageParams('123', 'Hello World'); const params = telegramPoller._createMessageParams(
'123',
'Hello World'
);
expect(params).toEqual({ expect(params).toEqual({
chat_id: '123', chat_id: '123',
text: 'Hello World' text: 'Hello World',
}); });
}); });
test('should create message parameters with reply', () => { 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({ expect(params).toEqual({
chat_id: '123', chat_id: '123',
text: 'Hello World', 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', () => { test('should create URL with parameters', () => {
const url = telegramPoller._createTelegramUrl('token123', 'getUpdates', { const url = telegramPoller._createTelegramUrl(
'token123',
'getUpdates',
{
offset: '100', offset: '100',
timeout: '30' timeout: '30',
}); }
expect(url).toBe('https://api.telegram.org/bottoken123/getUpdates?offset=100&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, pollInterval: 5000,
usersToPool: [], usersToPool: [],
userStatus: {}, userStatus: {},
processedUpdates: expect.any(Set) processedUpdates: expect.any(Set),
}); });
}); });
@ -165,7 +199,7 @@ describe('TelegramPoller Duplicate Prevention', () => {
running: false, running: false,
usersCount: 0, usersCount: 0,
pollInterval: 5000, pollInterval: 5000,
userStatus: {} userStatus: {},
}); });
}); });
}); });

17090
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff