From 819e4c9a2f070ae5c3996e8f42b2b5c9dc879330 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sat, 29 Nov 2025 22:46:37 +0100 Subject: [PATCH] feat(feedback): add centralized feedback system with AI-generated titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add shared-feedback-types package with TypeScript types - Add shared-feedback-service package with factory function - Add shared-feedback-ui package with Svelte 5 components - Add feedback module to mana-core-auth backend - Add AI service using Gemini 2.0 Flash for title/category generation - Add database schema and migration for feedback tables - Integrate feedback page into Chat web app - Add CORS support for X-App-Id header - Add COMMANDS.md documentation for all dev commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- COMMANDS.md | 343 ++++ .../apps/web/src/lib/services/feedback.ts | 15 + .../routes/(protected)/feedback/+page.svelte | 11 + packages/shared-feedback-service/package.json | 24 + .../src/createFeedbackService.ts | 150 ++ packages/shared-feedback-service/src/index.ts | 24 + packages/shared-feedback-service/src/types.ts | 13 + .../shared-feedback-service/tsconfig.json | 17 + packages/shared-feedback-types/package.json | 19 + packages/shared-feedback-types/src/api.ts | 41 + .../shared-feedback-types/src/feedback.ts | 59 + packages/shared-feedback-types/src/index.ts | 25 + packages/shared-feedback-types/tsconfig.json | 17 + packages/shared-feedback-ui/package.json | 55 + .../src/FeedbackCard.svelte | 165 ++ .../src/FeedbackForm.svelte | 195 ++ .../src/FeedbackList.svelte | 58 + .../src/FeedbackPage.svelte | 384 ++++ .../shared-feedback-ui/src/StatusBadge.svelte | 31 + .../shared-feedback-ui/src/VoteButton.svelte | 91 + packages/shared-feedback-ui/src/index.ts | 20 + packages/shared-feedback-ui/tsconfig.json | 16 + pnpm-lock.yaml | 568 +++--- services/mana-core-auth/package.json | 1 + services/mana-core-auth/src/ai/ai.module.ts | 9 + services/mana-core-auth/src/ai/ai.service.ts | 103 ++ services/mana-core-auth/src/ai/index.ts | 2 + services/mana-core-auth/src/app.module.ts | 4 + .../src/config/configuration.ts | 4 + .../src/db/migrations/0001_zippy_ma_gnuci.sql | 39 + .../src/db/migrations/meta/0001_snapshot.json | 1600 +++++++++++++++++ .../src/db/migrations/meta/_journal.json | 31 +- .../src/db/schema/feedback.schema.ts | 94 + .../mana-core-auth/src/db/schema/index.ts | 1 + .../src/feedback/dto/create-feedback.dto.ts | 21 + .../src/feedback/dto/feedback-query.dto.ts | 33 + .../mana-core-auth/src/feedback/dto/index.ts | 2 + .../src/feedback/feedback.controller.ts | 54 + .../src/feedback/feedback.module.ts | 10 + .../src/feedback/feedback.service.ts | 277 +++ services/mana-core-auth/src/main.ts | 2 +- 41 files changed, 4290 insertions(+), 338 deletions(-) create mode 100644 COMMANDS.md create mode 100644 apps/chat/apps/web/src/lib/services/feedback.ts create mode 100644 apps/chat/apps/web/src/routes/(protected)/feedback/+page.svelte create mode 100644 packages/shared-feedback-service/package.json create mode 100644 packages/shared-feedback-service/src/createFeedbackService.ts create mode 100644 packages/shared-feedback-service/src/index.ts create mode 100644 packages/shared-feedback-service/src/types.ts create mode 100644 packages/shared-feedback-service/tsconfig.json create mode 100644 packages/shared-feedback-types/package.json create mode 100644 packages/shared-feedback-types/src/api.ts create mode 100644 packages/shared-feedback-types/src/feedback.ts create mode 100644 packages/shared-feedback-types/src/index.ts create mode 100644 packages/shared-feedback-types/tsconfig.json create mode 100644 packages/shared-feedback-ui/package.json create mode 100644 packages/shared-feedback-ui/src/FeedbackCard.svelte create mode 100644 packages/shared-feedback-ui/src/FeedbackForm.svelte create mode 100644 packages/shared-feedback-ui/src/FeedbackList.svelte create mode 100644 packages/shared-feedback-ui/src/FeedbackPage.svelte create mode 100644 packages/shared-feedback-ui/src/StatusBadge.svelte create mode 100644 packages/shared-feedback-ui/src/VoteButton.svelte create mode 100644 packages/shared-feedback-ui/src/index.ts create mode 100644 packages/shared-feedback-ui/tsconfig.json create mode 100644 services/mana-core-auth/src/ai/ai.module.ts create mode 100644 services/mana-core-auth/src/ai/ai.service.ts create mode 100644 services/mana-core-auth/src/ai/index.ts create mode 100644 services/mana-core-auth/src/db/migrations/0001_zippy_ma_gnuci.sql create mode 100644 services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json create mode 100644 services/mana-core-auth/src/db/schema/feedback.schema.ts create mode 100644 services/mana-core-auth/src/feedback/dto/create-feedback.dto.ts create mode 100644 services/mana-core-auth/src/feedback/dto/feedback-query.dto.ts create mode 100644 services/mana-core-auth/src/feedback/dto/index.ts create mode 100644 services/mana-core-auth/src/feedback/feedback.controller.ts create mode 100644 services/mana-core-auth/src/feedback/feedback.module.ts create mode 100644 services/mana-core-auth/src/feedback/feedback.service.ts diff --git a/COMMANDS.md b/COMMANDS.md new file mode 100644 index 000000000..8b09289a0 --- /dev/null +++ b/COMMANDS.md @@ -0,0 +1,343 @@ +# Manacore Monorepo - Befehle + +# Alles starten (PostgreSQL, Redis, Auth, Chat) +pnpm docker:up:all + +pnpm docker:down + +pnpm dev:chat:app + +pnpm dev:picture:app + + + +Übersicht aller wichtigen Befehle zum Starten, Stoppen und Verwalten der Apps. + +## Inhaltsverzeichnis + +- [Voraussetzungen](#voraussetzungen) +- [Docker & Infrastruktur](#docker--infrastruktur) +- [Mana Core Auth](#mana-core-auth) +- [Chat](#chat) +- [Picture](#picture) +- [Manadeck](#manadeck) +- [Zitare](#zitare) +- [Presi](#presi) +- [Manacore](#manacore) +- [Mana Games](#mana-games) +- [Allgemeine Befehle](#allgemeine-befehle) +- [Stoppen & Aufräumen](#stoppen--aufräumen) + +--- + +## Voraussetzungen + +```bash +# Dependencies installieren (generiert auch .env Dateien) +pnpm install + +# Nur .env Dateien neu generieren +pnpm setup:env + +# Shared Packages bauen +pnpm build:packages +``` + +--- + +## Docker & Infrastruktur + +### Starten + +```bash +# Nur PostgreSQL + Redis (Basis-Infrastruktur) +pnpm docker:up + +# Mit Mana Core Auth Service +pnpm docker:up:auth + +# Mit Chat Backend +pnpm docker:up:chat + +# Alles starten (PostgreSQL, Redis, Auth, Chat) +pnpm docker:up:all +``` + +### Status & Logs + +```bash +# Container-Status anzeigen +pnpm docker:ps + +# Alle Logs anzeigen (live) +pnpm docker:logs + +# Nur Auth-Logs +pnpm docker:logs:auth + +# Nur Chat-Logs +pnpm docker:logs:chat +``` + +### Stoppen + +```bash +# Alle Container stoppen +pnpm docker:down + +# Stoppen + Volumes löschen (Datenbank-Reset!) +pnpm docker:clean +``` + +--- + +## Mana Core Auth + +Der zentrale Authentifizierungs-Service (Port 3001). + +```bash +# Im Docker starten (empfohlen) +pnpm docker:up:auth + +# Oder lokal starten +cd services/mana-core-auth +pnpm start:dev + +# Datenbank-Migration +cd services/mana-core-auth +pnpm migration:generate # Migration erstellen +pnpm migration:run # Migration ausführen +pnpm db:push # Schema direkt pushen (dev) +pnpm db:studio # Drizzle Studio öffnen +``` + +--- + +## Chat + +| App | Port | Befehl | +|-----|------|--------| +| Web | 5174 | `pnpm dev:chat:web` | +| Backend | 3002 | `pnpm dev:chat:backend` | +| Mobile | 8081 | `pnpm dev:chat:mobile` | +| Landing | - | `pnpm dev:chat:landing` | + +```bash +# Web + Backend zusammen starten +pnpm dev:chat:app + +# Alles (Web, Backend, Mobile, Landing) +pnpm chat:dev +``` + +--- + +## Picture + +| App | Port | Befehl | +|-----|------|--------| +| Web | 5173 | `pnpm dev:picture:web` | +| Backend | - | `pnpm dev:picture:backend` | +| Mobile | 8081 | `pnpm dev:picture:mobile` | +| Landing | - | `pnpm dev:picture:landing` | + +```bash +# Web + Backend zusammen starten +pnpm dev:picture:app + +# Alles +pnpm picture:dev +``` + +--- + +## Manadeck + +| App | Port | Befehl | +|-----|------|--------| +| Web | - | `pnpm dev:manadeck:web` | +| Backend | - | `pnpm dev:manadeck:backend` | +| Mobile | 8081 | `pnpm dev:manadeck:mobile` | +| Landing | - | `pnpm dev:manadeck:landing` | + +```bash +# Web + Backend zusammen starten +pnpm dev:manadeck:app + +# Alles +pnpm manadeck:dev +``` + +--- + +## Zitare + +| App | Port | Befehl | +|-----|------|--------| +| Web | - | `pnpm dev:zitare:web` | +| Backend | - | `pnpm dev:zitare:backend` | +| Mobile | 8081 | `pnpm dev:zitare:mobile` | +| Landing | - | `pnpm dev:zitare:landing` | + +```bash +# Web + Backend zusammen starten +pnpm dev:zitare:app + +# Alles +pnpm zitare:dev +``` + +--- + +## Presi + +| App | Port | Befehl | +|-----|------|--------| +| Web | - | `pnpm dev:presi:web` | +| Backend | - | `pnpm dev:presi:backend` | +| Mobile | 8081 | `pnpm dev:presi:mobile` | + +```bash +# Web + Backend zusammen starten +pnpm dev:presi:app + +# Alles +pnpm presi:dev + +# Datenbank +pnpm presi:db:push # Schema pushen +pnpm presi:db:studio # Drizzle Studio +pnpm presi:db:seed # Seed-Daten +``` + +--- + +## Manacore + +| App | Port | Befehl | +|-----|------|--------| +| Web | - | `pnpm dev:manacore:web` | +| Mobile | 8081 | `pnpm dev:manacore:mobile` | +| Landing | - | `pnpm dev:manacore:landing` | + +```bash +# Alles +pnpm manacore:dev +``` + +--- + +## Mana Games + +```bash +# Web + Backend zusammen starten +pnpm dev:mana-games:app + +# Einzeln +pnpm dev:mana-games:web +pnpm dev:mana-games:backend + +# Alles +pnpm mana-games:dev +``` + +--- + +## Allgemeine Befehle + +```bash +# Alles bauen +pnpm build + +# Type-Check +pnpm type-check + +# Linting +pnpm lint + +# Formatierung +pnpm format # Formatieren +pnpm format:check # Nur prüfen + +# Tests +pnpm test + +# Cache leeren +pnpm clean +``` + +--- + +## Stoppen & Aufräumen + +### Prozesse stoppen + +```bash +# Alle Node-Prozesse beenden (macOS/Linux) +pkill -f "node" + +# Spezifischen Port freigeben +lsof -ti:3001 | xargs kill -9 # Port 3001 (Auth) +lsof -ti:3002 | xargs kill -9 # Port 3002 (Chat Backend) +lsof -ti:5174 | xargs kill -9 # Port 5174 (Chat Web) + +# Turbo Cache leeren +pnpm clean +``` + +### Docker stoppen + +```bash +# Container stoppen (Daten bleiben) +pnpm docker:down + +# Container + Volumes löschen (Datenbank-Reset!) +pnpm docker:clean +``` + +### Komplettes Cleanup + +```bash +# Alles stoppen und aufräumen +pnpm docker:down +pkill -f "node" +pnpm clean +rm -rf node_modules/.cache +``` + +--- + +## Typische Workflows + +### Chat-Entwicklung starten + +```bash +# 1. Infrastruktur starten +pnpm docker:up + +# 2. Auth-Service starten (neues Terminal) +cd services/mana-core-auth && pnpm start:dev + +# 3. Chat App starten (neues Terminal) +pnpm dev:chat:app +``` + +### Alles mit Docker + +```bash +# Alles in Docker starten +pnpm docker:up:all + +# Nur Frontend lokal +pnpm dev:chat:web +``` + +### Nach Code-Änderungen in Shared Packages + +```bash +# Packages neu bauen +pnpm build:packages + +# Oder spezifisches Package +pnpm --filter @manacore/shared-ui build +``` diff --git a/apps/chat/apps/web/src/lib/services/feedback.ts b/apps/chat/apps/web/src/lib/services/feedback.ts new file mode 100644 index 000000000..bbef27247 --- /dev/null +++ b/apps/chat/apps/web/src/lib/services/feedback.ts @@ -0,0 +1,15 @@ +/** + * Feedback Service Instance for Chat Web App + */ + +import { createFeedbackService } from '@manacore/shared-feedback-service'; +import { authStore } from '$lib/stores/auth.svelte'; +import { PUBLIC_MANA_CORE_AUTH_URL } from '$env/static/public'; + +const MANA_AUTH_URL = PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; + +export const feedbackService = createFeedbackService({ + apiUrl: MANA_AUTH_URL, + appId: 'chat', + getAuthToken: async () => authStore.getAccessToken(), +}); diff --git a/apps/chat/apps/web/src/routes/(protected)/feedback/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/feedback/+page.svelte new file mode 100644 index 000000000..3dc3965bf --- /dev/null +++ b/apps/chat/apps/web/src/routes/(protected)/feedback/+page.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/shared-feedback-service/package.json b/packages/shared-feedback-service/package.json new file mode 100644 index 000000000..d6cdc97f1 --- /dev/null +++ b/packages/shared-feedback-service/package.json @@ -0,0 +1,24 @@ +{ + "name": "@manacore/shared-feedback-service", + "version": "1.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "main": "./src/index.ts", + "types": "./src/index.ts", + "files": ["src"], + "scripts": { + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@manacore/shared-feedback-types": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/shared-feedback-service/src/createFeedbackService.ts b/packages/shared-feedback-service/src/createFeedbackService.ts new file mode 100644 index 000000000..9213cc2eb --- /dev/null +++ b/packages/shared-feedback-service/src/createFeedbackService.ts @@ -0,0 +1,150 @@ +/** + * Feedback Service Factory + * + * Creates a feedback service instance configured for a specific app. + * Handles feedback submission, retrieval, and voting. + * + * @example + * ```ts + * import { createFeedbackService } from '@manacore/shared-feedback-service'; + * import { authStore } from '$lib/stores/auth.svelte'; + * + * export const feedbackService = createFeedbackService({ + * apiUrl: 'https://auth.manacore.app', + * appId: 'chat', + * getAuthToken: async () => authStore.getToken(), + * }); + * ``` + */ + +import type { + Feedback, + CreateFeedbackInput, + FeedbackQueryParams, + FeedbackResponse, + FeedbackListResponse, + VoteResponse, +} from '@manacore/shared-feedback-types'; +import type { FeedbackServiceConfig } from './types'; + +/** + * Create a feedback service instance + */ +export function createFeedbackService(config: FeedbackServiceConfig) { + const { apiUrl, appId, getAuthToken, feedbackEndpoint = '/api/v1/feedback' } = config; + + // Normalize API URL (remove trailing slash) + const baseUrl = apiUrl.replace(/\/$/, ''); + + /** + * Helper to make authenticated requests + */ + async function fetchWithAuth( + endpoint: string, + options: RequestInit = {} + ): Promise { + const token = await getAuthToken(); + + if (!token) { + throw new Error('Not authenticated'); + } + + const response = await fetch(`${baseUrl}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + 'X-App-Id': appId, + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + } + + /** + * Submit new feedback + */ + async function createFeedback(input: CreateFeedbackInput): Promise { + return fetchWithAuth(feedbackEndpoint, { + method: 'POST', + body: JSON.stringify(input), + }); + } + + /** + * Get public community feedback + */ + async function getPublicFeedback(query?: FeedbackQueryParams): Promise { + const params = new URLSearchParams(); + + // Always filter by current app + params.set('appId', appId); + + if (query?.status) params.set('status', query.status); + if (query?.category) params.set('category', query.category); + if (query?.sort) params.set('sort', query.sort); + if (query?.limit) params.set('limit', String(query.limit)); + if (query?.offset) params.set('offset', String(query.offset)); + + return fetchWithAuth(`${feedbackEndpoint}/public?${params}`); + } + + /** + * Get user's own feedback + */ + async function getMyFeedback(): Promise { + const params = new URLSearchParams(); + params.set('appId', appId); + + return fetchWithAuth(`${feedbackEndpoint}/my?${params}`); + } + + /** + * Vote on a feedback item + */ + async function vote(feedbackId: string): Promise { + return fetchWithAuth(`${feedbackEndpoint}/${feedbackId}/vote`, { + method: 'POST', + }); + } + + /** + * Remove vote from a feedback item + */ + async function unvote(feedbackId: string): Promise { + return fetchWithAuth(`${feedbackEndpoint}/${feedbackId}/vote`, { + method: 'DELETE', + }); + } + + /** + * Toggle vote on a feedback item + */ + async function toggleVote(feedbackId: string, currentlyVoted: boolean): Promise { + if (currentlyVoted) { + return unvote(feedbackId); + } else { + return vote(feedbackId); + } + } + + return { + createFeedback, + getPublicFeedback, + getMyFeedback, + vote, + unvote, + toggleVote, + }; +} + +/** + * Type for the feedback service instance + */ +export type FeedbackService = ReturnType; diff --git a/packages/shared-feedback-service/src/index.ts b/packages/shared-feedback-service/src/index.ts new file mode 100644 index 000000000..b5ccc9b75 --- /dev/null +++ b/packages/shared-feedback-service/src/index.ts @@ -0,0 +1,24 @@ +/** + * Shared feedback service for Manacore monorepo + * + * This package provides a factory function to create feedback service + * instances that can be used across all web and mobile apps. + */ + +export { createFeedbackService, type FeedbackService } from './createFeedbackService'; +export type { FeedbackServiceConfig } from './types'; + +// Re-export types from shared-feedback-types for convenience +export type { + Feedback, + FeedbackCategory, + FeedbackStatus, + FeedbackVote, + CreateFeedbackInput, + FeedbackQueryParams, + FeedbackResponse, + FeedbackListResponse, + VoteResponse, + FEEDBACK_CATEGORY_LABELS, + FEEDBACK_STATUS_CONFIG, +} from '@manacore/shared-feedback-types'; diff --git a/packages/shared-feedback-service/src/types.ts b/packages/shared-feedback-service/src/types.ts new file mode 100644 index 000000000..0dfaaf478 --- /dev/null +++ b/packages/shared-feedback-service/src/types.ts @@ -0,0 +1,13 @@ +/** + * Configuration for creating a feedback service instance + */ +export interface FeedbackServiceConfig { + /** Base API URL for the feedback endpoints */ + apiUrl: string; + /** App identifier for multi-app support */ + appId: string; + /** Function to get the current auth token */ + getAuthToken: () => Promise; + /** Optional custom endpoint prefix (default: '/api/v1/feedback') */ + feedbackEndpoint?: string; +} diff --git a/packages/shared-feedback-service/tsconfig.json b/packages/shared-feedback-service/tsconfig.json new file mode 100644 index 000000000..c828b6630 --- /dev/null +++ b/packages/shared-feedback-service/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/shared-feedback-types/package.json b/packages/shared-feedback-types/package.json new file mode 100644 index 000000000..abb793aa4 --- /dev/null +++ b/packages/shared-feedback-types/package.json @@ -0,0 +1,19 @@ +{ + "name": "@manacore/shared-feedback-types", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./feedback": "./src/feedback.ts", + "./api": "./src/api.ts" + }, + "scripts": { + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.7.3" + } +} diff --git a/packages/shared-feedback-types/src/api.ts b/packages/shared-feedback-types/src/api.ts new file mode 100644 index 000000000..53e961dd1 --- /dev/null +++ b/packages/shared-feedback-types/src/api.ts @@ -0,0 +1,41 @@ +/** + * API request/response types for feedback + */ + +import type { Feedback, FeedbackCategory, FeedbackStatus } from './feedback'; + +export interface CreateFeedbackInput { + title?: string; + feedbackText: string; + category?: FeedbackCategory; + deviceInfo?: Record; +} + +export interface FeedbackQueryParams { + appId?: string; + status?: FeedbackStatus; + category?: FeedbackCategory; + sort?: 'votes' | 'recent'; + limit?: number; + offset?: number; +} + +export interface FeedbackResponse { + success: boolean; + feedback?: Feedback; + error?: string; +} + +export interface FeedbackListResponse { + success: boolean; + items: Feedback[]; + total: number; + error?: string; +} + +export interface VoteResponse { + success: boolean; + newVoteCount: number; + userHasVoted: boolean; + error?: string; +} diff --git a/packages/shared-feedback-types/src/feedback.ts b/packages/shared-feedback-types/src/feedback.ts new file mode 100644 index 000000000..731678592 --- /dev/null +++ b/packages/shared-feedback-types/src/feedback.ts @@ -0,0 +1,59 @@ +/** + * Core feedback types + */ + +export type FeedbackCategory = 'bug' | 'feature' | 'improvement' | 'question' | 'other'; + +export type FeedbackStatus = + | 'submitted' + | 'under_review' + | 'planned' + | 'in_progress' + | 'completed' + | 'declined'; + +export interface Feedback { + id: string; + userId: string; + appId: string; + title?: string; + feedbackText: string; + category: FeedbackCategory; + status: FeedbackStatus; + isPublic: boolean; + adminResponse?: string; + voteCount: number; + userHasVoted: boolean; + deviceInfo?: Record; + createdAt: string; + updatedAt: string; + publishedAt?: string; + completedAt?: string; +} + +export interface FeedbackVote { + id: string; + feedbackId: string; + userId: string; + createdAt: string; +} + +export const FEEDBACK_CATEGORY_LABELS: Record = { + bug: 'Bug', + feature: 'Feature', + improvement: 'Verbesserung', + question: 'Frage', + other: 'Sonstiges', +}; + +export const FEEDBACK_STATUS_CONFIG: Record< + FeedbackStatus, + { label: string; color: string; icon: string } +> = { + submitted: { label: 'Eingereicht', color: '#999', icon: 'clock' }, + under_review: { label: 'Wird geprüft', color: '#3498DB', icon: 'eye' }, + planned: { label: 'Geplant', color: '#9B59B6', icon: 'calendar' }, + in_progress: { label: 'In Arbeit', color: '#F39C12', icon: 'loader' }, + completed: { label: 'Umgesetzt', color: '#27AE60', icon: 'check-circle' }, + declined: { label: 'Abgelehnt', color: '#E74C3C', icon: 'x-circle' }, +}; diff --git a/packages/shared-feedback-types/src/index.ts b/packages/shared-feedback-types/src/index.ts new file mode 100644 index 000000000..5b065b1e4 --- /dev/null +++ b/packages/shared-feedback-types/src/index.ts @@ -0,0 +1,25 @@ +/** + * Shared feedback types for Manacore monorepo + * + * This package contains TypeScript types for feedback submissions, + * voting, and API contracts. + */ + +// Feedback types +export { + type FeedbackCategory, + type FeedbackStatus, + type Feedback, + type FeedbackVote, + FEEDBACK_CATEGORY_LABELS, + FEEDBACK_STATUS_CONFIG, +} from './feedback'; + +// API types +export { + type CreateFeedbackInput, + type FeedbackQueryParams, + type FeedbackResponse, + type FeedbackListResponse, + type VoteResponse, +} from './api'; diff --git a/packages/shared-feedback-types/tsconfig.json b/packages/shared-feedback-types/tsconfig.json new file mode 100644 index 000000000..c828b6630 --- /dev/null +++ b/packages/shared-feedback-types/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/shared-feedback-ui/package.json b/packages/shared-feedback-ui/package.json new file mode 100644 index 000000000..afd4c6b35 --- /dev/null +++ b/packages/shared-feedback-ui/package.json @@ -0,0 +1,55 @@ +{ + "name": "@manacore/shared-feedback-ui", + "version": "1.0.0", + "private": true, + "type": "module", + "svelte": "./src/index.ts", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "svelte": "./src/index.ts", + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./FeedbackPage.svelte": { + "svelte": "./src/FeedbackPage.svelte", + "default": "./src/FeedbackPage.svelte" + }, + "./FeedbackForm.svelte": { + "svelte": "./src/FeedbackForm.svelte", + "default": "./src/FeedbackForm.svelte" + }, + "./FeedbackList.svelte": { + "svelte": "./src/FeedbackList.svelte", + "default": "./src/FeedbackList.svelte" + }, + "./FeedbackCard.svelte": { + "svelte": "./src/FeedbackCard.svelte", + "default": "./src/FeedbackCard.svelte" + }, + "./VoteButton.svelte": { + "svelte": "./src/VoteButton.svelte", + "default": "./src/VoteButton.svelte" + }, + "./StatusBadge.svelte": { + "svelte": "./src/StatusBadge.svelte", + "default": "./src/StatusBadge.svelte" + } + }, + "scripts": { + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@manacore/shared-feedback-types": "workspace:*", + "@manacore/shared-feedback-service": "workspace:*" + }, + "devDependencies": { + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.7.3" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } +} diff --git a/packages/shared-feedback-ui/src/FeedbackCard.svelte b/packages/shared-feedback-ui/src/FeedbackCard.svelte new file mode 100644 index 000000000..a31f48496 --- /dev/null +++ b/packages/shared-feedback-ui/src/FeedbackCard.svelte @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/shared-feedback-ui/src/FeedbackForm.svelte b/packages/shared-feedback-ui/src/FeedbackForm.svelte new file mode 100644 index 000000000..f8c43058c --- /dev/null +++ b/packages/shared-feedback-ui/src/FeedbackForm.svelte @@ -0,0 +1,195 @@ + + + + + diff --git a/packages/shared-feedback-ui/src/FeedbackList.svelte b/packages/shared-feedback-ui/src/FeedbackList.svelte new file mode 100644 index 000000000..7b2a06c67 --- /dev/null +++ b/packages/shared-feedback-ui/src/FeedbackList.svelte @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/shared-feedback-ui/src/FeedbackPage.svelte b/packages/shared-feedback-ui/src/FeedbackPage.svelte new file mode 100644 index 000000000..b9251e12c --- /dev/null +++ b/packages/shared-feedback-ui/src/FeedbackPage.svelte @@ -0,0 +1,384 @@ + + + + {pageTitle} - {appName} + + + + + diff --git a/packages/shared-feedback-ui/src/StatusBadge.svelte b/packages/shared-feedback-ui/src/StatusBadge.svelte new file mode 100644 index 000000000..b437c991d --- /dev/null +++ b/packages/shared-feedback-ui/src/StatusBadge.svelte @@ -0,0 +1,31 @@ + + + + {config.label} + + + diff --git a/packages/shared-feedback-ui/src/VoteButton.svelte b/packages/shared-feedback-ui/src/VoteButton.svelte new file mode 100644 index 000000000..433816963 --- /dev/null +++ b/packages/shared-feedback-ui/src/VoteButton.svelte @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/shared-feedback-ui/src/index.ts b/packages/shared-feedback-ui/src/index.ts new file mode 100644 index 000000000..5dc05bc95 --- /dev/null +++ b/packages/shared-feedback-ui/src/index.ts @@ -0,0 +1,20 @@ +/** + * Shared feedback UI components for Manacore monorepo + * + * This package provides Svelte 5 components for feedback functionality + * that can be used across all web apps. + */ + +// Page components +export { default as FeedbackPage } from './FeedbackPage.svelte'; + +// Form components +export { default as FeedbackForm } from './FeedbackForm.svelte'; + +// List components +export { default as FeedbackList } from './FeedbackList.svelte'; +export { default as FeedbackCard } from './FeedbackCard.svelte'; + +// Utility components +export { default as VoteButton } from './VoteButton.svelte'; +export { default as StatusBadge } from './StatusBadge.svelte'; diff --git a/packages/shared-feedback-ui/tsconfig.json b/packages/shared-feedback-ui/tsconfig.json new file mode 100644 index 000000000..1b9470b61 --- /dev/null +++ b/packages/shared-feedback-ui/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c724e48e..0bf9694ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,7 +76,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -109,7 +109,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -1897,7 +1897,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -2841,7 +2841,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -2880,6 +2880,9 @@ importers: services/mana-core-auth: dependencies: + '@google/generative-ai': + specifier: ^0.24.1 + version: 0.24.1 '@nestjs/common': specifier: ^10.4.15 version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -2952,7 +2955,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1) + version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -3006,10 +3009,10 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -4863,7 +4866,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -19093,7 +19096,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) + expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -19170,7 +19173,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e) + expo-router: 6.0.15(ohit2up6tuxb3x34brxduivol4) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -20410,6 +20413,41 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@22.19.1) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 @@ -20445,78 +20483,6 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.2.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -20943,32 +20909,6 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.19.12)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) - glob: 10.4.5 - inquirer: 8.2.6 - node-emoji: 1.11.0 - ora: 5.4.1 - tree-kill: 1.2.2 - tsconfig-paths: 4.2.0 - tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -20995,7 +20935,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -21006,14 +20946,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2 + webpack: 5.100.2(esbuild@0.19.12) webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -23447,7 +23387,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -23457,7 +23397,20 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + optional: true + + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': @@ -24419,11 +24372,11 @@ snapshots: - vite optional: true - '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/utils': 3.2.4 magic-string: 0.30.21 sirv: 3.0.2 @@ -24463,15 +24416,6 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - optional: true - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -24501,7 +24445,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -26183,13 +26127,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -27054,9 +26998,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -27071,9 +27015,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -27143,7 +27087,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -27154,22 +27098,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - get-tsconfig: 4.13.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.15 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -27193,25 +27122,25 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -27311,7 +27240,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -27322,7 +27251,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -27340,7 +27269,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -27351,7 +27280,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -28492,7 +28421,54 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): + expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + + expo-router@6.0.15(ohit2up6tuxb3x34brxduivol4): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 @@ -28526,12 +28502,12 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)) transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@types/react' @@ -29389,23 +29365,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -29440,6 +29399,23 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.9.3 + webpack: 5.100.2(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -29457,23 +29433,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.9.3 - webpack: 5.100.2 - form-data-encoder@1.7.2: {} form-data@3.0.4: @@ -30556,16 +30515,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -30575,15 +30534,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -30653,6 +30612,36 @@ snapshots: - ts-node optional: true + jest-config@29.7.0(@types/node@22.19.1): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -30684,38 +30673,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -30741,12 +30699,11 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -30773,9 +30730,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.19.12) - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + '@types/node': 20.19.25 + esbuild-register: 3.6.0(esbuild@0.27.0) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -31424,24 +31380,24 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -35055,6 +35011,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.100.2(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: acorn-loose: 8.5.2 @@ -35064,16 +35030,6 @@ snapshots: webpack: 5.100.2(esbuild@0.27.0) webpack-sources: 3.3.3 - react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - acorn-loose: 8.5.2 - neo-async: 2.6.2 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - webpack: 5.97.1(esbuild@0.19.12) - webpack-sources: 3.3.3 - optional: true - react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -36296,14 +36252,14 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.1 - webpack: 5.97.1(esbuild@0.19.12) + webpack: 5.100.2(esbuild@0.19.12) optionalDependencies: esbuild: 0.19.12 @@ -36329,15 +36285,6 @@ snapshots: optionalDependencies: esbuild: 0.27.0 - terser-webpack-plugin@5.3.14(webpack@5.100.2): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.100.2 - terser-webpack-plugin@5.3.14(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -36493,6 +36440,27 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -36514,25 +36482,15 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 semver: 7.7.3 - type-fest: 4.41.0 + source-map: 0.7.6 typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - jest-util: 30.2.0 + webpack: 5.100.2(esbuild@0.19.12) ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: @@ -36544,7 +36502,7 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2): + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.27.0)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.3 @@ -36552,17 +36510,7 @@ snapshots: semver: 7.7.3 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.100.2 - - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) + webpack: 5.97.1(esbuild@0.27.0) ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: @@ -37322,7 +37270,7 @@ snapshots: optionalDependencies: '@types/debug': 4.1.12 '@types/node': 24.10.1 - '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 27.2.0 transitivePeerDependencies: @@ -37485,7 +37433,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.100.2: + webpack@5.100.2(esbuild@0.19.12): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -37509,7 +37457,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.100.2) + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -37579,36 +37527,6 @@ snapshots: - esbuild - uglify-js - webpack@5.97.1(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/mana-core-auth/package.json b/services/mana-core-auth/package.json index bff3533d6..2bc0a8179 100644 --- a/services/mana-core-auth/package.json +++ b/services/mana-core-auth/package.json @@ -21,6 +21,7 @@ "db:studio": "drizzle-kit studio" }, "dependencies": { + "@google/generative-ai": "^0.24.1", "@nestjs/common": "^10.4.15", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", diff --git a/services/mana-core-auth/src/ai/ai.module.ts b/services/mana-core-auth/src/ai/ai.module.ts new file mode 100644 index 000000000..3b6d89254 --- /dev/null +++ b/services/mana-core-auth/src/ai/ai.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { AiService } from './ai.service'; + +@Global() +@Module({ + providers: [AiService], + exports: [AiService], +}) +export class AiModule {} diff --git a/services/mana-core-auth/src/ai/ai.service.ts b/services/mana-core-auth/src/ai/ai.service.ts new file mode 100644 index 000000000..87b38ad82 --- /dev/null +++ b/services/mana-core-auth/src/ai/ai.service.ts @@ -0,0 +1,103 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GoogleGenerativeAI } from '@google/generative-ai'; + +export interface FeedbackAnalysis { + title: string; + category: 'bug' | 'feature' | 'improvement' | 'question' | 'other'; +} + +@Injectable() +export class AiService { + private readonly logger = new Logger(AiService.name); + private genAI: GoogleGenerativeAI | null = null; + + constructor(private configService: ConfigService) { + const apiKey = this.configService.get('ai.geminiApiKey'); + if (apiKey) { + this.genAI = new GoogleGenerativeAI(apiKey); + } else { + this.logger.warn('GOOGLE_GENAI_API_KEY not configured - AI features disabled'); + } + } + + async analyzeFeedback(feedbackText: string): Promise { + // Fallback if AI not available + if (!this.genAI) { + return this.fallbackAnalysis(feedbackText); + } + + try { + const model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash' }); + + const prompt = `Analysiere dieses User-Feedback und generiere: +1. Einen kurzen, prägnanten deutschen Titel (max 60 Zeichen) der den Kern des Feedbacks zusammenfasst +2. Eine passende Kategorie aus: bug, feature, improvement, question, other + +Feedback: "${feedbackText}" + +Antworte NUR mit validem JSON in diesem Format (keine Markdown-Codeblocks, kein anderer Text): +{"title": "...", "category": "..."}`; + + const result = await model.generateContent(prompt); + const response = result.response.text().trim(); + + // Parse JSON response - handle potential markdown code blocks + let jsonStr = response; + if (response.includes('```')) { + const match = response.match(/```(?:json)?\s*([\s\S]*?)```/); + if (match) { + jsonStr = match[1].trim(); + } + } + + const parsed = JSON.parse(jsonStr) as FeedbackAnalysis; + + // Validate category + const validCategories = ['bug', 'feature', 'improvement', 'question', 'other']; + if (!validCategories.includes(parsed.category)) { + parsed.category = 'other'; + } + + // Ensure title is not too long + if (parsed.title.length > 60) { + parsed.title = parsed.title.substring(0, 57) + '...'; + } + + this.logger.debug(`AI analyzed feedback: ${JSON.stringify(parsed)}`); + return parsed; + } catch (error) { + this.logger.error(`AI analysis failed: ${error}`); + return this.fallbackAnalysis(feedbackText); + } + } + + private fallbackAnalysis(feedbackText: string): FeedbackAnalysis { + // Simple fallback: use first 60 chars as title, default category + const title = + feedbackText.length > 60 ? feedbackText.substring(0, 57) + '...' : feedbackText; + + // Simple keyword-based category detection + const lowerText = feedbackText.toLowerCase(); + let category: FeedbackAnalysis['category'] = 'feature'; + + if ( + lowerText.includes('bug') || + lowerText.includes('fehler') || + lowerText.includes('kaputt') || + lowerText.includes('funktioniert nicht') + ) { + category = 'bug'; + } else if (lowerText.includes('?') || lowerText.includes('frage') || lowerText.includes('wie')) { + category = 'question'; + } else if ( + lowerText.includes('besser') || + lowerText.includes('verbessern') || + lowerText.includes('optimieren') + ) { + category = 'improvement'; + } + + return { title, category }; + } +} diff --git a/services/mana-core-auth/src/ai/index.ts b/services/mana-core-auth/src/ai/index.ts new file mode 100644 index 000000000..4252f0044 --- /dev/null +++ b/services/mana-core-auth/src/ai/index.ts @@ -0,0 +1,2 @@ +export * from './ai.module'; +export * from './ai.service'; diff --git a/services/mana-core-auth/src/app.module.ts b/services/mana-core-auth/src/app.module.ts index c0f602532..0b7458ba5 100644 --- a/services/mana-core-auth/src/app.module.ts +++ b/services/mana-core-auth/src/app.module.ts @@ -5,6 +5,8 @@ import { APP_FILTER } from '@nestjs/core'; import configuration from './config/configuration'; import { AuthModule } from './auth/auth.module'; import { CreditsModule } from './credits/credits.module'; +import { FeedbackModule } from './feedback/feedback.module'; +import { AiModule } from './ai/ai.module'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @Module({ @@ -19,8 +21,10 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; limit: 100, // 100 requests per minute }, ]), + AiModule, AuthModule, CreditsModule, + FeedbackModule, ], providers: [ { diff --git a/services/mana-core-auth/src/config/configuration.ts b/services/mana-core-auth/src/config/configuration.ts index 7fb121897..0ed3a0c42 100644 --- a/services/mana-core-auth/src/config/configuration.ts +++ b/services/mana-core-auth/src/config/configuration.ts @@ -44,4 +44,8 @@ export default () => ({ signupBonus: parseInt(process.env.CREDITS_SIGNUP_BONUS || '150', 10), dailyFreeCredits: parseInt(process.env.CREDITS_DAILY_FREE || '5', 10), }, + + ai: { + geminiApiKey: process.env.GOOGLE_GENAI_API_KEY || '', + }, }); diff --git a/services/mana-core-auth/src/db/migrations/0001_zippy_ma_gnuci.sql b/services/mana-core-auth/src/db/migrations/0001_zippy_ma_gnuci.sql new file mode 100644 index 000000000..2ad2cfbfc --- /dev/null +++ b/services/mana-core-auth/src/db/migrations/0001_zippy_ma_gnuci.sql @@ -0,0 +1,39 @@ +CREATE SCHEMA "feedback"; +--> statement-breakpoint +CREATE TYPE "public"."feedback_category" AS ENUM('bug', 'feature', 'improvement', 'question', 'other');--> statement-breakpoint +CREATE TYPE "public"."feedback_status" AS ENUM('submitted', 'under_review', 'planned', 'in_progress', 'completed', 'declined');--> statement-breakpoint +CREATE TABLE "feedback"."feedback_votes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "feedback_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "feedback"."user_feedback" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid NOT NULL, + "app_id" text NOT NULL, + "title" text, + "feedback_text" text NOT NULL, + "category" "feedback_category" DEFAULT 'feature' NOT NULL, + "status" "feedback_status" DEFAULT 'submitted' NOT NULL, + "is_public" boolean DEFAULT false NOT NULL, + "admin_response" text, + "vote_count" integer DEFAULT 0 NOT NULL, + "device_info" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "published_at" timestamp with time zone, + "completed_at" timestamp with time zone +); +--> statement-breakpoint +ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_feedback_id_user_feedback_id_fk" FOREIGN KEY ("feedback_id") REFERENCES "feedback"."user_feedback"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback"."feedback_votes" ADD CONSTRAINT "feedback_votes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "feedback"."user_feedback" ADD CONSTRAINT "user_feedback_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "feedback_vote_unique" ON "feedback"."feedback_votes" USING btree ("feedback_id","user_id");--> statement-breakpoint +CREATE INDEX "feedback_votes_feedback_idx" ON "feedback"."feedback_votes" USING btree ("feedback_id");--> statement-breakpoint +CREATE INDEX "feedback_user_idx" ON "feedback"."user_feedback" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "feedback_app_idx" ON "feedback"."user_feedback" USING btree ("app_id");--> statement-breakpoint +CREATE INDEX "feedback_public_idx" ON "feedback"."user_feedback" USING btree ("is_public");--> statement-breakpoint +CREATE INDEX "feedback_status_idx" ON "feedback"."user_feedback" USING btree ("status");--> statement-breakpoint +CREATE INDEX "feedback_created_at_idx" ON "feedback"."user_feedback" USING btree ("created_at"); \ No newline at end of file diff --git a/services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json b/services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..4a2b5fe3e --- /dev/null +++ b/services/mana-core-auth/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,1600 @@ +{ + "id": "ecb1358f-1cde-49ae-973b-4a2d9d7d5c2b", + "prevId": "83697ac3-d241-4743-96a1-880ad990aa0b", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.accounts": { + "name": "accounts", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.passwords": { + "name": "passwords", + "schema": "auth", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "passwords_user_id_users_id_fk": { + "name": "passwords_user_id_users_id_fk", + "tableFrom": "passwords", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.security_events": { + "name": "security_events", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "security_events_user_id_users_id_fk": { + "name": "security_events_user_id_users_id_fk", + "tableFrom": "security_events", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + }, + "sessions_refresh_token_unique": { + "name": "sessions_refresh_token_unique", + "nullsNotDistinct": false, + "columns": [ + "refresh_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.two_factor_auth": { + "name": "two_factor_auth", + "schema": "auth", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "enabled_at": { + "name": "enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_auth_user_id_users_id_fk": { + "name": "two_factor_auth_user_id_users_id_fk", + "tableFrom": "two_factor_auth", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verification_tokens": { + "name": "verification_tokens", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "verification_tokens_user_id_users_id_fk": { + "name": "verification_tokens_user_id_users_id_fk", + "tableFrom": "verification_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verification_tokens_token_unique": { + "name": "verification_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "credits.balances": { + "name": "balances", + "schema": "credits", + "columns": { + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_credits_remaining": { + "name": "free_credits_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 150 + }, + "daily_free_credits": { + "name": "daily_free_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "last_daily_reset_at": { + "name": "last_daily_reset_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "total_earned": { + "name": "total_earned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_spent": { + "name": "total_spent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "balances_user_id_users_id_fk": { + "name": "balances_user_id_users_id_fk", + "tableFrom": "balances", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "credits.packages": { + "name": "packages", + "schema": "credits", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price_euro_cents": { + "name": "price_euro_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "packages_stripe_price_id_unique": { + "name": "packages_stripe_price_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_price_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "credits.purchases": { + "name": "purchases", + "schema": "credits", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "package_id": { + "name": "package_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "price_euro_cents": { + "name": "price_euro_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stripe_payment_intent_id": { + "name": "stripe_payment_intent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "transaction_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "purchases_user_id_idx": { + "name": "purchases_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchases_stripe_payment_intent_id_idx": { + "name": "purchases_stripe_payment_intent_id_idx", + "columns": [ + { + "expression": "stripe_payment_intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "purchases_user_id_users_id_fk": { + "name": "purchases_user_id_users_id_fk", + "tableFrom": "purchases", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "purchases_package_id_packages_id_fk": { + "name": "purchases_package_id_packages_id_fk", + "tableFrom": "purchases", + "tableTo": "packages", + "schemaTo": "credits", + "columnsFrom": [ + "package_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "purchases_stripe_payment_intent_id_unique": { + "name": "purchases_stripe_payment_intent_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_payment_intent_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "credits.transactions": { + "name": "transactions", + "schema": "credits", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "transaction_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance_before": { + "name": "balance_before", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance_after": { + "name": "balance_after", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "transactions_user_id_idx": { + "name": "transactions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_app_id_idx": { + "name": "transactions_app_id_idx", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_created_at_idx": { + "name": "transactions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactions_idempotency_key_idx": { + "name": "transactions_idempotency_key_idx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactions_user_id_users_id_fk": { + "name": "transactions_user_id_users_id_fk", + "tableFrom": "transactions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "transactions_idempotency_key_unique": { + "name": "transactions_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "credits.usage_stats": { + "name": "usage_stats", + "schema": "credits", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credits_used": { + "name": "credits_used", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "usage_stats_user_id_date_idx": { + "name": "usage_stats_user_id_date_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_stats_app_id_date_idx": { + "name": "usage_stats_app_id_date_idx", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_stats_user_id_users_id_fk": { + "name": "usage_stats_user_id_users_id_fk", + "tableFrom": "usage_stats", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "feedback.feedback_votes": { + "name": "feedback_votes", + "schema": "feedback", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_vote_unique": { + "name": "feedback_vote_unique", + "columns": [ + { + "expression": "feedback_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_feedback_idx": { + "name": "feedback_votes_feedback_idx", + "columns": [ + { + "expression": "feedback_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_feedback_id_user_feedback_id_fk": { + "name": "feedback_votes_feedback_id_user_feedback_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "user_feedback", + "schemaTo": "feedback", + "columnsFrom": [ + "feedback_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_votes_user_id_users_id_fk": { + "name": "feedback_votes_user_id_users_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "feedback.user_feedback": { + "name": "user_feedback", + "schema": "feedback", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_text": { + "name": "feedback_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "feedback_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'feature'" + }, + "status": { + "name": "status", + "type": "feedback_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "admin_response": { + "name": "admin_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vote_count": { + "name": "vote_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "device_info": { + "name": "device_info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "feedback_user_idx": { + "name": "feedback_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_app_idx": { + "name": "feedback_app_idx", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_public_idx": { + "name": "feedback_public_idx", + "columns": [ + { + "expression": "is_public", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_status_idx": { + "name": "feedback_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_created_at_idx": { + "name": "feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_feedback_user_id_users_id_fk": { + "name": "user_feedback_user_id_users_id_fk", + "tableFrom": "user_feedback", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "user", + "admin", + "service" + ] + }, + "public.transaction_status": { + "name": "transaction_status", + "schema": "public", + "values": [ + "pending", + "completed", + "failed", + "cancelled" + ] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "purchase", + "usage", + "refund", + "bonus", + "expiry", + "adjustment" + ] + }, + "public.feedback_category": { + "name": "feedback_category", + "schema": "public", + "values": [ + "bug", + "feature", + "improvement", + "question", + "other" + ] + }, + "public.feedback_status": { + "name": "feedback_status", + "schema": "public", + "values": [ + "submitted", + "under_review", + "planned", + "in_progress", + "completed", + "declined" + ] + } + }, + "schemas": { + "auth": "auth", + "credits": "credits", + "feedback": "feedback" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/services/mana-core-auth/src/db/migrations/meta/_journal.json b/services/mana-core-auth/src/db/migrations/meta/_journal.json index 8570252e1..e45e37ca4 100644 --- a/services/mana-core-auth/src/db/migrations/meta/_journal.json +++ b/services/mana-core-auth/src/db/migrations/meta/_journal.json @@ -1,13 +1,20 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1764089133415, - "tag": "0000_lush_ironclad", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1764089133415, + "tag": "0000_lush_ironclad", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1764448681401, + "tag": "0001_zippy_ma_gnuci", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/services/mana-core-auth/src/db/schema/feedback.schema.ts b/services/mana-core-auth/src/db/schema/feedback.schema.ts new file mode 100644 index 000000000..b16ed5ada --- /dev/null +++ b/services/mana-core-auth/src/db/schema/feedback.schema.ts @@ -0,0 +1,94 @@ +import { + pgSchema, + uuid, + text, + timestamp, + boolean, + jsonb, + integer, + index, + pgEnum, + uniqueIndex, +} from 'drizzle-orm/pg-core'; +import { users } from './auth.schema'; + +export const feedbackSchema = pgSchema('feedback'); + +// Category enum +export const feedbackCategoryEnum = pgEnum('feedback_category', [ + 'bug', + 'feature', + 'improvement', + 'question', + 'other', +]); + +// Status enum +export const feedbackStatusEnum = pgEnum('feedback_status', [ + 'submitted', + 'under_review', + 'planned', + 'in_progress', + 'completed', + 'declined', +]); + +// User feedback table +export const userFeedback = feedbackSchema.table( + 'user_feedback', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: uuid('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + appId: text('app_id').notNull(), // 'chat', 'picture', 'zitare', etc. + + // Content + title: text('title'), + feedbackText: text('feedback_text').notNull(), + category: feedbackCategoryEnum('category').default('feature').notNull(), + + // Status & Publishing + status: feedbackStatusEnum('status').default('submitted').notNull(), + isPublic: boolean('is_public').default(false).notNull(), + adminResponse: text('admin_response'), + + // Voting (denormalized for performance) + voteCount: integer('vote_count').default(0).notNull(), + + // Metadata + deviceInfo: jsonb('device_info'), + + // Timestamps + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + publishedAt: timestamp('published_at', { withTimezone: true }), + completedAt: timestamp('completed_at', { withTimezone: true }), + }, + (table) => ({ + userIdx: index('feedback_user_idx').on(table.userId), + appIdx: index('feedback_app_idx').on(table.appId), + publicIdx: index('feedback_public_idx').on(table.isPublic), + statusIdx: index('feedback_status_idx').on(table.status), + createdAtIdx: index('feedback_created_at_idx').on(table.createdAt), + }) +); + +// Feedback votes table +export const feedbackVotes = feedbackSchema.table( + 'feedback_votes', + { + id: uuid('id').primaryKey().defaultRandom(), + feedbackId: uuid('feedback_id') + .references(() => userFeedback.id, { onDelete: 'cascade' }) + .notNull(), + userId: uuid('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => ({ + uniqueVote: uniqueIndex('feedback_vote_unique').on(table.feedbackId, table.userId), + feedbackIdx: index('feedback_votes_feedback_idx').on(table.feedbackId), + }) +); diff --git a/services/mana-core-auth/src/db/schema/index.ts b/services/mana-core-auth/src/db/schema/index.ts index 727044bf2..cc5fb3c07 100644 --- a/services/mana-core-auth/src/db/schema/index.ts +++ b/services/mana-core-auth/src/db/schema/index.ts @@ -1,2 +1,3 @@ export * from './auth.schema'; export * from './credits.schema'; +export * from './feedback.schema'; diff --git a/services/mana-core-auth/src/feedback/dto/create-feedback.dto.ts b/services/mana-core-auth/src/feedback/dto/create-feedback.dto.ts new file mode 100644 index 000000000..76e3c148d --- /dev/null +++ b/services/mana-core-auth/src/feedback/dto/create-feedback.dto.ts @@ -0,0 +1,21 @@ +import { IsString, IsOptional, MaxLength, MinLength, IsEnum, IsObject } from 'class-validator'; + +export class CreateFeedbackDto { + @IsString() + @IsOptional() + @MaxLength(100) + title?: string; + + @IsString() + @MinLength(10, { message: 'Feedback must be at least 10 characters long' }) + @MaxLength(2000, { message: 'Feedback must be at most 2000 characters long' }) + feedbackText: string; + + @IsEnum(['bug', 'feature', 'improvement', 'question', 'other']) + @IsOptional() + category?: 'bug' | 'feature' | 'improvement' | 'question' | 'other'; + + @IsObject() + @IsOptional() + deviceInfo?: Record; +} diff --git a/services/mana-core-auth/src/feedback/dto/feedback-query.dto.ts b/services/mana-core-auth/src/feedback/dto/feedback-query.dto.ts new file mode 100644 index 000000000..08fdf5595 --- /dev/null +++ b/services/mana-core-auth/src/feedback/dto/feedback-query.dto.ts @@ -0,0 +1,33 @@ +import { IsString, IsOptional, IsEnum, IsInt, Min, Max } from 'class-validator'; +import { Transform } from 'class-transformer'; + +export class FeedbackQueryDto { + @IsString() + @IsOptional() + appId?: string; + + @IsEnum(['submitted', 'under_review', 'planned', 'in_progress', 'completed', 'declined']) + @IsOptional() + status?: string; + + @IsEnum(['bug', 'feature', 'improvement', 'question', 'other']) + @IsOptional() + category?: string; + + @IsEnum(['votes', 'recent']) + @IsOptional() + sort?: 'votes' | 'recent' = 'votes'; + + @Transform(({ value }) => parseInt(value, 10)) + @IsInt() + @Min(1) + @Max(50) + @IsOptional() + limit?: number = 20; + + @Transform(({ value }) => parseInt(value, 10)) + @IsInt() + @Min(0) + @IsOptional() + offset?: number = 0; +} diff --git a/services/mana-core-auth/src/feedback/dto/index.ts b/services/mana-core-auth/src/feedback/dto/index.ts new file mode 100644 index 000000000..c305648d4 --- /dev/null +++ b/services/mana-core-auth/src/feedback/dto/index.ts @@ -0,0 +1,2 @@ +export { CreateFeedbackDto } from './create-feedback.dto'; +export { FeedbackQueryDto } from './feedback-query.dto'; diff --git a/services/mana-core-auth/src/feedback/feedback.controller.ts b/services/mana-core-auth/src/feedback/feedback.controller.ts new file mode 100644 index 000000000..d1d2eb624 --- /dev/null +++ b/services/mana-core-auth/src/feedback/feedback.controller.ts @@ -0,0 +1,54 @@ +import { + Controller, + Get, + Post, + Delete, + Body, + Param, + Query, + UseGuards, + Headers, +} from '@nestjs/common'; +import { FeedbackService } from './feedback.service'; +import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; +import { CurrentUser, CurrentUserData } from '../common/decorators/current-user.decorator'; +import { CreateFeedbackDto, FeedbackQueryDto } from './dto'; + +@Controller('feedback') +@UseGuards(JwtAuthGuard) +export class FeedbackController { + constructor(private readonly feedbackService: FeedbackService) {} + + @Post() + async createFeedback( + @CurrentUser() user: CurrentUserData, + @Body() dto: CreateFeedbackDto, + @Headers('x-app-id') appIdHeader?: string + ) { + const appId = appIdHeader || 'unknown'; + return this.feedbackService.createFeedback(user.userId, appId, dto); + } + + @Get('public') + async getPublicFeedback(@CurrentUser() user: CurrentUserData, @Query() query: FeedbackQueryDto) { + return this.feedbackService.getPublicFeedback(user.userId, query); + } + + @Get('my') + async getMyFeedback( + @CurrentUser() user: CurrentUserData, + @Query('appId') appId?: string + ) { + return this.feedbackService.getMyFeedback(user.userId, appId); + } + + @Post(':id/vote') + async vote(@CurrentUser() user: CurrentUserData, @Param('id') feedbackId: string) { + return this.feedbackService.vote(user.userId, feedbackId); + } + + @Delete(':id/vote') + async unvote(@CurrentUser() user: CurrentUserData, @Param('id') feedbackId: string) { + return this.feedbackService.unvote(user.userId, feedbackId); + } +} diff --git a/services/mana-core-auth/src/feedback/feedback.module.ts b/services/mana-core-auth/src/feedback/feedback.module.ts new file mode 100644 index 000000000..28893c629 --- /dev/null +++ b/services/mana-core-auth/src/feedback/feedback.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FeedbackController } from './feedback.controller'; +import { FeedbackService } from './feedback.service'; + +@Module({ + controllers: [FeedbackController], + providers: [FeedbackService], + exports: [FeedbackService], +}) +export class FeedbackModule {} diff --git a/services/mana-core-auth/src/feedback/feedback.service.ts b/services/mana-core-auth/src/feedback/feedback.service.ts new file mode 100644 index 000000000..d64120215 --- /dev/null +++ b/services/mana-core-auth/src/feedback/feedback.service.ts @@ -0,0 +1,277 @@ +import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { eq, and, desc, sql, count } from 'drizzle-orm'; +import { getDb } from '../db/connection'; +import { userFeedback, feedbackVotes } from '../db/schema'; +import { CreateFeedbackDto, FeedbackQueryDto } from './dto'; +import { AiService } from '../ai/ai.service'; + +@Injectable() +export class FeedbackService { + private readonly logger = new Logger(FeedbackService.name); + + constructor( + private configService: ConfigService, + private aiService: AiService + ) {} + + private getDb() { + const databaseUrl = this.configService.get('database.url'); + return getDb(databaseUrl!); + } + + async createFeedback(userId: string, appId: string, dto: CreateFeedbackDto) { + const db = this.getDb(); + + // Use AI to generate title and category if not provided + let title = dto.title; + let category = dto.category; + + if (!title || !category) { + this.logger.debug('Analyzing feedback with AI...'); + const analysis = await this.aiService.analyzeFeedback(dto.feedbackText); + + if (!title) { + title = analysis.title; + } + if (!category) { + category = analysis.category; + } + this.logger.debug(`AI generated: title="${title}", category="${category}"`); + } + + const [feedback] = await db + .insert(userFeedback) + .values({ + userId, + appId, + title, + feedbackText: dto.feedbackText, + category: category || 'feature', + deviceInfo: dto.deviceInfo, + }) + .returning(); + + return { + success: true, + feedback: this.mapFeedback(feedback, false), + }; + } + + async getPublicFeedback(userId: string, query: FeedbackQueryDto) { + const db = this.getDb(); + const { appId, status, category, sort = 'votes', limit = 20, offset = 0 } = query; + + // Build conditions + const conditions = [eq(userFeedback.isPublic, true)]; + + if (appId) { + conditions.push(eq(userFeedback.appId, appId)); + } + if (status) { + conditions.push(eq(userFeedback.status, status as any)); + } + if (category) { + conditions.push(eq(userFeedback.category, category as any)); + } + + // Get feedback items + const feedbackItems = await db + .select() + .from(userFeedback) + .where(and(...conditions)) + .orderBy(sort === 'votes' ? desc(userFeedback.voteCount) : desc(userFeedback.createdAt)) + .limit(limit) + .offset(offset); + + // Get total count + const [{ total }] = await db + .select({ total: count() }) + .from(userFeedback) + .where(and(...conditions)); + + // Get user's votes + const feedbackIds = feedbackItems.map((f) => f.id); + const userVotes = + feedbackIds.length > 0 + ? await db + .select({ feedbackId: feedbackVotes.feedbackId }) + .from(feedbackVotes) + .where( + and( + eq(feedbackVotes.userId, userId), + sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})` + ) + ) + : []; + + const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId)); + + return { + success: true, + items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))), + total, + }; + } + + async getMyFeedback(userId: string, appId?: string) { + const db = this.getDb(); + + const conditions = [eq(userFeedback.userId, userId)]; + if (appId) { + conditions.push(eq(userFeedback.appId, appId)); + } + + const feedbackItems = await db + .select() + .from(userFeedback) + .where(and(...conditions)) + .orderBy(desc(userFeedback.createdAt)); + + // Get user's votes on their own feedback (for consistency) + const feedbackIds = feedbackItems.map((f) => f.id); + const userVotes = + feedbackIds.length > 0 + ? await db + .select({ feedbackId: feedbackVotes.feedbackId }) + .from(feedbackVotes) + .where( + and( + eq(feedbackVotes.userId, userId), + sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})` + ) + ) + : []; + + const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId)); + + return { + success: true, + items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))), + total: feedbackItems.length, + }; + } + + async vote(userId: string, feedbackId: string) { + const db = this.getDb(); + + // Check if feedback exists and is public + const [feedback] = await db + .select() + .from(userFeedback) + .where(eq(userFeedback.id, feedbackId)) + .limit(1); + + if (!feedback) { + throw new NotFoundException('Feedback not found'); + } + + if (!feedback.isPublic) { + throw new NotFoundException('Feedback not found or not public'); + } + + // Check if user already voted + const [existingVote] = await db + .select() + .from(feedbackVotes) + .where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId))) + .limit(1); + + if (existingVote) { + throw new ConflictException('Already voted'); + } + + // Add vote + await db.insert(feedbackVotes).values({ + feedbackId, + userId, + }); + + // Increment vote count + const [updated] = await db + .update(userFeedback) + .set({ + voteCount: sql`${userFeedback.voteCount} + 1`, + updatedAt: new Date(), + }) + .where(eq(userFeedback.id, feedbackId)) + .returning(); + + return { + success: true, + newVoteCount: updated.voteCount, + userHasVoted: true, + }; + } + + async unvote(userId: string, feedbackId: string) { + const db = this.getDb(); + + // Check if feedback exists + const [feedback] = await db + .select() + .from(userFeedback) + .where(eq(userFeedback.id, feedbackId)) + .limit(1); + + if (!feedback) { + throw new NotFoundException('Feedback not found'); + } + + // Check if user has voted + const [existingVote] = await db + .select() + .from(feedbackVotes) + .where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId))) + .limit(1); + + if (!existingVote) { + throw new NotFoundException('Vote not found'); + } + + // Remove vote + await db + .delete(feedbackVotes) + .where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId))); + + // Decrement vote count + const [updated] = await db + .update(userFeedback) + .set({ + voteCount: sql`GREATEST(${userFeedback.voteCount} - 1, 0)`, + updatedAt: new Date(), + }) + .where(eq(userFeedback.id, feedbackId)) + .returning(); + + return { + success: true, + newVoteCount: updated.voteCount, + userHasVoted: false, + }; + } + + private mapFeedback( + feedback: typeof userFeedback.$inferSelect, + userHasVoted: boolean + ): Record { + return { + id: feedback.id, + userId: feedback.userId, + appId: feedback.appId, + title: feedback.title, + feedbackText: feedback.feedbackText, + category: feedback.category, + status: feedback.status, + isPublic: feedback.isPublic, + adminResponse: feedback.adminResponse, + voteCount: feedback.voteCount, + userHasVoted, + deviceInfo: feedback.deviceInfo, + createdAt: feedback.createdAt.toISOString(), + updatedAt: feedback.updatedAt.toISOString(), + publishedAt: feedback.publishedAt?.toISOString(), + completedAt: feedback.completedAt?.toISOString(), + }; + } +} diff --git a/services/mana-core-auth/src/main.ts b/services/mana-core-auth/src/main.ts index 42328bc1d..3cd9fc4f0 100644 --- a/services/mana-core-auth/src/main.ts +++ b/services/mana-core-auth/src/main.ts @@ -20,7 +20,7 @@ async function bootstrap() { origin: corsOrigins, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-App-Id'], }); // Global validation pipe