fix: Build prueft wieder Typen -- 7 Fehler behoben, Notion-Anbindung war defekt
typescript.ignoreBuildErrors steht jetzt auf false. Ein erfolgreicher Build ist damit wieder ein Beleg dafuer, dass der Code uebersetzbar ist. Die Ursache der 30-Minuten-Laufzeit war nicht die Groesse von WeeklyView.tsx, wie ich vermutet hatte, sondern das include-Muster **/* in tsconfig.json. Auf src/**/* eingegrenzt laeuft der volle Typecheck in 19 Sekunden. Die sieben gefundenen Fehler: - notion-calendar.ts rief notion.databases.query() auf. Die Methode gibt es in @notionhq/client v5 nicht mehr -- die Notion-Anbindung war zur Laufzeit schlicht kaputt und niemandem aufgefallen. Migriert auf dataSources.query(), aber mangels Notion-Zugang ungetestet. Gespeicherte Verbindungen enthalten alte database_ids und muessen neu eingerichtet werden. - WeeklyView.tsx importierte isAllDayEvent noch aus format statt date-utils -- mein eigener Fehler aus v1.118.0, der ohne scharfen Typecheck durchgerutscht war. - Export-Helfer lagen in einer route.ts, wo Next.js nur Route-Handler erlaubt; SettingsSidebar importierte sie von dort und zog einen Server-Handler in den Client-Graphen. Jetzt in src/lib/export-fields.ts. - push-notifications.ts: BufferSource-Cast fuer applicationServerKey. - web-push hat keine Typen; lokale Deklaration statt neuer Abhaengigkeit. Verifiziert: npm run build Exit 0 mit scharfem Typecheck, 63 Unit-Tests gruen. v1.118.1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5c5f2a284e
commit
db99b486c7
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,6 +2,33 @@
|
||||
|
||||
All notable changes to this project are documented in this file.
|
||||
|
||||
## [1.118.1] - 2026-09-07
|
||||
|
||||
### Fixed
|
||||
- **Der Build prüft wieder Typen.** `typescript.ignoreBuildErrors` steht auf `false`.
|
||||
Damit ist ein erfolgreicher Build endlich wieder ein Beleg dafür, dass der Code
|
||||
übersetzbar ist — bisher war er es nicht.
|
||||
- **Notion-Anbindung war defekt.** Der Code rief `notion.databases.query()` auf; in
|
||||
`@notionhq/client` v5 gibt es diese Methode nicht mehr, der Aufruf lief zur Laufzeit in
|
||||
ein "is not a function". Umgestellt auf `dataSources.query()`, die Suche filtert jetzt
|
||||
auf `data_source`. **Ungetestet** (kein Notion-Zugang), und gespeicherte Verbindungen
|
||||
enthalten alte `database_id`s — betroffene Nutzer müssen Notion neu verbinden.
|
||||
- `WeeklyView.tsx` importierte `isAllDayEvent` weiterhin aus `format`, obwohl die Funktion
|
||||
in v1.118.0 nach `date-utils` gewandert war. Wäre zur Laufzeit gescheitert.
|
||||
- Export-Helfer (`EXPORT_FIELDS`, `isoWeek`, `weekLabel`, `csvCell` …) liegen jetzt in
|
||||
`src/lib/export-fields.ts`. Next.js erlaubt in einer `route.ts` nur Route-Handler als
|
||||
Exporte; zudem importierte `SettingsSidebar` sie direkt aus der Route und zog damit
|
||||
einen Server-Handler in den Client-Graphen.
|
||||
- `push-notifications.ts`: `applicationServerKey` braucht seit TypeScript 5.7 einen Cast
|
||||
auf `BufferSource`.
|
||||
- Typdeklaration für `web-push` in `src/types/web-push.d.ts` ergänzt — als lokale Datei
|
||||
statt als Abhängigkeit, weil `npm install` im Projektordner nicht läuft.
|
||||
|
||||
### Changed
|
||||
- `tsconfig.json`: `include` von `**/*` auf `src/**/*` eingegrenzt. **Das war die Ursache
|
||||
dafür, dass der Typecheck über 30 Minuten lief** — nicht die Größe von `WeeklyView.tsx`,
|
||||
wie zuvor vermutet. Der vollständige Lauf dauert jetzt **19 Sekunden**.
|
||||
|
||||
## [1.118.0] - 2026-09-07
|
||||
|
||||
### Added
|
||||
|
||||
29
TODO.md
29
TODO.md
@ -104,11 +104,13 @@ Anleitung siehe [`docs/secret-rotation.md`](docs/secret-rotation.md).
|
||||
- [x] **Schritt 1:** `eslint.ignoreDuringBuilds` in `next.config.js` auf `false`
|
||||
- [x] Gemeldete ESLint-Fehler abarbeiten — es waren nur **2** (`prefer-const` in
|
||||
`SettingsSidebar.tsx`), verifiziert: `next lint` endet jetzt mit Exit 0
|
||||
- [ ] **Schritt 2:** `npm run type-check` laufen lassen, Fehlerzahl notieren
|
||||
- [ ] Fehler abarbeiten — modulweise, nicht auf einmal
|
||||
- [ ] **Schritt 3:** `typescript.ignoreBuildErrors` auf `false`
|
||||
- [x] **Schritt 2:** `npm run type-check` — es waren **7 Fehler**, alle behoben
|
||||
- [x] **Schritt 3:** `typescript.ignoreBuildErrors` auf `false` — Build prüft jetzt Typen
|
||||
- [x] `tsconfig.json`: `include` von `**/*` auf `src/**/*` eingegrenzt. **Das war die
|
||||
Ursache der Langsamkeit**, nicht die Dateigröße: Der Lauf dauert jetzt **19 Sekunden**
|
||||
statt über 30 Minuten. Meine gegenteilige Vermutung war falsch.
|
||||
- [ ] **3 Dateien mit `@ts-nocheck`** aufspüren und entschärfen — die überspringen den
|
||||
Typecheck vollständig, `ignoreBuildErrors: false` würde sie gar nicht erfassen
|
||||
Typecheck vollständig, auch mit `ignoreBuildErrors: false`
|
||||
- [ ] `any`-Stellen reduzieren — ESLint zählt **470** `no-explicit-any` — fortlaufend, kein Blocker
|
||||
- [ ] **209** ungenutzte Variablen (`no-unused-vars`) aufräumen — fortlaufend
|
||||
- [ ] **11** unvollständige Hook-Abhängigkeiten (`react-hooks/exhaustive-deps`) prüfen —
|
||||
@ -254,6 +256,25 @@ F1. Kommt etwas anderes heraus, wurde es längst getauscht und du kannst F1 abha
|
||||
|
||||
---
|
||||
|
||||
## Block H — Aus dem scharfen Typecheck
|
||||
|
||||
### H1. Notion-Anbindung war defekt *(migriert, aber ungetestet)*
|
||||
- [ ] Migration praktisch prüfen (braucht Notion-Zugang)
|
||||
- [ ] Betroffene Nutzer informieren: Notion-Verbindung muss neu eingerichtet werden
|
||||
|
||||
**Warum:** Der Code rief `notion.databases.query()` auf. Die Methode gibt es in
|
||||
`@notionhq/client` v5 nicht mehr (nur noch retrieve/create/update) — der Aufruf lief zur
|
||||
Laufzeit in ein "is not a function". **Die Notion-Anbindung war damit schlicht kaputt**, und
|
||||
das ist niemandem aufgefallen, weil der Build Typfehler verschluckte.
|
||||
|
||||
Umgestellt auf `dataSources.query()`; die Suche filtert jetzt auf `data_source` statt
|
||||
`database`. Das ist in sich konsistent, aber **mangels Notion-Zugang nicht erprobt**.
|
||||
|
||||
⚠️ **Gespeicherte Verbindungen enthalten alte `database_id`s. Das sind keine
|
||||
`data_source_id`s** — betroffene Nutzer müssen Notion einmal neu verbinden.
|
||||
|
||||
---
|
||||
|
||||
## Block G — Infrastruktur (Supabase-LXC 201)
|
||||
|
||||
### G1. Container läuft privilegiert *(Sicherheitsthema)*
|
||||
|
||||
@ -9,7 +9,10 @@ const nextConfig = {
|
||||
ignoreDuringBuilds: false,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
// Aus: Der Build bricht bei Typfehlern ab. Bis v1.118.1 stand hier true — dadurch
|
||||
// liefen mehrfach fehlende Importe durch, die zur Laufzeit ReferenceError geworfen
|
||||
// haetten, waehrend der Build "erfolgreich" meldete.
|
||||
ignoreBuildErrors: false,
|
||||
},
|
||||
optimizeFonts: false,
|
||||
env: {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.118.0",
|
||||
"version": "1.118.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.118.0",
|
||||
"version": "1.118.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@auth/prisma-adapter": "^2.11.1",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "my-weekly-todo-list",
|
||||
"version": "1.118.0",
|
||||
"version": "1.118.1",
|
||||
"description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
EXPORT_FIELDS, ExportFieldKey,
|
||||
fetchExportData, weekLabel, fmtDate, priorityLabel, priorityColor,
|
||||
} from '../export/route';
|
||||
} from '@/lib/export-fields';
|
||||
|
||||
// ── PDF styles ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -2,162 +2,8 @@ import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// ── Field registry ─────────────────────────────────────────────────────────
|
||||
export type ExportFieldKey =
|
||||
| 'week'
|
||||
| 'weekGoal'
|
||||
| 'completedDate'
|
||||
| 'scheduledDate'
|
||||
| 'createdDate'
|
||||
| 'project'
|
||||
| 'list'
|
||||
| 'priority'
|
||||
| 'url'
|
||||
| 'notes'
|
||||
| 'description'
|
||||
| 'delegatedTo'
|
||||
| 'delegationNote'
|
||||
| 'startTime'
|
||||
| 'kanbanStage';
|
||||
|
||||
export const EXPORT_FIELDS: {
|
||||
key: ExportFieldKey;
|
||||
labelDe: string;
|
||||
labelEn: string;
|
||||
defaultOn: boolean;
|
||||
csvOnly?: boolean; // not a table column, only used as a section in PDF/CSV
|
||||
}[] = [
|
||||
{ key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true },
|
||||
{ key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true },
|
||||
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
|
||||
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
|
||||
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
|
||||
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
|
||||
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
|
||||
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
|
||||
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
|
||||
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
|
||||
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
|
||||
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
|
||||
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
|
||||
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false },
|
||||
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false },
|
||||
];
|
||||
|
||||
// ── Shared helpers (also used by export-pdf route) ─────────────────────────
|
||||
|
||||
export function isoWeek(date: Date): string {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } {
|
||||
const [year, w] = weekKey.split('-W');
|
||||
const d = new Date(Date.UTC(parseInt(year), 0, 1));
|
||||
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
|
||||
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6);
|
||||
return { mon: d, sun };
|
||||
}
|
||||
|
||||
export function weekLabel(weekKey: string, de: boolean): string {
|
||||
const [, w] = weekKey.split('-W');
|
||||
const year = weekKey.split('-W')[0];
|
||||
const { mon, sun } = weekMondaySunday(weekKey);
|
||||
const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||
return de
|
||||
? `KW ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`
|
||||
: `Week ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`;
|
||||
}
|
||||
|
||||
export function fmtDate(d: Date | null | undefined, de: boolean): string {
|
||||
if (!d) return '';
|
||||
return de
|
||||
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function priorityLabel(task: any, de: boolean): string {
|
||||
if (task.urgency != null && task.importance != null) {
|
||||
if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now';
|
||||
if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule';
|
||||
if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
|
||||
return de ? 'Eliminieren' : 'Eliminate';
|
||||
}
|
||||
if (task.priority) return task.priority;
|
||||
return '';
|
||||
}
|
||||
|
||||
export function priorityColor(task: any): string {
|
||||
if (task.urgency != null && task.importance != null) {
|
||||
if (task.urgency && task.importance) return '#ef4444';
|
||||
if (!task.urgency && task.importance) return '#3b82f6';
|
||||
if (task.urgency && !task.importance) return '#f97316';
|
||||
return '#9ca3af';
|
||||
}
|
||||
const map: Record<string, string> = { A: '#ef4444', B: '#f97316', C: '#eab308', D: '#3b82f6', E: '#9ca3af' };
|
||||
return task.priority ? (map[task.priority] || '#6b7280') : '#6b7280';
|
||||
}
|
||||
|
||||
function csvCell(value: string): string {
|
||||
return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`;
|
||||
}
|
||||
|
||||
const DEFAULT_GOAL = 'your goal of this week';
|
||||
|
||||
// ── Fetch shared export data (also used by PDF route) ──────────────────────
|
||||
|
||||
export async function fetchExportData(userId: string, startDate: string | null, endDate: string | null) {
|
||||
const where: any = { userId, completed: true, deletedAt: null };
|
||||
if (startDate || endDate) {
|
||||
where.updatedAt = {};
|
||||
if (startDate) where.updatedAt.gte = new Date(startDate);
|
||||
if (endDate) {
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
where.updatedAt.lte = end;
|
||||
}
|
||||
}
|
||||
|
||||
const [tasks, goals] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: {
|
||||
project: { select: { name: true, color: true } },
|
||||
somedayList: { select: { title: true } },
|
||||
},
|
||||
}),
|
||||
prisma.weeklyGoal.findMany({
|
||||
where: { userId },
|
||||
select: { weekStart: true, text: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Build week → goal map (weekStart is always Monday)
|
||||
const goalByWeek = new Map<string, string>();
|
||||
for (const g of goals) {
|
||||
if (g.text && g.text !== DEFAULT_GOAL) {
|
||||
goalByWeek.set(isoWeek(g.weekStart), g.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Group tasks by ISO week
|
||||
const byWeek = new Map<string, typeof tasks>();
|
||||
for (const task of tasks) {
|
||||
const key = isoWeek(task.updatedAt);
|
||||
if (!byWeek.has(key)) byWeek.set(key, []);
|
||||
byWeek.get(key)!.push(task);
|
||||
}
|
||||
|
||||
return { byWeek, goalByWeek };
|
||||
}
|
||||
|
||||
// ── CSV route ──────────────────────────────────────────────────────────────
|
||||
import { EXPORT_FIELDS, fetchExportData, weekLabel, fmtDate, priorityLabel, csvCell } from '@/lib/export-fields';
|
||||
import type { ExportFieldKey } from '@/lib/export-fields';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@ -10,7 +10,7 @@ import { ViewStyle, KanbanStage, Task } from "./WeeklyView";
|
||||
import { AVAILABLE_FONTS, isCustomFont } from "../lib/fontConstants";
|
||||
import { translations } from "../lib/weeklyViewTranslations";
|
||||
import { WeatherDisplayKey, WEATHER_DISPLAY_DEFAULTS } from "../lib/weeklyViewConstants";
|
||||
import { EXPORT_FIELDS, type ExportFieldKey } from "../app/api/user/export/route";
|
||||
import { EXPORT_FIELDS, type ExportFieldKey } from "@/lib/export-fields";
|
||||
import {
|
||||
ArrowLeftRight,
|
||||
Briefcase,
|
||||
|
||||
@ -105,9 +105,9 @@ import {
|
||||
} from "@/lib/weekly/device-settings";
|
||||
import type { DeviceViewSettingKey } from "@/lib/weekly/device-settings";
|
||||
import {
|
||||
stripHtml, DEFAULT_SOMEDAY_SLOT_COUNT, getSomedaySlotCount,
|
||||
isAllDayEvent, invertColor,
|
||||
stripHtml, DEFAULT_SOMEDAY_SLOT_COUNT, getSomedaySlotCount, invertColor,
|
||||
} from "@/lib/weekly/format";
|
||||
import { isAllDayEvent } from "@/lib/weekly/date-utils";
|
||||
|
||||
// Types
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
170
src/lib/export-fields.ts
Normal file
170
src/lib/export-fields.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Felddefinitionen und Helfer für den Datenexport.
|
||||
*
|
||||
* Lagen bis v1.118.1 in `app/api/user/export/route.ts`. Next.js erlaubt in einer route.ts
|
||||
* nur Route-Handler als Exporte — alles andere lässt die generierte Typprüfung auflaufen
|
||||
* ("does not satisfy the constraint"). Ausserdem importierte SettingsSidebar EXPORT_FIELDS
|
||||
* aus der Route und zog damit einen Server-Handler in den Client-Graphen.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// ── Field registry ─────────────────────────────────────────────────────────
|
||||
|
||||
export type ExportFieldKey =
|
||||
| 'week'
|
||||
| 'weekGoal'
|
||||
| 'completedDate'
|
||||
| 'scheduledDate'
|
||||
| 'createdDate'
|
||||
| 'project'
|
||||
| 'list'
|
||||
| 'priority'
|
||||
| 'url'
|
||||
| 'notes'
|
||||
| 'description'
|
||||
| 'delegatedTo'
|
||||
| 'delegationNote'
|
||||
| 'startTime'
|
||||
| 'kanbanStage';
|
||||
|
||||
export const EXPORT_FIELDS: {
|
||||
key: ExportFieldKey;
|
||||
labelDe: string;
|
||||
labelEn: string;
|
||||
defaultOn: boolean;
|
||||
csvOnly?: boolean; // not a table column, only used as a section in PDF/CSV
|
||||
}[] = [
|
||||
{ key: 'week', labelDe: 'Kalenderwoche', labelEn: 'Calendar week', defaultOn: true, csvOnly: true },
|
||||
{ key: 'weekGoal', labelDe: 'Wochenziel', labelEn: 'Goal of the week', defaultOn: true, csvOnly: true },
|
||||
{ key: 'completedDate', labelDe: 'Erledigt am', labelEn: 'Completed', defaultOn: true },
|
||||
{ key: 'scheduledDate', labelDe: 'Geplant für', labelEn: 'Scheduled for', defaultOn: true },
|
||||
{ key: 'createdDate', labelDe: 'Erstellt am', labelEn: 'Created', defaultOn: false },
|
||||
{ key: 'project', labelDe: 'Projekt', labelEn: 'Project', defaultOn: true },
|
||||
{ key: 'list', labelDe: 'Liste', labelEn: 'List', defaultOn: true },
|
||||
{ key: 'priority', labelDe: 'Priorität', labelEn: 'Priority', defaultOn: true },
|
||||
{ key: 'url', labelDe: 'Link / URL', labelEn: 'Link / URL', defaultOn: false },
|
||||
{ key: 'notes', labelDe: 'Notiz', labelEn: 'Notes', defaultOn: false },
|
||||
{ key: 'description', labelDe: 'Beschreibung', labelEn: 'Description', defaultOn: false },
|
||||
{ key: 'delegatedTo', labelDe: 'Delegiert an', labelEn: 'Delegated to', defaultOn: false },
|
||||
{ key: 'delegationNote', labelDe: 'Delegationsnotiz', labelEn: 'Delegation note', defaultOn: false },
|
||||
{ key: 'startTime', labelDe: 'Uhrzeit', labelEn: 'Start time', defaultOn: false },
|
||||
{ key: 'kanbanStage', labelDe: 'Kanban-Stage', labelEn: 'Kanban stage', defaultOn: false },
|
||||
];
|
||||
|
||||
// ── Shared helpers (also used by export-pdf route) ─────────────────────────
|
||||
|
||||
export function isoWeek(date: Date): string {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
||||
return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function weekMondaySunday(weekKey: string): { mon: Date; sun: Date } {
|
||||
const [year, w] = weekKey.split('-W');
|
||||
const d = new Date(Date.UTC(parseInt(year), 0, 1));
|
||||
d.setUTCDate(d.getUTCDate() + (parseInt(w) - 1) * 7 - (d.getUTCDay() || 7) + 1);
|
||||
const sun = new Date(d); sun.setUTCDate(d.getUTCDate() + 6);
|
||||
return { mon: d, sun };
|
||||
}
|
||||
|
||||
export function weekLabel(weekKey: string, de: boolean): string {
|
||||
const [, w] = weekKey.split('-W');
|
||||
const year = weekKey.split('-W')[0];
|
||||
const { mon, sun } = weekMondaySunday(weekKey);
|
||||
const fmt = (x: Date) => x.toLocaleDateString(de ? 'de-DE' : 'en-GB', { day: '2-digit', month: 'short', timeZone: 'UTC' });
|
||||
return de
|
||||
? `KW ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`
|
||||
: `Week ${w} — ${year} (${fmt(mon)} – ${fmt(sun)})`;
|
||||
}
|
||||
|
||||
export function fmtDate(d: Date | null | undefined, de: boolean): string {
|
||||
if (!d) return '';
|
||||
return de
|
||||
? d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
: d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
export function priorityLabel(task: any, de: boolean): string {
|
||||
if (task.urgency != null && task.importance != null) {
|
||||
if (task.urgency && task.importance) return de ? 'Sofort erledigen' : 'Do now';
|
||||
if (!task.urgency && task.importance) return de ? 'Planen' : 'Schedule';
|
||||
if (task.urgency && !task.importance) return de ? 'Delegieren' : 'Delegate';
|
||||
return de ? 'Eliminieren' : 'Eliminate';
|
||||
}
|
||||
if (task.priority) return task.priority;
|
||||
return '';
|
||||
}
|
||||
|
||||
export function priorityColor(task: any): string {
|
||||
if (task.urgency != null && task.importance != null) {
|
||||
if (task.urgency && task.importance) return '#ef4444';
|
||||
if (!task.urgency && task.importance) return '#3b82f6';
|
||||
if (task.urgency && !task.importance) return '#f97316';
|
||||
return '#9ca3af';
|
||||
}
|
||||
const map: Record<string, string> = { A: '#ef4444', B: '#f97316', C: '#eab308', D: '#3b82f6', E: '#9ca3af' };
|
||||
return task.priority ? (map[task.priority] || '#6b7280') : '#6b7280';
|
||||
}
|
||||
|
||||
export function csvCell(value: string): string {
|
||||
return `"${(value ?? '').replace(/"/g, '""').replace(/\n/g, ' ')}"`;
|
||||
}
|
||||
|
||||
const DEFAULT_GOAL = 'your goal of this week';
|
||||
|
||||
// ── Fetch shared export data (also used by PDF route) ──────────────────────
|
||||
|
||||
export async function fetchExportData(userId: string, startDate: string | null, endDate: string | null) {
|
||||
const where: any = { userId, completed: true, deletedAt: null };
|
||||
if (startDate || endDate) {
|
||||
where.updatedAt = {};
|
||||
if (startDate) where.updatedAt.gte = new Date(startDate);
|
||||
if (endDate) {
|
||||
const end = new Date(endDate);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
where.updatedAt.lte = end;
|
||||
}
|
||||
}
|
||||
|
||||
const [tasks, goals] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
where,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: {
|
||||
project: { select: { name: true, color: true } },
|
||||
somedayList: { select: { title: true } },
|
||||
},
|
||||
}),
|
||||
prisma.weeklyGoal.findMany({
|
||||
where: { userId },
|
||||
select: { weekStart: true, text: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
// Build week → goal map (weekStart is always Monday)
|
||||
const goalByWeek = new Map<string, string>();
|
||||
for (const g of goals) {
|
||||
if (g.text && g.text !== DEFAULT_GOAL) {
|
||||
goalByWeek.set(isoWeek(g.weekStart), g.text);
|
||||
}
|
||||
}
|
||||
|
||||
// Group tasks by ISO week
|
||||
const byWeek = new Map<string, typeof tasks>();
|
||||
for (const task of tasks) {
|
||||
const key = isoWeek(task.updatedAt);
|
||||
if (!byWeek.has(key)) byWeek.set(key, []);
|
||||
byWeek.get(key)!.push(task);
|
||||
}
|
||||
|
||||
return { byWeek, goalByWeek };
|
||||
}
|
||||
|
||||
// ── CSV route ──────────────────────────────────────────────────────────────
|
||||
@ -20,19 +20,37 @@ export interface NotionEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all databases shared with the integration
|
||||
* ACHTUNG — Migration auf @notionhq/client v5, ungetestet.
|
||||
*
|
||||
* Der bisherige Code rief `notion.databases.query()` auf. Diese Methode gibt es in SDK v5
|
||||
* nicht mehr (nur noch retrieve/create/update), der Aufruf lief also zur Laufzeit in ein
|
||||
* "is not a function". Die Notion-Anbindung war damit defekt — aufgefallen erst, als der
|
||||
* Typecheck scharf geschaltet wurde.
|
||||
*
|
||||
* Umgestellt auf `dataSources.query()`. Beides ist konsistent: Die Suche liefert jetzt
|
||||
* data sources, deren `id` direkt als `data_source_id` taugt.
|
||||
*
|
||||
* WICHTIG: Bereits gespeicherte Notion-Verbindungen enthalten alte database_ids. Die sind
|
||||
* KEINE data_source_ids — betroffene Nutzer müssen ihre Notion-Verbindung einmal neu
|
||||
* einrichten. Mangels Notion-Zugang ist nichts davon praktisch erprobt.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Find all data sources shared with the integration
|
||||
*/
|
||||
export async function getUserDatabases(accessToken: string): Promise<NotionDatabase[]> {
|
||||
const notion = new Client({ auth: accessToken });
|
||||
|
||||
// SDK v5: Was frueher "database" hiess, heisst jetzt "data source". Der alte Wert
|
||||
// 'database' ist kein gueltiger Filterwert mehr.
|
||||
const response = await notion.search({
|
||||
filter: { property: 'object', value: 'database' },
|
||||
filter: { property: 'object', value: 'data_source' },
|
||||
page_size: 50,
|
||||
});
|
||||
|
||||
const databases: NotionDatabase[] = [];
|
||||
for (const result of response.results) {
|
||||
if (result.object !== 'database') continue;
|
||||
if (result.object !== 'data_source') continue;
|
||||
const db = result as any;
|
||||
const title = db.title?.map((t: any) => t.plain_text).join('') || 'Untitled';
|
||||
|
||||
@ -79,8 +97,8 @@ export async function getUpcomingEvents(
|
||||
let startCursor: string | undefined;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await notion.databases.query({
|
||||
database_id: databaseId,
|
||||
const response = await notion.dataSources.query({
|
||||
data_source_id: databaseId,
|
||||
filter: {
|
||||
and: [
|
||||
{
|
||||
|
||||
@ -55,7 +55,10 @@ export async function subscribeToPush(
|
||||
try {
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
|
||||
// Cast noetig: TypeScript typisiert Uint8Array seit 5.7 als Uint8Array<ArrayBufferLike>,
|
||||
// BufferSource verlangt aber ArrayBuffer. urlBase64ToUint8Array liefert immer einen
|
||||
// echten ArrayBuffer, der SharedArrayBuffer-Fall kann hier nicht auftreten.
|
||||
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) as BufferSource,
|
||||
});
|
||||
return subscription;
|
||||
} catch (err) {
|
||||
|
||||
27
src/types/web-push.d.ts
vendored
Normal file
27
src/types/web-push.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Minimale Typdeklaration für `web-push`.
|
||||
*
|
||||
* Das Paket liefert keine eigenen Typen, und `@types/web-push` als Abhängigkeit aufzunehmen
|
||||
* scheitert daran, dass `npm install` im Projektordner nicht läuft (CIFS-Freigabe ohne
|
||||
* Symlink-Unterstützung). Deklariert ist nur, was das Projekt tatsächlich benutzt.
|
||||
*/
|
||||
declare module "web-push" {
|
||||
export interface PushSubscription {
|
||||
endpoint: string;
|
||||
keys: { p256dh: string; auth: string };
|
||||
}
|
||||
|
||||
export interface SendResult {
|
||||
statusCode: number;
|
||||
body: string;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export function setVapidDetails(subject: string, publicKey: string, privateKey: string): void;
|
||||
export function sendNotification(
|
||||
subscription: PushSubscription,
|
||||
payload?: string | Buffer | null,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<SendResult>;
|
||||
export function generateVAPIDKeys(): { publicKey: string; privateKey: string };
|
||||
}
|
||||
@ -31,8 +31,8 @@
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user