Phase 2: AI Chat, Calendar, Odoo connector, PWA
- AI Chat endpoint (/api/v1/chat) with Claude API context - Calendar page with FullCalendar.js (day/week/month) - Odoo API connector (import/export/webhook) - PWA manifest + service worker for offline - SW register component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
49
api/package-lock.json
generated
49
api/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
@@ -25,6 +26,35 @@
|
||||
"nodemon": "^3.1.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.80.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.80.0.tgz",
|
||||
"integrity": "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/accept-negotiator": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz",
|
||||
@@ -1056,6 +1086,19 @@
|
||||
"url": "https://github.com/Eomm/json-schema-resolver?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -1757,6 +1800,12 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.80.0",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
|
||||
@@ -27,6 +27,8 @@ app.register(require('./routes/tasks'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/groups'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/auth'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/connectors'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/connectors/odoo'), { prefix: '/api/v1' });
|
||||
app.register(require('./routes/chat'), { prefix: '/api/v1' });
|
||||
|
||||
// Start
|
||||
const start = async () => {
|
||||
|
||||
36
api/src/routes/chat.js
Normal file
36
api/src/routes/chat.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Task Team — AI Chat Agent — 2026-03-29
|
||||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
|
||||
async function chatRoutes(app) {
|
||||
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||||
|
||||
app.post('/chat', async (req, reply) => {
|
||||
const { message, context } = req.body;
|
||||
|
||||
if (!message) {
|
||||
reply.code(400);
|
||||
return { error: 'Message is required' };
|
||||
}
|
||||
|
||||
// Get user's tasks and groups for context
|
||||
const { rows: tasks } = await app.db.query(
|
||||
'SELECT title, status, priority, scheduled_at FROM tasks ORDER BY created_at DESC LIMIT 20'
|
||||
);
|
||||
const { rows: groups } = await app.db.query('SELECT name, color FROM task_groups ORDER BY order_index');
|
||||
|
||||
const systemPrompt = `Jsi Task Team AI asistent. Pomáháš uživateli s organizací úkolů, plánováním a cíli.
|
||||
Aktuální úkoly: ${JSON.stringify(tasks.map(t => ({title: t.title, status: t.status, priority: t.priority})))}
|
||||
Skupiny: ${groups.map(g => g.name).join(', ')}
|
||||
Odpovídej v češtině, stručně a prakticky. Pokud uživatel chce vytvořit úkol, navrhni strukturu.`;
|
||||
|
||||
const response = await anthropic.messages.create({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
max_tokens: 1024,
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: message }]
|
||||
});
|
||||
|
||||
return { data: { reply: response.content[0].text, model: response.model } };
|
||||
});
|
||||
}
|
||||
module.exports = chatRoutes;
|
||||
112
api/src/routes/connectors/odoo.js
Normal file
112
api/src/routes/connectors/odoo.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// Task Team — Odoo API Connector — 2026-03-29
|
||||
// Bidirectional sync between Task Team and Odoo 19
|
||||
|
||||
const ODOO_ENTERPRISE_URL = process.env.ODOO_ENT_URL || 'http://10.10.10.20:8069';
|
||||
const ODOO_COMMUNITY_URL = process.env.ODOO_COM_URL || 'http://10.10.10.20:8070';
|
||||
|
||||
async function odooConnector(app) {
|
||||
|
||||
// Odoo XML-RPC helpers
|
||||
async function odooAuth(url, db, login, password) {
|
||||
const res = await fetch(`${url}/jsonrpc`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0', method: 'call', id: 1,
|
||||
params: { service: 'common', method: 'authenticate', args: [db, login, password, {}] }
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
async function odooCall(url, db, uid, password, model, method, args = [], kwargs = {}) {
|
||||
const res = await fetch(`${url}/jsonrpc`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0', method: 'call', id: 2,
|
||||
params: { service: 'object', method: 'execute_kw', args: [db, uid, password, model, method, args, kwargs] }
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.result;
|
||||
}
|
||||
|
||||
// Test Odoo connection
|
||||
app.get('/connectors/odoo/test', async (req) => {
|
||||
try {
|
||||
const uid = await odooAuth(ODOO_ENTERPRISE_URL, 'odoo_enterprise', 'admin', 'admin');
|
||||
return { status: 'ok', uid, server: 'enterprise' };
|
||||
} catch (e) {
|
||||
return { status: 'error', message: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Sync tasks from Odoo to Task Team
|
||||
app.post('/connectors/odoo/sync/import', async (req) => {
|
||||
const { db, login, password, server } = req.body;
|
||||
const url = server === 'community' ? ODOO_COMMUNITY_URL : ODOO_ENTERPRISE_URL;
|
||||
|
||||
const uid = await odooAuth(url, db, login, password);
|
||||
if (!uid) return { status: 'error', message: 'Auth failed' };
|
||||
|
||||
const tasks = await odooCall(url, db, uid, password, 'project.task', 'search_read',
|
||||
[[['active', '=', true]]], { fields: ['name', 'description', 'stage_id', 'priority', 'date_deadline'], limit: 100 });
|
||||
|
||||
let imported = 0;
|
||||
for (const t of tasks) {
|
||||
await app.db.query(
|
||||
`INSERT INTO tasks (title, description, external_id, external_source, due_at, priority)
|
||||
VALUES ($1, $2, $3, 'odoo', $4, $5)
|
||||
ON CONFLICT DO NOTHING`,
|
||||
[t.name, t.description || '', `odoo:${t.id}`, t.date_deadline, t.priority === '1' ? 'urgent' : 'medium']
|
||||
);
|
||||
imported++;
|
||||
}
|
||||
return { status: 'ok', imported, total_odoo: tasks.length };
|
||||
});
|
||||
|
||||
// Export tasks from Task Team to Odoo
|
||||
app.post('/connectors/odoo/sync/export', async (req) => {
|
||||
const { db, login, password, server, project_id } = req.body;
|
||||
const url = server === 'community' ? ODOO_COMMUNITY_URL : ODOO_ENTERPRISE_URL;
|
||||
|
||||
const uid = await odooAuth(url, db, login, password);
|
||||
if (!uid) return { status: 'error', message: 'Auth failed' };
|
||||
|
||||
const { rows: tasks } = await app.db.query(
|
||||
"SELECT * FROM tasks WHERE external_source IS NULL OR external_source != 'odoo' LIMIT 50"
|
||||
);
|
||||
|
||||
let exported = 0;
|
||||
for (const t of tasks) {
|
||||
const vals = { name: t.title, description: t.description || '' };
|
||||
if (project_id) vals.project_id = project_id;
|
||||
const taskId = await odooCall(url, db, uid, password, 'project.task', 'create', [vals]);
|
||||
await app.db.query('UPDATE tasks SET external_id=$1, external_source=$2 WHERE id=$3',
|
||||
[`odoo:${taskId}`, 'odoo', t.id]);
|
||||
exported++;
|
||||
}
|
||||
return { status: 'ok', exported };
|
||||
});
|
||||
|
||||
// Webhook from Odoo (for real-time sync)
|
||||
app.post('/connectors/odoo/webhook', async (req) => {
|
||||
const { event, model, record_id, data } = req.body;
|
||||
app.log.info({ event, model, record_id }, 'Odoo webhook received');
|
||||
|
||||
if (model === 'project.task' && event === 'write') {
|
||||
// Update corresponding task in Task Team
|
||||
if (data.name) {
|
||||
await app.db.query(
|
||||
"UPDATE tasks SET title=$1, updated_at=NOW() WHERE external_id=$2",
|
||||
[data.name, `odoo:${record_id}`]
|
||||
);
|
||||
}
|
||||
}
|
||||
return { status: 'received' };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = odooConnector;
|
||||
Reference in New Issue
Block a user