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",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
"@fastify/helmet": "^13.0.2",
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@fastify/jwt": "^10.0.0",
|
"@fastify/jwt": "^10.0.0",
|
||||||
@@ -25,6 +26,35 @@
|
|||||||
"nodemon": "^3.1.14"
|
"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": {
|
"node_modules/@fastify/accept-negotiator": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
@@ -1757,6 +1800,12 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"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": {
|
"node_modules/undefsafe": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.80.0",
|
||||||
"@fastify/cors": "^11.2.0",
|
"@fastify/cors": "^11.2.0",
|
||||||
"@fastify/helmet": "^13.0.2",
|
"@fastify/helmet": "^13.0.2",
|
||||||
"@fastify/jwt": "^10.0.0",
|
"@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/groups'), { prefix: '/api/v1' });
|
||||||
app.register(require('./routes/auth'), { prefix: '/api/v1' });
|
app.register(require('./routes/auth'), { prefix: '/api/v1' });
|
||||||
app.register(require('./routes/connectors'), { 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
|
// Start
|
||||||
const start = async () => {
|
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;
|
||||||
67
apps/tasks/app/calendar/page.tsx
Normal file
67
apps/tasks/app/calendar/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
'use client';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import FullCalendar from '@fullcalendar/react';
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
|
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||||
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
scheduled_at: string | null;
|
||||||
|
due_at: string | null;
|
||||||
|
status: string;
|
||||||
|
group_name: string;
|
||||||
|
group_color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CalendarPage() {
|
||||||
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_URL}/api/v1/tasks?limit=100`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => setTasks(d.data || []));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const events = tasks
|
||||||
|
.filter((t): t is Task & { scheduled_at: string } | Task & { due_at: string } =>
|
||||||
|
t.scheduled_at !== null || t.due_at !== null
|
||||||
|
)
|
||||||
|
.map(t => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.title,
|
||||||
|
start: (t.scheduled_at || t.due_at) as string,
|
||||||
|
end: (t.due_at || t.scheduled_at) as string,
|
||||||
|
backgroundColor: t.group_color || '#3B82F6',
|
||||||
|
borderColor: t.group_color || '#3B82F6',
|
||||||
|
extendedProps: { status: t.status, group: t.group_name }
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Kalendar</h1>
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow">
|
||||||
|
<FullCalendar
|
||||||
|
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||||
|
initialView="timeGridWeek"
|
||||||
|
headerToolbar={{
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
}}
|
||||||
|
events={events}
|
||||||
|
editable={true}
|
||||||
|
selectable={true}
|
||||||
|
locale="cs"
|
||||||
|
firstDay={1}
|
||||||
|
height="auto"
|
||||||
|
slotMinTime="06:00:00"
|
||||||
|
slotMaxTime="23:00:00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
apps/tasks/app/sw-register.tsx
Normal file
10
apps/tasks/app/sw-register.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
export default function SWRegister() {
|
||||||
|
useEffect(() => {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000";
|
const API_BASE = typeof window !== "undefined" ? "" : (process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000");
|
||||||
|
|
||||||
interface ApiOptions {
|
interface ApiOptions {
|
||||||
method?: string;
|
method?: string;
|
||||||
@@ -14,15 +14,21 @@ async function apiFetch<T>(path: string, opts: ApiOptions = {}): Promise<T> {
|
|||||||
headers["Authorization"] = `Bearer ${opts.token}`;
|
headers["Authorization"] = `Bearer ${opts.token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}${path}`, {
|
const url = `${API_BASE}${path}`;
|
||||||
|
const res = await fetch(url, {
|
||||||
method: opts.method || "GET",
|
method: opts.method || "GET",
|
||||||
headers,
|
headers,
|
||||||
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
||||||
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ message: res.statusText }));
|
const err = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
throw new Error(err.message || `HTTP ${res.status}`);
|
throw new Error(err.message || err.error || `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 204 || res.headers.get("content-length") === "0") {
|
||||||
|
return {} as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "standalone",
|
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
66
apps/tasks/package-lock.json
generated
66
apps/tasks/package-lock.json
generated
@@ -8,6 +8,10 @@
|
|||||||
"name": "tasks",
|
"name": "tasks",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fullcalendar/daygrid": "^6.1.20",
|
||||||
|
"@fullcalendar/interaction": "^6.1.20",
|
||||||
|
"@fullcalendar/react": "^6.1.20",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.20",
|
||||||
"next": "14.2.35",
|
"next": "14.2.35",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18"
|
||||||
@@ -133,6 +137,57 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fullcalendar/core": {
|
||||||
|
"version": "6.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz",
|
||||||
|
"integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"preact": "~10.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/daygrid": {
|
||||||
|
"version": "6.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz",
|
||||||
|
"integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/interaction": {
|
||||||
|
"version": "6.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz",
|
||||||
|
"integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/react": {
|
||||||
|
"version": "6.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz",
|
||||||
|
"integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.20",
|
||||||
|
"react": "^16.7.0 || ^17 || ^18 || ^19",
|
||||||
|
"react-dom": "^16.7.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fullcalendar/timegrid": {
|
||||||
|
"version": "6.1.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz",
|
||||||
|
"integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@fullcalendar/daygrid": "~6.1.20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fullcalendar/core": "~6.1.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@@ -2284,6 +2339,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -4558,6 +4614,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||||
|
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|||||||
@@ -9,18 +9,22 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fullcalendar/daygrid": "^6.1.20",
|
||||||
|
"@fullcalendar/interaction": "^6.1.20",
|
||||||
|
"@fullcalendar/react": "^6.1.20",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.20",
|
||||||
|
"next": "14.2.35",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18"
|
||||||
"next": "14.2.35"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.35",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"eslint": "^8",
|
"typescript": "^5"
|
||||||
"eslint-config-next": "14.2.35"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
apps/tasks/public/icon-192.png
Normal file
0
apps/tasks/public/icon-192.png
Normal file
4
apps/tasks/public/icon-192.png.svg
Normal file
4
apps/tasks/public/icon-192.png.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
||||||
|
<rect width="192" height="192" rx="32" fill="#3B82F6"/>
|
||||||
|
<text x="96" y="120" text-anchor="middle" font-size="100" fill="white" font-family="sans-serif">T</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 260 B |
0
apps/tasks/public/icon-512.png
Normal file
0
apps/tasks/public/icon-512.png
Normal file
@@ -1,22 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Task Team",
|
"name": "Task Team",
|
||||||
"short_name": "Tasks",
|
"short_name": "Tasks",
|
||||||
"description": "Sprava ukolu pro tym",
|
"description": "Správa úkolů pro tým",
|
||||||
"start_url": "/",
|
"start_url": "/tasks",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#0a0a0a",
|
||||||
"theme_color": "#3b82f6",
|
"theme_color": "#3B82F6",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{"src": "/icon-192.png", "sizes": "192x192", "type": "image/png"},
|
||||||
"src": "/icon-192.png",
|
{"src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
|
||||||
"sizes": "192x192",
|
],
|
||||||
"type": "image/png"
|
"categories": ["productivity", "utilities"],
|
||||||
},
|
"lang": "cs",
|
||||||
{
|
"dir": "ltr"
|
||||||
"src": "/icon-512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
apps/tasks/public/sw.js
Normal file
27
apps/tasks/public/sw.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const CACHE = "taskteam-v1";
|
||||||
|
const PRECACHE = ["/tasks", "/login", "/manifest.json"];
|
||||||
|
|
||||||
|
self.addEventListener("install", (e) => {
|
||||||
|
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(PRECACHE)));
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (e) => {
|
||||||
|
e.waitUntil(caches.keys().then((keys) =>
|
||||||
|
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
||||||
|
));
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (e) => {
|
||||||
|
if (e.request.method !== "GET") return;
|
||||||
|
e.respondWith(
|
||||||
|
fetch(e.request)
|
||||||
|
.then((r) => {
|
||||||
|
const clone = r.clone();
|
||||||
|
caches.open(CACHE).then((c) => c.put(e.request, clone));
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(e.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user