# Enterprise Scheduling Engine (ESE) — Integration & Operations Guide

**Product:** GB5 / MM Module  
**Service base URL:** `http://<host>:5001`  
**Audience:** Frontend developers, system admins, implementation consultants

---

## 1. What Is the ESE?

The Enterprise Scheduling Engine is a **finite-capacity production scheduling system** built into the GB5 Material Management microservice. It answers the question: *"Given all open production demand and all available machines with their shifts, which machine does each job run on, and exactly when?"*

Standard MRP (already in GB5) tells you **what to make and when it's needed**. The ESE tells you **which machine runs it, and exactly what time it starts and ends** — down to the minute. It respects shift windows, machine downtime, planned maintenance, tooling changeover, batch rules, and job dependencies.

**Output:** Rows in `TRESOURCEPLAN` — the single table read by shopfloor execution, Gantt charts, and OEE dashboards.

---

## 2. Key Concepts

| Term | Plain meaning |
|---|---|
| **Execution Unit (EU)** | One schedulable atom of work — typically one production indent detail row, one nesting plan, etc. |
| **Resource Plan** | A time-slot assigned to a machine: *Machine 12 runs Job 4455 from 08:00 to 14:30 on Tuesday.* |
| **ResourcePlanCombineId** | Links multiple resource plan rows that are parts of the same logical job (interruptible segments or parallel machines share one CombineId). |
| **Scheduling Run** | One complete engine execution. Each run logs to `TSCHEDULINGRUNLOG` and gets a unique `SchedulingRunId`. |
| **Horizon** | The date range a scheduling run covers (`HorizonFrom` → `HorizonTo`). Only demand with deadlines inside the horizon is scheduled. |
| **RCCP** | Rough-Cut Capacity Planning — a capacity sanity check run before scheduling. Warns (does not block) when a work centre is > 90% loaded within the horizon. |
| **Locked plan** | A resource plan row with `IsLocked=1`. Reschedule runs treat locked rows as immovable and schedule around them. |
| **Mount Task** | An auto-created maintenance task (in `TTASK`) representing the time needed to change the pattern/tooling on a machine before a job starts. |

---

## 3. System Flow Diagram

```
Admin/UI                Backend (MM microservice)              Shopfloor
   │                           │                                    │
   │── POST TriggerSchedulingRun ──►                                │
   │                     Dapr pub/sub                               │
   │                   (schedulingrun.trigger)                      │
   │                           │                                    │
   │◄── SignalR: ReceiveProgress ──┤  SchedulingEngine.RunAsync()   │
   │◄── SignalR: ReceiveWarning  ──┤  (RCCP, slot-finding, scoring) │
   │◄── SignalR: ReceiveComplete ──┤                                │
   │                           │                                    │
   │── GET GetResourcePlanByMachine ──►                             │
   │◄── schedule rows ────────────┤                                 │
   │                           │                                    │
   │── PUT LockResourcePlan ──────►                                 │
   │                           │                                    │
   │                    TRESOURCEPLAN ─────────────────────────────►│
   │                    (shopfloor reads)                           │
```

---

## 4. Prerequisites — Master Data Setup

Before the first scheduling run, an admin or implementation consultant must configure the following master data. These screens need to be built by the FE team.

### 4a. WorkCenter Shift Map

Tells the engine which shifts run on which days for each work centre.

**Endpoints:**

| Method | Route | Purpose |
|---|---|---|
| `GET` | `/Scheduling/GetWorkCenterShiftMap?ShiftMapId={id}` | Fetch one record |
| `GET` | `/Scheduling/GetWorkCenterShiftMapList?WorkCenterId={id}` | Fetch all shifts for a work centre |
| `POST` | `/Scheduling/SaveWorkCenterShiftMap` | Create or update |
| `DELETE` | `/Scheduling/DeleteWorkCenterShiftMap?ShiftMapId={id}` | Delete |

**Request body for Save (JSON):**

```json
{
  "ShiftMapId": 0,
  "WorkCenterId": 14,
  "ShiftId": 3,
  "DayOfWeek": 1,
  "EffectiveFrom": "2025-01-01",
  "EffectiveTo": null,
  "ShiftStartTime": 480,
  "ShiftEndTime": 1020,
  "ShiftWorkingMinutes": 480,
  "BreakStartTime": 720,
  "BreakEndTime": 750,
  "Status": 1,
  "TenantId": 1
}
```

**Field reference:**

| Field | Type | Notes |
|---|---|---|
| `ShiftMapId` | int | `0` = create new; non-zero = update existing |
| `WorkCenterId` | int | FK → MWORKCENTER |
| `ShiftId` | int | FK → MSHIFT |
| `DayOfWeek` | byte | `0`=Sun `1`=Mon `2`=Tue `3`=Wed `4`=Thu `5`=Fri `6`=Sat |
| `EffectiveFrom` | date | Start of validity |
| `EffectiveTo` | date\|null | End of validity; `null` = open-ended |
| `ShiftStartTime` | short | Minutes from midnight. `480` = 08:00 |
| `ShiftEndTime` | short | Minutes from midnight. `1020` = 17:00 |
| `ShiftWorkingMinutes` | short | Net working minutes (after breaks) |
| `BreakStartTime` | short | Minutes from midnight; `0` = no break |
| `BreakEndTime` | short | Minutes from midnight |
| `Status` | byte | `1`=Active `0`=Inactive |

> **Implementation note:** Every machine on a work centre shares the same shift map. The engine skips days/hours not covered by any active shift map row.

### 4b. Batch Rules

Controls how the engine groups multiple small jobs into one batch run (e.g., minimum oven load, preferred campaign size).

**Endpoints:**

| Method | Route | Purpose |
|---|---|---|
| `GET` | `/Scheduling/GetBatchRule?BatchRuleId={id}` | Fetch one record |
| `GET` | `/Scheduling/GetBatchRuleList?WorkCenterId={id}` | All rules for a work centre |
| `POST` | `/Scheduling/SaveBatchRule` | Create or update |
| `DELETE` | `/Scheduling/DeleteBatchRule?BatchRuleId={id}` | Delete |

**Request body for Save (JSON):**

```json
{
  "BatchRuleId": 0,
  "WorkCenterId": 14,
  "ProcessId": 7,
  "BatchBasisType": 0,
  "BatchMinimum": 50.0,
  "BatchMaximum": 200.0,
  "PreferredBatchQty": 150.0,
  "BatchContinuity": 0,
  "Status": 1,
  "TenantId": 1
}
```

**Enum values:**

| Field | Value | Meaning |
|---|---|---|
| `BatchBasisType` | `0` | Fixed quantity rule |
| | `1` | Fill to machine capacity |
| | `2` | Governed by a process parameter |
| | `3` | Fill to capacity (like `1` but allows overflow) |
| `BatchContinuity` | `0` | Reform — a new batch is created for each scheduling run |
| | `1` | CarryForward — the same batch number continues if the job spans multiple days |

### 4c. Routing Version Detail — New Fields

These fields are now on the existing Routing screen. Implementation team needs to add them to the routing version detail form.

| Field | Type | Default | Purpose |
|---|---|---|---|
| `IsInterruptible` | byte (0/1) | `0` | `1` = job can pause mid-run (e.g., overnight stops) |
| `MinimumRunMinutes` | int | `0` | Minimum continuous run before a pause is allowed (interruptible only) |
| `MaxWaitMinutes` | int | `0` | Max minutes a job can wait after predecessor ends (shelf-life); `0` = no limit |
| `ExecutionMode` | byte | `0` | `0`=InHouse `1`=Subcontract `2`=Either |
| `SubcontractLeadTimeDays` | short | `0` | Calendar days added as a time-block when subcontracting |
| `ReworkRoutingDetailId` | int\|null | `null` | Points to the alternate routing step for rework |
| `BatchContinuity` | byte | `0` | `0`=Reform `1`=CarryForward — mirrors batch rule |
| `MonitorGroup` | string\|null | `null` | Foundry use: groups monitor-only pour steps |
| `IsGroupOutputStep` | byte (0/1) | `0` | Foundry use: marks the step where pour quantity becomes countable |

### 4d. Work Centre — Max Concurrent Machines

Add `MaxConcurrentMachines` (short, default 1) to the existing Work Centre master screen. This tells the engine the maximum number of parallel machines that can run simultaneously in that work centre.

---

## 5. API Reference — Scheduling Operations

All requests require a `Login` header containing the serialised `LoginDTO` JSON (standard GB5 auth pattern).

### 5a. Trigger a Scheduling Run

```
POST /Scheduling/TriggerSchedulingRun
```

**Returns immediately** (HTTP 200) with a message "Scheduling run triggered." The actual run is asynchronous. Track progress via SignalR (Section 6).

**Request body:**

```json
{
  "WorkOUId": 5,
  "TenantId": 1,
  "UserId": 42,
  "HorizonFrom": "2025-06-01",
  "HorizonTo": "2025-06-30",
  "DemandSources": 129,
  "IsReschedule": false,
  "LockedPlanIds": []
}
```

**`DemandSources` bitmask** — OR the values together to include multiple source types:

| Value | Demand Type |
|---|---|
| `1` | Production Indents |
| `2` | Batch Plans |
| `4` | Process Groups |
| `8` | Work Plans |
| `16` | Rework Jobs |
| `32` | Adhoc Corrections |
| `64` | NPD Trials |
| `128` | Nesting Plans |
| `255` | All sources |

**Example:** `DemandSources: 129` = Production Indents (1) + Nesting Plans (128).

> **Why fire-and-forget?** A scheduling run for a full month across 20 machines and 500 jobs can take 30–120 seconds. The HTTP call returns immediately; the FE connects to SignalR to stream live progress.

---

### 5b. Get Scheduling Run Status (poll fallback)

```
GET /Scheduling/GetSchedulingRunStatus?SchedulingRunId={id}
```

Use this if SignalR is not available (e.g., server-side reporting). Returns a single `SchedulingRunLogDTO`.

**Response data:**

```json
{
  "SchedulingRunId": 1001,
  "TenantId": 1,
  "TriggeredByUserId": 42,
  "HorizonFromDate": "2025-06-01",
  "HorizonToDate": "2025-06-30",
  "MachinesScheduled": 12,
  "UtilizationPct": 78.45,
  "DurationMs": 4812,
  "Status": 2,
  "ErrorMessage": null,
  "RccpJson": "[...]",
  "StartedAt": "2025-05-07T09:00:00Z",
  "CompletedAt": "2025-05-07T09:00:04Z"
}
```

**`Status` values:**

| Value | Meaning |
|---|---|
| `0` | Pending (queued) |
| `1` | Running |
| `2` | Completed successfully |
| `3` | Failed — see `ErrorMessage` |

---

### 5c. Get All Scheduling Run History

```
GET /Scheduling/GetSchedulingRunLog
```

Returns all runs for the current tenant (cached at client level). Use this to populate a run history table/dropdown.

---

### 5d. Read the Resource Plan — By Indent

```
GET /Scheduling/GetResourcePlan?IndentDetailId={id}
```

Returns all resource plan rows for a given production indent detail. Use this to show the schedule for a specific job on the indent detail screen.

**Response data (array of):**

```json
{
  "ResourcePlanId": 500001,
  "IndentDetailId": 8834,
  "MachineId": 12,
  "WorkCenterId": 4,
  "ProcessId": 7,
  "PlannedQuantity": 150.0,
  "ScheduledStartDate": "2025-06-03T08:00:00Z",
  "ScheduledEndDate": "2025-06-03T14:30:00Z",
  "ResourcePlanCombineId": 10010000001,
  "ParallelNumber": 1,
  "IsInterruptible": 0,
  "SchedulingRunId": 1001,
  "IsLocked": 0,
  "Particulars": null,
  "PlanNature": 0,
  "ResourceLevel": 1,
  "MountingTaskId": null,
  "ResourceSequence": 0
}
```

**`ResourceLevel` values:**

| Value | Meaning |
|---|---|
| `0` | Monitor-only step (foundry pour — no machine assigned) |
| `1` | Machine |
| `2` | Pattern / CNC program (nesting plans) |
| `3` | Die |
| `6` | Subcontract (no physical machine; a time-block reservation) |
| `7` | Internal Transfer |

**`PlanNature` values:**

| Value | Meaning |
|---|---|
| `0` | Production |
| `1` | Rework |
| `2` | Trial / NPD |

**`Particulars` field:** Contains `"SHELF_LIFE_VIOLATION"` if the engine could not find a slot within the `MaxWaitMinutes` constraint. The job is still scheduled but flagged for planner review.

**`ResourcePlanCombineId`:** When a job is interruptible and spans multiple days, each segment gets its own row but all share the same `ResourcePlanCombineId`. When a job runs on parallel machines, all N machine rows share the same `ResourcePlanCombineId`.

**`MountingTaskId`:** When a pattern changeover is required before the job, this field is populated with the `TTASKID` of the auto-created maintenance task. The maintenance task blocks the machine's calendar during the changeover window.

---

### 5e. Read the Resource Plan — By Machine (Gantt Data)

```
GET /Scheduling/GetResourcePlanByMachine?MachineId={id}&DateFrom={date}&DateTo={date}
```

Returns all scheduled slots for a machine in a date range. **This is the primary data source for Gantt chart views.**

**Example:**

```
GET /Scheduling/GetResourcePlanByMachine?MachineId=12&DateFrom=2025-06-01&DateTo=2025-06-07
```

Returns array of the same `ResourcePlanInsertDTO` structure shown above.

**Gantt rendering guidance:**
- Each row = one bar on the Gantt
- Group by `ResourcePlanCombineId` to visually link segments of the same job
- Colour by `PlanNature`: Green=Production, Amber=Rework, Blue=Trial
- Highlight `Particulars = "SHELF_LIFE_VIOLATION"` rows in red with a warning icon
- Show `IsLocked` rows with a padlock icon; these cannot be moved by reschedule

---

### 5f. Lock a Resource Plan Row

```
PUT /Scheduling/LockResourcePlan?ResourcePlanId={id}
```

Sets `IsLocked=1` on the specified row. Locked rows survive reschedule operations — they act as hard constraints and the engine schedules other jobs around them.

**When to lock:** When a planner confirms that a specific job is committed to a machine at a specific time (e.g., raw material already ordered for that machine window, or a customer delivery date is pinned).

**Response:** Standard success response.

> There is no "unlock" endpoint in the current API. If needed, it can be added as `PUT /Scheduling/UnlockResourcePlan`.

---

### 5g. Reschedule a Horizon

```
POST /Scheduling/RescheduleHorizon
```

**Deletes all unlocked resource plan rows from `HorizonFrom` onwards** and re-runs the scheduling engine. Locked rows are preserved and treated as hard constraints.

**Request body:** Same structure as `TriggerSchedulingRun`:

```json
{
  "WorkOUId": 5,
  "TenantId": 1,
  "UserId": 42,
  "HorizonFrom": "2025-06-10",
  "HorizonTo": "2025-06-30",
  "DemandSources": 255,
  "IsReschedule": true,
  "LockedPlanIds": [500001, 500002]
}
```

> `HorizonFrom` must be **today or a future date**. The engine rejects past dates to prevent overwriting historical actuals.

**Warning:** Reschedule is **synchronous and blocking** (unlike TriggerSchedulingRun). The HTTP call waits until the run completes. Use for smaller horizons only; for full-month reschedules, use TriggerSchedulingRun with `IsReschedule=true` instead.

---

## 6. Real-Time Progress — SignalR Hub

**Hub URL:** `ws://<host>:5001/hubs/schedulingrun`

Connect immediately after calling `TriggerSchedulingRun`. The hub uses OU-scoped groups — you automatically receive events for your organisational unit only.

### 6a. Connection (JavaScript / TypeScript example)

```typescript
import * as signalR from "@microsoft/signalr";

const connection = new signalR.HubConnectionBuilder()
  .withUrl("http://<host>:5001/hubs/schedulingrun", {
    headers: { "Login": JSON.stringify(loginDTO) }
  })
  .withAutomaticReconnect()
  .build();

connection.on("ReceiveProgress", (percent: number, step: string, currentItem: string) => {
  console.log(`[${percent}%] ${step} — ${currentItem}`);
  // Update progress bar in UI
});

connection.on("ReceiveComplete", (schedulingRunId: number, summary: SchedulingRunSummaryDTO) => {
  console.log(`Run ${schedulingRunId} complete. ${summary.EUsScheduled} jobs scheduled.`);
  // Refresh Gantt, hide progress bar
});

connection.on("ReceiveWarning", (message: string) => {
  console.warn("RCCP Warning:", message);
  // Show banner: "Work Centre X is at 94% capacity"
});

connection.on("ReceiveError", (message: string) => {
  console.error("Scheduling failed:", message);
  // Show error dialog
});

await connection.start();
```

### 6b. Hub Events Reference

| Event | Parameters | When fired |
|---|---|---|
| `ReceiveProgress` | `percent` (0–100), `step` (string), `currentItem` (string) | Every 10 execution units processed during the main loop |
| `ReceiveComplete` | `schedulingRunId` (int), `summary` (object) | After bulk insert completes successfully |
| `ReceiveWarning` | `message` (string) | For each work centre exceeding 90% capacity (RCCP) |
| `ReceiveError` | `message` (string) | If the engine throws an unrecoverable error |

### 6c. ReceiveComplete — Summary Object Shape

```typescript
interface SchedulingRunSummaryDTO {
  SchedulingRunId: number;
  EUsScheduled: number;           // jobs successfully placed on the schedule
  EUsFailed: number;              // jobs with no available machine slot found
  UtilizationByWorkCenter: RCCPResultDTO[];
  DurationMs: number;
  RCCPWarnings: string[];         // work centre messages (also sent as ReceiveWarning)
}

interface RCCPResultDTO {
  WorkCenterId: number;
  WorkCenterName: string;
  CapacityMinutes: number;        // total available shift minutes in horizon
  RequiredMinutes: number;        // total job minutes needed
  OverloadPct: number;            // (RequiredMinutes / CapacityMinutes) × 100
  IsOverloaded: boolean;          // true when OverloadPct > 90
}
```

### 6d. Progress Percentage Milestones

| Range | Phase |
|---|---|
| 5% | Loading demand from DB |
| 10% | Demand loaded (EU count known) |
| 15% | Batch grouping |
| 20% | Dependency graph sort |
| 25% | RCCP capacity check |
| 25–95% | Main scheduling loop (proportional to EU count) |
| 98% | Bulk insert to DB |
| `ReceiveComplete` | Done |

---

## 7. Screen-by-Screen Guide for FE Team

### Screen 1 — Scheduling Setup (Admin)

**Purpose:** One-time configuration before the first run.

**Widgets needed:**

1. **Shift Map grid** per Work Centre: day of week × shift assignment, times, break window.  
   APIs: `GetWorkCenterShiftMapList` + `SaveWorkCenterShiftMap` + `DeleteWorkCenterShiftMap`

2. **Batch Rules grid** per Work Centre: process, min/max/preferred quantity.  
   APIs: `GetBatchRuleList` + `SaveBatchRule` + `DeleteBatchRule`

3. **Max Concurrent Machines** — add inline to the existing Work Centre master screen (existing save endpoint, new field).

4. **Routing Version Detail** — add the 9 new fields to the existing routing operation detail form (see Section 4c).

---

### Screen 2 — Trigger & Monitor

**Purpose:** Planner initiates a scheduling run and watches progress.

**Workflow:**
1. User selects `HorizonFrom`, `HorizonTo`, demand source checkboxes.
2. On "Run Schedule" click → `POST /Scheduling/TriggerSchedulingRun`.
3. Immediately show a progress dialog; connect to SignalR hub.
4. `ReceiveProgress` → update progress bar + step label.
5. `ReceiveWarning` → show RCCP warnings in a collapsible panel.
6. `ReceiveComplete` → hide progress dialog, show summary card (jobs scheduled, failed, duration, utilization table).
7. `ReceiveError` → show error alert with retry option.

**Suggested UI labels for `step` strings:**

| Step string from API | Suggested display |
|---|---|
| `"Loading demand"` | "Loading production demand..." |
| `"Demand loaded"` | `currentItem` contains "N execution units" |
| `"Grouping batches"` | "Grouping batch jobs..." |
| `"Building dependency graph"` | "Analysing job sequence..." |
| `"RCCP capacity check"` | "Checking work centre capacity..." |
| `"RCCP Warning"` | Show `currentItem` as a yellow inline badge |
| `"Scheduling"` | `currentItem` contains "EU N/M — doc {id}" |
| `"Finalising"` | "Writing schedule to database..." |

---

### Screen 3 — Gantt / Machine Schedule View

**Purpose:** Visual schedule board per machine or work centre.

**Data source:** `GET /Scheduling/GetResourcePlanByMachine`

**Rendering rules:**

| Condition | Visual treatment |
|---|---|
| `PlanNature=0` (Production) | Green bar |
| `PlanNature=1` (Rework) | Amber bar |
| `PlanNature=2` (Trial/NPD) | Blue bar |
| `ResourceLevel=6` (Subcontract) | Purple bar |
| `ResourceLevel=0` (Monitor-only) | Grey bar |
| `Particulars="SHELF_LIFE_VIOLATION"` | Red border + warning icon |
| `IsLocked=1` | Padlock icon overlay |
| Same `ResourcePlanCombineId` | Dashed connector or same tooltip group |

**Interactivity:**
- Click a bar → show indent/job details in a side panel
- Right-click bar → "Lock this slot" → `PUT /Scheduling/LockResourcePlan`
- Show `ParallelNumber` on bars that share a CombineId and have N > 1 (e.g., "Machine 2 of 3")

---

### Screen 4 — Indent Detail (Scheduling Tab)

**Purpose:** Show the schedule for one specific job from within the indent detail screen.

**Add a "Scheduling" tab** to the existing Indent Detail screen:
- Calls `GET /Scheduling/GetResourcePlan?IndentDetailId={id}`
- Shows a mini timeline or table: Machine, Start, End, Quantity, Status
- If `MountingTaskId` is populated → show "Pattern changeover required before this slot" banner
- If `Particulars = "SHELF_LIFE_VIOLATION"` → show a red warning chip
- If multiple rows share a `ResourcePlanCombineId` → group them as "segments" of one job

---

### Screen 5 — Run History

**Purpose:** Audit trail and troubleshooting.

**Data source:** `GET /Scheduling/GetSchedulingRunLog`

**Table columns:** Run ID, Triggered By, Horizon From, Horizon To, Status, Machines Scheduled, Utilization %, Duration, Started At, Completed At.

**Status badge colours:**

| Status | Value | Colour |
|---|---|---|
| Pending | `0` | Grey |
| Running | `1` | Blue (animated pulse) |
| Completed | `2` | Green |
| Failed | `3` | Red |

- Click a completed row → expand to show RCCP data (parse `RccpJson` — it is a JSON array of `RCCPResultDTO`)
- Click a failed row → show `ErrorMessage` in a modal

---

### Screen 6 — Reschedule

**Purpose:** Re-run scheduling after changes (new urgent jobs, machine breakdown, plan revision).

**UI flow:**
1. "Reschedule from date" date picker — block past dates in the picker (API also enforces this).
2. Demand source checkboxes.
3. Show a warning: *"All unlocked resource plan rows from [date] onwards will be cleared and rescheduled."*
4. Optionally show currently locked rows so planners can review before proceeding.
5. On confirm → `POST /Scheduling/RescheduleHorizon`.
6. This call is **synchronous** — show a full-screen loading overlay; do not allow navigation away.
7. On success response → close overlay, refresh Gantt.

---

## 8. Admin Guide

### Initial Setup Checklist

- [ ] Run `DB/Migrations/ESE_001_Schema.sql` on the target database
- [ ] Run `DB/Migrations/ESE_002_BatchRule_Schema.sql` on the target database
- [ ] Verify MM microservice is running: `GET /` → "Hello from GB5 MM in .NET 9 API!"
- [ ] Verify SignalR hub is reachable: `ws://<host>:5001/hubs/schedulingrun`
- [ ] Configure shift maps for all active work centres (at least Mon–Fri with one shift each)
- [ ] Verify routing version details have `ROUTINGVERSIONDETAILFIXEDLEADTIME` populated (used as default duration when `MANUALDURATIONMINUTES` is not set)
- [ ] Optionally configure batch rules for work centres with batching requirements (e.g., heat treatment ovens, autoclaves)

### Running the First Schedule

1. Start with a **narrow horizon** (1 week, 1 or 2 work centres).
2. Use `DemandSources=1` (Production Indents only) for the first test run.
3. Watch `RCCPWarnings` — if a work centre is over 90%, add shift capacity or reduce the demand horizon.
4. Check `EUsFailed` in `ReceiveComplete`. If > 0, open the Run History screen → expand RCCP JSON to identify bottlenecks.
5. Validate sample resource plan rows via `GetResourcePlanByMachine` before opening up the full horizon.

### Monitoring Production Runs

- `GetSchedulingRunLog` provides a full audit trail.
- `UtilizationPct` in the run log = average utilization across all scheduled machines.
- If `DurationMs` > 300,000 (5 minutes) for a standard run → investigate machine/shift map coverage gaps causing excessive slot-search iterations.

### Troubleshooting

| Symptom | Likely cause | Action |
|---|---|---|
| `EUsFailed > 0` | No machine/shift available in the horizon for those jobs | Check shift maps for the work centre. Check `MaxWaitMinutes` — may be too tight. Widen horizon or add shift coverage. |
| `ReceiveError: "Circular dependency detected"` | A routing version has a step that depends on a downstream step (cycle in predecessor chain) | Review `ROUTINGVERSIONDETAILPREDECESSOR` values for the affected item's routing |
| `ReceiveWarning` for most work centres | Horizon too wide vs. available capacity | Shorten horizon, stagger demand, or add machines/shifts |
| Run stuck at 25% for a long time | RCCP query is slow | Check indexes on `MMACHINE (WORKCENTERID, STATUS, TENANTID)` and `MWORKCENTERSHIFTMAP (WORKCENTERID, DAYOFWEEK, TENANTID)` |
| Reschedule rejected | `HorizonFrom` is in the past | Use today's date or a future date |
| Gantt shows no bars after a run | TRESOURCEPLAN insert may have failed silently | Check `TSCHEDULINGRUNLOG.ERRORMESSAGE` for the run; check DB transaction logs |
| Mount task not visible in Task screen | TTASK row created but screen filters by different TASKNATURE | TASKNATURE=2 rows are the auto-created changeover tasks; ensure the Task screen includes TASKNATURE=2 in its query |

---

## 9. Implementation Consultant Guide

### Database Migration Order

```
1. ESE_001_Schema.sql   (new tables + ALTERs on existing tables)
2. ESE_002_BatchRule_Schema.sql   (MBATCHRULE table)
```

Run both in sequence. ESE_002 has no dependency on ESE_001 but should always follow it for consistency.

### New Tables Created

| Table | Purpose |
|---|---|
| `TSCHEDULINGRUNLOG` | Run history — one row per engine execution |
| `TBATCHCHAIN` | Links batch groups across process steps |
| `MWORKCENTERSHIFTMAP` | Shift assignments per work centre per day |
| `MBATCHRULE` | Batch size rules per work centre / process combination |

### Existing Tables Extended (non-breaking ALTERs — all have DEFAULT values)

| Table | New columns added |
|---|---|
| `MROUTINGVERSIONDETAIL` | `ISINTERRUPTIBLE`, `MINIMUMRUNMINUTES`, `MAXWAITMINUTES`, `EXECUTIONMODE`, `SUBCONTRACTLEADTIMEDAYS`, `REWORKROUTINGDETAILID`, `BATCHCONTINUITY`, `MONITORGROUP`, `ISGROUPOUTPUTSTEP` |
| `TINDENTDETAIL` | `MANUALDURATIONMINUTES`, `MACHINEID_OVERRIDE`, `WORKCENTERID_OVERRIDE`, `MONITORGROUP`, `MONITORGROUPSEQUENCE` |
| `TINDENT` | `ISNPD`, `NPDROUTINGMODE` |
| `TINDENTMATERIALPART` | `SCHEDULESTATUS`, `SCHEDULEDSTARTDATE`, `SCHEDULEDENDDATE` |
| `TNESTINGPLAN` | `SCHEDULESTATUS`, `RESOURCEPLANCOMBINEID`, `SCHEDULEDSTARTDATE`, `SCHEDULEDENDDATE`, `CMSID` |
| `TNestingPlanSummary` | `DEPENDENCYTYPE`, `TRANSFERQUANTITY` |
| `MWORKCENTER` | `MAXCONCURRENTMACHINES` |
| `TRESOURCEPLAN` | `RESOURCEPLANCOMBINEID`, `PARALLELNUMBER`, `ISINTERRUPTIBLE`, `SCHEDULINGRUNID`, `ISLOCKED`, `PARTICULARS`, `PLANNATURE`, `RESOURCELEVEL`, `MOUNTINGTASKID`, `RESOURCESEQUENCE` |

### Dropped Table

`TMACHINEDOWNTIME` is **dropped** by ESE_001. Machine unavailability is now managed via:
- `TTASK` (TASKNATURE=2) for planned downtime
- `TCALL` for breakdown calls

If any existing screens, reports, or integrations read from `TMACHINEDOWNTIME`, they must be updated to query `TTASK WHERE TASKNATURE=2` instead.

### Post-Migration Verification SQL

```sql
-- Verify TRESOURCEPLAN has all new columns
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'TRESOURCEPLAN'
  AND COLUMN_NAME IN (
    'RESOURCEPLANCOMBINEID','ISLOCKED','SCHEDULINGRUNID',
    'PLANNATURE','RESOURCELEVEL','MOUNTINGTASKID'
  );
-- Expect 6 rows

-- Verify all new tables exist
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME IN (
  'TSCHEDULINGRUNLOG','TBATCHCHAIN','MWORKCENTERSHIFTMAP','MBATCHRULE'
);
-- Expect 4 rows

-- Verify existing indent data was not corrupted by the ALTER
SELECT COUNT(*) AS NullScheduleStatus
FROM TINDENTDETAIL
WHERE SCHEDULESTATUS IS NULL;
-- Should be 0 (column has DEFAULT 0)

-- Verify TMACHINEDOWNTIME is gone
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'TMACHINEDOWNTIME';
-- Should be 0
```

---

## 10. Quick Reference Card

### All Scheduling Endpoints

| Method | Route | Description |
|---|---|---|
| `POST` | `/Scheduling/TriggerSchedulingRun` | Trigger async run (returns immediately) |
| `POST` | `/Scheduling/RescheduleHorizon` | Delete unlocked + re-run (synchronous) |
| `GET` | `/Scheduling/GetSchedulingRunStatus?SchedulingRunId=` | Status of one run |
| `GET` | `/Scheduling/GetSchedulingRunLog` | Full run history list |
| `GET` | `/Scheduling/GetResourcePlan?IndentDetailId=` | Schedule for one job |
| `GET` | `/Scheduling/GetResourcePlanByMachine?MachineId=&DateFrom=&DateTo=` | Gantt data by machine |
| `PUT` | `/Scheduling/LockResourcePlan?ResourcePlanId=` | Lock a scheduled slot |
| `GET` | `/Scheduling/GetWorkCenterShiftMap?ShiftMapId=` | Single shift map record |
| `GET` | `/Scheduling/GetWorkCenterShiftMapList?WorkCenterId=` | All shifts for a work centre |
| `POST` | `/Scheduling/SaveWorkCenterShiftMap` | Create or update shift map |
| `DELETE` | `/Scheduling/DeleteWorkCenterShiftMap?ShiftMapId=` | Delete shift map |
| `GET` | `/Scheduling/GetBatchRule?BatchRuleId=` | Single batch rule |
| `GET` | `/Scheduling/GetBatchRuleList?WorkCenterId=` | All rules for a work centre |
| `POST` | `/Scheduling/SaveBatchRule` | Create or update batch rule |
| `DELETE` | `/Scheduling/DeleteBatchRule?BatchRuleId=` | Delete batch rule |

### SignalR Hub

| Item | Value |
|---|---|
| Hub URL | `ws://<host>:5001/hubs/schedulingrun` |
| Group scope | Per `WorkOUId` — events received only for your organisational unit |
| Auth | `Login` JSON header on the WebSocket upgrade request |
| Events received | `ReceiveProgress`, `ReceiveComplete`, `ReceiveWarning`, `ReceiveError` |

### Key Enum Reference

| Enum | Value | Meaning |
|---|---|---|
| **Run Status** | 0 | Pending |
| | 1 | Running |
| | 2 | Completed |
| | 3 | Failed |
| **ResourceLevel** | 0 | Monitor-only (foundry) |
| | 1 | Machine |
| | 2 | Pattern / CNC |
| | 3 | Die |
| | 6 | Subcontract |
| | 7 | Internal Transfer |
| **PlanNature** | 0 | Production |
| | 1 | Rework |
| | 2 | Trial / NPD |
| **ExecutionMode** | 0 | In-house |
| | 1 | Subcontract |
| | 2 | Either |
| **BatchContinuity** | 0 | Reform |
| | 1 | CarryForward |
| **DayOfWeek** | 0–6 | Sunday–Saturday |
