feat(base): add npc staffing and labor classification collections
Adds the npc-staffing and labor-classifications collections, a shared staffing field factory applied to both structure blueprints (Staffing Defaults group) and game structure instances, blueprint-derived staffing defaults on game structure create, staff-hiring toggles on Game Rules, and their database migrations. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
854c525b07
commit
b2718b3633
19 changed files with 107494 additions and 3 deletions
|
|
@ -17,6 +17,7 @@ export const GameRules: GlobalConfig = {
|
||||||
description:
|
description:
|
||||||
'The base currency ("gold standard") used by all factions to pay for goods and services.',
|
'The base currency ("gold standard") used by all factions to pay for goods and services.',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Universal Currency",
|
label: "Universal Currency",
|
||||||
type: "collapsible",
|
type: "collapsible",
|
||||||
|
|
@ -65,7 +66,6 @@ export const GameRules: GlobalConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "commandAssignments",
|
name: "commandAssignments",
|
||||||
type: "relationship",
|
type: "relationship",
|
||||||
|
|
@ -124,5 +124,22 @@ export const GameRules: GlobalConfig = {
|
||||||
"Height of each member's personal locker grid, in cells. Applies to newly provisioned lockers.",
|
"Height of each member's personal locker grid, in cells. Applies to newly provisioned lockers.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "staffHiringEnabled",
|
||||||
|
type: "checkbox",
|
||||||
|
defaultValue: false,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"When off, logistics officers cannot hire staff anywhere (both labor crews and named NPC staff).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "staffHiringDisabledMessage",
|
||||||
|
type: "text",
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Message shown to logistics officers when hiring is disabled, e.g. explaining the in-universe reason.",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { CollectionConfig } from "payload";
|
import type { CollectionConfig } from "payload";
|
||||||
import { requirePermission } from "@/utils/access-control/hasPermission";
|
import { requirePermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
|
import { staffingDefaultsFromBlueprint } from "@/lib/base/staffingDefaults";
|
||||||
|
import { createStaffingFields } from "./staffingFields";
|
||||||
|
|
||||||
export const GameStructures: CollectionConfig = {
|
export const GameStructures: CollectionConfig = {
|
||||||
slug: "game-structures",
|
slug: "game-structures",
|
||||||
|
|
@ -15,6 +17,30 @@ export const GameStructures: CollectionConfig = {
|
||||||
delete: requirePermission("game-structures:delete"),
|
delete: requirePermission("game-structures:delete"),
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
|
beforeChange: [
|
||||||
|
async ({ data, operation, req }) => {
|
||||||
|
if (operation !== "create" || !data?.type) return data;
|
||||||
|
const blueprintId = typeof data.type === "object" ? data.type.id : data.type;
|
||||||
|
const blueprint = await req.payload.findByID({
|
||||||
|
collection: "structures",
|
||||||
|
id: blueprintId,
|
||||||
|
depth: 1,
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
const defaults = staffingDefaultsFromBlueprint(blueprint);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
...(data.laborRequirements === undefined
|
||||||
|
? { laborRequirements: defaults.laborRequirements }
|
||||||
|
: {}),
|
||||||
|
...(data.staffPositions === undefined ? { staffPositions: defaults.staffPositions } : {}),
|
||||||
|
...(data.maintenanceCost === undefined
|
||||||
|
? { maintenanceCost: defaults.maintenanceCost }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
afterChange: [
|
afterChange: [
|
||||||
async ({ doc, previousDoc, req }) => {
|
async ({ doc, previousDoc, req }) => {
|
||||||
const d = doc as any;
|
const d = doc as any;
|
||||||
|
|
@ -73,6 +99,7 @@ export const GameStructures: CollectionConfig = {
|
||||||
description: "The faction that owns this structure.",
|
description: "The faction that owns this structure.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...createStaffingFields("instance"),
|
||||||
{
|
{
|
||||||
label: "Geolocation",
|
label: "Geolocation",
|
||||||
type: "group",
|
type: "group",
|
||||||
|
|
|
||||||
67
src/collections/game/LaborClassifications.ts
Normal file
67
src/collections/game/LaborClassifications.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { CollectionConfig } from "payload";
|
||||||
|
import hasRoles from "@/utils/access-control/hasRoles";
|
||||||
|
|
||||||
|
export const LaborClassifications: CollectionConfig = {
|
||||||
|
slug: "labor-classifications",
|
||||||
|
admin: {
|
||||||
|
group: "Game",
|
||||||
|
useAsTitle: "name",
|
||||||
|
defaultColumns: ["name", "key", "defaultUnitSalary", "availableHeadcount"],
|
||||||
|
listSearchableFields: ["name", "key"],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req }) => Boolean(req.user),
|
||||||
|
create: ({ req }) => hasRoles(["admin", "developer"], req.user),
|
||||||
|
update: ({ req }) => hasRoles(["admin", "developer"], req.user),
|
||||||
|
delete: ({ req }) => hasRoles(["admin", "developer"], req.user),
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
label: "Name",
|
||||||
|
admin: {
|
||||||
|
description: "Display label for this labor trade (e.g. Mechanic).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
label: "Key",
|
||||||
|
admin: {
|
||||||
|
description: "Stable slug used to identify this classification (e.g. mechanic).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "defaultUnitSalary",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
label: "Default unit salary",
|
||||||
|
admin: {
|
||||||
|
description: "Prefills the wage-per-worker input in the hire dialog.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "textarea",
|
||||||
|
label: "Description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "availableHeadcount",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
defaultValue: 0,
|
||||||
|
label: "Available headcount",
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"How many workers of this classification are currently available to hire across the unit. Hiring consumes from this pool; releasing staff returns them.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
84
src/collections/game/NpcStaffing.ts
Normal file
84
src/collections/game/NpcStaffing.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import type { CollectionConfig } from "payload";
|
||||||
|
import { requirePermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
|
export const NpcStaffing: CollectionConfig = {
|
||||||
|
slug: "npc-staffing",
|
||||||
|
admin: {
|
||||||
|
group: "Game",
|
||||||
|
useAsTitle: "positionTitle",
|
||||||
|
defaultColumns: ["npc", "structure", "positionTitle", "salary", "status"],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
read: ({ req }) => Boolean(req.user),
|
||||||
|
create: requirePermission("structures:update"),
|
||||||
|
update: requirePermission("structures:update"),
|
||||||
|
delete: requirePermission("game-npcs:delete"),
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "npc",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "game-npcs",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "structure",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "game-structures",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positionType",
|
||||||
|
type: "select",
|
||||||
|
options: ["labor", "administrative", "technical", "other"],
|
||||||
|
defaultValue: "labor",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "positionTitle",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "headcount",
|
||||||
|
type: "number",
|
||||||
|
min: 1,
|
||||||
|
defaultValue: 1,
|
||||||
|
label: "Headcount",
|
||||||
|
admin: {
|
||||||
|
description: "Workers in this block; always 1 for named staff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "laborClassification",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "labor-classifications",
|
||||||
|
hasMany: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "salary",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
type: "select",
|
||||||
|
options: ["active", "terminated"],
|
||||||
|
defaultValue: "active",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hiredAt",
|
||||||
|
type: "date",
|
||||||
|
defaultValue: () => new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "terminatedAt",
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "notes",
|
||||||
|
type: "textarea",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
103
src/collections/game/staffingFields.ts
Normal file
103
src/collections/game/staffingFields.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import type { Field } from "payload";
|
||||||
|
|
||||||
|
type StaffingFieldContext = "instance" | "defaults";
|
||||||
|
|
||||||
|
export function createStaffingFields(context: StaffingFieldContext): Field[] {
|
||||||
|
const defaults = context === "defaults";
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "maintenanceCost",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
defaultValue: 0,
|
||||||
|
admin: {
|
||||||
|
description: defaults
|
||||||
|
? "Default funds charged per base tick for structure upkeep. Copied into newly created game-structures of this type."
|
||||||
|
: "Funds charged per base tick for structure upkeep, separate from staff salaries.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "laborRequirements",
|
||||||
|
type: "array",
|
||||||
|
admin: {
|
||||||
|
description: defaults
|
||||||
|
? "Default labor classifications copied into newly created game-structures of this type. Empty = no labor crews by default."
|
||||||
|
: "Which labor classifications this structure needs. Empty = no labor crews can be assigned here.",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "classification",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "labor-classifications",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requiredHeadcount",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
defaultValue: 0,
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: "Informational staffing target shown to logistics officers.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "maxHeadcount",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"Hard cap for this classification. Zero or empty means it cannot be hired here.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enforcedSalary",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
"When set, workers of this classification are always paid this wage; hirers cannot change it.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "staffPositions",
|
||||||
|
type: "array",
|
||||||
|
admin: {
|
||||||
|
description: defaults
|
||||||
|
? "Default named staff position types copied into newly created game-structures of this type. Empty = no named staff by default."
|
||||||
|
: "Which named staff position types this structure accepts. Empty = no named staff can be assigned here.",
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: "positionType",
|
||||||
|
type: "select",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: "Administrative", value: "administrative" },
|
||||||
|
{ label: "Technical", value: "technical" },
|
||||||
|
{ label: "Other", value: "other" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
admin: {
|
||||||
|
description: "Maximum simultaneous named staff of this type. Empty means unlimited.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enforcedSalary",
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
admin: {
|
||||||
|
description: "When set, named staff of this type are always paid this salary.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { CollectionConfig } from "payload";
|
||||||
import { isDeveloper } from "@/utils/access-control/isRole";
|
import { isDeveloper } from "@/utils/access-control/isRole";
|
||||||
import type { Resource } from "@/payload-types";
|
import type { Resource } from "@/payload-types";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
|
import { createStaffingFields } from "@/collections/game/staffingFields";
|
||||||
|
|
||||||
export const Structures: CollectionConfig = {
|
export const Structures: CollectionConfig = {
|
||||||
slug: "structures",
|
slug: "structures",
|
||||||
|
|
@ -271,7 +272,7 @@ export const Structures: CollectionConfig = {
|
||||||
{ label: "Revision Requested", value: "revision_requested" },
|
{ label: "Revision Requested", value: "revision_requested" },
|
||||||
],
|
],
|
||||||
defaultValue: "in_progress",
|
defaultValue: "in_progress",
|
||||||
filterOptions: ({ options, data, req }) => {
|
filterOptions: ({ options, data: _data, req }) => {
|
||||||
return !isDeveloper({ req })
|
return !isDeveloper({ req })
|
||||||
? options.filter((option) =>
|
? options.filter((option) =>
|
||||||
typeof option === "string"
|
typeof option === "string"
|
||||||
|
|
@ -498,6 +499,12 @@ export const Structures: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "Staffing Defaults",
|
||||||
|
type: "group",
|
||||||
|
fields: createStaffingFields("defaults"),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
label: "Production & Function",
|
label: "Production & Function",
|
||||||
type: "group",
|
type: "group",
|
||||||
|
|
|
||||||
27
src/lib/base/staffingDefaults.ts
Normal file
27
src/lib/base/staffingDefaults.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import type { Structure } from "@/payload-types";
|
||||||
|
|
||||||
|
export type StaffingDefaults = {
|
||||||
|
readonly maintenanceCost: number;
|
||||||
|
readonly laborRequirements: NonNullable<Structure["laborRequirements"]>;
|
||||||
|
readonly staffPositions: NonNullable<Structure["staffPositions"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function staffingDefaultsFromBlueprint(blueprint: Structure): StaffingDefaults {
|
||||||
|
return {
|
||||||
|
maintenanceCost: blueprint.maintenanceCost ?? 0,
|
||||||
|
laborRequirements: (blueprint.laborRequirements ?? []).map((requirement) => ({
|
||||||
|
classification:
|
||||||
|
typeof requirement.classification === "object"
|
||||||
|
? requirement.classification.id
|
||||||
|
: requirement.classification,
|
||||||
|
requiredHeadcount: requirement.requiredHeadcount,
|
||||||
|
maxHeadcount: requirement.maxHeadcount ?? null,
|
||||||
|
enforcedSalary: requirement.enforcedSalary ?? null,
|
||||||
|
})),
|
||||||
|
staffPositions: (blueprint.staffPositions ?? []).map((position) => ({
|
||||||
|
positionType: position.positionType,
|
||||||
|
max: position.max ?? null,
|
||||||
|
enforcedSalary: position.enforcedSalary ?? null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
21720
src/migrations/20260905_183940_base_management.json
Normal file
21720
src/migrations/20260905_183940_base_management.json
Normal file
File diff suppressed because it is too large
Load diff
48
src/migrations/20260905_183940_base_management.ts
Normal file
48
src/migrations/20260905_183940_base_management.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_npc_staffing_position_type" AS ENUM('labor', 'administrative', 'technical', 'other');
|
||||||
|
CREATE TYPE "public"."enum_npc_staffing_status" AS ENUM('active', 'terminated');
|
||||||
|
ALTER TYPE "public"."enum_game_event_logs_target_collection" ADD VALUE 'npc-staffing' BEFORE 'structures';
|
||||||
|
CREATE TABLE "npc_staffing" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"npc_id" integer NOT NULL,
|
||||||
|
"structure_id" integer NOT NULL,
|
||||||
|
"position_type" "enum_npc_staffing_position_type" DEFAULT 'labor' NOT NULL,
|
||||||
|
"position_title" varchar NOT NULL,
|
||||||
|
"salary" numeric DEFAULT 0,
|
||||||
|
"status" "enum_npc_staffing_status" DEFAULT 'active',
|
||||||
|
"hired_at" timestamp(3) with time zone,
|
||||||
|
"terminated_at" timestamp(3) with time zone,
|
||||||
|
"notes" varchar,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "npc_staffing_id" integer;
|
||||||
|
ALTER TABLE "npc_staffing" ADD CONSTRAINT "npc_staffing_npc_id_game_npcs_id_fk" FOREIGN KEY ("npc_id") REFERENCES "public"."game_npcs"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "npc_staffing" ADD CONSTRAINT "npc_staffing_structure_id_game_structures_id_fk" FOREIGN KEY ("structure_id") REFERENCES "public"."game_structures"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
CREATE INDEX "npc_staffing_npc_idx" ON "npc_staffing" USING btree ("npc_id");
|
||||||
|
CREATE INDEX "npc_staffing_structure_idx" ON "npc_staffing" USING btree ("structure_id");
|
||||||
|
CREATE INDEX "npc_staffing_updated_at_idx" ON "npc_staffing" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "npc_staffing_created_at_idx" ON "npc_staffing" USING btree ("created_at");
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_npc_staffing_fk" FOREIGN KEY ("npc_staffing_id") REFERENCES "public"."npc_staffing"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_npc_staffing_id_idx" ON "payload_locked_documents_rels" USING btree ("npc_staffing_id");`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "npc_staffing" DISABLE ROW LEVEL SECURITY;
|
||||||
|
DROP TABLE "npc_staffing" CASCADE;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_npc_staffing_fk";
|
||||||
|
|
||||||
|
ALTER TABLE "game_event_logs" ALTER COLUMN "target_collection" SET DATA TYPE text;
|
||||||
|
DROP TYPE "public"."enum_game_event_logs_target_collection";
|
||||||
|
CREATE TYPE "public"."enum_game_event_logs_target_collection" AS ENUM('game-structures', 'game-vehicles', 'game-npcs', 'structures', 'resources', 'assets', 'vehicles', 'factions', 'missions', 'campaigns', 'users', 'technologies', 'maps', 'shipments', 'bank-accounts', 'bank-transactions', 'ledger-entries', 'locker-storages', 'loadouts', 'market-listings', 'market-negotiations', 'evaluations', 'tickets', 'wiki-pages', 'wiki-revisions', 'wiki-templates', 'game-servers', 'assignment-transfers');
|
||||||
|
ALTER TABLE "game_event_logs" ALTER COLUMN "target_collection" SET DATA TYPE "public"."enum_game_event_logs_target_collection" USING "target_collection"::"public"."enum_game_event_logs_target_collection";
|
||||||
|
DROP INDEX "payload_locked_documents_rels_npc_staffing_id_idx";
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "npc_staffing_id";
|
||||||
|
DROP TYPE "public"."enum_npc_staffing_position_type";
|
||||||
|
DROP TYPE "public"."enum_npc_staffing_status";`)
|
||||||
|
}
|
||||||
21746
src/migrations/20260905_195456_labor_headcount.json
Normal file
21746
src/migrations/20260905_195456_labor_headcount.json
Normal file
File diff suppressed because it is too large
Load diff
17
src/migrations/20260905_195456_labor_headcount.ts
Normal file
17
src/migrations/20260905_195456_labor_headcount.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_npc_staffing_labor_classification" AS ENUM('general', 'doctor', 'mechanic', 'engineer', 'security', 'other');
|
||||||
|
ALTER TABLE "npc_staffing" ALTER COLUMN "npc_id" DROP NOT NULL;
|
||||||
|
ALTER TABLE "npc_staffing" ADD COLUMN "headcount" numeric DEFAULT 1;
|
||||||
|
ALTER TABLE "npc_staffing" ADD COLUMN "labor_classification" "enum_npc_staffing_labor_classification";`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "npc_staffing" ALTER COLUMN "npc_id" SET NOT NULL;
|
||||||
|
ALTER TABLE "npc_staffing" DROP COLUMN "headcount";
|
||||||
|
ALTER TABLE "npc_staffing" DROP COLUMN "labor_classification";
|
||||||
|
DROP TYPE "public"."enum_npc_staffing_labor_classification";`)
|
||||||
|
}
|
||||||
21913
src/migrations/20260905_210310_labor_classifications.json
Normal file
21913
src/migrations/20260905_210310_labor_classifications.json
Normal file
File diff suppressed because it is too large
Load diff
51
src/migrations/20260905_210310_labor_classifications.ts
Normal file
51
src/migrations/20260905_210310_labor_classifications.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||||
|
|
||||||
|
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE "labor_classifications" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"name" varchar NOT NULL,
|
||||||
|
"key" varchar NOT NULL,
|
||||||
|
"default_unit_salary" numeric,
|
||||||
|
"description" varchar,
|
||||||
|
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||||
|
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "npc_staffing" ADD COLUMN "labor_classification_id" integer;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "labor_classifications_id" integer;
|
||||||
|
CREATE UNIQUE INDEX "labor_classifications_name_idx" ON "labor_classifications" USING btree ("name");
|
||||||
|
CREATE UNIQUE INDEX "labor_classifications_key_idx" ON "labor_classifications" USING btree ("key");
|
||||||
|
CREATE INDEX "labor_classifications_updated_at_idx" ON "labor_classifications" USING btree ("updated_at");
|
||||||
|
CREATE INDEX "labor_classifications_created_at_idx" ON "labor_classifications" USING btree ("created_at");
|
||||||
|
ALTER TABLE "npc_staffing" ADD CONSTRAINT "npc_staffing_labor_classification_id_labor_classifications_id_fk" FOREIGN KEY ("labor_classification_id") REFERENCES "public"."labor_classifications"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_labor_classifications_fk" FOREIGN KEY ("labor_classifications_id") REFERENCES "public"."labor_classifications"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
CREATE INDEX "npc_staffing_labor_classification_idx" ON "npc_staffing" USING btree ("labor_classification_id");
|
||||||
|
CREATE INDEX "payload_locked_documents_rels_labor_classifications_id_idx" ON "payload_locked_documents_rels" USING btree ("labor_classifications_id");
|
||||||
|
ALTER TABLE "npc_staffing" DROP COLUMN "labor_classification";
|
||||||
|
DROP TYPE "public"."enum_npc_staffing_labor_classification";
|
||||||
|
INSERT INTO "labor_classifications" ("name", "key", "default_unit_salary", "description") VALUES
|
||||||
|
('General', 'general', NULL, NULL),
|
||||||
|
('Doctor', 'doctor', NULL, NULL),
|
||||||
|
('Mechanic', 'mechanic', NULL, NULL),
|
||||||
|
('Engineer', 'engineer', NULL, NULL),
|
||||||
|
('Security', 'security', NULL, NULL),
|
||||||
|
('Other', 'other', NULL, NULL)
|
||||||
|
ON CONFLICT ("key") DO NOTHING;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_npc_staffing_labor_classification" AS ENUM('general', 'doctor', 'mechanic', 'engineer', 'security', 'other');
|
||||||
|
ALTER TABLE "labor_classifications" DISABLE ROW LEVEL SECURITY;
|
||||||
|
DROP TABLE "labor_classifications" CASCADE;
|
||||||
|
ALTER TABLE "npc_staffing" DROP CONSTRAINT "npc_staffing_labor_classification_id_labor_classifications_id_fk";
|
||||||
|
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_labor_classifications_fk";
|
||||||
|
|
||||||
|
DROP INDEX "npc_staffing_labor_classification_idx";
|
||||||
|
DROP INDEX "payload_locked_documents_rels_labor_classifications_id_idx";
|
||||||
|
ALTER TABLE "npc_staffing" ADD COLUMN "labor_classification" "enum_npc_staffing_labor_classification";
|
||||||
|
ALTER TABLE "npc_staffing" DROP COLUMN "labor_classification_id";
|
||||||
|
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "labor_classifications_id";`);
|
||||||
|
}
|
||||||
20657
src/migrations/20260905_213824.json
Normal file
20657
src/migrations/20260905_213824.json
Normal file
File diff suppressed because it is too large
Load diff
48
src/migrations/20260905_213824.ts
Normal file
48
src/migrations/20260905_213824.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_game_structures_staff_positions_position_type" AS ENUM('administrative', 'technical', 'other');
|
||||||
|
CREATE TABLE "game_structures_labor_requirements" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"classification_id" integer NOT NULL,
|
||||||
|
"required_headcount" numeric DEFAULT 0 NOT NULL,
|
||||||
|
"max_headcount" numeric,
|
||||||
|
"enforced_salary" numeric
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "game_structures_staff_positions" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"position_type" "enum_game_structures_staff_positions_position_type" NOT NULL,
|
||||||
|
"max" numeric,
|
||||||
|
"enforced_salary" numeric
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "game_structures" ADD COLUMN "maintenance_cost" numeric DEFAULT 0;
|
||||||
|
ALTER TABLE "labor_classifications" ADD COLUMN "available_headcount" numeric DEFAULT 0;
|
||||||
|
ALTER TABLE "game_rules" ADD COLUMN "staff_hiring_enabled" boolean DEFAULT false;
|
||||||
|
ALTER TABLE "game_rules" ADD COLUMN "staff_hiring_disabled_message" varchar;
|
||||||
|
ALTER TABLE "game_structures_labor_requirements" ADD CONSTRAINT "game_structures_labor_requirements_classification_id_labor_classifications_id_fk" FOREIGN KEY ("classification_id") REFERENCES "public"."labor_classifications"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "game_structures_labor_requirements" ADD CONSTRAINT "game_structures_labor_requirements_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."game_structures"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "game_structures_staff_positions" ADD CONSTRAINT "game_structures_staff_positions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."game_structures"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
CREATE INDEX "game_structures_labor_requirements_order_idx" ON "game_structures_labor_requirements" USING btree ("_order");
|
||||||
|
CREATE INDEX "game_structures_labor_requirements_parent_id_idx" ON "game_structures_labor_requirements" USING btree ("_parent_id");
|
||||||
|
CREATE INDEX "game_structures_labor_requirements_classification_idx" ON "game_structures_labor_requirements" USING btree ("classification_id");
|
||||||
|
CREATE INDEX "game_structures_staff_positions_order_idx" ON "game_structures_staff_positions" USING btree ("_order");
|
||||||
|
CREATE INDEX "game_structures_staff_positions_parent_id_idx" ON "game_structures_staff_positions" USING btree ("_parent_id");`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
DROP TABLE "game_structures_labor_requirements" CASCADE;
|
||||||
|
DROP TABLE "game_structures_staff_positions" CASCADE;
|
||||||
|
ALTER TABLE "game_structures" DROP COLUMN "maintenance_cost";
|
||||||
|
ALTER TABLE "labor_classifications" DROP COLUMN "available_headcount";
|
||||||
|
ALTER TABLE "game_rules" DROP COLUMN "staff_hiring_enabled";
|
||||||
|
ALTER TABLE "game_rules" DROP COLUMN "staff_hiring_disabled_message";
|
||||||
|
DROP TYPE "public"."enum_game_structures_staff_positions_position_type";`);
|
||||||
|
}
|
||||||
20881
src/migrations/20260905_215658.json
Normal file
20881
src/migrations/20260905_215658.json
Normal file
File diff suppressed because it is too large
Load diff
42
src/migrations/20260905_215658.ts
Normal file
42
src/migrations/20260905_215658.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TYPE "public"."enum_structures_staff_positions_position_type" AS ENUM('administrative', 'technical', 'other');
|
||||||
|
CREATE TABLE "structures_labor_requirements" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"classification_id" integer NOT NULL,
|
||||||
|
"required_headcount" numeric DEFAULT 0 NOT NULL,
|
||||||
|
"max_headcount" numeric,
|
||||||
|
"enforced_salary" numeric
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "structures_staff_positions" (
|
||||||
|
"_order" integer NOT NULL,
|
||||||
|
"_parent_id" integer NOT NULL,
|
||||||
|
"id" varchar PRIMARY KEY NOT NULL,
|
||||||
|
"position_type" "enum_structures_staff_positions_position_type" NOT NULL,
|
||||||
|
"max" numeric,
|
||||||
|
"enforced_salary" numeric
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "structures" ADD COLUMN "maintenance_cost" numeric DEFAULT 0;
|
||||||
|
ALTER TABLE "structures_labor_requirements" ADD CONSTRAINT "structures_labor_requirements_classification_id_labor_classifications_id_fk" FOREIGN KEY ("classification_id") REFERENCES "public"."labor_classifications"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
ALTER TABLE "structures_labor_requirements" ADD CONSTRAINT "structures_labor_requirements_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."structures"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
ALTER TABLE "structures_staff_positions" ADD CONSTRAINT "structures_staff_positions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."structures"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
CREATE INDEX "structures_labor_requirements_order_idx" ON "structures_labor_requirements" USING btree ("_order");
|
||||||
|
CREATE INDEX "structures_labor_requirements_parent_id_idx" ON "structures_labor_requirements" USING btree ("_parent_id");
|
||||||
|
CREATE INDEX "structures_labor_requirements_classification_idx" ON "structures_labor_requirements" USING btree ("classification_id");
|
||||||
|
CREATE INDEX "structures_staff_positions_order_idx" ON "structures_staff_positions" USING btree ("_order");
|
||||||
|
CREATE INDEX "structures_staff_positions_parent_id_idx" ON "structures_staff_positions" USING btree ("_parent_id");`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
DROP TABLE "structures_labor_requirements" CASCADE;
|
||||||
|
DROP TABLE "structures_staff_positions" CASCADE;
|
||||||
|
ALTER TABLE "structures" DROP COLUMN "maintenance_cost";
|
||||||
|
DROP TYPE "public"."enum_structures_staff_positions_position_type";`);
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,11 @@ import * as migration_20260903_061000_add_radio_incorrect_counts from './2026090
|
||||||
import * as migration_20260903_062000_add_aim_trainer_stats from './20260903_062000_add_aim_trainer_stats';
|
import * as migration_20260903_062000_add_aim_trainer_stats from './20260903_062000_add_aim_trainer_stats';
|
||||||
import * as migration_20260904_230838_add_wiki_collections from './20260904_230838_add_wiki_collections';
|
import * as migration_20260904_230838_add_wiki_collections from './20260904_230838_add_wiki_collections';
|
||||||
import * as migration_20260906_045502 from './20260906_045502';
|
import * as migration_20260906_045502 from './20260906_045502';
|
||||||
|
import * as migration_20260905_183940_base_management from './20260905_183940_base_management';
|
||||||
|
import * as migration_20260905_195456_labor_headcount from './20260905_195456_labor_headcount';
|
||||||
|
import * as migration_20260905_210310_labor_classifications from './20260905_210310_labor_classifications';
|
||||||
|
import * as migration_20260905_213824 from './20260905_213824';
|
||||||
|
import * as migration_20260905_215658 from './20260905_215658';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -149,5 +154,30 @@ export const migrations = [
|
||||||
up: migration_20260906_045502.up,
|
up: migration_20260906_045502.up,
|
||||||
down: migration_20260906_045502.down,
|
down: migration_20260906_045502.down,
|
||||||
name: '20260906_045502',
|
name: '20260906_045502',
|
||||||
|
}, {
|
||||||
|
up: migration_20260905_183940_base_management.up,
|
||||||
|
down: migration_20260905_183940_base_management.down,
|
||||||
|
name: '20260905_183940_base_management',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260905_195456_labor_headcount.up,
|
||||||
|
down: migration_20260905_195456_labor_headcount.down,
|
||||||
|
name: '20260905_195456_labor_headcount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260905_210310_labor_classifications.up,
|
||||||
|
down: migration_20260905_210310_labor_classifications.down,
|
||||||
|
name: '20260905_210310_labor_classifications',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260905_213824.up,
|
||||||
|
down: migration_20260905_213824.down,
|
||||||
|
name: '20260905_213824',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260905_215658.up,
|
||||||
|
down: migration_20260905_215658.down,
|
||||||
|
name: '20260905_215658',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import { GameHardResources } from "@/collections/game/GameHardResources";
|
||||||
import { GameEventLogs } from "@/collections/game/GameEventLogs";
|
import { GameEventLogs } from "@/collections/game/GameEventLogs";
|
||||||
import { GameVehicles } from "@/collections/game/GameVehicles";
|
import { GameVehicles } from "@/collections/game/GameVehicles";
|
||||||
import { GameNpcs } from "@/collections/game/GameNpcs";
|
import { GameNpcs } from "@/collections/game/GameNpcs";
|
||||||
|
import { NpcStaffing } from "@/collections/game/NpcStaffing";
|
||||||
|
import { LaborClassifications } from "@/collections/game/LaborClassifications";
|
||||||
import { Experience } from "@/collections/users/Experience";
|
import { Experience } from "@/collections/users/Experience";
|
||||||
import { BankAccounts } from "@/collections/banking/BankAccounts";
|
import { BankAccounts } from "@/collections/banking/BankAccounts";
|
||||||
import { BankTransactions } from "@/collections/banking/BankTransactions";
|
import { BankTransactions } from "@/collections/banking/BankTransactions";
|
||||||
|
|
@ -151,6 +153,8 @@ const collections = [
|
||||||
GameStructures,
|
GameStructures,
|
||||||
GameVehicles,
|
GameVehicles,
|
||||||
GameNpcs,
|
GameNpcs,
|
||||||
|
NpcStaffing,
|
||||||
|
LaborClassifications,
|
||||||
GameEventLogs,
|
GameEventLogs,
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
|
|
@ -329,6 +333,8 @@ export default buildConfig({
|
||||||
[GameStructures.slug]: true,
|
[GameStructures.slug]: true,
|
||||||
[GameVehicles.slug]: true,
|
[GameVehicles.slug]: true,
|
||||||
[GameNpcs.slug]: true,
|
[GameNpcs.slug]: true,
|
||||||
|
[NpcStaffing.slug]: true,
|
||||||
|
[LaborClassifications.slug]: true,
|
||||||
[GameEventLogs.slug]: true,
|
[GameEventLogs.slug]: true,
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue