diff --git a/.phase10-state.json b/.phase10-state.json new file mode 100644 index 0000000..9227da6 --- /dev/null +++ b/.phase10-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "markdown-notes-time-entry", + "started_at": "2026-05-23T06:10:51.530595" +} \ No newline at end of file diff --git a/.phase9-state.json b/.phase9-state.json index 2047fbd..ae2c053 100644 --- a/.phase9-state.json +++ b/.phase9-state.json @@ -7,6 +7,7 @@ "two-factor-auth-stub", "billing-stub", "integrations-page", - "api-client-phase9" + "api-client-phase9", + "router-phase9" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 3832ed4..b27d8d0 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -1128,3 +1128,45 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `06:08:29` **INFO** Committed feature router-phase9 +- `06:08:29` **INFO** Pushed: rc=0 + +## Phase-9 Run beendet (2026-05-23 06:08:29) + +- `06:08:29` **INFO** OK: 0, Attempted: 6, Total: 6 +- `06:08:29` **INFO** Running db:generate + db:migrate… +- `06:08:30` **INFO** db:generate rc=0: cts 5 columns 0 indexes 1 fks +time_entries 7 columns 0 indexes 2 fks +users 6 columns 0 indexes 0 fks +webhooks 6 columns 0 indexes 1 fks + +[✓] Your SQL migration file ➜ drizzle/0004_clammy_random.sql 🚀 + +- `06:08:30` **INFO** db:migrate rc=0: one/api@0.0.1 db:migrate /home/dark/Developer/EmberClone/apps/api +> tsx src/db/migrate.ts + +Running migrations... +Migrations completed successfully +Checking for admin user... +Admin user already exists + + +## 🚀 Phase-10 Codegen-Run gestartet (2026-05-23 06:10:51) + + +## Phase-3 Feature: markdown-notes-time-entry (2026-05-23 06:10:51) + +- `06:10:51` **INFO** Description: Markdown-Notes-Feld pro Time-Entry + Render in Liste +- `06:10:51` **INFO** Generating apps/api/src/db/schema.ts (ERWEITERT — füge `notes: text('notes')` Spalte (nullable) zu time_entr…) +- `06:11:17` **INFO** wrote 3000 chars in 25.8s (attempt 1) +- `06:11:17` **INFO** Generating apps/web/src/pages/TimeEntries.tsx (ERWEITERT — behalte alles. Füge `notes` Textarea (optional, expandable…) +- `06:13:04` **INFO** wrote 13463 chars in 107.5s (attempt 1) +- `06:13:04` **INFO** Running tsc --noEmit on api… +- `06:13:06` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/drizzle/0004_clammy_random.sql b/apps/api/drizzle/0004_clammy_random.sql new file mode 100644 index 0000000..1642afb --- /dev/null +++ b/apps/api/drizzle/0004_clammy_random.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "webhooks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "url" text NOT NULL, + "event" text NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "created_by" uuid +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "webhooks" ADD CONSTRAINT "webhooks_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/apps/api/drizzle/meta/0004_snapshot.json b/apps/api/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..2aa603b --- /dev/null +++ b/apps/api/drizzle/meta/0004_snapshot.json @@ -0,0 +1,518 @@ +{ + "id": "a4312ab5-9818-4a13-a906-33971e96f38a", + "prevId": "d1bfef3f-3451-45a0-b9f6-60dee1f580e9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_settings": { + "name": "app_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'EmberClone'" + }, + "default_billable": { + "name": "default_billable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "week_start": { + "name": "week_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audit_log_user_id_users_id_fk": { + "name": "audit_log_user_id_users_id_fk", + "tableFrom": "audit_log", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "documents_user_id_users_id_fk": { + "name": "documents_user_id_users_id_fk", + "tableFrom": "documents", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_customer_id_customers_id_fk": { + "name": "projects_customer_id_customers_id_fk", + "tableFrom": "projects", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.time_entries": { + "name": "time_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_time": { + "name": "start_time", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_time": { + "name": "end_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "time_entries_user_id_users_id_fk": { + "name": "time_entries_user_id_users_id_fk", + "tableFrom": "time_entries", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "time_entries_project_id_projects_id_fk": { + "name": "time_entries_project_id_projects_id_fk", + "tableFrom": "time_entries", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhooks": { + "name": "webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "webhooks_created_by_users_id_fk": { + "name": "webhooks_created_by_users_id_fk", + "tableFrom": "webhooks", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 08f2e24..470df64 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1779507955317, "tag": "0003_illegal_ben_parker", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1779509310016, + "tag": "0004_clammy_random", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index e961c20..16e4391 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -35,6 +35,7 @@ export const timeEntries = pgTable("time_entries", { userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), projectId: uuid("project_id").references(() => projects.id, { onDelete: "cascade" }), description: text("description").notNull(), + notes: text("notes"), startTime: timestamp("start_time").notNull(), endTime: timestamp("end_time"), createdAt: timestamp("created_at").notNull().defaultNow() diff --git a/apps/web/src/pages/TimeEntries.tsx b/apps/web/src/pages/TimeEntries.tsx index 1952a22..50b13bf 100644 --- a/apps/web/src/pages/TimeEntries.tsx +++ b/apps/web/src/pages/TimeEntries.tsx @@ -5,6 +5,19 @@ import { EmptyState } from "../components/EmptyState" import { LoadingSpinner } from "../components/LoadingSpinner" import type { TimeEntryInsert } from "@emberclone/shared" +function renderSimpleMarkdown(text: string | null) { + if (!text) return null + return text + .split('\n') + .map((line, i) => ( +
+ {line + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1')} +
+ )) +} + export default function TimeEntries() { const queryClient = useQueryClient() @@ -12,7 +25,8 @@ export default function TimeEntries() { description: "", startTime: "", endTime: "", - projectId: "" + projectId: "", + notes: "" }) const [filters, setFilters] = useState({ @@ -22,6 +36,7 @@ export default function TimeEntries() { }) const [selectedIds, setSelectedIds] = useState([]) + const [expandedRows, setExpandedRows] = useState>({}) const { data: entries, isLoading, isError } = useQuery({ queryKey: ["time-entries", filters.from, filters.to], @@ -35,7 +50,7 @@ export default function TimeEntries() { mutationFn: (data: Partial) => api.createTimeEntry(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["time-entries"] }) - setFormData({ description: "", startTime: "", endTime: "", projectId: "" }) + setFormData({ description: "", startTime: "", endTime: "", projectId: "", notes: "" }) } }) @@ -67,7 +82,8 @@ export default function TimeEntries() { description: formData.description, startTime: new Date(formData.startTime) as any, endTime: new Date(formData.endTime) as any, - projectId: formData.projectId || undefined + projectId: formData.projectId || undefined, + notes: formData.notes || undefined }) } @@ -92,6 +108,10 @@ export default function TimeEntries() { ) } + const toggleRow = (id: string) => { + setExpandedRows(prev => ({ ...prev, [id]: !prev[id] })) + } + if (isError) return
Error loading time entries.
return ( @@ -103,55 +123,66 @@ export default function TimeEntries() {

Log New Entry

-
-
- - setFormData({ ...formData, description: e.target.value })} - placeholder="What did you work on?" - /> + +
+
+ + setFormData({ ...formData, description: e.target.value })} + placeholder="What did you work on?" + /> +
+
+ + setFormData({ ...formData, startTime: e.target.value })} + /> +
+
+ + setFormData({ ...formData, endTime: e.target.value })} + /> +
+
+ + setFormData({ ...formData, projectId: e.target.value })} + placeholder="Optional" + /> +
- - setFormData({ ...formData, startTime: e.target.value })} + +