Add order editing and qualified downtime history
This commit is contained in:
@@ -18,6 +18,7 @@ from .schemas import (
|
|||||||
MachineConfigUpdate,
|
MachineConfigUpdate,
|
||||||
MachineEventIn,
|
MachineEventIn,
|
||||||
ProductionOrderCreate,
|
ProductionOrderCreate,
|
||||||
|
ProductionOrderUpdate,
|
||||||
ScrapCreate,
|
ScrapCreate,
|
||||||
SimulatorEventCreate,
|
SimulatorEventCreate,
|
||||||
SimulatorScenarioCreate,
|
SimulatorScenarioCreate,
|
||||||
@@ -466,24 +467,27 @@ def get_machine_downtimes(machine_id: int, limit: int = Query(default=100, le=50
|
|||||||
@app.get("/api/production-orders")
|
@app.get("/api/production-orders")
|
||||||
def list_production_orders(db: Session = Depends(get_db)) -> list[dict]:
|
def list_production_orders(db: Session = Depends(get_db)) -> list[dict]:
|
||||||
rows = db.execute(select(ProductionOrder).order_by(ProductionOrder.created_at.desc())).scalars()
|
rows = db.execute(select(ProductionOrder).order_by(ProductionOrder.created_at.desc())).scalars()
|
||||||
return [
|
return [_serialize_production_order(db, row) for row in rows]
|
||||||
{
|
|
||||||
"id": row.id,
|
|
||||||
"order_number": row.order_number,
|
def _serialize_production_order(db: Session, row: ProductionOrder) -> dict:
|
||||||
"machine_id": row.machine_id,
|
machine = db.get(Machine, row.machine_id)
|
||||||
"article_ref": row.article_ref,
|
return {
|
||||||
"article_name": row.article_name,
|
"id": row.id,
|
||||||
"mold_ref": row.mold_ref,
|
"order_number": row.order_number,
|
||||||
"material_ref": row.material_ref,
|
"machine_id": row.machine_id,
|
||||||
"planned_qty": row.planned_qty,
|
"machine_code": machine.code if machine else None,
|
||||||
"cavities": row.cavities,
|
"article_ref": row.article_ref,
|
||||||
"theoretical_cycle_time_sec": row.theoretical_cycle_time_sec,
|
"article_name": row.article_name,
|
||||||
"status": row.status,
|
"mold_ref": row.mold_ref,
|
||||||
"started_at": row.started_at.isoformat() if row.started_at else None,
|
"material_ref": row.material_ref,
|
||||||
"ended_at": row.ended_at.isoformat() if row.ended_at else None,
|
"planned_qty": row.planned_qty,
|
||||||
}
|
"cavities": row.cavities,
|
||||||
for row in rows
|
"theoretical_cycle_time_sec": row.theoretical_cycle_time_sec,
|
||||||
]
|
"status": row.status,
|
||||||
|
"started_at": row.started_at.isoformat() if row.started_at else None,
|
||||||
|
"ended_at": row.ended_at.isoformat() if row.ended_at else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/production-orders")
|
@app.post("/api/production-orders")
|
||||||
@@ -503,6 +507,36 @@ def _find_order_or_404(db: Session, order_id: int) -> ProductionOrder:
|
|||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/api/production-orders/{order_id}")
|
||||||
|
def update_production_order(order_id: int, payload: ProductionOrderUpdate, db: Session = Depends(get_db)) -> dict:
|
||||||
|
order = _find_order_or_404(db, order_id)
|
||||||
|
updates = payload.model_dump(exclude_unset=True)
|
||||||
|
if not updates:
|
||||||
|
return _serialize_production_order(db, order)
|
||||||
|
|
||||||
|
if "order_number" in updates and updates["order_number"] != order.order_number:
|
||||||
|
existing = db.execute(select(ProductionOrder).where(ProductionOrder.order_number == updates["order_number"])).scalar_one_or_none()
|
||||||
|
if existing is not None:
|
||||||
|
raise HTTPException(status_code=400, detail="Production order number already exists")
|
||||||
|
|
||||||
|
if "machine_id" in updates and updates["machine_id"] != order.machine_id:
|
||||||
|
target_machine = db.get(Machine, updates["machine_id"])
|
||||||
|
if target_machine is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Machine not found")
|
||||||
|
if order.status == "running":
|
||||||
|
running_order = get_running_order(db, updates["machine_id"])
|
||||||
|
if running_order and running_order.id != order.id:
|
||||||
|
raise HTTPException(status_code=400, detail="Another order is already running on the target machine")
|
||||||
|
|
||||||
|
for field, value in updates.items():
|
||||||
|
setattr(order, field, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(order)
|
||||||
|
hub.notify({"type": "refresh", "source": "api", "machine_id": order.machine_id, "event_type": "production_order_updated"})
|
||||||
|
return _serialize_production_order(db, order)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/production-orders/{order_id}/start")
|
@app.post("/api/production-orders/{order_id}/start")
|
||||||
def start_production_order(order_id: int, db: Session = Depends(get_db)) -> dict:
|
def start_production_order(order_id: int, db: Session = Depends(get_db)) -> dict:
|
||||||
order = _find_order_or_404(db, order_id)
|
order = _find_order_or_404(db, order_id)
|
||||||
@@ -565,6 +599,14 @@ def get_unqualified_downtimes(db: Session = Depends(get_db)) -> list[dict]:
|
|||||||
return [_serialize_downtime_for_qualification(db, row) for row in rows]
|
return [_serialize_downtime_for_qualification(db, row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/downtimes/qualified")
|
||||||
|
def get_qualified_downtimes(limit: int = Query(default=200, le=500), db: Session = Depends(get_db)) -> list[dict]:
|
||||||
|
rows = db.execute(
|
||||||
|
select(Downtime).where(Downtime.reason_code.is_not(None)).order_by(Downtime.start_time.desc()).limit(limit)
|
||||||
|
).scalars()
|
||||||
|
return [_serialize_downtime_for_qualification(db, row) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def _serialize_downtime_for_qualification(db: Session, row: Downtime) -> dict:
|
def _serialize_downtime_for_qualification(db: Session, row: Downtime) -> dict:
|
||||||
machine = db.get(Machine, row.machine_id)
|
machine = db.get(Machine, row.machine_id)
|
||||||
order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None
|
order = db.get(ProductionOrder, row.production_order_id) if row.production_order_id else None
|
||||||
|
|||||||
@@ -27,6 +27,18 @@ class ProductionOrderCreate(BaseModel):
|
|||||||
theoretical_cycle_time_sec: float = Field(gt=0)
|
theoretical_cycle_time_sec: float = Field(gt=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionOrderUpdate(BaseModel):
|
||||||
|
order_number: str | None = None
|
||||||
|
machine_id: int | None = None
|
||||||
|
article_ref: str | None = None
|
||||||
|
article_name: str | None = None
|
||||||
|
mold_ref: str | None = None
|
||||||
|
material_ref: str | None = None
|
||||||
|
planned_qty: int | None = Field(default=None, gt=0)
|
||||||
|
cavities: int | None = Field(default=None, gt=0)
|
||||||
|
theoretical_cycle_time_sec: float | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class DowntimeQualify(BaseModel):
|
class DowntimeQualify(BaseModel):
|
||||||
reason_code: str
|
reason_code: str
|
||||||
comment: str | None = None
|
comment: str | None = None
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ export default function App() {
|
|||||||
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
const [dashboard, setDashboard] = useState<DashboardSnapshot | null>(null);
|
||||||
const [orders, setOrders] = useState<ProductionOrder[]>([]);
|
const [orders, setOrders] = useState<ProductionOrder[]>([]);
|
||||||
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
|
const [unqualifiedDowntimes, setUnqualifiedDowntimes] = useState<Downtime[]>([]);
|
||||||
|
const [qualifiedDowntimes, setQualifiedDowntimes] = useState<Downtime[]>([]);
|
||||||
const [scraps, setScraps] = useState<Scrap[]>([]);
|
const [scraps, setScraps] = useState<Scrap[]>([]);
|
||||||
const [references, setReferences] = useState<ReferenceData | null>(null);
|
const [references, setReferences] = useState<ReferenceData | null>(null);
|
||||||
const [dailyOee, setDailyOee] = useState<DailyOee | null>(null);
|
const [dailyOee, setDailyOee] = useState<DailyOee | null>(null);
|
||||||
const [selectedMachineId, setSelectedMachineId] = useState<number | null>(null);
|
const [selectedMachineId, setSelectedMachineId] = useState<number | null>(null);
|
||||||
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
||||||
const [activeModule, setActiveModule] = useState<ModuleKey>("atelier");
|
const [activeModule, setActiveModule] = useState<ModuleKey>("atelier");
|
||||||
const [machineFilter, setMachineFilter] = useState("all");
|
const [machineFilter, setMachineFilter] = useState("all");
|
||||||
const [machineSort, setMachineSort] = useState("code");
|
const [machineSort, setMachineSort] = useState("code");
|
||||||
@@ -78,16 +80,19 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCollections() {
|
async function loadCollections() {
|
||||||
const [loadedOrders, loadedUnqualifiedDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([
|
const [loadedOrders, loadedUnqualifiedDowntimes, loadedQualifiedDowntimes, loadedScraps, loadedReferences, loadedDailyOee] = await Promise.all([
|
||||||
fetchJson<ProductionOrder[]>("/api/production-orders"),
|
fetchJson<ProductionOrder[]>("/api/production-orders"),
|
||||||
fetchJson<Downtime[]>("/api/downtimes/unqualified"),
|
fetchJson<Downtime[]>("/api/downtimes/unqualified"),
|
||||||
|
fetchJson<Downtime[]>("/api/downtimes/qualified"),
|
||||||
fetchJson<Scrap[]>("/api/scraps"),
|
fetchJson<Scrap[]>("/api/scraps"),
|
||||||
fetchJson<ReferenceData>("/api/reference-data"),
|
fetchJson<ReferenceData>("/api/reference-data"),
|
||||||
fetchJson<DailyOee>(`/api/oee/daily?date=${today}`),
|
fetchJson<DailyOee>(`/api/oee/daily?date=${today}`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setOrders(loadedOrders);
|
setOrders(loadedOrders);
|
||||||
|
setSelectedOrderId((current) => (current && loadedOrders.some((order) => order.id === current) ? current : loadedOrders[0]?.id ?? null));
|
||||||
setUnqualifiedDowntimes(loadedUnqualifiedDowntimes);
|
setUnqualifiedDowntimes(loadedUnqualifiedDowntimes);
|
||||||
|
setQualifiedDowntimes(loadedQualifiedDowntimes);
|
||||||
setScraps(loadedScraps);
|
setScraps(loadedScraps);
|
||||||
setReferences(loadedReferences);
|
setReferences(loadedReferences);
|
||||||
setDailyOee(loadedDailyOee);
|
setDailyOee(loadedDailyOee);
|
||||||
@@ -192,6 +197,7 @@ export default function App() {
|
|||||||
|
|
||||||
const selectedMachine = dashboard?.machines.find((machine) => machine.id === selectedMachineId) ?? null;
|
const selectedMachine = dashboard?.machines.find((machine) => machine.id === selectedMachineId) ?? null;
|
||||||
const machines = dashboard?.machines ?? [];
|
const machines = dashboard?.machines ?? [];
|
||||||
|
const selectedOrder = orders.find((order) => order.id === selectedOrderId) ?? null;
|
||||||
const openDowntimeCount = unqualifiedDowntimes.filter((downtime) => downtime.end_time === null).length;
|
const openDowntimeCount = unqualifiedDowntimes.filter((downtime) => downtime.end_time === null).length;
|
||||||
const selectedMachineUnqualifiedDowntimes =
|
const selectedMachineUnqualifiedDowntimes =
|
||||||
selectedMachineId === null ? [] : unqualifiedDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId);
|
selectedMachineId === null ? [] : unqualifiedDowntimes.filter((downtime) => downtime.machine_id === selectedMachineId);
|
||||||
@@ -270,7 +276,7 @@ export default function App() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setActionErrors((current) => ({ ...current, order: null }));
|
setActionErrors((current) => ({ ...current, order: null }));
|
||||||
await postJson("/api/production-orders", {
|
const createdOrder = await postJson<{ id: number; status: string }>("/api/production-orders", {
|
||||||
order_number: formData.get("order_number"),
|
order_number: formData.get("order_number"),
|
||||||
machine_id: Number(formData.get("machine_id")),
|
machine_id: Number(formData.get("machine_id")),
|
||||||
article_ref: formData.get("article_ref"),
|
article_ref: formData.get("article_ref"),
|
||||||
@@ -283,6 +289,7 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
setSelectedOrderId(createdOrder.id);
|
||||||
setFormErrors((current) => ({ ...current, order: {} }));
|
setFormErrors((current) => ({ ...current, order: {} }));
|
||||||
showToast("order", "Production order created.");
|
showToast("order", "Production order created.");
|
||||||
await loadAll();
|
await loadAll();
|
||||||
@@ -291,6 +298,40 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitOrderUpdate(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!selectedOrder) {
|
||||||
|
setActionErrors((current) => ({ ...current, order: "Select an OF before updating." }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const form = event.currentTarget;
|
||||||
|
if (!validateForm(form, "order")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setActionErrors((current) => ({ ...current, order: null }));
|
||||||
|
await patchJson(`/api/production-orders/${selectedOrder.id}`, {
|
||||||
|
order_number: formData.get("order_number"),
|
||||||
|
machine_id: Number(formData.get("machine_id")),
|
||||||
|
article_ref: formData.get("article_ref"),
|
||||||
|
article_name: formData.get("article_name"),
|
||||||
|
mold_ref: formData.get("mold_ref"),
|
||||||
|
material_ref: formData.get("material_ref"),
|
||||||
|
planned_qty: Number(formData.get("planned_qty")),
|
||||||
|
cavities: Number(formData.get("cavities")),
|
||||||
|
theoretical_cycle_time_sec: Number(formData.get("theoretical_cycle_time_sec")),
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormErrors((current) => ({ ...current, order: {} }));
|
||||||
|
showToast("order", "Production order updated.");
|
||||||
|
await loadAll();
|
||||||
|
} catch (error) {
|
||||||
|
setActionErrors((current) => ({ ...current, order: error instanceof Error ? error.message : "Order update failed." }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitDowntimeQualification(event: FormEvent<HTMLFormElement>) {
|
async function submitDowntimeQualification(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = event.currentTarget;
|
const form = event.currentTarget;
|
||||||
@@ -442,6 +483,30 @@ export default function App() {
|
|||||||
return downtime.end_time === null ? "Open - waiting qualification" : "Ended - waiting qualification";
|
return downtime.end_time === null ? "Open - waiting qualification" : "Ended - waiting qualification";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDurationSeconds(durationSec: number | null) {
|
||||||
|
if (durationSec === null) {
|
||||||
|
return "In progress";
|
||||||
|
}
|
||||||
|
if (durationSec < 60) {
|
||||||
|
return `${durationSec} s`;
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(durationSec / 60);
|
||||||
|
const seconds = durationSec % 60;
|
||||||
|
if (minutes < 60) {
|
||||||
|
return seconds > 0 ? `${minutes} min ${seconds} s` : `${minutes} min`;
|
||||||
|
}
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainingMinutes = minutes % 60;
|
||||||
|
return remainingMinutes > 0 ? `${hours} h ${remainingMinutes} min` : `${hours} h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderMachineLabel(order: ProductionOrder) {
|
||||||
|
if (order.machine_code) {
|
||||||
|
return order.machine_code;
|
||||||
|
}
|
||||||
|
return machines.find((machine) => machine.id === order.machine_id)?.code ?? `Machine ${order.machine_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderModule(): ReactNode {
|
function renderModule(): ReactNode {
|
||||||
if (activeModule === "atelier") {
|
if (activeModule === "atelier") {
|
||||||
return (
|
return (
|
||||||
@@ -590,7 +655,7 @@ export default function App() {
|
|||||||
|
|
||||||
if (activeModule === "orders") {
|
if (activeModule === "orders") {
|
||||||
return (
|
return (
|
||||||
<Panel title="Ordres de Fabrication" subtitle="Create and manage production orders">
|
<Panel title="Ordres de Fabrication" subtitle={selectedOrder ? `Selected ${selectedOrder.order_number}` : "Create and manage production orders"}>
|
||||||
<div className="module-grid module-grid--form-table">
|
<div className="module-grid module-grid--form-table">
|
||||||
<form
|
<form
|
||||||
className="form-grid module-form"
|
className="form-grid module-form"
|
||||||
@@ -639,16 +704,22 @@ export default function App() {
|
|||||||
<th>OF</th>
|
<th>OF</th>
|
||||||
<th>Machine</th>
|
<th>Machine</th>
|
||||||
<th>Article</th>
|
<th>Article</th>
|
||||||
|
<th>Planned</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<tr key={order.id}>
|
<tr key={order.id} className={order.id === selectedOrderId ? "is-selected-row" : ""}>
|
||||||
<td>{order.order_number}</td>
|
<td>
|
||||||
<td>{order.machine_id}</td>
|
<button type="button" className="link-button" onClick={() => setSelectedOrderId(order.id)}>
|
||||||
|
{order.order_number}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>{orderMachineLabel(order)}</td>
|
||||||
<td>{order.article_name}</td>
|
<td>{order.article_name}</td>
|
||||||
|
<td>{order.planned_qty}</td>
|
||||||
<td>{order.status}</td>
|
<td>{order.status}</td>
|
||||||
<td className="table-actions">
|
<td className="table-actions">
|
||||||
<button type="button" onClick={() => changeOrderStatus(order.id, "start")}>
|
<button type="button" onClick={() => changeOrderStatus(order.id, "start")}>
|
||||||
@@ -666,6 +737,79 @@ export default function App() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedOrder ? (
|
||||||
|
<section className="order-detail">
|
||||||
|
<div className="order-detail__header">
|
||||||
|
<div>
|
||||||
|
<span className="meta-label">OF detail</span>
|
||||||
|
<h3>{selectedOrder.order_number}</h3>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={selectedOrder.status} />
|
||||||
|
</div>
|
||||||
|
<div className="order-detail__meta">
|
||||||
|
<div>
|
||||||
|
<span className="meta-label">Machine</span>
|
||||||
|
<strong>{orderMachineLabel(selectedOrder)}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="meta-label">Article</span>
|
||||||
|
<strong>{selectedOrder.article_ref} / {selectedOrder.article_name}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="meta-label">Cycle / cavities</span>
|
||||||
|
<strong>{selectedOrder.theoretical_cycle_time_sec} s / {selectedOrder.cavities}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="meta-label">Dates</span>
|
||||||
|
<strong>
|
||||||
|
{selectedOrder.started_at ? new Date(selectedOrder.started_at).toLocaleString() : "Not started"}
|
||||||
|
{selectedOrder.ended_at ? ` -> ${new Date(selectedOrder.ended_at).toLocaleString()}` : ""}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="form-grid config-form"
|
||||||
|
key={selectedOrder.id}
|
||||||
|
onSubmit={submitOrderUpdate}
|
||||||
|
onBlurCapture={(event) => handleFormBlur("order", event.currentTarget)}
|
||||||
|
onInputCapture={(event) => validateForm(event.currentTarget, "order")}
|
||||||
|
onChangeCapture={(event) => validateForm(event.currentTarget, "order")}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<input name="order_number" defaultValue={selectedOrder.order_number} placeholder="OF number" required />
|
||||||
|
<select name="machine_id" required defaultValue={selectedOrder.machine_id}>
|
||||||
|
{machines.map((machine) => (
|
||||||
|
<option key={machine.id} value={machine.id}>
|
||||||
|
{machine.code}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input name="article_ref" defaultValue={selectedOrder.article_ref} placeholder="Article ref" required />
|
||||||
|
<input name="article_name" defaultValue={selectedOrder.article_name} placeholder="Article name" required />
|
||||||
|
<input name="mold_ref" defaultValue={selectedOrder.mold_ref ?? ""} placeholder="Mold ref" />
|
||||||
|
<input name="material_ref" defaultValue={selectedOrder.material_ref ?? ""} placeholder="Material ref" />
|
||||||
|
<input name="planned_qty" type="number" min="1" defaultValue={selectedOrder.planned_qty} placeholder="Planned qty" required />
|
||||||
|
<input name="cavities" type="number" min="1" defaultValue={selectedOrder.cavities} placeholder="Cavities" required />
|
||||||
|
<input
|
||||||
|
name="theoretical_cycle_time_sec"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="0.1"
|
||||||
|
defaultValue={selectedOrder.theoretical_cycle_time_sec}
|
||||||
|
placeholder="Theoretical cycle"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" className="primary-button" disabled={Object.keys(formErrors.order).length > 0}>
|
||||||
|
Update OF
|
||||||
|
</button>
|
||||||
|
{actionError("order")}
|
||||||
|
{toast("order")}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">Select an OF to see and update details.</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
@@ -763,6 +907,32 @@ export default function App() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<span className="meta-label">Qualified stops history</span>
|
||||||
|
<ul className="timeline-list timeline-list--qualified">
|
||||||
|
{qualifiedDowntimes.map((item) => (
|
||||||
|
<li key={item.id} className={item.machine_id === selectedMachineId ? "is-current-machine" : ""}>
|
||||||
|
<div>
|
||||||
|
<strong>
|
||||||
|
{downtimeMachineLabel(item)} / {item.reason_code}
|
||||||
|
</strong>
|
||||||
|
<span>
|
||||||
|
{item.order_number ? `${item.order_number} / ` : ""}
|
||||||
|
{item.qualified_by ? `By ${item.qualified_by} / ` : ""}
|
||||||
|
{formatDurationSeconds(item.duration_sec)}
|
||||||
|
{item.comment ? ` / ${item.comment}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<time>
|
||||||
|
{new Date(item.start_time).toLocaleString()}
|
||||||
|
{item.end_time ? ` -> ${new Date(item.end_time).toLocaleString()}` : ""}
|
||||||
|
</time>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{qualifiedDowntimes.length === 0 ? <div className="empty-state">No qualified downtime history yet.</div> : null}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{unqualifiedDowntimes.length === 0 ? <div className="empty-state">No downtime currently needs qualification.</div> : null}
|
{unqualifiedDowntimes.length === 0 ? <div className="empty-state">No downtime currently needs qualification.</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export type ProductionOrder = {
|
|||||||
id: number;
|
id: number;
|
||||||
order_number: string;
|
order_number: string;
|
||||||
machine_id: number;
|
machine_id: number;
|
||||||
|
machine_code?: string | null;
|
||||||
article_ref: string;
|
article_ref: string;
|
||||||
article_name: string;
|
article_name: string;
|
||||||
mold_ref: string | null;
|
mold_ref: string | null;
|
||||||
|
|||||||
@@ -562,6 +562,22 @@ th {
|
|||||||
background: var(--status-off-plan);
|
background: var(--status-off-plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-badge--planned {
|
||||||
|
background: var(--status-off-plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--running {
|
||||||
|
background: var(--status-production);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--paused {
|
||||||
|
background: var(--status-unqualified);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge--closed {
|
||||||
|
background: var(--status-qualified);
|
||||||
|
}
|
||||||
|
|
||||||
.detail-stack {
|
.detail-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -765,6 +781,10 @@ th {
|
|||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeline-list--qualified li::before {
|
||||||
|
background: var(--status-production);
|
||||||
|
}
|
||||||
|
|
||||||
.downtime-board {
|
.downtime-board {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -907,6 +927,41 @@ table button {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-detail {
|
||||||
|
display: grid;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-panel-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail__header h3 {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail__meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-detail__meta strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@@ -923,6 +978,22 @@ td {
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr.is-selected-row td {
|
||||||
|
background: var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
min-height: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--teal-900);
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.table-actions {
|
.table-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -981,6 +1052,7 @@ td {
|
|||||||
.oee-grid,
|
.oee-grid,
|
||||||
.machine-context,
|
.machine-context,
|
||||||
.detail-columns,
|
.detail-columns,
|
||||||
|
.order-detail__meta,
|
||||||
.config-form {
|
.config-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user