Linting cleanup (#99)
* Add eslint and prettier dependencies and configs * Lint project.
This commit is contained in:
parent
dd6ec117d0
commit
e594d1075b
97 changed files with 26554 additions and 39840 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
.idea
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.sqlite3-shm
|
*.sqlite3-shm
|
||||||
*.sqlite3-wal
|
*.sqlite3-wal
|
||||||
|
|
|
||||||
6
backend/.prettierrc.json
Normal file
6
backend/.prettierrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
|
|
@ -19,37 +19,60 @@ const sessionStore = new SequelizeStore({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Middlewares
|
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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
40
backend/eslint.config.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
files: ['**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
globals: {
|
||||||
|
require: 'readonly',
|
||||||
|
module: 'readonly',
|
||||||
|
exports: 'readonly',
|
||||||
|
__dirname: 'readonly',
|
||||||
|
__filename: 'readonly',
|
||||||
|
process: 'readonly',
|
||||||
|
console: 'readonly',
|
||||||
|
Buffer: 'readonly',
|
||||||
|
global: 'readonly',
|
||||||
|
setTimeout: 'readonly',
|
||||||
|
clearTimeout: 'readonly',
|
||||||
|
setInterval: 'readonly',
|
||||||
|
clearInterval: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
prettier: require('eslint-plugin-prettier'),
|
||||||
|
jest: require('eslint-plugin-jest'),
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...require('eslint-plugin-prettier').configs.recommended.rules,
|
||||||
|
...require('eslint-plugin-jest').configs.recommended.rules,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.test.js', '**/*.spec.js', 'tests/**/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...require('eslint-plugin-jest').environments.globals.globals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
module.exports = {
|
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,
|
||||||
};
|
};
|
||||||
|
|
@ -27,5 +27,5 @@ const requireAuth = async (req, res, next) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
requireAuth
|
requireAuth,
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
4370
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,37 +23,43 @@
|
||||||
"migration:undo": "npx sequelize-cli db:migrate:undo",
|
"migration:undo": "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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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') +
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -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
17090
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue