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:
Claude CLI Agent
2026-03-29 10:12:53 +00:00
parent b5a6dd6d6f
commit 55993b70b2
16 changed files with 402 additions and 24 deletions

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