.
This commit is contained in:
parent
d8f2407ddf
commit
9689bfcc18
1 changed files with 1815 additions and 955 deletions
|
|
@ -5,6 +5,7 @@ import { downloadCsv } from '../lib/export';
|
|||
import {
|
||||
flooringCuts,
|
||||
groupFlooringCuts,
|
||||
groupSkirtingCuts,
|
||||
skirtingCuts,
|
||||
uncutFlooring,
|
||||
uncutSkirting,
|
||||
|
|
@ -12,32 +13,6 @@ import {
|
|||
import { FlooringPreview, SkirtingPreview } from './CutPreview';
|
||||
import { CutDialog } from './CutDialog';
|
||||
|
||||
function StockAllocations({
|
||||
title,
|
||||
items,
|
||||
}: {
|
||||
title: string;
|
||||
items: { id: string; pieces: string[]; size: string; remainder: string }[];
|
||||
}) {
|
||||
if (!items.length) return null;
|
||||
return (
|
||||
<details className="stock-allocations">
|
||||
<summary>{title}</summary>
|
||||
<ul className="allocation-list">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="stock-card">
|
||||
<strong>
|
||||
{item.id} <span>· {item.size}</span>
|
||||
</strong>
|
||||
<span>{item.pieces.join(' + ')}</span>
|
||||
<small>{item.remainder}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
project: Project;
|
||||
flooring: FlooringPlan;
|
||||
|
|
@ -49,29 +24,23 @@ interface Props {
|
|||
export function CutList({ project, flooring, skirting, selectedPiece, select }: Props) {
|
||||
const floorCuts = flooringCuts(flooring),
|
||||
skirtCuts = skirtingCuts(skirting);
|
||||
const floorGroups = groupFlooringCuts(flooring);
|
||||
const floorIds = new Set(floorCuts.map((piece) => piece.id));
|
||||
const skirtIds = new Set(skirtCuts.map((piece) => piece.id));
|
||||
const wholeFloor = uncutFlooring(flooring, project.flooring);
|
||||
const wholeSkirt = uncutSkirting(skirting);
|
||||
const selectedFloor = floorCuts.find((piece) => piece.id === selectedPiece);
|
||||
const selectedSkirt = skirtCuts.find((piece) => piece.id === selectedPiece);
|
||||
const roomName = (id: string) => project.rooms.find((room) => room.id === id)?.name ?? id;
|
||||
const errors = [...flooring.issues, ...skirting.issues].some(
|
||||
(issue) => issue.severity === 'error',
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="cut-list-card" aria-labelledby="cut-plan-title">
|
||||
<div className="cut-list-header">
|
||||
<div>
|
||||
<h2 id="cut-plan-title">Cut plan</h2>
|
||||
<p className="helper">
|
||||
Cut pieces and offcut placements. All measurements are in millimetres.
|
||||
Each group comes from one board. Select a piece for its cutting diagram.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="button secondary small"
|
||||
className="button"
|
||||
onClick={() => downloadCsv(project, flooring, skirting)}
|
||||
disabled={errors || (!flooring.pieces.length && !skirting.pieces.length)}
|
||||
>
|
||||
|
|
@ -79,186 +48,138 @@ export function CutList({ project, flooring, skirting, selectedPiece, select }:
|
|||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="material-cuts flooring-cuts" aria-labelledby="flooring-cuts-title">
|
||||
<h3 id="flooring-cuts-title">
|
||||
Flooring cuts <span className="count-badge">{floorCuts.length}</span>
|
||||
</h3>
|
||||
<p className="cut-summary">
|
||||
Buy {flooring.boardCount} boards · {flooring.pieces.length} installed pieces ·{' '}
|
||||
{wholeFloor.wholeBoards} full boards laid uncut
|
||||
Buy {flooring.boardCount} boards · {wholeFloor.wholeBoards} laid uncut
|
||||
{wholeFloor.readyOffcuts > 0 &&
|
||||
` · ${wholeFloor.readyOffcuts} offcuts need no further cuts`}
|
||||
.
|
||||
</p>
|
||||
{floorCuts.length ? (
|
||||
<div className="table-scroll">
|
||||
<table className="cut-table">
|
||||
<caption className="sr-only">Flooring cuts and offcut placements</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Pieces</th>
|
||||
<th scope="col">Qty</th>
|
||||
<th scope="col">Cut size · mm</th>
|
||||
<th scope="col">Room</th>
|
||||
<th scope="col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{floorGroups.map((group) => {
|
||||
const first = group[0];
|
||||
return (
|
||||
<tr
|
||||
key={first.id}
|
||||
className={`flooring-batch ${group.some((piece) => piece.id === selectedPiece) ? 'selected' : ''}`}
|
||||
>
|
||||
<td data-label="Pieces" className="batch-codes">
|
||||
<div className="piece-chips">
|
||||
{group.map((piece) => (
|
||||
<button
|
||||
key={piece.id}
|
||||
className={`piece-link piece-chip cut-preview-button flooring-preview-button ${piece.id === selectedPiece ? 'selected' : ''}`}
|
||||
aria-label={`Preview flooring cut ${piece.id}`}
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => select(piece.id, piece.roomId)}
|
||||
>
|
||||
<span className="cut-thumbnail" aria-hidden="true">
|
||||
<FlooringPreview piece={piece} compact />
|
||||
</span>
|
||||
<span className="cut-preview-code">{piece.id}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Qty" className="batch-qty numeric">
|
||||
{group.length}
|
||||
</td>
|
||||
<td data-label="Cut size · mm" className="batch-size numeric">
|
||||
{mm(first.length)} × {mm(first.width)}
|
||||
</td>
|
||||
<td data-label="Room" className="batch-room">
|
||||
{roomName(first.roomId)}
|
||||
</td>
|
||||
<td data-label="Action" className="batch-action">
|
||||
{!first.cut
|
||||
? 'Use offcut · no new cut'
|
||||
: first.shaped
|
||||
? 'Shaped cut'
|
||||
: 'Rectangular cut'}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
{groupFlooringCuts(flooring).map((group) => (
|
||||
<section
|
||||
className="stock-cut-group"
|
||||
data-stock-id={group[0].stockId}
|
||||
key={group[0].stockId}
|
||||
aria-label={`Floorboard ${group[0].stockId}`}
|
||||
>
|
||||
<h4>
|
||||
Floorboard {group[0].stockId}
|
||||
<span>
|
||||
{mm(project.flooring.length)} × {mm(project.flooring.width)} mm
|
||||
</span>
|
||||
</h4>
|
||||
<ul className="board-cuts">
|
||||
{group.map((piece) => (
|
||||
<li className="board-cut" key={piece.id} data-cut-id={piece.id}>
|
||||
<button
|
||||
className="cut-preview-button flooring-preview-button"
|
||||
aria-label={`Preview flooring cut ${piece.id}`}
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => select(piece.id, piece.roomId)}
|
||||
>
|
||||
<span className="cut-thumbnail" aria-hidden="true">
|
||||
<FlooringPreview piece={piece} compact />
|
||||
</span>
|
||||
<span className="cut-preview-code">
|
||||
{piece.id} <span aria-hidden="true">↗</span>
|
||||
</span>
|
||||
</button>
|
||||
<div>
|
||||
<p className="cut-size">
|
||||
{mm(piece.length)} × {mm(piece.width)} mm
|
||||
</p>
|
||||
<p className="cut-location">
|
||||
{roomName(piece.roomId)} · Row {piece.row}
|
||||
</p>
|
||||
{piece.reused && (
|
||||
<p className="cut-note">
|
||||
{piece.cut ? 'Cut from the remaining offcut' : 'Use the offcut as it is'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
{!floorCuts.length && (
|
||||
<p className="empty-state">
|
||||
{flooring.pieces.length
|
||||
? 'No flooring cuts needed.'
|
||||
: 'Complete the outline and flooring measurements to calculate cuts.'}
|
||||
</p>
|
||||
)}
|
||||
<StockAllocations
|
||||
title="Source boards for flooring cuts"
|
||||
items={flooring.stocks
|
||||
.filter((stock) => stock.pieceIds.some((id) => floorIds.has(id)))
|
||||
.map((stock) => ({
|
||||
id: stock.id,
|
||||
pieces: stock.pieceIds.filter((id) => floorIds.has(id)),
|
||||
size: `${mm(project.flooring.length)} × ${mm(project.flooring.width)} mm`,
|
||||
remainder: `${mm(stock.remaining)} mm remaining`,
|
||||
}))}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="material-cuts skirting-cuts" aria-labelledby="skirting-cuts-title">
|
||||
<h3 id="skirting-cuts-title">
|
||||
Skirting cuts <span className="count-badge">{skirtCuts.length}</span>
|
||||
</h3>
|
||||
<p className="cut-summary">
|
||||
Buy {skirting.stockCount} lengths · {skirting.pieces.length} installed pieces ·{' '}
|
||||
{wholeSkirt.reduce((sum, group) => sum + group.count, 0)} full lengths installed uncut.
|
||||
Buy {skirting.stockCount} lengths ·{' '}
|
||||
{wholeSkirt.reduce((sum, group) => sum + group.count, 0)} installed uncut
|
||||
</p>
|
||||
<p className="helper">
|
||||
Lengths use the longest point. Mitres are saw settings from square; left and right follow
|
||||
the wall direction.
|
||||
</p>
|
||||
{skirtCuts.length ? (
|
||||
<div className="table-scroll">
|
||||
<table className="cut-table">
|
||||
<caption className="sr-only">Skirting pieces requiring cuts</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Piece</th>
|
||||
<th scope="col">Room / wall</th>
|
||||
<th scope="col">Length · mm</th>
|
||||
<th scope="col">Left / right</th>
|
||||
<th scope="col">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{skirtCuts.map((piece) => (
|
||||
<tr key={piece.id} className={selectedPiece === piece.id ? 'selected' : ''}>
|
||||
<td data-label="Piece">
|
||||
<button
|
||||
className="piece-link cut-preview-button skirting-preview-button"
|
||||
aria-label={`Preview skirting cut ${piece.id}`}
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => select(piece.id, piece.roomId)}
|
||||
>
|
||||
<span className="cut-thumbnail" aria-hidden="true">
|
||||
<SkirtingPreview
|
||||
piece={piece}
|
||||
thickness={project.skirting.thickness}
|
||||
compact
|
||||
/>
|
||||
</span>
|
||||
<span className="cut-preview-code">{piece.id}</span>
|
||||
</button>
|
||||
</td>
|
||||
<td data-label="Room / wall">
|
||||
{roomName(piece.roomId)}
|
||||
<small>Wall {piece.wallIndex + 1}</small>
|
||||
</td>
|
||||
<td data-label="Length · mm" className="numeric">
|
||||
{mm(piece.length)}
|
||||
<small>{mm(piece.backLength)} at wall</small>
|
||||
</td>
|
||||
<td data-label="Left / right">
|
||||
{degree(piece.leftMitre)} / {degree(piece.rightMitre)}
|
||||
<small>
|
||||
{piece.leftType} / {piece.rightType}
|
||||
</small>
|
||||
</td>
|
||||
<td data-label="Source">{piece.stockId}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
{groupSkirtingCuts(skirting).map((group) => (
|
||||
<section
|
||||
className="stock-cut-group"
|
||||
data-stock-id={group[0].stockId}
|
||||
key={group[0].stockId}
|
||||
aria-label={`Skirting board ${group[0].stockId}`}
|
||||
>
|
||||
<h4>
|
||||
Skirting board {group[0].stockId}
|
||||
<span>
|
||||
{mm(skirting.stocks.find((stock) => stock.id === group[0].stockId)!.length)} mm
|
||||
</span>
|
||||
</h4>
|
||||
<ul className="board-cuts">
|
||||
{group.map((piece) => (
|
||||
<li className="board-cut" key={piece.id} data-cut-id={piece.id}>
|
||||
<button
|
||||
className="cut-preview-button skirting-preview-button"
|
||||
aria-label={`Preview skirting cut ${piece.id}`}
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => select(piece.id, piece.roomId)}
|
||||
>
|
||||
<span className="cut-thumbnail" aria-hidden="true">
|
||||
<SkirtingPreview
|
||||
piece={piece}
|
||||
thickness={project.skirting.thickness}
|
||||
compact
|
||||
/>
|
||||
</span>
|
||||
<span className="cut-preview-code">
|
||||
{piece.id} <span aria-hidden="true">↗</span>
|
||||
</span>
|
||||
</button>
|
||||
<div>
|
||||
<p className="cut-size">
|
||||
{mm(piece.length)} mm <span className="cut-note">long point</span>
|
||||
</p>
|
||||
<p className="cut-location">
|
||||
{roomName(piece.roomId)} · Wall {piece.wallIndex + 1}
|
||||
</p>
|
||||
<p className="cut-note">
|
||||
Left {degree(piece.leftMitre)} · Right {degree(piece.rightMitre)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
{!skirtCuts.length && (
|
||||
<p className="empty-state">
|
||||
{skirting.pieces.length
|
||||
? 'No skirting cuts needed.'
|
||||
: 'Complete the outline and skirting measurements to calculate cuts.'}
|
||||
</p>
|
||||
)}
|
||||
<StockAllocations
|
||||
title="Source lengths for skirting cuts"
|
||||
items={skirting.stocks
|
||||
.filter((stock) => stock.pieceIds.some((id) => skirtIds.has(id)))
|
||||
.map((stock) => ({
|
||||
id: stock.id,
|
||||
pieces: stock.pieceIds.filter((id) => skirtIds.has(id)),
|
||||
size: `${mm(stock.length)} mm`,
|
||||
remainder: `${mm(stock.waste)} mm waste including kerf`,
|
||||
}))}
|
||||
/>
|
||||
</section>
|
||||
<CutDialog
|
||||
floor={selectedFloor}
|
||||
skirt={selectedSkirt}
|
||||
floor={floorCuts.find((piece) => piece.id === selectedPiece)}
|
||||
skirt={skirtCuts.find((piece) => piece.id === selectedPiece)}
|
||||
project={project}
|
||||
close={() => select(null)}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in a new issue