createCalendarTable(); $this->createCanvasTable(); $this->createCanvasItemsTable(); $this->createApprovalsTable(); $this->createClientsTable(); $this->createCommentTable(); $this->createFileTable(); $this->createGcallinksTable(); $this->createNoteTable(); $this->createProjectsTable(); $this->createPunchClockTable(); $this->createReadTable(); $this->createRelationUserProjectTable(); $this->createTicketHistoryTable(); $this->createTicketsTable(); $this->createGoalHistoryTable(); $this->createTimesheetsTable(); $this->createUserTable(); $this->createSprintsTable(); $this->createStatsTable(); $this->createSettingsTable(); $this->createAuditTable(); $this->createQueueTable(); $this->createPluginsTable(); $this->createNotificationsTable(); $this->createEntityRelationshipTable(); $this->createIntegrationTable(); $this->createReactionsTable(); $this->createAccessTokensTable(); $this->createJobsTable(); $this->createRecurringPatternsTable(); $this->createWorkStructureTables(); $this->createRolesTable(); $this->createPermissionsTable(); $this->createRolePermissionsTable(); $this->createBomTables(); } /** * Insert initial data after tables are created. * * @param array $values Installation form values containing company, email, firstname, lastname * @param string $pwReset Password reset token */ public function insertInitialData(array $values, string $pwReset): void { // Insert initial client/company DB::table('zp_clients')->insert([ 'id' => 1, 'name' => $values['company'], 'street' => '', 'zip' => 0, 'city' => '', 'state' => '', 'country' => '', 'phone' => '', 'internet' => '', 'published' => null, 'age' => null, 'email' => '', ]); // Insert initial admin user (status 'i' = inactive, requires password reset) DB::table('zp_user')->insert([ 'id' => 1, 'username' => $values['email'], 'password' => '', 'firstname' => $values['firstname'], 'lastname' => $values['lastname'], 'phone' => '', 'profileId' => '', 'lastlogin' => null, 'lastpwd_change' => null, 'status' => 'i', 'expires' => null, 'role' => '50', 'session' => '', 'sessiontime' => '', 'wage' => 0, 'hours' => 0, 'description' => null, 'clientId' => 0, 'notifications' => 1, 'createdOn' => now(), 'pwReset' => $pwReset, ]); // Insert initial settings DB::table('zp_settings')->insert([ ['key' => 'db-version', 'value' => $this->appSettings->dbVersion], ['key' => 'companysettings.telemetry.active', 'value' => 'true'], ]); // zp_clients and zp_user are seeded with explicit id = 1 above. MySQL // auto-advances AUTO_INCREMENT after an explicit-id insert; Postgres // leaves the BIGSERIAL sequence at 1, so the next auto-id INSERT (e.g. // OIDC JIT user creation) collides on the primary key. Re-sync the // sequences to MAX(id) on Postgres. No-op on MySQL. (#3380) $this->resyncSequences(['zp_clients', 'zp_user']); } /** * Re-sync BIGSERIAL sequences to MAX(id) for tables seeded with explicit * ids. Postgres-only; a no-op on every other driver. * * @param string[] $tables Tables whose `id` sequence should be advanced. */ private function resyncSequences(array $tables): void { if (DB::connection()->getDriverName() !== 'pgsql') { return; } foreach ($tables as $table) { DB::statement( "SELECT setval(pg_get_serial_sequence(?, 'id'), COALESCE((SELECT MAX(id) FROM {$table}), 1))", [$table] ); } } /** * Create zp_calendar table. */ private function createCalendarTable(): void { Schema::create('zp_calendar', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->dateTime('dateFrom')->nullable(); $table->dateTime('dateTo')->nullable(); $table->text('description')->nullable(); $table->string('kind', 255)->nullable(); $table->string('allDay', 10)->nullable(); $table->index(['userId', 'dateFrom', 'dateTo'], 'idx_calendar_userId_dateFrom_dateTo'); }); } /** * Create zp_canvas table. */ private function createCanvasTable(): void { Schema::create('zp_canvas', function (Blueprint $table) { $table->id(); $table->string('title', 255)->nullable(); $table->integer('author')->nullable(); $table->dateTime('created')->nullable(); $table->integer('projectId')->nullable(); $table->string('type', 45)->nullable(); $table->text('description')->nullable(); $table->string('color', 50)->nullable()->default('ocean'); $table->dateTime('modified')->nullable(); $table->index(['projectId', 'type'], 'ProjectIdType'); $table->index(['type', 'id'], 'idx_canvas_type_id'); }); } /** * Create zp_canvas_items table. */ private function createCanvasItemsTable(): void { Schema::create('zp_canvas_items', function (Blueprint $table) { $table->id(); $table->text('description')->nullable(); $table->text('assumptions')->nullable(); $table->text('data')->nullable(); $table->text('conclusion')->nullable(); $table->text('why_this_matters')->nullable(); $table->text('starting_picture')->nullable(); $table->string('box', 255)->nullable(); $table->integer('author')->nullable(); $table->dateTime('created')->nullable(); $table->dateTime('modified')->nullable(); $table->integer('canvasId')->nullable(); $table->integer('sortindex')->nullable(); $table->string('status', 255)->nullable(); $table->string('relates', 255)->nullable(); $table->string('milestoneId', 255)->nullable(); $table->string('title', 255)->nullable(); $table->integer('parent')->nullable(); $table->integer('featured')->nullable(); $table->text('tags')->nullable(); $table->integer('kpi')->nullable(); $table->text('data1')->nullable(); $table->text('data2')->nullable(); $table->text('data3')->nullable(); $table->text('data4')->nullable(); $table->text('data5')->nullable(); $table->dateTime('startDate')->nullable(); $table->dateTime('endDate')->nullable(); $table->text('setting')->nullable(); $table->string('metricType', 45)->nullable(); $table->decimal('startValue', 10, 2)->nullable(); $table->decimal('currentValue', 10, 2)->nullable(); $table->decimal('endValue', 10, 2)->nullable(); $table->integer('impact')->nullable(); $table->integer('effort')->nullable(); $table->integer('probability')->nullable(); $table->text('action')->nullable(); $table->integer('assignedTo')->nullable(); $table->index(['canvasId', 'box'], 'CanvasLookUp'); $table->index(['box', 'milestoneId'], 'idx_canvas_items_box_milestoneId'); $table->index(['box', 'status', 'author'], 'idx_canvas_items_box_status_author'); $table->index(['parent', 'title'], 'idx_canvas_items_parent_title'); }); } /** * Create zp_approvals table. */ private function createApprovalsTable(): void { Schema::create('zp_approvals', function (Blueprint $table) { $table->id(); $table->string('module', 100)->nullable(); $table->integer('entityId')->nullable(); $table->integer('requestorId')->nullable(); $table->integer('approverId')->nullable(); $table->integer('approvalStatus')->nullable(); $table->dateTime('requestedOn')->nullable(); $table->dateTime('lastStatusChange')->nullable(); }); } /** * Create zp_clients table. */ private function createClientsTable(): void { Schema::create('zp_clients', function (Blueprint $table) { $table->id(); $table->string('name', 200)->nullable(); $table->string('street', 200)->nullable(); $table->integer('zip')->nullable(); $table->string('city', 50)->nullable(); $table->string('state', 50)->nullable(); $table->string('country', 50)->nullable(); $table->string('phone', 50)->nullable(); $table->string('internet', 200)->nullable(); $table->integer('published')->nullable(); $table->integer('age')->nullable(); $table->string('email', 255)->nullable(); $table->dateTime('modified')->nullable(); }); } /** * Create zp_comment table. */ private function createCommentTable(): void { Schema::create('zp_comment', function (Blueprint $table) { $table->id(); $table->string('module', 200)->nullable(); $table->integer('userId')->nullable(); $table->integer('commentParent')->nullable(); $table->dateTime('date')->nullable(); $table->integer('moduleId')->nullable(); $table->text('text')->nullable(); $table->string('status', 50)->nullable(); $table->index(['moduleId', 'module', 'commentParent'], 'idx_comment_moduleId_module_commentParent'); $table->index(['userId', 'module'], 'idx_comment_userId_module'); $table->index(['moduleId', 'module', 'date'], 'idx_comment_moduleId_module_date'); }); } /** * Create zp_file table. * * Note: MySQL uses ENUM for module, but for cross-database compatibility * we use a string column with application-level validation. */ private function createFileTable(): void { Schema::create('zp_file', function (Blueprint $table) { $table->id(); // Using string instead of enum for PostgreSQL compatibility // Valid values: project, ticket, client, user, lead, export, private $table->string('module', 50)->nullable(); $table->integer('moduleId')->nullable(); $table->integer('userId')->nullable(); $table->string('extension', 10)->nullable(); $table->string('encName', 255)->nullable(); $table->string('realName', 255)->nullable(); $table->dateTime('date')->nullable(); $table->index(['module', 'moduleId', 'userId'], 'idx_file_module_moduleId_userId'); }); } /** * Create zp_gcallinks table. */ private function createGcallinksTable(): void { Schema::create('zp_gcallinks', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->text('url')->nullable(); $table->string('name', 255)->nullable(); $table->string('colorClass', 100)->nullable(); $table->index(['userId'], 'idx_gcallinks_userId'); }); } /** * Create zp_note table. */ private function createNoteTable(): void { Schema::create('zp_note', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->string('title', 255)->nullable(); $table->text('description')->nullable(); }); } /** * Create zp_projects table. */ private function createProjectsTable(): void { Schema::create('zp_projects', function (Blueprint $table) { $table->id(); $table->string('name', 100)->nullable(); $table->integer('clientId')->nullable(); $table->text('details')->nullable(); $table->integer('state')->nullable(); $table->string('hourBudget', 255)->nullable()->default(''); $table->integer('dollarBudget')->nullable(); $table->integer('active')->nullable(); $table->text('menuType')->nullable(); $table->text('psettings')->nullable(); $table->integer('parent')->nullable(); $table->string('type', 45)->nullable(); $table->dateTime('start')->nullable(); $table->dateTime('end')->nullable(); $table->dateTime('created')->nullable(); $table->dateTime('modified')->nullable(); $table->text('avatar')->nullable(); $table->text('cover')->nullable(); $table->integer('sortIndex')->nullable(); }); } /** * Create zp_punch_clock table. * * Note: Has a composite primary key (id, userId). */ private function createPunchClockTable(): void { Schema::create('zp_punch_clock', function (Blueprint $table) { $table->id(); $table->integer('userId'); $table->integer('minutes')->nullable(); $table->integer('hours')->nullable(); $table->integer('punchIn')->nullable(); $table->index(['userId'], 'idx_punch_clock_userId'); }); } /** * Create zp_read table. * * Note: MySQL uses ENUM for module, but for cross-database compatibility * we use a string column with application-level validation. */ private function createReadTable(): void { Schema::create('zp_read', function (Blueprint $table) { $table->id(); // Using string instead of enum for PostgreSQL compatibility // Valid values: ticket, message $table->string('module', 50)->nullable(); $table->integer('moduleId')->nullable(); $table->integer('userId')->nullable(); $table->index(['userId', 'module', 'moduleId'], 'idx_read_userId_module_moduleId'); }); } /** * Create zp_relationuserproject table (junction table for user-project relationships). */ private function createRelationUserProjectTable(): void { Schema::create('zp_relationuserproject', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->integer('projectId')->nullable(); $table->integer('wage')->nullable(); $table->string('projectRole', 20)->nullable(); $table->index(['projectId'], 'zp_relationuserproject_projectId_index'); $table->index(['userId'], 'zp_relationuserproject_userId_index'); $table->index(['userId', 'projectId'], 'idx_relationuserproject_userId_projectId'); }); } /** * Create zp_tickethistory table. */ private function createTicketHistoryTable(): void { Schema::create('zp_tickethistory', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->integer('ticketId')->nullable(); $table->string('changeType', 255)->nullable(); $table->string('changeValue', 150)->nullable(); $table->dateTime('dateModified')->nullable(); $table->index(['ticketId'], 'idx_tickethistory_ticketId'); }); } /** * Create zp_goal_history table (append-only record of goal metric values over time). */ private function createGoalHistoryTable(): void { Schema::create('zp_goal_history', function (Blueprint $table) { $table->increments('id'); $table->integer('itemId'); $table->double('value')->nullable(); $table->integer('userId')->nullable(); $table->dateTime('dateRecorded')->nullable(); $table->index(['itemId', 'dateRecorded'], 'idx_goal_history_item_date'); }); } /** * Create zp_tickets table. */ private function createTicketsTable(): void { Schema::create('zp_tickets', function (Blueprint $table) { $table->id(); $table->integer('projectId')->nullable(); $table->string('headline', 255)->nullable(); $table->text('description')->nullable(); $table->text('acceptanceCriteria')->nullable(); $table->text('outcomeImpact')->nullable(); $table->dateTime('date')->nullable(); $table->dateTime('dateToFinish')->nullable(); $table->string('priority', 60)->nullable(); $table->integer('status')->nullable(); $table->integer('userId')->nullable(); $table->string('os', 30)->nullable(); $table->string('browser', 30)->nullable(); $table->string('resolution', 30)->nullable(); $table->string('component', 100)->nullable(); $table->string('version', 20)->nullable(); $table->string('url', 100)->nullable(); $table->integer('dependingTicketId')->nullable(); $table->dateTime('editFrom')->nullable(); $table->dateTime('editTo')->nullable(); $table->string('editorId', 75)->nullable(); $table->float('planHours')->nullable(); $table->float('hourRemaining')->nullable(); $table->string('type', 255)->nullable(); $table->integer('production')->default(0); $table->integer('staging')->default(0); $table->float('storypoints')->nullable(); $table->integer('sprint')->nullable(); $table->bigInteger('sortindex')->nullable(); $table->bigInteger('kanbanSortIndex')->nullable(); $table->string('tags', 255)->nullable(); $table->integer('milestoneid')->nullable(); $table->integer('leancanvasitemid')->nullable(); $table->integer('retrospectiveid')->nullable(); $table->integer('ideaid')->nullable(); $table->string('zp_ticketscol', 45)->nullable(); $table->dateTime('modified')->nullable(); $table->index(['projectId', 'userId'], 'ProjectUserId'); $table->index(['status', 'sprint'], 'StatusSprint'); $table->index(['sortindex'], 'Sorting'); $table->index(['editorId'], 'idx_tickets_editorId'); $table->index(['milestoneid'], 'idx_tickets_milestoneid'); $table->index(['editFrom'], 'idx_tickets_editFrom'); $table->index(['editTo'], 'idx_tickets_editTo'); $table->index(['dateToFinish'], 'idx_tickets_dateToFinish'); $table->index(['modified'], 'idx_tickets_modified'); $table->index(['projectId', 'status'], 'idx_tickets_projectId_status'); $table->index(['projectId', 'type'], 'idx_tickets_projectId_type'); $table->index(['status', 'type'], 'idx_tickets_status_type'); $table->index(['dependingTicketId'], 'idx_tickets_dependingTicketId'); }); } /** * Create zp_timesheets table. */ private function createTimesheetsTable(): void { Schema::create('zp_timesheets', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->integer('ticketId')->nullable(); $table->dateTime('workDate')->nullable(); $table->float('hours')->nullable(); $table->text('description')->nullable(); $table->string('kind', 175)->nullable(); $table->integer('invoicedEmpl')->nullable(); $table->integer('invoicedComp')->nullable(); $table->dateTime('invoicedEmplDate')->nullable(); $table->dateTime('invoicedCompDate')->nullable(); $table->string('rate', 255)->nullable(); $table->integer('paid')->nullable(); $table->dateTime('paidDate')->nullable(); $table->dateTime('modified')->nullable(); $table->unique(['userId', 'ticketId', 'workDate', 'kind'], 'Unique'); $table->index(['ticketId'], 'idx_timesheets_ticketId'); $table->index(['userId', 'workDate'], 'idx_timesheets_userId_workDate'); $table->index(['ticketId', 'workDate'], 'idx_timesheets_ticketId_workDate'); }); } /** * Create zp_user table. */ private function createUserTable(): void { Schema::create('zp_user', function (Blueprint $table) { $table->id(); $table->string('username', 175); $table->string('password', 255)->nullable()->default(''); $table->string('firstname', 100)->nullable()->default(''); $table->string('lastname', 100)->nullable()->default(''); $table->string('phone', 25)->nullable()->default(''); $table->string('profileId', 100)->nullable()->default(''); $table->dateTime('lastlogin')->nullable(); $table->string('status', 1)->default('A'); $table->dateTime('expires')->nullable(); $table->string('role', 200); $table->string('session', 100)->nullable(); $table->string('sessiontime', 50)->nullable(); $table->integer('wage')->nullable(); $table->integer('hours')->nullable(); $table->integer('weekly_hours')->nullable(); $table->string('employment_type', 20)->nullable(); $table->text('description')->nullable(); $table->integer('clientId')->nullable(); $table->integer('notifications')->nullable(); $table->string('pwReset', 100)->nullable(); $table->dateTime('pwResetExpiration')->nullable(); $table->integer('pwResetCount')->nullable(); $table->tinyInteger('forcePwReset')->nullable(); $table->dateTime('lastpwd_change')->nullable(); $table->text('settings')->nullable(); $table->tinyInteger('twoFAEnabled')->default(0); $table->string('twoFASecret', 200)->nullable(); $table->dateTime('createdOn')->nullable(); $table->string('source', 200)->nullable(); $table->string('jobTitle', 200)->nullable(); $table->string('jobLevel', 50)->nullable(); $table->string('department', 200)->nullable(); $table->dateTime('modified')->nullable(); $table->unique(['username'], 'username'); $table->index(['clientId'], 'idx_user_clientId'); }); } /** * Create zp_sprints table. */ private function createSprintsTable(): void { Schema::create('zp_sprints', function (Blueprint $table) { $table->id(); $table->integer('projectId')->nullable(); $table->string('name', 45)->nullable(); $table->dateTime('startDate')->nullable(); $table->dateTime('endDate')->nullable(); $table->dateTime('modified')->nullable(); $table->index(['projectId', 'startDate', 'endDate'], 'idx_sprints_projectId_startDate_endDate'); }); } /** * Create zp_stats table. * * Note: This table has no primary key, only indexes. */ private function createStatsTable(): void { Schema::create('zp_stats', function (Blueprint $table) { $table->integer('sprintId')->nullable(); $table->integer('projectId')->nullable(); $table->dateTime('date')->nullable(); $table->integer('sum_todos')->nullable(); $table->integer('sum_open_todos')->nullable(); $table->integer('sum_progres_todos')->nullable(); $table->integer('sum_closed_todos')->nullable(); $table->float('sum_planned_hours')->nullable(); $table->float('sum_estremaining_hours')->nullable(); $table->float('sum_logged_hours')->nullable(); $table->integer('sum_points')->nullable(); $table->integer('sum_points_done')->nullable(); $table->integer('sum_points_progress')->nullable(); $table->integer('sum_points_open')->nullable(); $table->integer('sum_todos_xs')->nullable(); $table->integer('sum_todos_s')->nullable(); $table->integer('sum_todos_m')->nullable(); $table->integer('sum_todos_l')->nullable(); $table->integer('sum_todos_xl')->nullable(); $table->integer('sum_todos_xxl')->nullable(); $table->integer('sum_todos_none')->nullable(); $table->integer('tickets')->nullable(); $table->float('daily_avg_hours_booked_todo')->nullable(); $table->float('daily_avg_hours_booked_point')->nullable(); $table->float('daily_avg_hours_planned_todo')->nullable(); $table->float('daily_avg_hours_planned_point')->nullable(); $table->float('daily_avg_hours_remaining_point')->nullable(); $table->float('daily_avg_hours_remaining_todo')->nullable(); $table->integer('sum_teammembers')->nullable(); $table->index(['projectId', 'sprintId'], 'idx_stats_projectId'); $table->index(['projectId', 'sprintId', 'date'], 'idx_stats_projectId_sprintId_date'); $table->index(['sprintId', 'date'], 'idx_stats_sprintId_date'); }); } /** * Create zp_settings table (key-value store). */ private function createSettingsTable(): void { Schema::create('zp_settings', function (Blueprint $table) { $table->string('key', 175)->primary(); $table->text('value')->nullable(); }); } /** * Create zp_roles table — the DB-backed role definitions for the native * permission engine. Ships the six built-in roles (seeded separately) and is * extensible with custom roles. `level` preserves the legacy "at least" * hierarchy (5..50); `isSystem` flags the built-ins as protected. */ private function createRolesTable(): void { Schema::create('zp_roles', function (Blueprint $table) { $table->id(); $table->string('name', 50); $table->string('displayName', 100)->nullable(); $table->integer('level'); $table->tinyInteger('isSystem')->default(0); $table->text('description')->nullable(); $table->dateTime('createdOn')->nullable(); $table->dateTime('modified')->nullable(); $table->unique(['name'], 'idx_roles_name'); $table->index(['level'], 'idx_roles_level'); }); } /** * Create zp_permissions table — the `domain.action` vocabulary, synced from each * domain's ProvidesPermissions declarations by `permissions:sync`. `isProjectScoped` * marks capabilities evaluated against a project's role vs company-wide. */ private function createPermissionsTable(): void { Schema::create('zp_permissions', function (Blueprint $table) { $table->id(); $table->string('permissionKey', 150); $table->string('domain', 60); $table->string('action', 100); $table->string('label', 191)->nullable(); $table->tinyInteger('isProjectScoped')->default(1); $table->dateTime('createdOn')->nullable(); $table->dateTime('modified')->nullable(); $table->unique(['permissionKey'], 'idx_permissions_key'); $table->index(['domain'], 'idx_permissions_domain'); }); } /** * Create zp_role_permissions table — the many-to-many grant map linking roles to * the permissions they hold. Edited by an administrator (future role-management UI). */ private function createRolePermissionsTable(): void { Schema::create('zp_role_permissions', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('roleId'); $table->unsignedBigInteger('permissionId'); $table->unique(['roleId', 'permissionId'], 'idx_role_permissions_unique'); $table->index(['roleId'], 'idx_role_permissions_roleId'); $table->index(['permissionId'], 'idx_role_permissions_permissionId'); }); } /** * Create zp_audit table. */ private function createAuditTable(): void { Schema::create('zp_audit', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->integer('projectId')->nullable(); $table->string('action', 45)->nullable(); $table->string('entity', 45)->nullable(); $table->integer('entityId')->nullable(); $table->text('values')->nullable(); $table->dateTime('date')->nullable(); $table->index(['projectId'], 'idx_audit_projectId'); $table->index(['projectId', 'action'], 'idx_audit_projectAction'); $table->index(['projectId', 'entity', 'entityId'], 'idx_audit_projectEntityEntityId'); }); } /** * Create zp_queue table (message queue). */ private function createQueueTable(): void { Schema::create('zp_queue', function (Blueprint $table) { $table->string('msghash', 50)->primary(); $table->string('channel', 255)->nullable(); $table->integer('userId'); $table->string('subject', 255)->nullable(); $table->text('message'); $table->dateTime('thedate'); $table->integer('projectId'); $table->index(['projectId'], 'idx_queue_projectId'); $table->index(['userId'], 'idx_queue_userId'); }); } /** * Create zp_plugins table. */ private function createPluginsTable(): void { Schema::create('zp_plugins', function (Blueprint $table) { $table->id(); $table->string('name', 45)->nullable(); $table->tinyInteger('enabled')->nullable(); $table->string('description', 255)->nullable(); $table->string('version', 45)->nullable(); $table->dateTime('installdate')->nullable(); $table->string('foldername', 45)->nullable(); $table->string('homepage', 255)->nullable(); $table->string('authors', 255)->nullable(); $table->text('license')->nullable(); $table->string('format', 45)->nullable(); }); } /** * Create zp_notifications table. */ private function createNotificationsTable(): void { Schema::create('zp_notifications', function (Blueprint $table) { $table->id(); $table->integer('userId'); $table->integer('read')->nullable(); $table->string('type', 45)->nullable(); $table->string('module', 45)->nullable(); $table->integer('moduleId')->nullable(); $table->dateTime('datetime')->nullable(); $table->string('url', 255)->nullable(); $table->integer('authorId')->nullable(); $table->text('message')->nullable(); $table->index(['userId'], 'idx_notifications_userId'); $table->index(['userId', 'datetime'], 'idx_notifications_userId_datetime'); $table->index(['userId', 'read'], 'idx_notifications_userId_read'); }); } /** * Create zp_entity_relationship table (polymorphic relationships). */ private function createEntityRelationshipTable(): void { Schema::create('zp_entity_relationship', function (Blueprint $table) { $table->id(); $table->integer('entityA')->nullable(); $table->string('entityAType', 45)->nullable(); $table->integer('entityB')->nullable(); $table->string('entityBType', 45)->nullable(); $table->string('relationship', 45)->nullable(); $table->dateTime('createdOn')->nullable(); $table->integer('createdBy')->nullable(); $table->text('meta')->nullable(); $table->index(['entityA', 'entityAType', 'relationship'], 'idx_entity_relationship_entityA'); $table->index(['entityB', 'entityBType', 'relationship'], 'idx_entity_relationship_entityB'); }); } /** * Create zp_integration table. */ private function createIntegrationTable(): void { Schema::create('zp_integration', function (Blueprint $table) { $table->id(); $table->string('providerId', 45)->nullable(); $table->string('method', 45)->nullable(); $table->string('entity', 45)->nullable(); $table->text('fields')->nullable(); $table->string('schedule', 45)->nullable(); $table->string('notes', 45)->nullable(); $table->text('auth')->nullable(); $table->string('meta', 45)->nullable(); $table->dateTime('createdOn')->nullable(); $table->integer('createdBy')->nullable(); $table->string('lastSync', 45)->nullable(); }); } /** * Create zp_reactions table (emoji reactions). */ private function createReactionsTable(): void { Schema::create('zp_reactions', function (Blueprint $table) { $table->id(); $table->integer('userId')->nullable(); $table->integer('moduleId')->nullable(); $table->string('module', 45)->nullable(); $table->string('reaction', 45)->nullable(); $table->dateTime('date')->nullable(); $table->index(['moduleId', 'module', 'reaction'], 'idx_reactions_entity'); $table->index(['userId', 'moduleId', 'module', 'reaction'], 'idx_reactions_user'); }); } /** * Create zp_access_tokens table (Laravel Sanctum personal access tokens). * * Public because update_sql_30504() reuses it to self-heal installs that * reached that migration without the table (see #3706). Keeping one * definition here also keeps the create database-agnostic, unlike the raw * MySQL in update_sql_30400(). */ public function createAccessTokensTable(): void { Schema::create('zp_access_tokens', function (Blueprint $table) { $table->id(); $table->string('tokenable_type', 255); $table->unsignedBigInteger('tokenable_id'); $table->string('name', 255); $table->string('token', 64); $table->text('abilities')->nullable(); $table->timestamp('last_used_at')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamp('created_at')->nullable(); $table->timestamp('updated_at')->nullable(); $table->unique(['token'], 'personal_access_tokens_token_unique'); $table->index(['tokenable_type', 'tokenable_id'], 'personal_access_tokens_tokenable_type_tokenable_id_index'); }); } /** * Create zp_jobs table (Laravel job queue). */ private function createJobsTable(): void { Schema::create('zp_jobs', function (Blueprint $table) { $table->id(); $table->string('queue', 255); $table->longText('payload'); $table->unsignedTinyInteger('attempts'); $table->unsignedInteger('reserved_at')->nullable(); $table->unsignedInteger('available_at'); $table->unsignedInteger('created_at'); $table->index(['queue'], 'zp_jobs_queue_index'); }); } /** * Create zp_recurring_patterns table. */ private function createRecurringPatternsTable(): void { Schema::create('zp_recurring_patterns', function (Blueprint $table) { $table->id(); $table->integer('entityId'); $table->string('module', 50); $table->string('type', 50); $table->string('trigger', 50); $table->integer('interval')->default(1); $table->text('weekDays')->nullable(); $table->integer('monthDay')->nullable(); $table->text('months')->nullable(); $table->string('action', 20)->default('reset'); $table->dateTime('lastProcessed')->nullable(); $table->dateTime('nextProcessingDate')->nullable(); $table->tinyInteger('enabled')->default(1); $table->index(['entityId'], 'idx_recurring_patterns_entityId'); }); } /** * Create the WorkStructure tables: structure definitions, their element * types, intra-structure relationships, and cross-structure mappings. */ private function createWorkStructureTables(): void { Schema::create('zp_work_structures', function (Blueprint $table) { $table->id(); $table->string('title', 255); $table->text('description')->nullable(); $table->string('type', 50)->default('custom'); $table->integer('created_by')->nullable(); $table->json('meta')->nullable(); $table->dateTime('created_at')->nullable(); $table->dateTime('modified_at')->nullable(); $table->unique(['title'], 'idx_work_structures_title'); $table->index(['type'], 'idx_work_structures_type'); }); Schema::create('zp_work_structure_elements', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('structure_id'); $table->string('type_key', 50); $table->string('label', 100); $table->text('description')->nullable(); $table->string('domain_reference', 255)->nullable(); $table->integer('sort_order')->default(0); $table->json('meta')->nullable(); $table->dateTime('created_at')->nullable(); $table->index(['structure_id'], 'idx_wse_structure_id'); $table->unique(['structure_id', 'type_key'], 'idx_wse_structure_type_key'); }); Schema::create('zp_work_structure_relationships', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('structure_id'); $table->unsignedBigInteger('from_element_id'); $table->unsignedBigInteger('to_element_id'); $table->string('relationship_type', 50); $table->text('description')->nullable(); $table->json('meta')->nullable(); $table->index(['structure_id'], 'idx_wsr_structure_id'); }); Schema::create('zp_work_structure_mappings', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('source_structure_id'); $table->unsignedBigInteger('source_element_id'); $table->unsignedBigInteger('target_structure_id'); $table->unsignedBigInteger('target_element_id'); $table->string('mapping_type', 50)->default('generates'); $table->json('meta')->nullable(); $table->index(['source_structure_id', 'target_structure_id'], 'idx_wsm_source_target'); // Enforce the same idempotency key StructureRegistry::registerMappings() // checks in application code, so a race can't insert a duplicate mapping // for the same source element → target structure. $table->unique(['source_structure_id', 'source_element_id', 'target_structure_id'], 'idx_wsm_unique_mapping'); }); } /** * Create BOM (bill-of-materials) tables for the OneBot BOM module. */ private function createBomTables(): void { Schema::create('zp_bom', function (Blueprint $table) { $table->id(); $table->string('type', 20)->nullable()->default('bom'); $table->integer('projectId')->nullable(); $table->string('bomNo', 100)->nullable(); $table->string('productName', 255)->nullable(); $table->string('specification', 255)->nullable(); $table->string('drawingNo', 100)->nullable(); $table->string('version', 50)->nullable(); $table->string('status', 20)->nullable()->default('0'); $table->text('remark')->nullable(); $table->text('hiddenColumns')->nullable(); $table->dateTime('createdOn')->nullable(); $table->dateTime('modifiedOn')->nullable(); $table->index(['projectId'], 'idx_bom_projectId'); $table->index(['type'], 'idx_bom_type'); }); Schema::create('zp_bom_item', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('bomId'); $table->integer('seq')->nullable()->default(0); $table->string('partNo', 100)->nullable(); $table->string('partName', 255)->nullable(); $table->string('partDrawingNo', 100)->nullable(); $table->string('material', 100)->nullable(); $table->string('spec', 255)->nullable(); $table->string('qtyPerUnit', 50)->nullable(); $table->string('unit', 50)->nullable(); $table->string('process', 100)->nullable(); $table->text('remark')->nullable(); $table->text('extra')->nullable(); $table->index(['bomId'], 'idx_bom_item_bomId'); }); Schema::create('zp_bom_column', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('bomId'); $table->string('key', 191); $table->string('label', 191)->nullable(); $table->integer('sortOrder')->nullable()->default(0); $table->index(['bomId'], 'idx_bom_column_bomId'); $table->unique(['bomId', 'key'], 'idx_bom_column_unique'); }); Schema::create('zp_bom_source', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('bomId'); $table->string('name', 255)->nullable(); $table->string('baseUrl', 1000)->nullable(); $table->string('tableId', 100)->nullable(); $table->text('token')->nullable(); $table->string('remark', 500)->nullable(); $table->dateTime('createdOn')->nullable(); $table->index(['bomId'], 'idx_bom_source_bomId'); }); // 项目引用层(单向,项目级追加列/值不写回全局主数据) Schema::create('zp_master_ref', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('projectId'); $table->unsignedBigInteger('masterId'); $table->dateTime('createdOn')->nullable(); $table->index(['projectId'], 'idx_mref_project'); $table->index(['masterId'], 'idx_mref_master'); $table->unique(['projectId', 'masterId'], 'idx_mref_unique'); }); Schema::create('zp_master_ref_column', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('refId'); $table->string('key', 191); $table->string('label', 191)->nullable(); $table->integer('sortOrder')->nullable()->default(0); $table->index(['refId'], 'idx_mrefcol_ref'); $table->unique(['refId', 'key'], 'idx_mrefcol_unique'); }); Schema::create('zp_master_ref_value', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('refId'); $table->unsignedBigInteger('columnId'); $table->unsignedBigInteger('itemId'); $table->text('value')->nullable(); $table->index(['refId', 'itemId'], 'idx_mrefval_refitem'); $table->unique(['refId', 'columnId', 'itemId'], 'idx_mrefval_unique'); }); } }