Projektlogo: quadratisches Logo hochladen & anzeigen
ci / quality (push) Failing after 42s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 53s

Logo kann bei der Projekterstellung und nachträglich im Projekt-Header
hochgeladen werden. Client-seitiger Center-Crop auf 256px WebP, gespeichert
als Data-URL in neuer Spalte project.logo_url (Migration 0016). Anzeige in
der Bereichs-Projektliste (Card) und im Detail-Header, mit Initialen-
Platzhalter wenn kein Logo gesetzt ist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 20:44:58 +02:00
1 parent 45a87711fb
commit 1206dc5e0f
9 files changed
+3652 -119

No files matched your search

@@ -0,0 +1 @@
ALTER TABLE "project" ADD COLUMN "logo_url" text;
File diff suppressed because it is too large. Load diff
+123 -116
View File
@@ -1,118 +1,125 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1779736070490,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1779737202483,
"tag": "0001_day_rhythm",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1779737584310,
"tag": "0002_areas_projects",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1779737764727,
"tag": "0003_sales_marketing",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1779739115697,
"tag": "0004_is_admin",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1779740456944,
"tag": "0005_todo_carry_over",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1780410287425,
"tag": "0006_time_entry",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1780415208906,
"tag": "0007_project_description",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1780577284868,
"tag": "0008_lead_crm",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1780669990802,
"tag": "0009_pm_insights",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1780672129310,
"tag": "0010_leitfaden_phases",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1780754557002,
"tag": "0011_lead_project_link",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1780761840614,
"tag": "0012_todo_unplanned",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1780763141548,
"tag": "0013_parallel_timers",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1780764129593,
"tag": "0014_checkout_kimai",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1780765384409,
"tag": "0015_delete_break_entries",
"breakpoints": true
}
]
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1779736070490,
"tag": "0000_init",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1779737202483,
"tag": "0001_day_rhythm",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1779737584310,
"tag": "0002_areas_projects",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1779737764727,
"tag": "0003_sales_marketing",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1779739115697,
"tag": "0004_is_admin",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1779740456944,
"tag": "0005_todo_carry_over",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1780410287425,
"tag": "0006_time_entry",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1780415208906,
"tag": "0007_project_description",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1780577284868,
"tag": "0008_lead_crm",
"breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1780669990802,
"tag": "0009_pm_insights",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1780672129310,
"tag": "0010_leitfaden_phases",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1780754557002,
"tag": "0011_lead_project_link",
"breakpoints": true
},
{
"idx": 12,
"version": "7",
"when": 1780761840614,
"tag": "0012_todo_unplanned",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1780763141548,
"tag": "0013_parallel_timers",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1780764129593,
"tag": "0014_checkout_kimai",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1780765384409,
"tag": "0015_delete_break_entries",
"breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1780771242601,
"tag": "0016_project_logo_url",
"breakpoints": true
}
]
}
+2
View File
@@ -125,6 +125,8 @@ export const project = pgTable(
.references(() => area.id, { onDelete: 'restrict' }),
name: text('name').notNull(),
description: text('description'),
// Quadratisches Projektlogo als Data-URL (client-seitig auf 256px verkleinert).
logoUrl: text('logo_url'),
// Maxime / roter Faden (ein Satz) — Leitsatz fürs ganze Team (Leitfaden: Strategie).
maxime: text('maxime'),
// Definition (Leitfaden Phase 1) — Kurzfassung; Volltext-Steckbrief lebt in Outline.
+10
View File
@@ -3,10 +3,19 @@ import { z } from 'zod';
const ampelSchema = z.enum(['green', 'amber', 'red']);
const trendSchema = z.enum(['besser', 'gleich', 'schlechter']);
// Quadratisches Logo als Data-URL (client verkleinert auf 256px ≈ <100 KB).
const logoUrlSchema = z
.string()
.max(500_000)
.refine((s) => s.startsWith('data:image/') || /^https?:\/\//.test(s), {
message: 'Logo muss ein Bild (Data-URL) oder eine http(s)-URL sein.',
});
export const createProjectInput = z.object({
areaId: z.string().uuid(),
name: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
logoUrl: logoUrlSchema.optional(),
kind: z.enum(['kundenprojekt', 'internes_projekt']),
verantwortlicheUserId: z.string().uuid().optional(),
arbeitsmodus: z.enum(['cycle', 'kanban']).optional(),
@@ -104,6 +113,7 @@ export const updateProjectInput = z.object({
nichtZiele: z.string().max(2000).nullable().optional(),
liefergegenstaende: z.string().max(2000).nullable().optional(),
zieldreieckFlex: zieldreieckFlexSchema.nullable().optional(),
logoUrl: logoUrlSchema.nullable().optional(),
outlineSteckbriefUrl: z.string().url().nullable().optional(),
planeProjectUrl: z.string().url().nullable().optional(),
risikolisteUrl: z.string().url().nullable().optional(),