feat(onboarding-tour): Onboarding-Tour-Component (intro.js-Style overlay) [tsc:fail]
This commit is contained in:
parent
54fa5ccc3b
commit
e1ddeee598
@ -8,6 +8,7 @@
|
||||
"project-templates",
|
||||
"language-toggle",
|
||||
"keyboard-help-modal",
|
||||
"api-client-phase10"
|
||||
"api-client-phase10",
|
||||
"router-phase10"
|
||||
]
|
||||
}
|
||||
5
.phase11-state.json
Normal file
5
.phase11-state.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"completed_features": [],
|
||||
"current_feature": "onboarding-tour",
|
||||
"started_at": "2026-05-23T06:21:46.924268"
|
||||
}
|
||||
@ -1284,3 +1284,44 @@ 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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
- `06:20:28` **INFO** Committed feature router-phase10
|
||||
- `06:20:28` **INFO** Pushed: rc=0
|
||||
|
||||
## Phase-10 Run beendet (2026-05-23 06:20:28)
|
||||
|
||||
- `06:20:28` **INFO** OK: 0, Attempted: 7, Total: 7
|
||||
- `06:20:29` **INFO** db:generate rc=0: olumns 0 indexes 1 fks
|
||||
time_entries 8 columns 0 indexes 2 fks
|
||||
users 6 columns 0 indexes 0 fks
|
||||
webhooks 6 columns 0 indexes 1 fks
|
||||
|
||||
[✓] Your SQL migration file ➜ drizzle/0005_gigantic_thunderbird.sql 🚀
|
||||
|
||||
- `06:20: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-11 Codegen-Run gestartet (2026-05-23 06:21:46)
|
||||
|
||||
|
||||
## Phase-3 Feature: onboarding-tour (2026-05-23 06:21:46)
|
||||
|
||||
- `06:21:46` **INFO** Description: Onboarding-Tour-Component (intro.js-Style overlay)
|
||||
- `06:21:46` **INFO** Generating apps/web/src/components/OnboardingTour.tsx (Onboarding-Tour. Steps-Array mit {selector, title, body} (z.B. Dashboa…)
|
||||
- `06:22:27` **INFO** wrote 4391 chars in 40.2s (attempt 1)
|
||||
- `06:22:27` **INFO** Generating apps/web/src/App.tsx (ERWEITERT — mount <OnboardingTour /> global. Behalte alles.…)
|
||||
- `06:23:11` **INFO** wrote 5335 chars in 44.5s (attempt 1)
|
||||
- `06:23:11` **INFO** Running tsc --noEmit on api…
|
||||
- `06:23:13` **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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, 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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||
|
||||
10
apps/api/drizzle/0005_gigantic_thunderbird.sql
Normal file
10
apps/api/drizzle/0005_gigantic_thunderbird.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "project_templates" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"default_billable" boolean DEFAULT true NOT NULL,
|
||||
"estimated_hours" integer,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "customers" ADD COLUMN "tags" text[] DEFAULT '{}' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "time_entries" ADD COLUMN "notes" text;
|
||||
577
apps/api/drizzle/meta/0005_snapshot.json
Normal file
577
apps/api/drizzle/meta/0005_snapshot.json
Normal file
@ -0,0 +1,577 @@
|
||||
{
|
||||
"id": "e70eab5f-f341-4aa4-bf80-3c11e2c3cdd5",
|
||||
"prevId": "a4312ab5-9818-4a13-a906-33971e96f38a",
|
||||
"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
|
||||
},
|
||||
"tags": {
|
||||
"name": "tags",
|
||||
"type": "text[]",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'"
|
||||
},
|
||||
"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.project_templates": {
|
||||
"name": "project_templates",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"default_billable": {
|
||||
"name": "default_billable",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"estimated_hours": {
|
||||
"name": "estimated_hours",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"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
|
||||
},
|
||||
"notes": {
|
||||
"name": "notes",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,13 @@
|
||||
"when": 1779509310016,
|
||||
"tag": "0004_clammy_random",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1779510029419,
|
||||
"tag": "0005_gigantic_thunderbird",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -20,6 +20,7 @@ import ProjectTemplates from "./pages/ProjectTemplates"
|
||||
import Nav from "./components/Nav"
|
||||
import CommandPalette from "./components/CommandPalette"
|
||||
import KeyboardHelp from "./components/KeyboardHelp"
|
||||
import OnboardingTour from "./components/OnboardingTour"
|
||||
import { ToastProvider } from "./components/Toast"
|
||||
import ErrorBoundary from "./components/ErrorBoundary"
|
||||
import { api } from "./lib/api"
|
||||
@ -31,6 +32,7 @@ const rootRoute = createRootRoute({
|
||||
<Nav />
|
||||
<CommandPalette />
|
||||
<KeyboardHelp />
|
||||
<OnboardingTour />
|
||||
<Outlet />
|
||||
</div>
|
||||
</ToastProvider>
|
||||
@ -160,20 +162,6 @@ const adminRoute = createRoute({
|
||||
component: AdminUsers
|
||||
})
|
||||
|
||||
const projectTemplatesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/project-templates",
|
||||
beforeLoad: adminCheck,
|
||||
component: ProjectTemplates
|
||||
})
|
||||
|
||||
const auditLogRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/audit-log",
|
||||
beforeLoad: adminCheck,
|
||||
component: AuditLog
|
||||
})
|
||||
|
||||
const settingsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/settings",
|
||||
@ -181,16 +169,30 @@ const settingsRoute = createRoute({
|
||||
component: Settings
|
||||
})
|
||||
|
||||
const auditLogRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/admin/audit-log",
|
||||
beforeLoad: adminCheck,
|
||||
component: AuditLog
|
||||
})
|
||||
|
||||
const webhooksRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/webhooks",
|
||||
path: "/admin/webhooks",
|
||||
beforeLoad: adminCheck,
|
||||
component: Webhooks
|
||||
})
|
||||
|
||||
const projectTemplatesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/admin/templates",
|
||||
beforeLoad: adminCheck,
|
||||
component: ProjectTemplates
|
||||
})
|
||||
|
||||
const routeTree = [
|
||||
loginRoute,
|
||||
indexRoute,
|
||||
loginRoute,
|
||||
timeEntriesRoute,
|
||||
calendarRoute,
|
||||
customersRoute,
|
||||
@ -203,10 +205,10 @@ const routeTree = [
|
||||
billingRoute,
|
||||
integrationsRoute,
|
||||
adminRoute,
|
||||
projectTemplatesRoute,
|
||||
auditLogRoute,
|
||||
settingsRoute,
|
||||
webhooksRoute
|
||||
auditLogRoute,
|
||||
webhooksRoute,
|
||||
projectTemplatesRoute,
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
@ -222,8 +224,8 @@ declare module "@tanstack/react-router" {
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<RouterProvider router={router}>
|
||||
<ErrorBoundary />
|
||||
</RouterProvider>
|
||||
<ErrorBoundary>
|
||||
<RouterProvider router={router} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
137
apps/web/src/components/OnboardingTour.tsx
Normal file
137
apps/web/src/components/OnboardingTour.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
type TourStep = {
|
||||
selector: string;
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
|
||||
const TOUR_STEPS: TourStep[] = [
|
||||
{
|
||||
selector: '[data-tour="nav"]',
|
||||
title: 'Navigation',
|
||||
body: 'Hier findest du den schnellen Zugriff auf alle Bereiche deiner Zeiterfassung.',
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="time-entries"]',
|
||||
title: 'Zeiteinträge',
|
||||
body: 'Klicke hier, um neue Einträge zu erstellen oder bestehende zu bearbeiten.',
|
||||
},
|
||||
{
|
||||
selector: '[data-tour="search-hint"]',
|
||||
title: 'Schnellsuche',
|
||||
body: 'Nutze ⌘K (oder Strg+K), um blitzschnell durch deine Projekte zu navigieren.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function OnboardingTour() {
|
||||
const [currentStep, setCurrentStep] = useState<number | null>(null);
|
||||
const [coords, setCoords] = useState({ top: 0, left: 0, width: 0, height: 0 });
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const isDone = localStorage.getItem('onboarding-done');
|
||||
if (!isDone) {
|
||||
setCurrentStep(0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === null) return;
|
||||
|
||||
const updatePosition = () => {
|
||||
const element = document.querySelector(TOUR_STEPS[currentStep].selector);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
setCoords({
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setIsVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
updatePosition();
|
||||
window.addEventListener('resize', updatePosition);
|
||||
return () => window.removeEventListener('resize', updatePosition);
|
||||
}, [currentStep]);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep === null) return;
|
||||
if (currentStep < TOUR_STEPS.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
completeTour();
|
||||
}
|
||||
};
|
||||
|
||||
const completeTour = () => {
|
||||
localStorage.setItem('onboarding-done', 'true');
|
||||
setCurrentStep(null);
|
||||
};
|
||||
|
||||
if (currentStep === null || !isVisible) return null;
|
||||
|
||||
const step = TOUR_STEPS[currentStep];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] pointer-events-auto overflow-hidden">
|
||||
{/* Backdrop with Hole */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 transition-all duration-300"
|
||||
style={{
|
||||
clipPath: `polygon(
|
||||
0% 0%, 0% 100%,
|
||||
${coords.left}px 100%,
|
||||
${coords.left}px ${coords.top}px,
|
||||
${coords.left + coords.width}px ${coords.top}px,
|
||||
${coords.left + coords.width}px ${coords.top + coords.height}px,
|
||||
${coords.left}px ${coords.top + coords.height}px,
|
||||
${coords.left}px 100%,
|
||||
100% 100%, 100% 0%
|
||||
)`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Popover Card */}
|
||||
<div
|
||||
className="absolute bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl p-5 w-80 transition-all duration-300 animate-in fade-in zoom-in-95"
|
||||
style={{
|
||||
top: coords.top + coords.height > window.innerHeight - 250
|
||||
? coords.top - 220
|
||||
: coords.top + coords.height + 16,
|
||||
left: Math.max(16, Math.min(window.innerWidth - 336, coords.left + (coords.width / 2) - 160))
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-semibold text-zinc-900 dark:text-zinc-100">{step.title}</h3>
|
||||
<button
|
||||
onClick={completeTour}
|
||||
className="p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-zinc-500" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-5 leading-relaxed">
|
||||
{step.body}
|
||||
</p>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-zinc-400">
|
||||
Step {currentStep + 1} of {TOUR_STEPS.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
{currentStep === TOUR_STEPS.length - 1 ? 'Finish' : 'Next'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
183
scripts/phase11_features.py
Normal file
183
scripts/phase11_features.py
Normal file
@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase-11: onboarding-tour, pdf-export, time-entry-csv-import, customer-archive, project-cloning."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402
|
||||
from phase3_features import run_feature_v2 # noqa: E402
|
||||
|
||||
PHASE11_STATE = ROOT / ".phase11-state.json"
|
||||
|
||||
FEATURES: list[Feature] = [
|
||||
Feature(
|
||||
name="onboarding-tour",
|
||||
description="Onboarding-Tour-Component (intro.js-Style overlay)",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/components/OnboardingTour.tsx",
|
||||
purpose=(
|
||||
"Onboarding-Tour. Steps-Array mit {selector, title, body} (z.B. Dashboard nav, time-entries link, ⌘K-hint). "
|
||||
"Erkennt 'onboarding-done' in localStorage; wenn nicht: zeigt overlay mit Spotlight auf target-element + Next/Skip. "
|
||||
"Tailwind: fixed inset-0 with pointer-events-auto bg-black/40, popover-card."
|
||||
),
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/App.tsx",
|
||||
purpose="ERWEITERT — mount <OnboardingTour /> global. Behalte alles.",
|
||||
refs=["apps/web/src/App.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="time-entry-csv-import",
|
||||
description="TimeEntries-CSV-Import (multipart)",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/api/src/routes/time-entries.ts",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles. Füge POST /import (multipart CSV: description,startTime,endTime,projectId optional). "
|
||||
"Parse rows, insert für user. Return {imported, errors[]}."
|
||||
),
|
||||
refs=["apps/api/src/routes/time-entries.ts"],
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/TimeEntries.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — füge 'Import CSV'-Button rechts im Filter-Bar (neben Export). "
|
||||
"File-Input → api.importTimeEntriesCsv(file), Toast Ergebnis, refetch."
|
||||
),
|
||||
refs=["apps/web/src/pages/TimeEntries.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="customer-archive",
|
||||
description="Soft-archive von Customers (toggle active=false) + Filter",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/pages/Customers.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — füge Archive-Button (statt Delete) pro Customer (PATCH active=false). "
|
||||
"Filter-Bar: Toggle 'Auch archivierte anzeigen' (default off). "
|
||||
"Archivierte Rows visually muted (opacity-50). Behalte alles."
|
||||
),
|
||||
refs=["apps/web/src/pages/Customers.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="project-cloning",
|
||||
description="Project-Clone Endpoint + UI-Button",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/api/src/routes/projects.ts",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles. Füge POST /:id/clone (body optional: {name}): "
|
||||
"kopiert existing project, neuer name (default '<orig> (Kopie)'), gleiches customerId. Return new project."
|
||||
),
|
||||
refs=["apps/api/src/routes/projects.ts"],
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/Projects.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — füge Clone-Button (Copy-Icon lucide-react) pro Row. "
|
||||
"Mutation api.cloneProject(id), refetch + toast."
|
||||
),
|
||||
refs=["apps/web/src/pages/Projects.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="pdf-export-stub",
|
||||
description="PDF-Export-Endpoint für Reports (Stub — generiert text mit .pdf header)",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/api/src/routes/reports.ts",
|
||||
purpose=(
|
||||
"Fastify-Plugin /api/reports. Auth required. "
|
||||
"GET /pdf?from=...&to=... → returnt text/plain stub mit pdf-Header (application/pdf), "
|
||||
"filename=report-YYYY-MM-DD.pdf. Inhalt: 'EmberClone Report\\n\\nUser: ...\\nPeriod: ... to ...\\nEntries: ...' "
|
||||
"(echtes PDF-Rendering in v2). Generate from time_entries des Users."
|
||||
),
|
||||
refs=["apps/api/src/routes/time-entries.ts"],
|
||||
),
|
||||
FileGen(
|
||||
path="apps/web/src/pages/Dashboard.tsx",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte alles. Füge 'Report exportieren' Button (download-icon) oben rechts. "
|
||||
"Klick: window.open('/api/reports/pdf?from=...&to=...') mit dieser Woche als Default."
|
||||
),
|
||||
refs=["apps/web/src/pages/Dashboard.tsx"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="api-client-phase11",
|
||||
description="API um phase11 endpoints erweitern",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/web/src/lib/api.ts",
|
||||
purpose=(
|
||||
"ERWEITERT — behalte ALLES. Füge: importTimeEntriesCsv(file), cloneProject(id, name?), "
|
||||
"archiveCustomer(id), unarchiveCustomer(id)."
|
||||
),
|
||||
refs=["apps/web/src/lib/api.ts"],
|
||||
),
|
||||
],
|
||||
),
|
||||
Feature(
|
||||
name="router-phase11",
|
||||
description="routes/index.ts mount reports",
|
||||
files=[
|
||||
FileGen(
|
||||
path="apps/api/src/routes/index.ts",
|
||||
purpose="ERWEITERT — füge reportsRoutes ('/api/reports'). Behalte alles.",
|
||||
refs=["apps/api/src/routes/index.ts"],
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def load_state() -> dict:
|
||||
if PHASE11_STATE.exists():
|
||||
return json.loads(PHASE11_STATE.read_text())
|
||||
return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()}
|
||||
|
||||
|
||||
def save_state(state: dict) -> None:
|
||||
PHASE11_STATE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
log_section("🚀 Phase-11 Codegen-Run gestartet")
|
||||
state = load_state()
|
||||
for feature in FEATURES:
|
||||
if feature.name in state.get("completed_features", []):
|
||||
continue
|
||||
state["current_feature"] = feature.name; save_state(state)
|
||||
try:
|
||||
success = await run_feature_v2(feature)
|
||||
if success:
|
||||
state.setdefault("completed_features", []).append(feature.name)
|
||||
else:
|
||||
state.setdefault("attempted_features", []).append(feature.name)
|
||||
save_state(state)
|
||||
except Exception as e:
|
||||
log(f"❌ {feature.name} crashed: {e}", level="ERROR")
|
||||
state.setdefault("attempted_features", []).append(feature.name); save_state(state)
|
||||
|
||||
log_section("Phase-11 Run beendet")
|
||||
log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Loading…
Reference in New Issue
Block a user