Files
claw3d/src/features/retro-office/core/furnitureDefaults.ts
T
Luke The Dev a997f13601 feat(kanban): Interactive Kanban board with real-time task tracking (#83)
* feat(kanban): add Kanban board with task-manager skill, modal UI, and desk clutter

Implement a full Kanban board system for tracking agent tasks:
- Add task-manager skill with shared JSON task store for persistence
- Render board as a floating modal over the live 3D office (not immersive)
- Auto-create tasks from actionable user messages with heuristic filtering
- Sync task status through OpenClaw agent lifecycle events
- Collapse task details panel by default, expand on card click
- Add dynamic desk clutter (papers, folders, etc.) reflecting active task count
- Exclude done tasks from desk clutter count
- Extract KANBAN_CLUTTER_OFFSET for easy positioning adjustment
- Add install flow with progress bar for the task-manager skill
- Include unit and e2e test coverage

Made-with: Cursor

* feat(kanban): production-harden task board with AI-free classification, resilient persistence, and modal UX

- Harden shared task store with atomic writes, payload size limits, and server-side enum validation
- Add client resilience: request timeouts (AbortController), exponential backoff retries, poll deduplication
- Implement optimistic UI with rollback on all card mutations (update, move, archive)
- Add modal accessibility: focus trap, Escape to close, aria-modal, keyboard card navigation
- Trust OpenClaw agent lifecycle phase=start as task classification signal instead of regex heuristics
- Keep regex heuristic only as lightweight filter for direct chat events (conversational noise)
- Expand verb recognition with typo tolerance and broader action vocabulary
- Create tasks from agent runs even when no chat event is received (external channel support)
- Merge dual header bars into single bar; reposition close button outside modal corner
- Exclude done tasks from desk clutter count; make clutter position configurable via KANBAN_CLUTTER_OFFSET
- Update default furniture layout to match user configuration
- Ensure kanban_board furniture persists in local storage across sessions
- Add comprehensive test coverage for store, API route, and controller logic

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
2026-03-30 22:58:18 -05:00

764 lines
23 KiB
TypeScript

import {
DOOR_LENGTH,
DOOR_THICKNESS,
EAST_WING_DOOR_Y,
EAST_WING_ROOM_HEIGHT,
EAST_WING_ROOM_TOP_Y,
GYM_ROOM_END_X,
GYM_ROOM_X,
QA_LAB_END_X,
QA_LAB_X,
WALL_THICKNESS,
} from "@/features/retro-office/core/constants";
import { nextUid } from "@/features/retro-office/core/geometry";
import {
hasAtmMigrationApplied,
hasGymRoomMigrationApplied,
hasPhoneBoothMigrationApplied,
hasQaLabMigrationApplied,
hasSmsBoothMigrationApplied,
hasServerRoomMigrationApplied,
} from "@/features/retro-office/core/persistence";
import type {
FurnitureItem,
FurnitureSeed,
} from "@/features/retro-office/core/types";
const DEFAULT_PINGPONG_TABLE: FurnitureSeed = {
type: "pingpong",
x: 950,
y: 600,
w: 100,
h: 60,
};
const DEFAULT_ATM_MACHINE: FurnitureSeed = {
type: "atm",
x: 430,
y: 210,
facing: 90,
};
const DEFAULT_PHONE_BOOTH: FurnitureSeed = {
type: "phone_booth",
x: 1050,
y: 190,
facing: 270,
};
const DEFAULT_SMS_BOOTH: FurnitureSeed = {
type: "sms_booth",
x: 700,
y: 10,
facing: 0,
};
const DEFAULT_JUKEBOX: FurnitureSeed = {
type: "jukebox",
x: 20,
y: 380,
facing: 90,
};
const DEFAULT_KANBAN_BOARD: FurnitureSeed = {
type: "kanban_board",
x: 460,
y: -60,
facing: 180,
};
const PREVIOUS_SERVER_ROOM_ITEMS_BOTTOM_RIGHT: FurnitureSeed[] = [
{ type: "wall", x: 820, y: 540, w: 280, h: WALL_THICKNESS },
{ type: "wall", x: 820, y: 540, w: WALL_THICKNESS, h: 70 },
{ type: "wall", x: 820, y: 650, w: WALL_THICKNESS, h: 70 },
{
type: "door",
x: 820,
y: 610,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{ type: "server_rack", x: 885, y: 575, facing: 180 },
{ type: "server_rack", x: 955, y: 575, facing: 180 },
{ type: "server_terminal", x: 930, y: 640, facing: 0 },
];
const PREVIOUS_SERVER_ROOM_ITEMS_TOP_RIGHT: FurnitureSeed[] = [
{ type: "wall", x: 820, y: 0, w: WALL_THICKNESS, h: 130 },
{ type: "wall", x: 820, y: 170, w: WALL_THICKNESS, h: 60 },
{
type: "door",
x: 820,
y: 130,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{ type: "wall", x: 820, y: 230, w: 280, h: WALL_THICKNESS },
{ type: "server_rack", x: 875, y: 95, facing: 180 },
{ type: "server_rack", x: 950, y: 95, facing: 180 },
{ type: "server_terminal", x: 930, y: 185, facing: 0 },
];
const DEFAULT_DINING_ITEMS: FurnitureSeed[] = [
{ type: "round_table", x: 890, y: 100, r: 50 },
{ type: "chair", x: 930, y: 100, facing: 0 },
{ type: "chair", x: 930, y: 180, facing: 180 },
{ type: "chair", x: 880, y: 130, facing: 90 },
{ type: "chair", x: 970, y: 130, facing: 270 },
];
const DEFAULT_SERVER_ROOM_ITEMS: FurnitureSeed[] = [
{ type: "wall", x: 0, y: 560, w: 230, h: WALL_THICKNESS },
{ type: "wall", x: 220, y: 560, w: WALL_THICKNESS, h: 60 },
{
type: "door",
x: 210,
y: 630,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{ type: "wall", x: 220, y: 660, w: WALL_THICKNESS, h: 60 },
{ type: "server_rack", x: 50, y: 595, facing: 0 },
{ type: "server_rack", x: 125, y: 595, facing: 0 },
{ type: "server_terminal", x: 110, y: 645, facing: 180 },
];
const LEGACY_GYM_ROOM_ITEMS: FurnitureSeed[] = [
{ type: "wall", x: 1092, y: 0, w: WALL_THICKNESS, h: 260 },
{
type: "door",
x: 1092,
y: 260,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{ type: "wall", x: 1092, y: 300, w: WALL_THICKNESS, h: 420 },
{ type: "wall", x: 1092, y: 0, w: 358, h: WALL_THICKNESS },
{ type: "wall", x: 1092, y: 712, w: 358, h: WALL_THICKNESS },
{ type: "wall", x: 1442, y: 0, w: WALL_THICKNESS, h: 260 },
{
type: "door",
x: 1442,
y: 260,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{ type: "wall", x: 1442, y: 300, w: WALL_THICKNESS, h: 420 },
{ type: "treadmill", x: 1160, y: 90, facing: 90 },
{ type: "treadmill", x: 1160, y: 210, facing: 90 },
{ type: "rowing_machine", x: 1150, y: 340, facing: 90 },
{ type: "weight_bench", x: 1240, y: 120, facing: 90 },
{ type: "weight_bench", x: 1240, y: 260, facing: 90 },
{ type: "dumbbell_rack", x: 1320, y: 90, facing: 180 },
{ type: "dumbbell_rack", x: 1320, y: 220, facing: 180 },
{ type: "kettlebell_rack", x: 1310, y: 330, facing: 180 },
{ type: "exercise_bike", x: 1180, y: 410, facing: 90 },
{ type: "exercise_bike", x: 1180, y: 540, facing: 90 },
{ type: "punching_bag", x: 1360, y: 390, facing: 0 },
{ type: "punching_bag", x: 1360, y: 560, facing: 0 },
{ type: "yoga_mat", x: 1240, y: 470, facing: 0, color: "#0f766e" },
{ type: "yoga_mat", x: 1240, y: 560, facing: 0, color: "#7c3aed" },
{ type: "plant", x: 1400, y: 40 },
{ type: "plant", x: 1400, y: 660 },
];
const LEGACY_QA_LAB_ITEMS: FurnitureSeed[] = [
{ type: "wall", x: 1442, y: 0, w: 358, h: WALL_THICKNESS },
{ type: "wall", x: 1442, y: 712, w: 358, h: WALL_THICKNESS },
{ type: "wall", x: 1792, y: 0, w: WALL_THICKNESS, h: 720 },
{ type: "qa_terminal", x: 1530, y: 95, facing: 90 },
{ type: "device_rack", x: 1650, y: 90, facing: 180 },
{ type: "device_rack", x: 1650, y: 220, facing: 180 },
{ type: "test_bench", x: 1520, y: 320, facing: 90 },
{ type: "test_bench", x: 1520, y: 470, facing: 90 },
{ type: "plant", x: 1750, y: 40 },
{ type: "plant", x: 1750, y: 660 },
];
const EAST_WING_ROOM_BOTTOM_Y = EAST_WING_ROOM_TOP_Y + EAST_WING_ROOM_HEIGHT;
const EAST_WING_ROOM_BOTTOM_WALL_Y = EAST_WING_ROOM_BOTTOM_Y - WALL_THICKNESS;
const EAST_WING_DOOR_BOTTOM_Y = EAST_WING_DOOR_Y + DOOR_LENGTH;
const EAST_WING_TOP_WALL_HEIGHT = EAST_WING_DOOR_Y - EAST_WING_ROOM_TOP_Y;
const EAST_WING_BOTTOM_WALL_HEIGHT =
EAST_WING_ROOM_BOTTOM_Y - EAST_WING_DOOR_BOTTOM_Y;
const PREVIOUS_GYM_ROOM_ITEMS: FurnitureSeed[] = [
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_ROOM_HEIGHT,
},
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_TOP_Y,
w: GYM_ROOM_END_X - GYM_ROOM_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_BOTTOM_WALL_Y,
w: GYM_ROOM_END_X - GYM_ROOM_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: GYM_ROOM_END_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_TOP_WALL_HEIGHT,
},
{
type: "door",
x: GYM_ROOM_END_X,
y: EAST_WING_DOOR_Y,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{
type: "wall",
x: GYM_ROOM_END_X,
y: EAST_WING_DOOR_BOTTOM_Y,
w: WALL_THICKNESS,
h: EAST_WING_BOTTOM_WALL_HEIGHT,
},
{ type: "treadmill", x: 1188, y: 88, facing: 90 },
{ type: "weight_bench", x: 1250, y: 92, facing: 90 },
{ type: "dumbbell_rack", x: 1272, y: 160, facing: 180 },
{ type: "rowing_machine", x: 1186, y: 248, facing: 90 },
{ type: "kettlebell_rack", x: 1278, y: 268, facing: 180 },
{ type: "exercise_bike", x: 1192, y: 370, facing: 90 },
{ type: "punching_bag", x: 1310, y: 394, facing: 0 },
{ type: "yoga_mat", x: 1218, y: 544, facing: 0, color: "#0f766e" },
{ type: "plant", x: 1312, y: 82 },
{ type: "plant", x: 1312, y: 622 },
];
const PREVIOUS_QA_LAB_ITEMS: FurnitureSeed[] = [
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_TOP_WALL_HEIGHT,
},
{
type: "door",
x: QA_LAB_X,
y: EAST_WING_DOOR_Y,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_DOOR_BOTTOM_Y,
w: WALL_THICKNESS,
h: EAST_WING_BOTTOM_WALL_HEIGHT,
},
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_TOP_Y,
w: QA_LAB_END_X - QA_LAB_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_BOTTOM_WALL_Y,
w: QA_LAB_END_X - QA_LAB_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: QA_LAB_END_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_ROOM_HEIGHT,
},
{ type: "qa_terminal", x: 1496, y: 92, facing: 90 },
{ type: "device_rack", x: 1568, y: 88, facing: 180 },
{ type: "device_rack", x: 1568, y: 194, facing: 180 },
{ type: "test_bench", x: 1492, y: 300, facing: 90 },
{ type: "test_bench", x: 1492, y: 434, facing: 90 },
{ type: "plant", x: 1604, y: 82 },
{ type: "plant", x: 1604, y: 622 },
];
const DEFAULT_GYM_ITEMS: FurnitureSeed[] = [
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_ROOM_HEIGHT,
},
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_TOP_Y,
w: GYM_ROOM_END_X - GYM_ROOM_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: GYM_ROOM_X,
y: EAST_WING_ROOM_BOTTOM_WALL_Y,
w: GYM_ROOM_END_X - GYM_ROOM_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: GYM_ROOM_END_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: 220,
},
{
type: "door",
x: 1280,
y: 280,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{
type: "wall",
x: GYM_ROOM_END_X,
y: 300,
w: WALL_THICKNESS,
h: 380,
},
{ type: "treadmill", x: 1142, y: 90, facing: 90 },
{ type: "weight_bench", x: 1204, y: 92, facing: 90 },
{ type: "dumbbell_rack", x: 1220, y: 160, facing: 180 },
{ type: "rowing_machine", x: 1140, y: 222, facing: 90 },
{ type: "kettlebell_rack", x: 1224, y: 248, facing: 180 },
{ type: "exercise_bike", x: 1146, y: 366, facing: 90 },
{ type: "punching_bag", x: 1266, y: 380, facing: 0 },
{ type: "yoga_mat", x: 1168, y: 542, facing: 0, color: "#0f766e" },
{ type: "plant", x: 1268, y: 82 },
{ type: "plant", x: 1268, y: 622 },
];
const DEFAULT_QA_LAB_ITEMS: FurnitureSeed[] = [
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: 220,
},
{
type: "door",
x: 1340,
y: 280,
w: DOOR_LENGTH,
h: DOOR_THICKNESS,
facing: 90,
},
{
type: "wall",
x: QA_LAB_X,
y: 300,
w: WALL_THICKNESS,
h: 380,
},
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_TOP_Y,
w: QA_LAB_END_X - QA_LAB_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: QA_LAB_X,
y: EAST_WING_ROOM_BOTTOM_WALL_Y,
w: QA_LAB_END_X - QA_LAB_X + WALL_THICKNESS,
h: WALL_THICKNESS,
},
{
type: "wall",
x: QA_LAB_END_X,
y: EAST_WING_ROOM_TOP_Y,
w: WALL_THICKNESS,
h: EAST_WING_ROOM_HEIGHT,
},
{ type: "qa_terminal", x: 1374, y: 92, facing: 90 },
{ type: "device_rack", x: 1454, y: 92, facing: 180 },
{ type: "device_rack", x: 1454, y: 204, facing: 180 },
{ type: "test_bench", x: 1372, y: 316, facing: 90 },
{ type: "test_bench", x: 1372, y: 450, facing: 90 },
{ type: "plant", x: 1496, y: 82 },
{ type: "plant", x: 1496, y: 622 },
];
const DEFAULT_ART_ROOM_ITEMS: FurnitureSeed[] = [
{ type: "wall", x: 260, y: 40, w: 8, h: 230 },
{ type: "wall", x: 260, y: 40, w: 178, h: 8 },
{ type: "wall", x: 260, y: 262, w: 178, h: 8 },
{ type: "wall", x: 430, y: 40, w: 8, h: 90 },
{ type: "door", x: 420, y: 150, w: 40, h: 8, facing: 90 },
{ type: "wall", x: 430, y: 170, w: 8, h: 100 },
{ type: "easel", x: 278, y: 84, facing: 90 },
{ type: "easel", x: 278, y: 158, facing: 90 },
{ type: "plant", x: 280, y: 60 },
{ type: "plant", x: 280, y: 240 },
];
const DEFAULT_FURNITURE: FurnitureSeed[] = [
{ type: "round_table", x: 50, y: 50, r: 90 },
{ type: "chair", x: 130, y: 50, facing: 0 },
{ type: "chair", x: 200, y: 90, facing: 325 },
{ type: "chair", x: 180, y: 170, facing: 240 },
{ type: "chair", x: 120, y: 480, facing: 180 },
{ type: "chair", x: 50, y: 150, facing: 105 },
{ type: "chair", x: 60, y: 80, facing: 60 },
{ type: "chair", x: 550, y: 50, facing: 0 },
{ type: "bookshelf", x: 600, y: 30, w: 80, h: 120 },
{ type: "couch", x: 270, y: 90, w: 40, h: 80, vertical: true, facing: 180 },
{ type: "fridge", x: 1050, y: 20, w: 40, h: 80 },
{ type: "stove", x: 920, y: 20 },
{ type: "cabinet", x: 980, y: 30, w: 40, h: 40 },
{ type: "microwave", x: 1030, y: 10, facing: 0 },
{ type: "sink", x: 970, y: 20 },
{ type: "dishwasher", x: 950, y: 20, w: 40, h: 40 },
{ type: "cabinet", x: 840, y: 30, w: 80, h: 40, elevation: 0 },
{ type: "coffee_machine", x: 880, y: 30, elevation: 0.56 },
{ type: "wall_cabinet", x: 960, y: 10, w: 80, h: 20, elevation: 0.9 },
{ type: "wall_cabinet", x: 880, y: 10, w: 80, h: 20, elevation: 0.9 },
{ type: "round_table", x: 890, y: 100, r: 50 },
{ type: "chair", x: 930, y: 100, facing: 0 },
{ type: "chair", x: 930, y: 180, facing: 180 },
{ type: "chair", x: 880, y: 130, facing: 90 },
{ type: "chair", x: 970, y: 130, facing: 270 },
{ type: "vending", x: 790, y: 10 },
{ type: "trash", x: 210, y: 20 },
{ type: "desk_cubicle", x: 100, y: 300, id: "desk_0" },
{ type: "chair", x: 120, y: 290, facing: 180 },
{ type: "computer", x: 120, y: 287 },
{ type: "keyboard", x: 130, y: 295 },
{ type: "mouse", x: 152, y: 295 },
{ type: "trash", x: 170, y: 290 },
{ type: "desk_cubicle", x: 300, y: 300, id: "desk_1" },
{ type: "chair", x: 320, y: 290, facing: 180 },
{ type: "computer", x: 320, y: 287 },
{ type: "keyboard", x: 330, y: 295 },
{ type: "mouse", x: 352, y: 295 },
{ type: "trash", x: 370, y: 290 },
{ type: "desk_cubicle", x: 500, y: 300, id: "desk_2" },
{ type: "chair", x: 520, y: 290, facing: 180 },
{ type: "computer", x: 520, y: 287 },
{ type: "keyboard", x: 530, y: 295 },
{ type: "mouse", x: 552, y: 295 },
{ type: "trash", x: 570, y: 290 },
{ type: "desk_cubicle", x: 700, y: 300, id: "desk_3" },
{ type: "chair", x: 720, y: 290, facing: 180 },
{ type: "computer", x: 720, y: 287 },
{ type: "keyboard", x: 730, y: 295 },
{ type: "mouse", x: 752, y: 295 },
{ type: "trash", x: 770, y: 290 },
{ type: "desk_cubicle", x: 100, y: 500, id: "desk_4" },
{ type: "computer", x: 120, y: 487 },
{ type: "keyboard", x: 130, y: 490 },
{ type: "mouse", x: 152, y: 495 },
{ type: "trash", x: 170, y: 490 },
{ type: "desk_cubicle", x: 300, y: 500, id: "desk_5" },
{ type: "chair", x: 310, y: 490, facing: 180 },
{ type: "computer", x: 320, y: 487 },
{ type: "keyboard", x: 330, y: 495 },
{ type: "mouse", x: 352, y: 495 },
{ type: "trash", x: 370, y: 500 },
{ type: "desk_cubicle", x: 500, y: 500, id: "desk_6" },
{ type: "chair", x: 520, y: 490, facing: 180 },
{ type: "computer", x: 520, y: 487 },
{ type: "keyboard", x: 530, y: 495 },
{ type: "mouse", x: 552, y: 495 },
{ type: "trash", x: 570, y: 490 },
{ type: "desk_cubicle", x: 700, y: 500, id: "desk_7" },
{ type: "chair", x: 720, y: 490, facing: 180 },
{ type: "computer", x: 720, y: 487 },
{ type: "keyboard", x: 730, y: 495 },
{ type: "mouse", x: 752, y: 495 },
{ type: "trash", x: 770, y: 490 },
{ type: "couch", x: 1000, y: 380, w: 100, h: 40, facing: 90 },
{ type: "couch", x: 390, y: 630, w: 100, h: 40 },
{ type: "table_rect", x: 980, y: 380, w: 60, h: 30, facing: 270 },
{ type: "pingpong", x: 950, y: 600, w: 100, h: 60 },
{ type: "beanbag", x: 1000, y: 330, color: "#e65100", facing: 90 },
{ type: "beanbag", x: 1000, y: 410, color: "#1565c0", facing: 90 },
DEFAULT_ATM_MACHINE,
DEFAULT_PHONE_BOOTH,
DEFAULT_KANBAN_BOARD,
{ type: "whiteboard", x: 40, y: 200, w: 10, h: 60 },
{ type: "clock", x: 550, y: 5 },
{ type: "lamp", x: 430, y: 100 },
{ type: "lamp", x: 980, y: 390 },
{ type: "trash", x: 830, y: 20 },
{ type: "plant", x: 40, y: 40 },
{ type: "plant", x: 660, y: 30 },
{ type: "plant", x: 340, y: 700 },
{ type: "plant", x: 450, y: 450 },
{ type: "plant", x: 1090, y: 310 },
{ type: "plant", x: 1100, y: 490 },
{ type: "plant", x: 530, y: 700 },
...DEFAULT_SERVER_ROOM_ITEMS,
...DEFAULT_GYM_ITEMS,
...DEFAULT_QA_LAB_ITEMS,
...DEFAULT_ART_ROOM_ITEMS,
DEFAULT_SMS_BOOTH,
{ type: "chair", x: 100, y: 200, facing: 180 },
];
export const materializeDefaults = (): FurnitureItem[] =>
DEFAULT_FURNITURE.map((item, index) => ({
...item,
_uid: `default_${index}`,
}));
export const isRetiredPingPongLamp = (item: FurnitureItem) =>
item.type === "lamp" &&
((item.x === 870 && item.y === 470) || (item.x === 900 && item.y === 580));
const createFurnitureSignature = (item: FurnitureSeed | FurnitureItem) =>
[
item.type,
item.x,
item.y,
item.w ?? "",
item.h ?? "",
item.r ?? "",
item.facing ?? "",
item.vertical ? 1 : 0,
item.elevation ?? "",
].join(":");
const PREVIOUS_SERVER_ROOM_SIGNATURES = new Set(
[
...PREVIOUS_SERVER_ROOM_ITEMS_BOTTOM_RIGHT,
...PREVIOUS_SERVER_ROOM_ITEMS_TOP_RIGHT,
].map(createFurnitureSignature),
);
const SERVER_ROOM_SIGNATURES = new Set(
DEFAULT_SERVER_ROOM_ITEMS.map(createFurnitureSignature),
);
const LEGACY_GYM_ROOM_SIGNATURES = new Set(
LEGACY_GYM_ROOM_ITEMS.map(createFurnitureSignature),
);
const PREVIOUS_GYM_ROOM_SIGNATURES = new Set(
PREVIOUS_GYM_ROOM_ITEMS.map(createFurnitureSignature),
);
const GYM_ROOM_SIGNATURES = new Set(
DEFAULT_GYM_ITEMS.map(createFurnitureSignature),
);
const LEGACY_QA_LAB_SIGNATURES = new Set(
LEGACY_QA_LAB_ITEMS.map(createFurnitureSignature),
);
const PREVIOUS_QA_LAB_SIGNATURES = new Set(
PREVIOUS_QA_LAB_ITEMS.map(createFurnitureSignature),
);
const QA_LAB_SIGNATURES = new Set(
DEFAULT_QA_LAB_ITEMS.map(createFurnitureSignature),
);
const hasSignature = (items: FurnitureItem[], signatures: Set<string>) =>
items.some((item) => signatures.has(createFurnitureSignature(item)));
const hasAllSignatures = (items: FurnitureItem[], signatures: Set<string>) => {
const itemSignatures = new Set(items.map(createFurnitureSignature));
return [...signatures].every((signature) => itemSignatures.has(signature));
};
const replaceBySignatureSet = (
items: FurnitureItem[],
signatures: Set<string>,
) => items.filter((item) => !signatures.has(createFurnitureSignature(item)));
export const ensureOfficePingPongTable = (
items: FurnitureItem[],
): FurnitureItem[] => {
if (items.some((item) => item.type === "pingpong")) return items;
return [...items, { ...DEFAULT_PINGPONG_TABLE, _uid: nextUid() }];
};
export const ensureOfficeAtm = (items: FurnitureItem[]): FurnitureItem[] => {
if (items.some((item) => item.type === "atm")) return items;
if (hasAtmMigrationApplied()) return items;
return [...items, { ...DEFAULT_ATM_MACHINE, _uid: nextUid() }];
};
export const ensureOfficeJukebox = (items: FurnitureItem[]): FurnitureItem[] => {
if (items.some((item) => item.type === "jukebox")) return items;
return [...items, { ...DEFAULT_JUKEBOX, _uid: nextUid() }];
};
export const ensureOfficeKanbanBoard = (items: FurnitureItem[]): FurnitureItem[] => {
if (items.some((item) => item.type === "kanban_board")) return items;
return [...items, { ...DEFAULT_KANBAN_BOARD, _uid: nextUid() }];
};
export const ensureOfficePhoneBooth = (
items: FurnitureItem[],
): FurnitureItem[] => {
let found = false;
const nextItems = items.map((item) => {
if (item.type === "phone_booth") {
found = true;
if (item.x === 980 && item.y === 560) {
return { ...item, x: 1050, y: 190 };
}
}
return item;
});
if (found) return nextItems;
if (hasPhoneBoothMigrationApplied()) return nextItems;
return [...nextItems, { ...DEFAULT_PHONE_BOOTH, _uid: nextUid() }];
};
export const ensureOfficeSmsBooth = (
items: FurnitureItem[],
): FurnitureItem[] => {
if (items.some((item) => item.type === "sms_booth")) return items;
if (hasSmsBoothMigrationApplied()) return items;
return [...items, { ...DEFAULT_SMS_BOOTH, _uid: nextUid() }];
};
export const ensureOfficeServerRoom = (
items: FurnitureItem[],
): FurnitureItem[] => {
const hasCurrentServerRoom = items.some((item) =>
SERVER_ROOM_SIGNATURES.has(createFurnitureSignature(item)),
);
if (hasCurrentServerRoom) return items;
const hasPreviousServerRoom = items.some((item) =>
PREVIOUS_SERVER_ROOM_SIGNATURES.has(createFurnitureSignature(item)),
);
if (hasPreviousServerRoom) {
const withoutPreviousServerRoom = items.filter(
(item) =>
!PREVIOUS_SERVER_ROOM_SIGNATURES.has(createFurnitureSignature(item)) &&
item.type !== "server_rack" &&
item.type !== "server_terminal",
);
const nextItems = [...withoutPreviousServerRoom];
for (const diningItem of DEFAULT_DINING_ITEMS) {
const hasDiningItem = nextItems.some(
(item) =>
createFurnitureSignature(item) ===
createFurnitureSignature(diningItem),
);
if (!hasDiningItem) {
nextItems.push({ ...diningItem, _uid: nextUid() });
}
}
return [
...nextItems,
...DEFAULT_SERVER_ROOM_ITEMS.map((item) => ({
...item,
_uid: nextUid(),
})),
];
}
if (items.some((item) => item.type === "server_terminal")) return items;
if (hasServerRoomMigrationApplied()) return items;
return [
...items,
...DEFAULT_SERVER_ROOM_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
};
export const ensureOfficeGymRoom = (
items: FurnitureItem[],
): FurnitureItem[] => {
const hasCurrentGymRoom = hasSignature(items, GYM_ROOM_SIGNATURES);
if (hasCurrentGymRoom) return items;
const hasPreviousGymRoom = hasAllSignatures(
items,
PREVIOUS_GYM_ROOM_SIGNATURES,
);
if (hasPreviousGymRoom) {
return [
...replaceBySignatureSet(items, PREVIOUS_GYM_ROOM_SIGNATURES),
...DEFAULT_GYM_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
}
const hasLegacyGymRoom = hasAllSignatures(items, LEGACY_GYM_ROOM_SIGNATURES);
if (hasLegacyGymRoom) {
return [
...replaceBySignatureSet(items, LEGACY_GYM_ROOM_SIGNATURES),
...DEFAULT_GYM_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
}
const hasGymEquipment = items.some((item) =>
[
"treadmill",
"weight_bench",
"dumbbell_rack",
"exercise_bike",
"punching_bag",
"rowing_machine",
"kettlebell_rack",
"yoga_mat",
].includes(item.type),
);
if (hasGymEquipment) return items;
if (hasGymRoomMigrationApplied()) return items;
return [
...items,
...DEFAULT_GYM_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
};
export const ensureOfficeQaLab = (items: FurnitureItem[]): FurnitureItem[] => {
const hasCurrentQaLab = hasSignature(items, QA_LAB_SIGNATURES);
if (hasCurrentQaLab) return items;
const hasPreviousQaLab = hasAllSignatures(items, PREVIOUS_QA_LAB_SIGNATURES);
if (hasPreviousQaLab) {
return [
...replaceBySignatureSet(items, PREVIOUS_QA_LAB_SIGNATURES),
...DEFAULT_QA_LAB_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
}
const hasLegacyQaLab = hasAllSignatures(items, LEGACY_QA_LAB_SIGNATURES);
if (hasLegacyQaLab) {
return [
...replaceBySignatureSet(items, LEGACY_QA_LAB_SIGNATURES),
...DEFAULT_QA_LAB_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
}
const hasQaFurniture = items.some((item) =>
["qa_terminal", "device_rack", "test_bench"].includes(item.type),
);
if (hasQaFurniture) return items;
if (hasQaLabMigrationApplied()) return items;
return [
...items,
...DEFAULT_QA_LAB_ITEMS.map((item) => ({ ...item, _uid: nextUid() })),
];
};