# Manual Project & Order Creation — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Let users manually create projects on `/projects` and orders without an offer on `/orders`, reusing the existing shared order/project numbering pool with correct tracking. **Architecture:** Backend re-introduces `createProject` (removed in `82919d3`) but takes the next _shared_ number via `generateSharedNumber(tx)` inside a transaction; the manual `createOrder` (already exists) gains an optional linked-project. Frontend adds inline create modals on the Projects and Orders list pages, mirroring the `Vehicles.tsx` `FormModal`/`FormField` pattern. `projects.create` permission is re-added via a migration + seed. **Tech Stack:** Fastify 5, Prisma 6 (MySQL), Zod 4, React 18 + TanStack Query v5, Vitest. **Spec:** `docs/superpowers/specs/2026-06-06-manual-project-order-creation-design.md` **Conventions to honor (from CLAUDE.md):** - Routes: `success()`/`error()`/`parseId()`/`parseBody()`; never raw `reply.send` for new code. - `AuthData.roleName` (admin bypasses permission checks). - Services return `{ data }`-ish or `{ error, status }`; own Prisma; routes stay thin. - Czech user-facing strings, English identifiers. - Broad query invalidation (`["projects"]`, not `["projects","list"]`). - Numbering: NEVER hardcode — always go through `numbering.service`. - Permissions/data changes ship as a **migration** (+ seed for dev), never seed-only. --- ## File Map | File | Change | | --------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `src/schemas/projects.schema.ts` | Add `CreateProjectSchema` + `CreateProjectInput` type | | `src/services/projects.service.ts` | Add `createProject`, `getNextProjectNumber`; extend imports | | `src/routes/admin/projects.ts` | Add `POST /`, `GET /next-number` | | `src/schemas/orders.schema.ts` | Add `create_project` to `CreateOrderSchema` | | `src/services/orders.service.ts` | `CreateOrderData.create_project`; create linked project in `createOrder` | | `src/routes/admin/orders.ts` | Audit the linked project on manual create | | `prisma/migrations/_add_projects_create_permission/migration.sql` | New (idempotent INSERT + grant) | | `prisma/seed.ts` | Add `projects.create` to `PERMISSIONS` | | `src/admin/lib/queries/projects.ts` | Add `projectNextNumberOptions` | | `src/admin/lib/queries/orders.ts` | Add `orderNextNumberOptions` | | `src/admin/pages/Projects.tsx` | Inline create modal, "Přidat projekt" button, Typ badge, empty-state copy | | `src/admin/pages/Orders.tsx` | Inline create modal, "Vytvořit objednávku" button | | `src/__tests__/manual-create.test.ts` | New (service-level numbering/tracking tests) | --- ## Task 1: `createProject` service + schema (TDD) **Files:** - Create test: `src/__tests__/manual-create.test.ts` - Modify: `src/schemas/projects.schema.ts` - Modify: `src/services/projects.service.ts:1-5` (imports) and add `createProject`/`getNextProjectNumber` - [ ] **Step 1: Write the failing test** — create `src/__tests__/manual-create.test.ts`: ```ts import { describe, it, expect, afterEach } from "vitest"; import prisma from "../config/database"; import { createProject } from "../services/projects.service"; const createdProjectIds: number[] = []; const createdOrderIds: number[] = []; afterEach(async () => { for (const id of createdProjectIds) await prisma.projects.deleteMany({ where: { id } }); for (const id of createdOrderIds) await prisma.orders.deleteMany({ where: { id } }); createdProjectIds.length = 0; createdOrderIds.length = 0; await prisma.number_sequences.deleteMany({ where: { type: "shared" } }); }); describe("createProject (manual, standalone)", () => { it("assigns a shared number and is not linked to an order", async () => { const result = await createProject({ name: "Test projekt", status: "aktivni", }); expect("project_number" in result).toBe(true); if (!("project_number" in result)) return; createdProjectIds.push(result.id); expect(typeof result.project_number).toBe("string"); expect(result.project_number!.length).toBeGreaterThan(0); expect(result.order_id).toBeNull(); }); it("gives consecutive standalone projects distinct numbers", async () => { const a = await createProject({ name: "Projekt A", status: "aktivni" }); const b = await createProject({ name: "Projekt B", status: "aktivni" }); if ("project_number" in a) createdProjectIds.push(a.id); if ("project_number" in b) createdProjectIds.push(b.id); expect("project_number" in a && "project_number" in b).toBe(true); if ("project_number" in a && "project_number" in b) { expect(a.project_number).not.toBe(b.project_number); } }); }); ``` - [ ] **Step 2: Run it and confirm it fails** Run: `npx vitest run src/__tests__/manual-create.test.ts` Expected: FAIL — `createProject` is not exported from `projects.service`. - [ ] **Step 3: Add `CreateProjectSchema`** to `src/schemas/projects.schema.ts` (above `CreateProjectNoteSchema`). Match the file's existing `z.object` style; add NaN guards on the new number coercions (CLAUDE.md) and treat `""` as null so an empty ``s populated from the queries; the number preview is read-only: ```tsx setShowCreate(false)} onSubmit={handleCreate} title="Přidat projekt" submitLabel="Vytvořit" loading={creating} >
{ setCreateForm({ ...createForm, name: e.target.value }); setCreateErrors((p) => ({ ...p, name: "" })); }} className="admin-form-input" placeholder="Název projektu" aria-invalid={!!createErrors.name} />
setCreateForm({ ...createForm, start_date: e.target.value }) } className="admin-form-input" /> setCreateForm({ ...createForm, end_date: e.target.value }) } className="admin-form-input" />