refactor: zentralisiere Konfiguration in src/config/constants.ts

- RATING_OPTIONS, INST_COLORS, AI_CONFIG, RATING_NUM_MAP aus Komponenten extrahiert
- #0070AD als @theme-Farbe 'brand' in Tailwind v4 (index.css)
- Alle ~70 hardcoded #0070AD-Vorkommen in Pages durch 'brand' ersetzt
- BRAND_COLOR als JS-Konstante für style-Props (App.tsx)
This commit is contained in:
Lars 2026-07-02 10:18:51 +02:00
parent ece111c0a7
commit 97e3eeb8a6
14 changed files with 139 additions and 116 deletions

View File

@ -1,4 +1,5 @@
import { Routes, Route, NavLink } from 'react-router-dom'
import { BRAND_COLOR } from './config/constants'
import Dashboard from './pages/Dashboard'
import Configuration from './pages/Configuration'
import Consultants from './pages/Consultants'
@ -13,7 +14,7 @@ import Evaluation from './pages/Evaluation'
export default function App() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
<header className="bg-[#0070AD] text-white px-4 py-3 flex items-center gap-3 shadow">
<header className="text-white px-4 py-3 flex items-center gap-3 shadow" style={{ backgroundColor: BRAND_COLOR }}>
<span className="font-bold text-lg tracking-tight">Assignment Monitor</span>
</header>
<main className="flex-1 p-4 max-w-2xl mx-auto w-full pb-20">
@ -42,8 +43,9 @@ export default function App() {
to={to}
end
className={({ isActive }) =>
`text-xs px-3 py-1 rounded ${isActive ? 'text-[#0070AD] font-semibold' : 'text-gray-500'}`
`text-xs px-3 py-1 rounded ${isActive ? 'font-semibold' : 'text-gray-500'}`
}
style={({ isActive }) => isActive ? { color: BRAND_COLOR } : undefined}
>
{label}
</NavLink>

46
src/config/constants.ts Normal file
View File

@ -0,0 +1,46 @@
import type { FeedbackRating } from '../db'
export const BRAND_COLOR = '#0070AD'
/**
* Kanonische Reihenfolge der Rating-Skala (nicht ändern DB-Werte sind stabil).
* `color` = Badge/Display-State (inaktiv)
* `active` = ausgewählter Button-State
*/
export const RATING_OPTIONS: ReadonlyArray<{
value: FeedbackRating
short: string
label: string
color: string
active: string
}> = [
{ value: 'na', short: 'N/A', label: 'N/A (0)', color: 'bg-gray-100 text-gray-500 border-gray-300', active: 'bg-gray-400 text-white border-gray-400' },
{ value: 'not_client_ready', short: 'Not', label: 'Not (1)', color: 'bg-red-100 text-red-700 border-red-300', active: 'bg-red-500 text-white border-red-500' },
{ value: 'partially_client_ready', short: 'Partially', label: 'Partially (2)', color: 'bg-orange-100 text-orange-700 border-orange-300', active: 'bg-orange-500 text-white border-orange-500' },
{ value: 'nearly_client_ready', short: 'Nearly', label: 'Nearly (3)', color: 'bg-green-100 text-green-600 border-green-300', active: 'bg-green-400 text-white border-green-400' },
{ value: 'client_ready', short: 'Fully', label: 'Fully (4)', color: 'bg-green-100 text-green-800 border-green-400', active: 'bg-green-700 text-white border-green-700' },
]
/** Farb-Schemata für Teilnehmer-Badges (Index = Reihenfolge im Assignment). */
export const INST_COLORS = [
{ bg: 'bg-blue-50', border: 'border-blue-300', tag: 'bg-blue-500', text: 'text-blue-700' },
{ bg: 'bg-purple-50', border: 'border-purple-300', tag: 'bg-purple-500', text: 'text-purple-700' },
{ bg: 'bg-emerald-50', border: 'border-emerald-300', tag: 'bg-emerald-500', text: 'text-emerald-700' },
{ bg: 'bg-orange-50', border: 'border-orange-300', tag: 'bg-orange-500', text: 'text-orange-700' },
] as const
export const AI_CONFIG = {
localStorageKeyApiKey: 'ai_api_key',
localStorageKeyModel: 'ai_model',
defaultModel: 'openai/gpt-4o-mini',
openRouterBaseUrl: 'https://openrouter.ai/api/v1',
} as const
/** Numerische Gewichte für Rating-Aggregation. N/A wird aus Berechnungen ausgeschlossen. */
export const RATING_NUM_MAP: Record<FeedbackRating, number> = {
na: 0,
not_client_ready: 1,
partially_client_ready: 2,
nearly_client_ready: 3,
client_ready: 4,
}

View File

@ -1 +1,6 @@
@import "tailwindcss";
@theme {
--color-brand: #0070AD;
--color-brand-dark: #005a8e;
}

View File

@ -88,13 +88,13 @@ export default function AssignmentCreate() {
{/* Basisdaten */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 space-y-3">
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Titel des Assignments" value={form.title}
onChange={e => setForm(p => ({ ...p, title: e.target.value }))} />
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Kunde / Unternehmen" value={form.client}
onChange={e => setForm(p => ({ ...p, client: e.target.value }))} />
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
<input className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Lead-Name (dein Name)" value={form.leadName}
onChange={e => setForm(p => ({ ...p, leadName: e.target.value }))} />
<select className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm"
@ -125,7 +125,7 @@ export default function AssignmentCreate() {
<div className="flex items-center justify-between">
<h2 className="font-semibold text-gray-700 text-sm">Institutees (Berater)</h2>
<button onClick={() => setShowAddInstitutee(p => !p)}
className="text-xs text-[#0070AD] border border-[#0070AD] px-2.5 py-1 rounded-lg">
className="text-xs text-brand border border-brand px-2.5 py-1 rounded-lg">
+ Neu anlegen
</button>
</div>
@ -135,11 +135,11 @@ export default function AssignmentCreate() {
<div className="bg-blue-50 border border-blue-100 rounded-lg p-3 space-y-2">
<div className="text-xs font-semibold text-blue-600 mb-1">Neuen Berater anlegen</div>
<div className="flex gap-2">
<input className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
<input className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Vorname" value={newFirst}
onChange={e => setNewFirst(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addInstituteeInline()} />
<input className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
<input className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Nachname" value={newLast}
onChange={e => setNewLast(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addInstituteeInline()} />
@ -155,7 +155,7 @@ export default function AssignmentCreate() {
<div className="flex gap-2">
<button onClick={addInstituteeInline}
disabled={!newFirst.trim() || !newLast.trim()}
className="flex-1 py-2 rounded-lg text-sm font-medium bg-[#0070AD] text-white disabled:bg-gray-200 disabled:text-gray-400">
className="flex-1 py-2 rounded-lg text-sm font-medium bg-brand text-white disabled:bg-gray-200 disabled:text-gray-400">
Hinzufügen
</button>
<button onClick={() => { setShowAddInstitutee(false); setNewFirst(''); setNewLast('') }}
@ -182,7 +182,7 @@ export default function AssignmentCreate() {
{groupConsultants.map(c => (
<label key={c.id} className="flex items-center gap-3 py-1.5 cursor-pointer">
<input type="checkbox" checked={form.instituteeIds.includes(c.id!)}
onChange={() => toggleInstitutee(c.id!)} className="w-4 h-4 accent-[#0070AD]" />
onChange={() => toggleInstitutee(c.id!)} className="w-4 h-4 accent-brand" />
<span className="text-sm">{c.firstName} {c.lastName}</span>
</label>
))}
@ -192,7 +192,7 @@ export default function AssignmentCreate() {
{/* Gewählte Berater aus anderen Gruppen */}
{selectedInstitutees.filter(c => c.groupId !== form.selectedGroupId).map(c => (
<div key={c.id} className="flex items-center gap-2 py-1">
<span className="w-4 h-4 rounded bg-[#0070AD] flex items-center justify-center">
<span className="w-4 h-4 rounded bg-brand flex items-center justify-center">
<span className="text-white text-xs"></span>
</span>
<span className="text-sm">{c.firstName} {c.lastName}</span>
@ -203,7 +203,7 @@ export default function AssignmentCreate() {
<button onClick={create} disabled={!canCreate}
className={`w-full py-3 rounded-xl text-sm font-medium shadow transition ${
canCreate ? 'bg-[#0070AD] text-white hover:bg-[#005a8e]' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
canCreate ? 'bg-brand text-white hover:bg-brand-dark' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}>
Assignment anlegen
</button>

View File

@ -150,17 +150,17 @@ export default function AssignmentDetail() {
return (
<div className="space-y-4">
<div>
<button onClick={() => navigate('/')} className="text-sm text-[#0070AD] mb-2 block"> Zurück</button>
<button onClick={() => navigate('/')} className="text-sm text-brand mb-2 block"> Zurück</button>
<div className="flex items-start justify-between gap-2">
<div>
{assignment.assignmentNumber && (
<div className="text-xs font-semibold text-[#0070AD] uppercase tracking-wide">{assignment.assignmentNumber}</div>
<div className="text-xs font-semibold text-brand uppercase tracking-wide">{assignment.assignmentNumber}</div>
)}
<h1 className="text-xl font-bold text-gray-800 leading-tight">{assignment.title}</h1>
</div>
<button
onClick={() => navigate(`/assignment/${assignmentId}/edit`)}
className="flex-shrink-0 text-xs px-3 py-1.5 border border-gray-200 rounded-lg text-gray-500 hover:border-[#0070AD] hover:text-[#0070AD] transition">
className="flex-shrink-0 text-xs px-3 py-1.5 border border-gray-200 rounded-lg text-gray-500 hover:border-brand hover:text-brand transition">
Bearbeiten
</button>
</div>
@ -175,7 +175,7 @@ export default function AssignmentDetail() {
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-700">Ad-hoc Meeting starten</span>
<button onClick={() => setShowAdHoc(p => !p)}
className="text-xs bg-[#0070AD] text-white px-3 py-1.5 rounded-lg font-medium">
className="text-xs bg-brand text-white px-3 py-1.5 rounded-lg font-medium">
{showAdHoc ? 'Abbrechen' : '+ Ad-hoc'}
</button>
</div>
@ -187,7 +187,7 @@ export default function AssignmentDetail() {
const count = phaseRows.find(r => r.template.id === t.id)?.instances.length ?? 0
startMeeting(t.id!, count)
}}
className="py-2.5 px-3 text-left rounded-lg border border-gray-200 hover:border-[#0070AD] hover:bg-blue-50 transition">
className="py-2.5 px-3 text-left rounded-lg border border-gray-200 hover:border-brand hover:bg-blue-50 transition">
<div className="text-sm font-medium text-gray-700">{t.label}</div>
<div className={`text-xs mt-0.5 ${t.withWhom === 'client' ? 'text-blue-500' : 'text-purple-500'}`}>
{t.withWhom === 'client' ? 'Kundentermin' : 'Lead-intern'}
@ -237,7 +237,7 @@ export default function AssignmentDetail() {
<button
onClick={() => canStart && startMeeting(template.id!, instances.length)}
className={`ml-2 flex-shrink-0 text-xs px-3 py-1.5 rounded-lg font-medium ${
canStart ? 'bg-[#0070AD] text-white' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
canStart ? 'bg-brand text-white' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
disabled={!canStart}>
{instances.length === 0 ? 'Starten' : template.isRepeatable ? '+ Wiederholen' : 'Erledigt'}

View File

@ -96,7 +96,7 @@ export default function AssignmentEdit() {
{/* Header */}
<div>
<button onClick={() => navigate(`/assignment/${assignmentId}`)}
className="text-sm text-[#0070AD] mb-2 block"> Zurück</button>
className="text-sm text-brand mb-2 block"> Zurück</button>
<h1 className="text-xl font-bold text-gray-800">Assignment bearbeiten</h1>
</div>
@ -105,7 +105,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Assignment-Nummer</label>
<input
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="z. B. CGI-2024-001"
value={assignmentNumber}
onChange={e => setAssignmentNumber(e.target.value)}
@ -114,7 +114,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Titel</label>
<input
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={title}
onChange={e => setTitle(e.target.value)}
/>
@ -122,7 +122,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Kunde</label>
<input
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={client}
onChange={e => setClient(e.target.value)}
/>
@ -130,7 +130,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Beschreibung</label>
<textarea
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
rows={3}
value={description}
onChange={e => setDescription(e.target.value)}
@ -139,7 +139,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Assignment-Typ</label>
<select
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={assignmentTypeId}
onChange={e => handleTypeChange(Number(e.target.value))}>
{assignmentTypes.map(t => (
@ -155,7 +155,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Lead</label>
<input
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={leadName}
onChange={e => setLeadName(e.target.value)}
/>
@ -163,7 +163,7 @@ export default function AssignmentEdit() {
<div className="space-y-1">
<label className="text-xs font-medium text-gray-500">Status</label>
<select
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={status}
onChange={e => setStatus(e.target.value as 'active' | 'done')}>
<option value="active">Aktiv</option>
@ -193,7 +193,7 @@ export default function AssignmentEdit() {
<span className="text-xs text-gray-400">Von</span>
<input
type="date"
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-[#0070AD]"
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
value={enrollment?.from ?? ''}
onChange={e => updateEnrollment(cid, 'from', e.target.value)}
/>
@ -202,7 +202,7 @@ export default function AssignmentEdit() {
<span className="text-xs text-gray-400">Bis</span>
<input
type="date"
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-[#0070AD]"
className="border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
value={enrollment?.to ?? ''}
onChange={e => updateEnrollment(cid, 'to', e.target.value)}
/>
@ -220,7 +220,7 @@ export default function AssignmentEdit() {
{availableConsultants.length > 0 && (
<div className="flex gap-2">
<select
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
value={addInstituteeId}
onChange={e => setAddInstituteeId(Number(e.target.value))}>
<option value={0}>Institutee auswählen</option>
@ -231,7 +231,7 @@ export default function AssignmentEdit() {
<button
onClick={addInstitutee}
disabled={!addInstituteeId}
className="px-4 py-2 bg-[#0070AD] text-white text-sm rounded-lg font-medium disabled:opacity-40">
className="px-4 py-2 bg-brand text-white text-sm rounded-lg font-medium disabled:opacity-40">
Hinzufügen
</button>
</div>
@ -242,7 +242,7 @@ export default function AssignmentEdit() {
<div className="flex gap-3">
<button
onClick={handleSave}
className="flex-1 py-3 bg-[#0070AD] text-white rounded-xl text-sm font-semibold">
className="flex-1 py-3 bg-brand text-white rounded-xl text-sm font-semibold">
Speichern
</button>
<button

View File

@ -12,40 +12,24 @@ import {
type FeedbackDimensionText,
type FeedbackRating,
} from '../db'
import { RATING_OPTIONS, RATING_NUM_MAP, AI_CONFIG } from '../config/constants'
const ratingToNum = (r: FeedbackRating | null | undefined): number => {
const m: Record<FeedbackRating, number> = { na: 0, not_client_ready: 1, partially_client_ready: 2, nearly_client_ready: 3, client_ready: 4 }
return r ? (m[r] ?? 0) : 0
}
// ── Rating-Skala ─────────────────────────────────────────────────────────────
const RATINGS: { value: FeedbackRating; short: string; label: string; color: string }[] = [
{ value: 'na', short: 'N/A', label: 'N/A (0)', color: 'bg-gray-100 text-gray-500 border-gray-300' },
{ value: 'not_client_ready', short: 'Not', label: 'Not (1)', color: 'bg-red-100 text-red-700 border-red-300' },
{ value: 'partially_client_ready', short: 'Partially', label: 'Partially (2)', color: 'bg-orange-100 text-orange-700 border-orange-300' },
{ value: 'nearly_client_ready', short: 'Nearly', label: 'Nearly (3)', color: 'bg-green-100 text-green-600 border-green-300' },
{ value: 'client_ready', short: 'Fully', label: 'Fully (4)', color: 'bg-green-100 text-green-800 border-green-400' },
]
const ratingToNum = (r: FeedbackRating | null | undefined): number =>
r ? (RATING_NUM_MAP[r] ?? 0) : 0
const ratingColor = (r: FeedbackRating | undefined) =>
RATINGS.find(x => x.value === r)?.color ?? 'bg-gray-50 text-gray-400 border-gray-200'
// ── KI-Einstellungen aus localStorage ────────────────────────────────────────
const AI_KEY = 'ai_api_key'
const AI_MODEL = 'ai_model'
RATING_OPTIONS.find(x => x.value === r)?.color ?? 'bg-gray-50 text-gray-400 border-gray-200'
function getAiSettings() {
return {
apiKey: localStorage.getItem(AI_KEY) ?? '',
model: localStorage.getItem(AI_MODEL) ?? 'openai/gpt-4o-mini',
apiKey: localStorage.getItem(AI_CONFIG.localStorageKeyApiKey) ?? '',
model: localStorage.getItem(AI_CONFIG.localStorageKeyModel) ?? AI_CONFIG.defaultModel,
}
}
function saveAiSettings(apiKey: string, model: string) {
localStorage.setItem(AI_KEY, apiKey)
localStorage.setItem(AI_MODEL, model)
localStorage.setItem(AI_CONFIG.localStorageKeyApiKey, apiKey)
localStorage.setItem(AI_CONFIG.localStorageKeyModel, model)
}
// ── Prompt-Builder ────────────────────────────────────────────────────────────
@ -95,7 +79,7 @@ async function buildAiPrompt(
const dimCats = categories.filter(c => c.dimensionId === dim.id)
const lines = dimCats.map(cat => {
const r = ratings.find(r => r.feedbackCategoryId === cat.id)
const rating = RATINGS.find(x => x.value === r?.rating)?.label ?? 'nicht bewertet'
const rating = RATING_OPTIONS.find(x => x.value === r?.rating)?.label ?? 'nicht bewertet'
return ` ${cat.name}: ${rating}`
}).join('\n')
return ` Dimension "${dim.name}":\n${lines}`
@ -338,7 +322,7 @@ export default function AssignmentFeedbackPage() {
<div className="flex items-start justify-between">
<div>
<button onClick={() => navigate(`/assignment/${assignmentId}`)}
className="text-xs text-[#0070AD] mb-0.5 block"> Assignment</button>
className="text-xs text-brand mb-0.5 block"> Assignment</button>
<h1 className="text-xl font-bold text-gray-800">
Feedback: {institutee.firstName} {institutee.lastName}
</h1>
@ -369,7 +353,7 @@ export default function AssignmentFeedbackPage() {
</div>
<input
type="password"
className="w-full border border-blue-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD] bg-white"
className="w-full border border-blue-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand bg-white"
placeholder="sk-or-..."
value={aiKey}
onChange={e => setAiKey(e.target.value)}
@ -386,7 +370,7 @@ export default function AssignmentFeedbackPage() {
</select>
<div className="flex gap-2">
<button onClick={() => { saveAiSettings(aiKey, aiModel); setShowAiSettings(false) }}
className="flex-1 py-2 rounded-lg text-sm font-medium bg-[#0070AD] text-white">
className="flex-1 py-2 rounded-lg text-sm font-medium bg-brand text-white">
Speichern
</button>
<button onClick={() => setShowAiSettings(false)}
@ -405,7 +389,7 @@ export default function AssignmentFeedbackPage() {
className={`flex-1 py-3 rounded-xl text-sm font-medium transition ${
generating
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-[#0070AD] text-white hover:bg-[#005a8e]'
: 'bg-brand text-white hover:bg-brand-dark'
}`}>
{generating ? 'Generiere Entwurf…' : '✦ KI-Feedback-Entwurf generieren'}
</button>
@ -446,7 +430,7 @@ export default function AssignmentFeedbackPage() {
<span className="font-semibold text-gray-800 text-sm">{dim.name}</span>
{worstRating && (
<span className={`text-xs px-2 py-0.5 rounded-full border font-medium ${summaryColor}`}>
{RATINGS.find(r => r.value === worstRating)?.short}
{RATING_OPTIONS.find(r => r.value === worstRating)?.short}
</span>
)}
</div>
@ -465,7 +449,7 @@ export default function AssignmentFeedbackPage() {
{/* Rating-Buttons */}
<div className="flex gap-1 flex-wrap">
{RATINGS.map(r => (
{RATING_OPTIONS.map(r => (
<button key={r.value}
onClick={() => setRating(cat.id!, r.value)}
className={`px-2.5 py-1.5 rounded-lg text-xs font-semibold border transition ${

View File

@ -117,14 +117,14 @@ export default function AssignmentTypeConfig() {
{/* Neuer Typ */}
<div className="flex gap-2">
<input
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Neuer Assignment-Typ…"
value={newTypeName}
onChange={e => setNewTypeName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addType()}
/>
<button onClick={addType}
className="px-4 py-2 bg-[#0070AD] text-white text-sm rounded-lg font-medium">
className="px-4 py-2 bg-brand text-white text-sm rounded-lg font-medium">
+ Typ
</button>
</div>
@ -145,7 +145,7 @@ export default function AssignmentTypeConfig() {
{/* Header */}
<div className="flex items-center gap-2 px-4 py-3 bg-gray-50 border-b border-gray-100">
<input
className="flex-1 bg-transparent text-sm font-semibold text-gray-800 focus:outline-none border-b border-transparent focus:border-[#0070AD] py-0.5"
className="flex-1 bg-transparent text-sm font-semibold text-gray-800 focus:outline-none border-b border-transparent focus:border-brand py-0.5"
value={type.name}
onChange={e => updateTypeName(type.id!, e.target.value)}
/>
@ -170,7 +170,7 @@ export default function AssignmentTypeConfig() {
<button key={t}
onClick={() => setActiveTab(prev => ({ ...prev, [type.id!]: t }))}
className={`flex-1 py-2 text-xs font-medium transition ${
tab === t ? 'text-[#0070AD] border-b-2 border-[#0070AD]' : 'text-gray-400'
tab === t ? 'text-brand border-b-2 border-brand' : 'text-gray-400'
}`}>
{t === 'phases' ? `Phasen (${typPhases.length})` : `Kriterien (${selected.length || 'alle'})`}
@ -185,14 +185,14 @@ export default function AssignmentTypeConfig() {
<div key={phase.id} className="border border-gray-100 rounded-lg px-3 py-2 space-y-1.5">
<div className="flex items-center gap-2">
<input
className="flex-1 text-sm text-gray-700 border-b border-transparent focus:border-[#0070AD] focus:outline-none bg-transparent"
className="flex-1 text-sm text-gray-700 border-b border-transparent focus:border-brand focus:outline-none bg-transparent"
value={phase.label}
onChange={e => updatePhase(phase.id!, { label: e.target.value })}
/>
<select
value={phase.withWhom}
onChange={e => updatePhase(phase.id!, { withWhom: e.target.value as 'lead' | 'client' })}
className="text-xs border border-gray-200 rounded px-1.5 py-1 focus:outline-none focus:border-[#0070AD]">
className="text-xs border border-gray-200 rounded px-1.5 py-1 focus:outline-none focus:border-brand">
<option value="lead">Lead</option>
<option value="client">Kunde</option>
</select>
@ -224,7 +224,7 @@ export default function AssignmentTypeConfig() {
<div className="text-xs font-medium text-gray-500">Neue Phase</div>
<div className="flex gap-2">
<input
className="flex-1 border border-gray-200 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[#0070AD] bg-white"
className="flex-1 border border-gray-200 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-brand bg-white"
placeholder="Phasenname…"
value={np.label}
onChange={e => setNewPhase(prev => ({ ...prev, [type.id!]: { ...np, label: e.target.value } }))}
@ -233,7 +233,7 @@ export default function AssignmentTypeConfig() {
<select
value={np.withWhom}
onChange={e => setNewPhase(prev => ({ ...prev, [type.id!]: { ...np, withWhom: e.target.value as 'lead' | 'client' } }))}
className="border border-gray-200 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-[#0070AD] bg-white">
className="border border-gray-200 rounded px-2 py-1.5 text-sm focus:outline-none focus:border-brand bg-white">
<option value="lead">Lead</option>
<option value="client">Kunde</option>
</select>
@ -256,7 +256,7 @@ export default function AssignmentTypeConfig() {
</label>
</div>
<button onClick={() => addPhase(type.id!)}
className="px-4 py-1.5 bg-[#0070AD] text-white text-xs rounded-lg font-medium">
className="px-4 py-1.5 bg-brand text-white text-xs rounded-lg font-medium">
+ Hinzufügen
</button>
</div>

View File

@ -41,15 +41,15 @@ export default function Configuration() {
{/* Tab-Switcher */}
<div className="flex bg-gray-100 rounded-xl p-1 gap-0.5">
<button onClick={() => setTab('feedback')}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'feedback' ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'}`}>
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'feedback' ? 'bg-white text-brand shadow-sm' : 'text-gray-500'}`}>
Feedback
</button>
<button onClick={() => setTab('types')}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'types' ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'}`}>
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'types' ? 'bg-white text-brand shadow-sm' : 'text-gray-500'}`}>
Ass.-Typen
</button>
<button onClick={() => setTab('backup')}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'backup' ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'}`}>
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${tab === 'backup' ? 'bg-white text-brand shadow-sm' : 'text-gray-500'}`}>
Backup
</button>
</div>
@ -76,7 +76,7 @@ export default function Configuration() {
Diese Datei kannst du sicher aufbewahren und bei Bedarf wiederherstellen.
</p>
<button onClick={handleExport}
className="w-full py-3 bg-[#0070AD] text-white rounded-xl text-sm font-semibold">
className="w-full py-3 bg-brand text-white rounded-xl text-sm font-semibold">
Vollständiges Backup herunterladen
</button>
</div>

View File

@ -47,7 +47,7 @@ export default function Consultants() {
onChange={e => setNewGroup(p => ({ ...p, name: e.target.value }))}
onKeyDown={e => e.key === 'Enter' && addGroup()}
/>
<button onClick={addGroup} className="bg-[#0070AD] text-white px-4 py-2 rounded-lg text-sm">+</button>
<button onClick={addGroup} className="bg-brand text-white px-4 py-2 rounded-lg text-sm">+</button>
</div>
</section>
@ -67,7 +67,7 @@ export default function Consultants() {
<input className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm" placeholder="Nachname"
value={newCons.lastName} onChange={e => setNewCons(p => ({ ...p, lastName: e.target.value }))} />
</div>
<button onClick={addConsultant} className="w-full bg-[#0070AD] text-white py-2 rounded-lg text-sm font-medium">Berater hinzufügen</button>
<button onClick={addConsultant} className="w-full bg-brand text-white py-2 rounded-lg text-sm font-medium">Berater hinzufügen</button>
</section>
{groups.map(g => {

View File

@ -53,7 +53,7 @@ export default function Dashboard() {
<h1 className="text-xl font-bold text-gray-800">Assignments</h1>
<button
onClick={() => navigate('/assignment/new')}
className="bg-[#0070AD] text-white px-4 py-2 rounded-lg text-sm font-medium"
className="bg-brand text-white px-4 py-2 rounded-lg text-sm font-medium"
>
+ Neues
</button>
@ -66,13 +66,13 @@ export default function Dashboard() {
placeholder="Suche nach Nummer, Titel, Kunde, Berater…"
value={query}
onChange={e => setQuery(e.target.value)}
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD] bg-white"
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-sm focus:outline-none focus:border-brand bg-white"
/>
<div className="flex bg-gray-100 rounded-xl p-1 gap-0.5">
{([['all', 'Alle'], ['active', 'Aktiv'], ['done', 'Abgeschlossen']] as const).map(([val, label]) => (
<button key={val} onClick={() => setStatusFilter(val)}
className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition ${
statusFilter === val ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'
statusFilter === val ? 'bg-white text-brand shadow-sm' : 'text-gray-500'
}`}>
{label}
</button>
@ -96,7 +96,7 @@ export default function Dashboard() {
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
{a.assignmentNumber && (
<div className="text-xs font-semibold text-[#0070AD] uppercase tracking-wide leading-none mb-0.5">
<div className="text-xs font-semibold text-brand uppercase tracking-wide leading-none mb-0.5">
{a.assignmentNumber}
</div>
)}

View File

@ -159,7 +159,7 @@ export default function FeedbackDraftPage() {
<div className="space-y-4">
<div>
<button onClick={() => navigate(`/assignment/${assignmentId}`)}
className="text-sm text-[#0070AD] mb-2 block">
className="text-sm text-brand mb-2 block">
Assignment
</button>
<h1 className="text-xl font-bold text-gray-800">
@ -191,12 +191,12 @@ export default function FeedbackDraftPage() {
<div className="flex items-center justify-between">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wide">Feedback-Entwurf</div>
<button onClick={generateDraft}
className="text-xs bg-[#0070AD] text-white px-3 py-1.5 rounded-lg font-medium">
className="text-xs bg-brand text-white px-3 py-1.5 rounded-lg font-medium">
{generated ? 'Neu generieren' : 'Automatisch generieren'}
</button>
</div>
<textarea
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-brand"
rows={18}
placeholder="Tippe deinen Feedback-Entwurf hier, oder nutze 'Automatisch generieren'…"
value={content}

View File

@ -198,14 +198,14 @@ export default function FeedbackStructureConfig() {
{/* Neue Dimension */}
<div className="flex gap-2">
<input
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="flex-1 border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
placeholder="Neue Dimension…"
value={newDimName}
onChange={e => setNewDimName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addDimension()}
/>
<button onClick={addDimension}
className="px-4 py-2 bg-[#0070AD] text-white text-sm rounded-lg font-medium">
className="px-4 py-2 bg-brand text-white text-sm rounded-lg font-medium">
+ Dimension
</button>
</div>
@ -226,7 +226,7 @@ export default function FeedbackStructureConfig() {
className="text-xs text-gray-400 hover:text-gray-600 leading-none disabled:opacity-30"></button>
</div>
<input
className="flex-1 bg-transparent font-semibold text-sm text-gray-800 focus:outline-none border-b border-transparent focus:border-[#0070AD]"
className="flex-1 bg-transparent font-semibold text-sm text-gray-800 focus:outline-none border-b border-transparent focus:border-brand"
value={dim.name}
onChange={e => updateDimension(dim.id!, e.target.value)}
/>
@ -275,7 +275,7 @@ export default function FeedbackStructureConfig() {
value={item.weight ?? 1}
onChange={e => updateItemWeight(item.id!, Number(e.target.value))}
title="Gewichtung"
className="text-xs border border-gray-200 rounded px-1 py-0.5 text-gray-500 bg-white focus:outline-none focus:border-[#0070AD]">
className="text-xs border border-gray-200 rounded px-1 py-0.5 text-gray-500 bg-white focus:outline-none focus:border-brand">
{[1,2,3,4,5].map(w => <option key={w} value={w}>×{w}</option>)}
</select>
<button onClick={() => deleteItem(item.id!)}
@ -284,7 +284,7 @@ export default function FeedbackStructureConfig() {
))}
<div className="flex gap-1.5 mt-1">
<input
className="flex-1 border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-[#0070AD]"
className="flex-1 border border-gray-200 rounded px-2 py-1 text-xs focus:outline-none focus:border-brand"
placeholder="Neues Kriterium…"
value={newItemName[cat.id!] ?? ''}
onChange={e => setNewItemName(p => ({ ...p, [cat.id!]: e.target.value }))}
@ -302,7 +302,7 @@ export default function FeedbackStructureConfig() {
{/* Neue Kategorie */}
<div className="flex gap-1.5">
<input
className="flex-1 border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-[#0070AD]"
className="flex-1 border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-brand"
placeholder="Neue Kategorie…"
value={newCatName[dim.id!] ?? ''}
onChange={e => setNewCatName(p => ({ ...p, [dim.id!]: e.target.value }))}

View File

@ -15,13 +15,7 @@ import {
} from '../db'
import { exportMeetingAsMarkdown, exportMeetingAsJson, downloadFile } from '../utils/meetingExport'
import { NotationText } from '../utils/notationParser'
const INST_COLORS = [
{ bg: 'bg-blue-50', border: 'border-blue-300', tag: 'bg-blue-500', text: 'text-blue-700' },
{ bg: 'bg-purple-50', border: 'border-purple-300', tag: 'bg-purple-500', text: 'text-purple-700' },
{ bg: 'bg-emerald-50', border: 'border-emerald-300', tag: 'bg-emerald-500', text: 'text-emerald-700' },
{ bg: 'bg-orange-50', border: 'border-orange-300', tag: 'bg-orange-500', text: 'text-orange-700' },
]
import { INST_COLORS, RATING_OPTIONS } from '../config/constants'
const NOTATIONS = [
{ sym: '+', tip: 'Gut' },
@ -32,14 +26,6 @@ const NOTATIONS = [
{ sym: '[…]', tip: 'Slide-Referenz' },
]
const RATING_OPTIONS: { value: FeedbackRating; short: string; color: string; active: string }[] = [
{ value: 'na', short: 'N/A', color: 'bg-gray-50 text-gray-400 border-gray-200', active: 'bg-gray-400 text-white border-gray-400' },
{ value: 'not_client_ready', short: 'Not', color: 'bg-red-50 text-red-400 border-red-200', active: 'bg-red-500 text-white border-red-500' },
{ value: 'partially_client_ready', short: 'Partially', color: 'bg-orange-50 text-orange-400 border-orange-200', active: 'bg-orange-500 text-white border-orange-500' },
{ value: 'nearly_client_ready', short: 'Nearly', color: 'bg-green-50 text-green-500 border-green-200', active: 'bg-green-400 text-white border-green-400' },
{ value: 'client_ready', short: 'Fully', color: 'bg-green-50 text-green-800 border-green-300', active: 'bg-green-700 text-white border-green-700' },
]
type Tab = 'conversation' | 'assessment'
export default function MeetingView() {
@ -328,9 +314,9 @@ export default function MeetingView() {
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<button onClick={() => navigate(`/assignment/${assignment?.id}`)}
className="text-xs text-[#0070AD] mb-0.5 block"> Assignment</button>
className="text-xs text-brand mb-0.5 block"> Assignment</button>
{assignment?.assignmentNumber && (
<div className="text-xs font-semibold text-[#0070AD] uppercase tracking-wide leading-none mb-0.5">{assignment.assignmentNumber}</div>
<div className="text-xs font-semibold text-brand uppercase tracking-wide leading-none mb-0.5">{assignment.assignmentNumber}</div>
)}
<div className="text-sm font-semibold text-gray-600 truncate">{assignment?.title}</div>
<div className="font-bold text-gray-800 truncate">{template.label}</div>
@ -392,7 +378,7 @@ export default function MeetingView() {
{editingGeneralNotes ? (
<textarea
autoFocus
className="w-full border border-[#0070AD] rounded-xl px-3 py-2 text-sm bg-white focus:outline-none resize-none"
className="w-full border border-brand rounded-xl px-3 py-2 text-sm bg-white focus:outline-none resize-none"
style={{ minHeight: '4rem', fieldSizing: 'content' } as React.CSSProperties}
placeholder="Allgemeine Notizen zum Meeting… (+ gut, ! negativ, > Kundeninput)"
value={generalNotes}
@ -416,7 +402,7 @@ export default function MeetingView() {
{hasConv && (
<button onClick={() => setTab('conversation')}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
tab === 'conversation' ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'
tab === 'conversation' ? 'bg-white text-brand shadow-sm' : 'text-gray-500'
}`}>
Gesprächsprotokoll
</button>
@ -424,7 +410,7 @@ export default function MeetingView() {
{hasAssess && (
<button onClick={() => setTab('assessment')}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${
tab === 'assessment' ? 'bg-white text-[#0070AD] shadow-sm' : 'text-gray-500'
tab === 'assessment' ? 'bg-white text-brand shadow-sm' : 'text-gray-500'
}`}>
Gesamtbewertung
</button>
@ -438,7 +424,7 @@ export default function MeetingView() {
{/* Aktiver Sprecher-Bereich */}
<div className={`bg-white rounded-xl shadow-sm border-2 p-4 space-y-3 ${
activeInstId !== null ? (activeColor?.border ?? 'border-[#0070AD]') : 'border-gray-200'
activeInstId !== null ? (activeColor?.border ?? 'border-brand') : 'border-gray-200'
}`}>
<div>
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wide mb-2">Wer spricht?</div>
@ -502,7 +488,7 @@ export default function MeetingView() {
{/* Notiz */}
<textarea
ref={noteRef}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-[#0070AD]"
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-brand"
rows={4}
placeholder="Notizen… (+ gut, (!) negativ mit Abstrichen, > Kundeninput, [Slide 3])"
value={entryNote}
@ -576,7 +562,7 @@ export default function MeetingView() {
{isEditing ? (
<div className="space-y-2">
<textarea
className="w-full border border-gray-300 rounded-lg px-2 py-1.5 text-sm focus:outline-none focus:border-[#0070AD] bg-white"
className="w-full border border-gray-300 rounded-lg px-2 py-1.5 text-sm focus:outline-none focus:border-brand bg-white"
rows={3}
value={editNote}
onChange={e => setEditNote(e.target.value)}
@ -592,7 +578,7 @@ export default function MeetingView() {
</div>
<div className="flex gap-2">
<button onClick={() => saveEdit(entry.id!)}
className="text-xs bg-[#0070AD] text-white px-3 py-1.5 rounded-lg">Speichern</button>
className="text-xs bg-brand text-white px-3 py-1.5 rounded-lg">Speichern</button>
<button onClick={() => setEditingId(null)}
className="text-xs border border-gray-200 text-gray-500 px-3 py-1.5 rounded-lg">Abbrechen</button>
</div>
@ -707,7 +693,7 @@ export default function MeetingView() {
<span className={`text-sm font-medium ${headerRo ? 'text-white' : 'text-gray-700'}`}>{cat.name}</span>
<div className="flex items-center gap-2">
{!headerRo && ratedCount > 0 && (
<span className="text-xs text-[#0070AD] font-medium">{ratedCount}/{catItems.length}</span>
<span className="text-xs text-brand font-medium">{ratedCount}/{catItems.length}</span>
)}
<span className={`text-xs ${headerRo ? 'text-white/70' : 'text-gray-300'}`}>{isOpen ? '▲' : '▼'}</span>
</div>
@ -737,7 +723,7 @@ export default function MeetingView() {
})}
<div className="px-3 py-2 bg-gray-50">
<textarea
className="w-full border border-gray-200 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-[#0070AD] bg-white"
className="w-full border border-gray-200 rounded-lg px-3 py-1.5 text-xs focus:outline-none focus:border-brand bg-white"
rows={1}
placeholder="Kommentar zur Kategorie"
defaultValue={assessments.find(a => a.instituteeId === inst.id && a.criteriaId === cat.id)?.note ?? ''}