feat(ai-act-kompass): calendar reminders via iCalendar (.ics) export
- New RFC 5545 generator: all future obligation deadlines (per system) plus upcoming AI Act application dates as all-day events, each with two DISPLAY alarms (30 and 7 days ahead), stable UIDs for re-import/update, proper escaping and 75-octet line folding, CRLF - Export button in the tasks view downloads ai-act-fristen-<date>.ics (text/calendar) — opens in Outlook, Apple Calendar, Google Calendar, Thunderbird on phone and desktop; no server involved (local-first) - Event titles and descriptions localized via the active content pack (all nine languages); button and hint translated in all dictionaries - 5 new tests: calendar frame, future-only filter, stable UIDs, alarm count, escaping/folding, localized summaries (63 total) Claude-Session: https://claude.ai/code/session_017YjohVNx1ZGNM4MX7tHpfv
This commit is contained in:
parent
9b25985281
commit
3a08392669
105
ai-act-kompass/src/engine/ics.test.ts
Normal file
105
ai-act-kompass/src/engine/ics.test.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { CONTENT_PACK, getContentPack } from '../content/pack';
|
||||||
|
import { buildChecklist } from './checklist';
|
||||||
|
import { checklistIcs } from './ics';
|
||||||
|
import { classify } from './classify';
|
||||||
|
import type { AISystem, AssessmentAnswers } from './types';
|
||||||
|
|
||||||
|
const answers: AssessmentAnswers = {
|
||||||
|
isAISystem: true,
|
||||||
|
role: 'deployer',
|
||||||
|
isGPAIModel: false,
|
||||||
|
prohibitedPractices: [],
|
||||||
|
annexIIICategories: ['a3-employment'],
|
||||||
|
art63Exception: false,
|
||||||
|
involvesProfiling: false,
|
||||||
|
isAnnexISafetyComponent: false,
|
||||||
|
transparencyTriggers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const system: AISystem = {
|
||||||
|
id: 'sys-1',
|
||||||
|
name: 'Bewerber-Screening; GmbH, Test',
|
||||||
|
purpose: 'Vorauswahl',
|
||||||
|
vendor: '',
|
||||||
|
owner: '',
|
||||||
|
createdAt: '2026-07-01T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-07-01T00:00:00.000Z',
|
||||||
|
answers,
|
||||||
|
classification: classify(answers, CONTENT_PACK),
|
||||||
|
obligationStatus: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const TODAY = '2026-07-04';
|
||||||
|
const TIMESTAMP = '2026-07-04T12:00:00.000Z';
|
||||||
|
|
||||||
|
function makeIcs(): string {
|
||||||
|
const checklist = buildChecklist([system], CONTENT_PACK, ['DE'], TODAY);
|
||||||
|
return checklistIcs({
|
||||||
|
entries: [...checklist.dueNow, ...checklist.upcoming],
|
||||||
|
deadlines: CONTENT_PACK.deadlines,
|
||||||
|
today: TODAY,
|
||||||
|
timestamp: TIMESTAMP,
|
||||||
|
calendarName: 'AI-Act-Kompass',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('checklistIcs', () => {
|
||||||
|
it('erzeugt einen gültigen VCALENDAR-Rahmen mit CRLF', () => {
|
||||||
|
const ics = makeIcs();
|
||||||
|
expect(ics.startsWith('BEGIN:VCALENDAR\r\n')).toBe(true);
|
||||||
|
expect(ics.endsWith('END:VCALENDAR\r\n')).toBe(true);
|
||||||
|
expect(ics).toContain('VERSION:2.0');
|
||||||
|
expect(ics).toContain('X-WR-CALNAME:AI-Act-Kompass');
|
||||||
|
expect(ics.includes('\n') && !ics.replace(/\r\n/g, '').includes('\n')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exportiert nur zukünftige Fristen als ganztägige Termine mit stabiler UID', () => {
|
||||||
|
const ics = makeIcs();
|
||||||
|
// Art.-26-Pflichten (2027-12-02) sind enthalten …
|
||||||
|
expect(ics).toContain('DTSTART;VALUE=DATE:20271202');
|
||||||
|
expect(ics).toContain('DTEND;VALUE=DATE:20271203');
|
||||||
|
expect(ics).toContain('UID:sys-1-ob-deployer-oversight@ai-act-kompass');
|
||||||
|
// … verstrichene (KI-Kompetenz 2025-02-02) nicht:
|
||||||
|
expect(ics).not.toContain('DTSTART;VALUE=DATE:20250202');
|
||||||
|
// Allgemeine zukünftige Geltungsfristen sind enthalten:
|
||||||
|
expect(ics).toContain('UID:deadline-dl-annex3@ai-act-kompass');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setzt zwei Erinnerungen (30 und 7 Tage vorher) je Termin', () => {
|
||||||
|
const ics = makeIcs();
|
||||||
|
const eventCount = (ics.match(/BEGIN:VEVENT/g) ?? []).length;
|
||||||
|
expect((ics.match(/TRIGGER:-P30D/g) ?? []).length).toBe(eventCount);
|
||||||
|
expect((ics.match(/TRIGGER:-P7D/g) ?? []).length).toBe(eventCount);
|
||||||
|
expect(eventCount).toBeGreaterThan(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maskiert Sonderzeichen und faltet lange Zeilen RFC-konform', () => {
|
||||||
|
const ics = makeIcs();
|
||||||
|
// Systemname enthält ; und , — müssen maskiert sein:
|
||||||
|
expect(ics).toContain('Bewerber-Screening\\;');
|
||||||
|
expect(ics.replace(/\r\n /g, '')).toContain('GmbH\\, Test');
|
||||||
|
// Keine entfaltete Zeile länger als 75 Oktette:
|
||||||
|
for (const line of ics.split('\r\n')) {
|
||||||
|
expect(new TextEncoder().encode(line).length).toBeLessThanOrEqual(75);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lokalisiert Termintitel über den Content-Pack', () => {
|
||||||
|
const packFr = getContentPack('fr');
|
||||||
|
const checklist = buildChecklist(
|
||||||
|
[{ ...system, classification: classify(answers, packFr, 'fr') }],
|
||||||
|
packFr,
|
||||||
|
[],
|
||||||
|
TODAY,
|
||||||
|
);
|
||||||
|
const ics = checklistIcs({
|
||||||
|
entries: [...checklist.dueNow, ...checklist.upcoming],
|
||||||
|
deadlines: packFr.deadlines,
|
||||||
|
today: TODAY,
|
||||||
|
timestamp: TIMESTAMP,
|
||||||
|
calendarName: 'AI-Act-Kompass',
|
||||||
|
});
|
||||||
|
expect(ics.replace(/\r\n /g, '')).toContain('contrôle humain');
|
||||||
|
});
|
||||||
|
});
|
||||||
157
ai-act-kompass/src/engine/ics.ts
Normal file
157
ai-act-kompass/src/engine/ics.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import type { ChecklistEntry } from './checklist';
|
||||||
|
import type { Deadline } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iCalendar-Export (RFC 5545) für Fristen und Pflichten.
|
||||||
|
*
|
||||||
|
* Erzeugt eine .ics-Datei mit ganztägigen Terminen und eingebauten
|
||||||
|
* Erinnerungen (VALARM, 30 und 7 Tage vor Fälligkeit). Die Datei lässt
|
||||||
|
* sich in Outlook, Apple Kalender, Google Kalender, Thunderbird u. a.
|
||||||
|
* importieren — auf Smartphone und Desktop, komplett ohne Server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Erinnerungszeitpunkte in Tagen vor der Frist. */
|
||||||
|
const ALARM_DAYS_BEFORE: readonly number[] = [30, 7];
|
||||||
|
|
||||||
|
/** Maskiert Text nach RFC 5545 (Backslash, Semikolon, Komma, Zeilenumbruch). */
|
||||||
|
function escapeIcs(text: string): string {
|
||||||
|
return text
|
||||||
|
.replaceAll('\\', '\\\\')
|
||||||
|
.replaceAll(';', '\\;')
|
||||||
|
.replaceAll(',', '\\,')
|
||||||
|
.replaceAll('\r\n', '\\n')
|
||||||
|
.replaceAll('\n', '\\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Faltet eine Content-Zeile nach RFC 5545 (max. 75 Oktette je Zeile,
|
||||||
|
* Fortsetzungszeilen beginnen mit einem Leerzeichen).
|
||||||
|
*/
|
||||||
|
function foldLine(line: string): string {
|
||||||
|
const bytes = new TextEncoder().encode(line);
|
||||||
|
if (bytes.length <= 75) return line;
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
let current = '';
|
||||||
|
let currentBytes = 0;
|
||||||
|
for (const char of line) {
|
||||||
|
const charBytes = new TextEncoder().encode(char).length;
|
||||||
|
const limit = parts.length === 0 ? 75 : 74; // Folgezeilen: 1 Oktett für das Leerzeichen
|
||||||
|
if (currentBytes + charBytes > limit) {
|
||||||
|
parts.push(current);
|
||||||
|
current = char;
|
||||||
|
currentBytes = charBytes;
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
currentBytes += charBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (current !== '') parts.push(current);
|
||||||
|
return parts.join('\r\n ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYY-MM-DD → YYYYMMDD (DATE-Wert nach RFC 5545). */
|
||||||
|
function toIcsDate(isoDate: string): string {
|
||||||
|
return isoDate.replaceAll('-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ISO-Zeitstempel → YYYYMMDDTHHMMSSZ (UTC, DTSTAMP). */
|
||||||
|
function toIcsTimestamp(isoDateTime: string): string {
|
||||||
|
return `${isoDateTime.slice(0, 19).replace(/[-:]/g, '')}Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Folgetag eines ISO-Datums (DTEND ganztägiger Termine ist exklusiv). */
|
||||||
|
function nextDay(isoDate: string): string {
|
||||||
|
const [y, m, d] = isoDate.split('-').map(Number);
|
||||||
|
const date = new Date(Date.UTC(y ?? 1970, (m ?? 1) - 1, d ?? 1));
|
||||||
|
date.setUTCDate(date.getUTCDate() + 1);
|
||||||
|
return date.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IcsEvent {
|
||||||
|
readonly uid: string;
|
||||||
|
readonly date: string;
|
||||||
|
readonly summary: string;
|
||||||
|
readonly description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventBlock(event: IcsEvent, dtstamp: string): readonly string[] {
|
||||||
|
const lines = [
|
||||||
|
'BEGIN:VEVENT',
|
||||||
|
`UID:${event.uid}`,
|
||||||
|
`DTSTAMP:${dtstamp}`,
|
||||||
|
`DTSTART;VALUE=DATE:${toIcsDate(event.date)}`,
|
||||||
|
`DTEND;VALUE=DATE:${toIcsDate(nextDay(event.date))}`,
|
||||||
|
`SUMMARY:${escapeIcs(event.summary)}`,
|
||||||
|
`DESCRIPTION:${escapeIcs(event.description)}`,
|
||||||
|
'CATEGORIES:AI-Act-Kompass',
|
||||||
|
'TRANSP:TRANSPARENT',
|
||||||
|
];
|
||||||
|
for (const days of ALARM_DAYS_BEFORE) {
|
||||||
|
lines.push(
|
||||||
|
'BEGIN:VALARM',
|
||||||
|
'ACTION:DISPLAY',
|
||||||
|
`TRIGGER:-P${days}D`,
|
||||||
|
`DESCRIPTION:${escapeIcs(event.summary)}`,
|
||||||
|
'END:VALARM',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push('END:VEVENT');
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parameter für {@link checklistIcs}. */
|
||||||
|
export interface ChecklistIcsParams {
|
||||||
|
/** Offene Aufgaben (fällig + anstehend) aus der Checkliste. */
|
||||||
|
readonly entries: readonly ChecklistEntry[];
|
||||||
|
/** Allgemeine Geltungsfristen des AI Act (Content-Pack). */
|
||||||
|
readonly deadlines: readonly Deadline[];
|
||||||
|
/** Stichtag im ISO-Format — nur Termine ab diesem Tag werden exportiert. */
|
||||||
|
readonly today: string;
|
||||||
|
/** Erzeugungszeitpunkt (ISO-Datetime) für DTSTAMP. */
|
||||||
|
readonly timestamp: string;
|
||||||
|
/** Anzeigename des Kalenders. */
|
||||||
|
readonly calendarName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erzeugt eine iCalendar-Datei mit allen zukünftigen Pflicht-Fristen
|
||||||
|
* (je System) und den allgemeinen AI-Act-Geltungsfristen — jeweils als
|
||||||
|
* ganztägiger Termin mit Erinnerungen 30 und 7 Tage vorher.
|
||||||
|
*
|
||||||
|
* @returns Inhalt der .ics-Datei (CRLF-Zeilenenden, RFC-5545-Faltung)
|
||||||
|
*/
|
||||||
|
export function checklistIcs(params: ChecklistIcsParams): string {
|
||||||
|
const dtstamp = toIcsTimestamp(params.timestamp);
|
||||||
|
|
||||||
|
const taskEvents: IcsEvent[] = params.entries
|
||||||
|
.filter((e) => e.obligation.deadline >= params.today)
|
||||||
|
.map((e) => ({
|
||||||
|
uid: `${e.systemId}-${e.obligation.id}@ai-act-kompass`,
|
||||||
|
date: e.obligation.deadline,
|
||||||
|
summary: `AI Act: ${e.obligation.title} — ${e.systemName}`,
|
||||||
|
description: `${e.obligation.articles} · ${e.obligation.description}\n\nAI-Act-Kompass`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const deadlineEvents: IcsEvent[] = params.deadlines
|
||||||
|
.filter((d) => d.date >= params.today)
|
||||||
|
.map((d) => ({
|
||||||
|
uid: `deadline-${d.id}@ai-act-kompass`,
|
||||||
|
date: d.date,
|
||||||
|
summary: `AI Act: ${d.label}`,
|
||||||
|
description: `${d.description}\n\nAI-Act-Kompass`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'BEGIN:VCALENDAR',
|
||||||
|
'VERSION:2.0',
|
||||||
|
'PRODID:-//AI-Act-Kompass//Compliance Reminders//DE',
|
||||||
|
'CALSCALE:GREGORIAN',
|
||||||
|
'METHOD:PUBLISH',
|
||||||
|
`X-WR-CALNAME:${escapeIcs(params.calendarName)}`,
|
||||||
|
...[...taskEvents, ...deadlineEvents].flatMap((e) => eventBlock(e, dtstamp)),
|
||||||
|
'END:VCALENDAR',
|
||||||
|
];
|
||||||
|
|
||||||
|
return lines.map(foldLine).join('\r\n') + '\r\n';
|
||||||
|
}
|
||||||
@ -41,6 +41,8 @@ export const CS: UIStrings = {
|
|||||||
tasksEmpty: 'Žádné úkoly — nejprve posuďte systém AI.',
|
tasksEmpty: 'Žádné úkoly — nejprve posuďte systém AI.',
|
||||||
tasksAllDone: 'V této skupině je vše hotovo.',
|
tasksAllDone: 'V této skupině je vše hotovo.',
|
||||||
tasksDue: 'Lhůta',
|
tasksDue: 'Lhůta',
|
||||||
|
calExport: 'Přidat do kalendáře (.ics)',
|
||||||
|
calHint: 'Exportuje všechny otevřené lhůty jako soubor kalendáře s připomínkami (30 a 7 dní předem) — lze importovat do Outlooku, kalendáře Apple i Google, v telefonu i počítači.',
|
||||||
|
|
||||||
wizardTitleNew: 'Posoudit nový systém AI',
|
wizardTitleNew: 'Posoudit nový systém AI',
|
||||||
wizardTitleRe: 'Nové posouzení: {name}',
|
wizardTitleRe: 'Nové posouzení: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const ES: UIStrings = {
|
|||||||
tasksEmpty: 'No hay tareas — evalúe primero un sistema de IA.',
|
tasksEmpty: 'No hay tareas — evalúe primero un sistema de IA.',
|
||||||
tasksAllDone: 'Todo hecho en este grupo.',
|
tasksAllDone: 'Todo hecho en este grupo.',
|
||||||
tasksDue: 'Plazo',
|
tasksDue: 'Plazo',
|
||||||
|
calExport: 'Añadir al calendario (.ics)',
|
||||||
|
calHint: 'Exporta todos los plazos abiertos como archivo de calendario con recordatorios (30 y 7 días antes) — importable en Outlook, Apple y Google Calendar, en el móvil y el ordenador.',
|
||||||
|
|
||||||
wizardTitleNew: 'Evaluar un nuevo sistema de IA',
|
wizardTitleNew: 'Evaluar un nuevo sistema de IA',
|
||||||
wizardTitleRe: 'Reevaluación: {name}',
|
wizardTitleRe: 'Reevaluación: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const FR: UIStrings = {
|
|||||||
tasksEmpty: 'Aucune tâche — évaluez d’abord un système d’IA.',
|
tasksEmpty: 'Aucune tâche — évaluez d’abord un système d’IA.',
|
||||||
tasksAllDone: 'Tout est fait dans ce groupe.',
|
tasksAllDone: 'Tout est fait dans ce groupe.',
|
||||||
tasksDue: 'Échéance',
|
tasksDue: 'Échéance',
|
||||||
|
calExport: 'Ajouter au calendrier (.ics)',
|
||||||
|
calHint: 'Exporte toutes les échéances ouvertes sous forme de fichier calendrier avec rappels (30 et 7 jours avant) — importable dans Outlook, Apple et Google Agenda, sur téléphone et ordinateur.',
|
||||||
|
|
||||||
wizardTitleNew: 'Évaluer un nouveau système d’IA',
|
wizardTitleNew: 'Évaluer un nouveau système d’IA',
|
||||||
wizardTitleRe: 'Réévaluation : {name}',
|
wizardTitleRe: 'Réévaluation : {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const IT: UIStrings = {
|
|||||||
tasksEmpty: 'Nessuna attività — valutate prima un sistema di IA.',
|
tasksEmpty: 'Nessuna attività — valutate prima un sistema di IA.',
|
||||||
tasksAllDone: 'Tutto fatto in questo gruppo.',
|
tasksAllDone: 'Tutto fatto in questo gruppo.',
|
||||||
tasksDue: 'Scadenza',
|
tasksDue: 'Scadenza',
|
||||||
|
calExport: 'Aggiungi al calendario (.ics)',
|
||||||
|
calHint: 'Esporta tutte le scadenze aperte come file di calendario con promemoria (30 e 7 giorni prima) — importabile in Outlook, Apple e Google Calendar, su telefono e computer.',
|
||||||
|
|
||||||
wizardTitleNew: 'Valutare un nuovo sistema di IA',
|
wizardTitleNew: 'Valutare un nuovo sistema di IA',
|
||||||
wizardTitleRe: 'Rivalutazione: {name}',
|
wizardTitleRe: 'Rivalutazione: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const PL: UIStrings = {
|
|||||||
tasksEmpty: 'Brak zadań — najpierw oceń system AI.',
|
tasksEmpty: 'Brak zadań — najpierw oceń system AI.',
|
||||||
tasksAllDone: 'Wszystko zrobione w tej grupie.',
|
tasksAllDone: 'Wszystko zrobione w tej grupie.',
|
||||||
tasksDue: 'Termin',
|
tasksDue: 'Termin',
|
||||||
|
calExport: 'Dodaj do kalendarza (.ics)',
|
||||||
|
calHint: 'Eksportuje wszystkie otwarte terminy jako plik kalendarza z przypomnieniami (30 i 7 dni wcześniej) — do zaimportowania w Outlooku, kalendarzu Apple i Google, na telefonie i komputerze.',
|
||||||
|
|
||||||
wizardTitleNew: 'Oceń nowy system AI',
|
wizardTitleNew: 'Oceń nowy system AI',
|
||||||
wizardTitleRe: 'Ponowna ocena: {name}',
|
wizardTitleRe: 'Ponowna ocena: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const RO: UIStrings = {
|
|||||||
tasksEmpty: 'Nicio sarcină — evaluați mai întâi un sistem de IA.',
|
tasksEmpty: 'Nicio sarcină — evaluați mai întâi un sistem de IA.',
|
||||||
tasksAllDone: 'Totul este finalizat în acest grup.',
|
tasksAllDone: 'Totul este finalizat în acest grup.',
|
||||||
tasksDue: 'Termen',
|
tasksDue: 'Termen',
|
||||||
|
calExport: 'Adaugă în calendar (.ics)',
|
||||||
|
calHint: 'Exportă toate termenele deschise ca fișier de calendar cu mementouri (cu 30 și 7 zile înainte) — importabil în Outlook, Apple și Google Calendar, pe telefon și pe computer.',
|
||||||
|
|
||||||
wizardTitleNew: 'Evaluează un sistem de IA nou',
|
wizardTitleNew: 'Evaluează un sistem de IA nou',
|
||||||
wizardTitleRe: 'Reevaluare: {name}',
|
wizardTitleRe: 'Reevaluare: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ export const SK: UIStrings = {
|
|||||||
tasksEmpty: 'Žiadne úlohy — najprv posúďte systém AI.',
|
tasksEmpty: 'Žiadne úlohy — najprv posúďte systém AI.',
|
||||||
tasksAllDone: 'V tejto skupine je všetko hotové.',
|
tasksAllDone: 'V tejto skupine je všetko hotové.',
|
||||||
tasksDue: 'Lehota',
|
tasksDue: 'Lehota',
|
||||||
|
calExport: 'Pridať do kalendára (.ics)',
|
||||||
|
calHint: 'Exportuje všetky otvorené lehoty ako súbor kalendára s pripomienkami (30 a 7 dní vopred) — možno importovať do Outlooku, kalendára Apple aj Google, v telefóne aj počítači.',
|
||||||
|
|
||||||
wizardTitleNew: 'Posúdiť nový systém AI',
|
wizardTitleNew: 'Posúdiť nový systém AI',
|
||||||
wizardTitleRe: 'Opätovné posúdenie: {name}',
|
wizardTitleRe: 'Opätovné posúdenie: {name}',
|
||||||
|
|||||||
@ -41,6 +41,8 @@ const DE = {
|
|||||||
tasksEmpty: 'Keine Aufgaben — bewerten Sie zuerst ein KI-System.',
|
tasksEmpty: 'Keine Aufgaben — bewerten Sie zuerst ein KI-System.',
|
||||||
tasksAllDone: 'Alles erledigt in dieser Gruppe.',
|
tasksAllDone: 'Alles erledigt in dieser Gruppe.',
|
||||||
tasksDue: 'Frist',
|
tasksDue: 'Frist',
|
||||||
|
calExport: 'In Kalender übernehmen (.ics)',
|
||||||
|
calHint: 'Exportiert alle offenen Fristen als Kalenderdatei mit Erinnerungen (30 und 7 Tage vorher) — importierbar in Outlook, Apple- und Google-Kalender auf Handy und Rechner.',
|
||||||
|
|
||||||
wizardTitleNew: 'Neues KI-System bewerten',
|
wizardTitleNew: 'Neues KI-System bewerten',
|
||||||
wizardTitleRe: 'Neubewertung: {name}',
|
wizardTitleRe: 'Neubewertung: {name}',
|
||||||
@ -214,6 +216,8 @@ const EN: UIStrings = {
|
|||||||
tasksEmpty: 'No tasks — assess an AI system first.',
|
tasksEmpty: 'No tasks — assess an AI system first.',
|
||||||
tasksAllDone: 'Everything done in this group.',
|
tasksAllDone: 'Everything done in this group.',
|
||||||
tasksDue: 'Deadline',
|
tasksDue: 'Deadline',
|
||||||
|
calExport: 'Add to calendar (.ics)',
|
||||||
|
calHint: 'Exports all open deadlines as a calendar file with reminders (30 and 7 days ahead) — importable into Outlook, Apple and Google Calendar on phone and desktop.',
|
||||||
|
|
||||||
wizardTitleNew: 'Assess a new AI system',
|
wizardTitleNew: 'Assess a new AI system',
|
||||||
wizardTitleRe: 'Reassessment: {name}',
|
wizardTitleRe: 'Reassessment: {name}',
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { buildChecklist } from '../engine/checklist';
|
import { buildChecklist } from '../engine/checklist';
|
||||||
import type { ChecklistEntry } from '../engine/checklist';
|
import type { ChecklistEntry } from '../engine/checklist';
|
||||||
|
import { checklistIcs } from '../engine/ics';
|
||||||
import type { AISystem, CompanyProfile } from '../engine/types';
|
import type { AISystem, CompanyProfile } from '../engine/types';
|
||||||
import { useI18n } from '../i18n/context';
|
import { useI18n } from '../i18n/context';
|
||||||
import { fmt } from '../i18n/ui';
|
import { fmt } from '../i18n/ui';
|
||||||
import { todayISO, uiDate } from './common';
|
import { downloadText, todayISO, uiDate } from './common';
|
||||||
|
|
||||||
interface ChecklistViewProps {
|
interface ChecklistViewProps {
|
||||||
readonly systems: readonly AISystem[];
|
readonly systems: readonly AISystem[];
|
||||||
@ -103,6 +104,29 @@ export function ChecklistView({
|
|||||||
<div className="progress-track">
|
<div className="progress-track">
|
||||||
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
<div className="progress-fill" style={{ width: `${pct}%` }} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="toolbar" style={{ margin: '14px 0 0' }}>
|
||||||
|
<button
|
||||||
|
className="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
downloadText(
|
||||||
|
`ai-act-fristen-${today}.ics`,
|
||||||
|
checklistIcs({
|
||||||
|
entries: [...checklist.dueNow, ...checklist.upcoming],
|
||||||
|
deadlines: pack.deadlines,
|
||||||
|
today,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
calendarName: 'AI-Act-Kompass',
|
||||||
|
}),
|
||||||
|
'text/calendar',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t.calExport}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="hint" style={{ marginBottom: 0 }}>
|
||||||
|
{t.calHint}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group
|
<Group
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user