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 { Routes, Route, NavLink } from 'react-router-dom'
import { BRAND_COLOR } from './config/constants'
import Dashboard from './pages/Dashboard' import Dashboard from './pages/Dashboard'
import Configuration from './pages/Configuration' import Configuration from './pages/Configuration'
import Consultants from './pages/Consultants' import Consultants from './pages/Consultants'
@ -13,7 +14,7 @@ import Evaluation from './pages/Evaluation'
export default function App() { export default function App() {
return ( return (
<div className="min-h-screen bg-gray-50 flex flex-col"> <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> <span className="font-bold text-lg tracking-tight">Assignment Monitor</span>
</header> </header>
<main className="flex-1 p-4 max-w-2xl mx-auto w-full pb-20"> <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} to={to}
end end
className={({ isActive }) => 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} {label}
</NavLink> </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"; @import "tailwindcss";
@theme {
--color-brand: #0070AD;
--color-brand-dark: #005a8e;
}

View File

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

View File

@ -150,17 +150,17 @@ export default function AssignmentDetail() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div> <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 className="flex items-start justify-between gap-2">
<div> <div>
{assignment.assignmentNumber && ( {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> <h1 className="text-xl font-bold text-gray-800 leading-tight">{assignment.title}</h1>
</div> </div>
<button <button
onClick={() => navigate(`/assignment/${assignmentId}/edit`)} 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 Bearbeiten
</button> </button>
</div> </div>
@ -175,7 +175,7 @@ export default function AssignmentDetail() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-semibold text-gray-700">Ad-hoc Meeting starten</span> <span className="text-sm font-semibold text-gray-700">Ad-hoc Meeting starten</span>
<button onClick={() => setShowAdHoc(p => !p)} <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'} {showAdHoc ? 'Abbrechen' : '+ Ad-hoc'}
</button> </button>
</div> </div>
@ -187,7 +187,7 @@ export default function AssignmentDetail() {
const count = phaseRows.find(r => r.template.id === t.id)?.instances.length ?? 0 const count = phaseRows.find(r => r.template.id === t.id)?.instances.length ?? 0
startMeeting(t.id!, count) 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-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'}`}> <div className={`text-xs mt-0.5 ${t.withWhom === 'client' ? 'text-blue-500' : 'text-purple-500'}`}>
{t.withWhom === 'client' ? 'Kundentermin' : 'Lead-intern'} {t.withWhom === 'client' ? 'Kundentermin' : 'Lead-intern'}
@ -237,7 +237,7 @@ export default function AssignmentDetail() {
<button <button
onClick={() => canStart && startMeeting(template.id!, instances.length)} onClick={() => canStart && startMeeting(template.id!, instances.length)}
className={`ml-2 flex-shrink-0 text-xs px-3 py-1.5 rounded-lg font-medium ${ 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}> disabled={!canStart}>
{instances.length === 0 ? 'Starten' : template.isRepeatable ? '+ Wiederholen' : 'Erledigt'} {instances.length === 0 ? 'Starten' : template.isRepeatable ? '+ Wiederholen' : 'Erledigt'}

View File

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

View File

@ -12,40 +12,24 @@ import {
type FeedbackDimensionText, type FeedbackDimensionText,
type FeedbackRating, type FeedbackRating,
} from '../db' } from '../db'
import { RATING_OPTIONS, RATING_NUM_MAP, AI_CONFIG } from '../config/constants'
const ratingToNum = (r: FeedbackRating | null | undefined): number => { 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 } r ? (RATING_NUM_MAP[r] ?? 0) : 0
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 ratingColor = (r: FeedbackRating | undefined) => const ratingColor = (r: FeedbackRating | undefined) =>
RATINGS.find(x => x.value === r)?.color ?? 'bg-gray-50 text-gray-400 border-gray-200' RATING_OPTIONS.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'
function getAiSettings() { function getAiSettings() {
return { return {
apiKey: localStorage.getItem(AI_KEY) ?? '', apiKey: localStorage.getItem(AI_CONFIG.localStorageKeyApiKey) ?? '',
model: localStorage.getItem(AI_MODEL) ?? 'openai/gpt-4o-mini', model: localStorage.getItem(AI_CONFIG.localStorageKeyModel) ?? AI_CONFIG.defaultModel,
} }
} }
function saveAiSettings(apiKey: string, model: string) { function saveAiSettings(apiKey: string, model: string) {
localStorage.setItem(AI_KEY, apiKey) localStorage.setItem(AI_CONFIG.localStorageKeyApiKey, apiKey)
localStorage.setItem(AI_MODEL, model) localStorage.setItem(AI_CONFIG.localStorageKeyModel, model)
} }
// ── Prompt-Builder ──────────────────────────────────────────────────────────── // ── Prompt-Builder ────────────────────────────────────────────────────────────
@ -95,7 +79,7 @@ async function buildAiPrompt(
const dimCats = categories.filter(c => c.dimensionId === dim.id) const dimCats = categories.filter(c => c.dimensionId === dim.id)
const lines = dimCats.map(cat => { const lines = dimCats.map(cat => {
const r = ratings.find(r => r.feedbackCategoryId === cat.id) 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}` return ` ${cat.name}: ${rating}`
}).join('\n') }).join('\n')
return ` Dimension "${dim.name}":\n${lines}` return ` Dimension "${dim.name}":\n${lines}`
@ -338,7 +322,7 @@ export default function AssignmentFeedbackPage() {
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<button onClick={() => navigate(`/assignment/${assignmentId}`)} <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"> <h1 className="text-xl font-bold text-gray-800">
Feedback: {institutee.firstName} {institutee.lastName} Feedback: {institutee.firstName} {institutee.lastName}
</h1> </h1>
@ -369,7 +353,7 @@ export default function AssignmentFeedbackPage() {
</div> </div>
<input <input
type="password" 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-..." placeholder="sk-or-..."
value={aiKey} value={aiKey}
onChange={e => setAiKey(e.target.value)} onChange={e => setAiKey(e.target.value)}
@ -386,7 +370,7 @@ export default function AssignmentFeedbackPage() {
</select> </select>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => { saveAiSettings(aiKey, aiModel); setShowAiSettings(false) }} <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 Speichern
</button> </button>
<button onClick={() => setShowAiSettings(false)} <button onClick={() => setShowAiSettings(false)}
@ -405,7 +389,7 @@ export default function AssignmentFeedbackPage() {
className={`flex-1 py-3 rounded-xl text-sm font-medium transition ${ className={`flex-1 py-3 rounded-xl text-sm font-medium transition ${
generating generating
? 'bg-gray-200 text-gray-400 cursor-not-allowed' ? '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'} {generating ? 'Generiere Entwurf…' : '✦ KI-Feedback-Entwurf generieren'}
</button> </button>
@ -446,7 +430,7 @@ export default function AssignmentFeedbackPage() {
<span className="font-semibold text-gray-800 text-sm">{dim.name}</span> <span className="font-semibold text-gray-800 text-sm">{dim.name}</span>
{worstRating && ( {worstRating && (
<span className={`text-xs px-2 py-0.5 rounded-full border font-medium ${summaryColor}`}> <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> </span>
)} )}
</div> </div>
@ -465,7 +449,7 @@ export default function AssignmentFeedbackPage() {
{/* Rating-Buttons */} {/* Rating-Buttons */}
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{RATINGS.map(r => ( {RATING_OPTIONS.map(r => (
<button key={r.value} <button key={r.value}
onClick={() => setRating(cat.id!, r.value)} onClick={() => setRating(cat.id!, r.value)}
className={`px-2.5 py-1.5 rounded-lg text-xs font-semibold border transition ${ 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 */} {/* Neuer Typ */}
<div className="flex gap-2"> <div className="flex gap-2">
<input <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…" placeholder="Neuer Assignment-Typ…"
value={newTypeName} value={newTypeName}
onChange={e => setNewTypeName(e.target.value)} onChange={e => setNewTypeName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addType()} onKeyDown={e => e.key === 'Enter' && addType()}
/> />
<button onClick={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 + Typ
</button> </button>
</div> </div>
@ -145,7 +145,7 @@ export default function AssignmentTypeConfig() {
{/* Header */} {/* Header */}
<div className="flex items-center gap-2 px-4 py-3 bg-gray-50 border-b border-gray-100"> <div className="flex items-center gap-2 px-4 py-3 bg-gray-50 border-b border-gray-100">
<input <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} value={type.name}
onChange={e => updateTypeName(type.id!, e.target.value)} onChange={e => updateTypeName(type.id!, e.target.value)}
/> />
@ -170,7 +170,7 @@ export default function AssignmentTypeConfig() {
<button key={t} <button key={t}
onClick={() => setActiveTab(prev => ({ ...prev, [type.id!]: t }))} onClick={() => setActiveTab(prev => ({ ...prev, [type.id!]: t }))}
className={`flex-1 py-2 text-xs font-medium transition ${ 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'})`} {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 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"> <div className="flex items-center gap-2">
<input <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} value={phase.label}
onChange={e => updatePhase(phase.id!, { label: e.target.value })} onChange={e => updatePhase(phase.id!, { label: e.target.value })}
/> />
<select <select
value={phase.withWhom} value={phase.withWhom}
onChange={e => updatePhase(phase.id!, { withWhom: e.target.value as 'lead' | 'client' })} 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="lead">Lead</option>
<option value="client">Kunde</option> <option value="client">Kunde</option>
</select> </select>
@ -224,7 +224,7 @@ export default function AssignmentTypeConfig() {
<div className="text-xs font-medium text-gray-500">Neue Phase</div> <div className="text-xs font-medium text-gray-500">Neue Phase</div>
<div className="flex gap-2"> <div className="flex gap-2">
<input <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…" placeholder="Phasenname…"
value={np.label} value={np.label}
onChange={e => setNewPhase(prev => ({ ...prev, [type.id!]: { ...np, label: e.target.value } }))} onChange={e => setNewPhase(prev => ({ ...prev, [type.id!]: { ...np, label: e.target.value } }))}
@ -233,7 +233,7 @@ export default function AssignmentTypeConfig() {
<select <select
value={np.withWhom} value={np.withWhom}
onChange={e => setNewPhase(prev => ({ ...prev, [type.id!]: { ...np, withWhom: e.target.value as 'lead' | 'client' } }))} 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="lead">Lead</option>
<option value="client">Kunde</option> <option value="client">Kunde</option>
</select> </select>
@ -256,7 +256,7 @@ export default function AssignmentTypeConfig() {
</label> </label>
</div> </div>
<button onClick={() => addPhase(type.id!)} <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 + Hinzufügen
</button> </button>
</div> </div>

View File

@ -41,15 +41,15 @@ export default function Configuration() {
{/* Tab-Switcher */} {/* Tab-Switcher */}
<div className="flex bg-gray-100 rounded-xl p-1 gap-0.5"> <div className="flex bg-gray-100 rounded-xl p-1 gap-0.5">
<button onClick={() => setTab('feedback')} <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 Feedback
</button> </button>
<button onClick={() => setTab('types')} <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 Ass.-Typen
</button> </button>
<button onClick={() => setTab('backup')} <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 Backup
</button> </button>
</div> </div>
@ -76,7 +76,7 @@ export default function Configuration() {
Diese Datei kannst du sicher aufbewahren und bei Bedarf wiederherstellen. Diese Datei kannst du sicher aufbewahren und bei Bedarf wiederherstellen.
</p> </p>
<button onClick={handleExport} <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 Vollständiges Backup herunterladen
</button> </button>
</div> </div>

View File

@ -47,7 +47,7 @@ export default function Consultants() {
onChange={e => setNewGroup(p => ({ ...p, name: e.target.value }))} onChange={e => setNewGroup(p => ({ ...p, name: e.target.value }))}
onKeyDown={e => e.key === 'Enter' && addGroup()} 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> </div>
</section> </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" <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 }))} /> value={newCons.lastName} onChange={e => setNewCons(p => ({ ...p, lastName: e.target.value }))} />
</div> </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> </section>
{groups.map(g => { {groups.map(g => {

View File

@ -53,7 +53,7 @@ export default function Dashboard() {
<h1 className="text-xl font-bold text-gray-800">Assignments</h1> <h1 className="text-xl font-bold text-gray-800">Assignments</h1>
<button <button
onClick={() => navigate('/assignment/new')} 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 + Neues
</button> </button>
@ -66,13 +66,13 @@ export default function Dashboard() {
placeholder="Suche nach Nummer, Titel, Kunde, Berater…" placeholder="Suche nach Nummer, Titel, Kunde, Berater…"
value={query} value={query}
onChange={e => setQuery(e.target.value)} 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"> <div className="flex bg-gray-100 rounded-xl p-1 gap-0.5">
{([['all', 'Alle'], ['active', 'Aktiv'], ['done', 'Abgeschlossen']] as const).map(([val, label]) => ( {([['all', 'Alle'], ['active', 'Aktiv'], ['done', 'Abgeschlossen']] as const).map(([val, label]) => (
<button key={val} onClick={() => setStatusFilter(val)} <button key={val} onClick={() => setStatusFilter(val)}
className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition ${ 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} {label}
</button> </button>
@ -96,7 +96,7 @@ export default function Dashboard() {
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{a.assignmentNumber && ( {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} {a.assignmentNumber}
</div> </div>
)} )}

View File

@ -159,7 +159,7 @@ export default function FeedbackDraftPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<button onClick={() => navigate(`/assignment/${assignmentId}`)} <button onClick={() => navigate(`/assignment/${assignmentId}`)}
className="text-sm text-[#0070AD] mb-2 block"> className="text-sm text-brand mb-2 block">
Assignment Assignment
</button> </button>
<h1 className="text-xl font-bold text-gray-800"> <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="flex items-center justify-between">
<div className="text-xs font-semibold text-gray-400 uppercase tracking-wide">Feedback-Entwurf</div> <div className="text-xs font-semibold text-gray-400 uppercase tracking-wide">Feedback-Entwurf</div>
<button onClick={generateDraft} <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'} {generated ? 'Neu generieren' : 'Automatisch generieren'}
</button> </button>
</div> </div>
<textarea <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} rows={18}
placeholder="Tippe deinen Feedback-Entwurf hier, oder nutze 'Automatisch generieren'…" placeholder="Tippe deinen Feedback-Entwurf hier, oder nutze 'Automatisch generieren'…"
value={content} value={content}

View File

@ -198,14 +198,14 @@ export default function FeedbackStructureConfig() {
{/* Neue Dimension */} {/* Neue Dimension */}
<div className="flex gap-2"> <div className="flex gap-2">
<input <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…" placeholder="Neue Dimension…"
value={newDimName} value={newDimName}
onChange={e => setNewDimName(e.target.value)} onChange={e => setNewDimName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addDimension()} onKeyDown={e => e.key === 'Enter' && addDimension()}
/> />
<button onClick={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 + Dimension
</button> </button>
</div> </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> className="text-xs text-gray-400 hover:text-gray-600 leading-none disabled:opacity-30"></button>
</div> </div>
<input <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} value={dim.name}
onChange={e => updateDimension(dim.id!, e.target.value)} onChange={e => updateDimension(dim.id!, e.target.value)}
/> />
@ -275,7 +275,7 @@ export default function FeedbackStructureConfig() {
value={item.weight ?? 1} value={item.weight ?? 1}
onChange={e => updateItemWeight(item.id!, Number(e.target.value))} onChange={e => updateItemWeight(item.id!, Number(e.target.value))}
title="Gewichtung" 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>)} {[1,2,3,4,5].map(w => <option key={w} value={w}>×{w}</option>)}
</select> </select>
<button onClick={() => deleteItem(item.id!)} <button onClick={() => deleteItem(item.id!)}
@ -284,7 +284,7 @@ export default function FeedbackStructureConfig() {
))} ))}
<div className="flex gap-1.5 mt-1"> <div className="flex gap-1.5 mt-1">
<input <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…" placeholder="Neues Kriterium…"
value={newItemName[cat.id!] ?? ''} value={newItemName[cat.id!] ?? ''}
onChange={e => setNewItemName(p => ({ ...p, [cat.id!]: e.target.value }))} onChange={e => setNewItemName(p => ({ ...p, [cat.id!]: e.target.value }))}
@ -302,7 +302,7 @@ export default function FeedbackStructureConfig() {
{/* Neue Kategorie */} {/* Neue Kategorie */}
<div className="flex gap-1.5"> <div className="flex gap-1.5">
<input <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…" placeholder="Neue Kategorie…"
value={newCatName[dim.id!] ?? ''} value={newCatName[dim.id!] ?? ''}
onChange={e => setNewCatName(p => ({ ...p, [dim.id!]: e.target.value }))} onChange={e => setNewCatName(p => ({ ...p, [dim.id!]: e.target.value }))}

View File

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