DefectReport
Severity, a location by a tap on the drawing with its grid square and room, photos, and a clear summary to send.
Demo
Report a defect
Where is it? Tap the drawing.
No pin yet.
How serious?
Summary
SEVERITY NOT SET: Where: Level 2 Job: Block C, unit 12 By: J. Tremblay
Nothing sent yet.
Get it
It needs theme.qwe, as every component does, and photo_evidence.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/photo_evidence.qwe$ curl -O https://quillolang.org/components/ui/defect_report.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/defect_report.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/defect_report.qwe
// A plan drawn for the demo, 800 by 600, in rooms a test can reason about.let defect_report_demo_svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600"><rect width="800" height="600" fill="#fff"/><g fill="none" stroke="#111" stroke-width="6"><rect x="10" y="10" width="780" height="580"/><path d="M400 10v300M10 310h780M560 310v280"/></g><g font-family="sans-serif" font-size="28" fill="#333"><text x="120" y="170">Kitchen</text><text x="520" y="170">Living room</text><text x="200" y="460">Bedroom</text><text x="620" y="460">Bath</text></g></svg>`let defect_report_demo_plans = [ { id: "l2", label: "Level 2", src: `data:image/svg+xml,${url_encode(defect_report_demo_svg)}`, width: 800, height: 600, zones: [{ name: "Kitchen", x: 0, y: 0, w: 50, h: 50 }, { name: "Living room", x: 50, y: 0, w: 50, h: 50 }, { name: "Bedroom", x: 0, y: 50, w: 70, h: 50 }, { name: "Bath", x: 70, y: 50, w: 30, h: 50 }] }, { id: "l3", label: "Level 3", src: `data:image/svg+xml,${url_encode(defect_report_demo_svg)}`, width: 800, height: 600 },]component DefectReportDemoLive() { state said = "Nothing sent yet." <div.demo-stack> <DefectReport reporter="J. Tremblay" job="Block C, unit 12" drawings={defect_report_demo_plans} on_send={(r) => { said = `Sent ${r.grid} ${r.zone} ${r.severity} ${len(r.photos)}` }}/> <p.demo-note id="defect-demo-said">{said}</p> </div>}view DefectReportDemo() = <DefectReportDemoLive/>Source
ui/defect_report.qwe
// DefectReport: snagging on site. How serious it is, where it is by a tap on the drawing,// photos, and a plain summary that can go to whoever needs it, as words, from the phone.// The pin says the drawing's grid square and the room it falls in, so "C4, Kitchen" means// the same to the person who reads it. With no connection, the report waits and goes later.//// A tap sets the pin; a finger scrolling the drawing does not. The pin is dragged to move it,// or moved with the arrow keys, Shift for bigger steps. Rooms can be chosen from a list,// and Middle of the drawing is there for anyone who cannot point, so the drawing never// stands between a person and the report.//// import ui/defect_report.qwe use DefectReport//// <DefectReport reporter="J. Tremblay" job="Block C" on_send={(r) => send(r)} drawings={[// { id: "l2", label: "Level 2", src: "/plans/level2.svg", width: 800, height: 600,// zones: [{ name: "Kitchen", x: 0, y: 0, w: 40, h: 50 }] }]}/>//// drawings: each { id, label, src, width, height, zones }. zones: rooms, as percentages of// the drawing: { name, x, y, w, h }; the smallest around the pin names it. grid: columns and// rows of the drawing's grid ([8, 6]). severities: replaces the four levels, each { id,// label, hint }. min_photos: how many are needed (1). reporter, job: on the report and// the photos' stamps. use_place: asks for a place for photos (true). prefix: starts the// reference ("DR"). title ("Report a defect"). id: a prefix for its ids.//// on_send(report): when Send is pressed, and again when a connection comes back if it could// not go; answer false, an error, or a Promise of either to try again later. report: { ref,// severity, severity_label, title, detail, drawing, x, y, grid, zone, photos, reporter, job,// at, offset, text }. text is the summary, which Share sends by the system's sheet.//// defect_report_grid(x, y, cols, rows), defect_report_zone(zones, x, y),// defect_report_missing(form, min_photos) and defect_report_text(report) are the plain logic.import ui/theme.qwe use ui_clsimport ui/button.qwe use Buttonimport ui/photo_evidence.qwe use PhotoEvidenceAdd, PhotoEvidenceThumbs, photo_evidence_whenlet defect_report_css = css`.ui-defect { display: grid; gap: 12px; min-width: 0; container-type: inline-size; color: var(--ui-fg); font: 15px/1.4 var(--ui-font); }.ui-defect h3 { margin: 0; font-size: 18px; }.ui-defect h4 { margin: 0; font-size: 15px; }.ui-defect-cols { display: grid; gap: 14px; min-width: 0; }.ui-defect-col { display: grid; gap: 12px; min-width: 0; align-content: start; }.ui-defect-part { display: grid; gap: 6px; min-width: 0; }.ui-defect-levels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; margin: 0; padding: 0; border: 0; min-width: 0; }.ui-defect-level { position: relative; display: grid; align-content: center; min-height: 64px; padding: 6px 10px; border: 2px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); font-weight: 700; cursor: pointer; touch-action: manipulation; }.ui-defect-level small { color: var(--ui-muted); font-size: 13px; font-weight: 500; }.ui-defect-level input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }.ui-defect-level:has(input:focus-visible) { box-shadow: var(--ui-focus); }.ui-defect-level:has(input:checked) { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-defect-level:has(input[value="critical"]:checked) { border-color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-defect-label { display: grid; gap: 2px; min-width: 0; color: var(--ui-muted); font-size: 13px; }/* 16 px: anything smaller and iOS zooms the page when the field is touched. */.ui-defect-label input, .ui-defect-label textarea, .ui-defect-label select { box-sizing: border-box; width: 100%; min-height: 48px; padding: 8px 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 16px/1.4 var(--ui-font); }.ui-defect-label input:focus, .ui-defect-label textarea:focus, .ui-defect-label select:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-defect-plans { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; border: 0; }.ui-defect-plan { position: relative; display: inline-flex; align-items: center; min-height: 44px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: 999px; background: var(--ui-surface); font-weight: 600; cursor: pointer; }.ui-defect-plan input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }.ui-defect-plan:has(input:checked) { border-color: var(--ui-accent); background: var(--ui-accent-soft); }.ui-defect-plan:has(input:focus-visible) { box-shadow: var(--ui-focus); }/* The page scrolls past the drawing, and so does the drawing when zoomed: a finger that moves is scrolling, and the browser says so with pointercancel. */.ui-defect-view { position: relative; overflow: auto; max-height: 62dvh; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: #fff; touch-action: pan-x pan-y; overscroll-behavior: contain; }.ui-defect-sheet { position: relative; cursor: crosshair; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }.ui-defect-sheet img { display: block; width: 100%; height: auto; pointer-events: none; -webkit-user-drag: none; }/* The place is on this wrapper and the drag's transform on the pin inside it. An element with style={...} in its markup has the whole attribute written again on a state change, which would wipe the transform and send a held pin home. */.ui-defect-spot { position: absolute; width: 0; height: 0; }.ui-defect-pin { position: absolute; left: -22px; top: -44px; display: grid; place-items: end center; width: 44px; height: 44px; padding: 0; border: 0; background: transparent; color: var(--ui-danger); cursor: grab; touch-action: none; transition: transform .18s cubic-bezier(.2, .8, .2, 1); }.ui-defect-pin[data-drag] { cursor: grabbing; transition: none; }.ui-defect-pin svg { display: block; width: 36px; height: 36px; pointer-events: none; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .5)); }.ui-defect-pin:focus-visible { outline: none; }.ui-defect-pin:focus-visible svg { outline: 3px solid var(--ui-accent); outline-offset: 1px; border-radius: 50%; }.ui-defect-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.ui-defect-where { flex: 1 1 100%; margin: 0; font-weight: 600; }.ui-defect-tools > .ui-btn { flex: 1 1 96px; }.ui-defect-sum { margin: 0; padding: 10px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); font: 14px/1.5 var(--ui-mono); white-space: pre-wrap; overflow-wrap: anywhere; }.ui-defect-said { margin: 0; font-weight: 600; }.ui-defect-said:empty { display: none; }.ui-defect-open { margin: 0; padding: 0 0 0 18px; color: var(--ui-danger); font-weight: 600; }.ui-defect-acts { display: flex; flex-wrap: wrap; gap: 8px; }.ui-defect-acts > * { flex: 1 1 130px; }.ui-defect-note { margin: 0; padding: 8px 12px; border-radius: var(--ui-radius-sm); background: var(--ui-warning-soft); font-size: 14px; }@container (min-width: 640px) { .ui-defect-cols { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 20px; align-items: start; } .ui-defect-levels { grid-template-columns: repeat(2, minmax(0, 1fr)); }}@media (prefers-reduced-motion: reduce) { .ui-defect-pin { transition: none; } }`let defect_report_levels = [ { id: "low", label: "Low", hint: "Cosmetic" }, { id: "medium", label: "Medium", hint: "Needs fixing" }, { id: "high", label: "High", hint: "Stops the work" }, { id: "critical", label: "Critical", hint: "Unsafe" },]let defect_report_letters = split("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "")// A place on the drawing, kept within it.fn defect_report_clamp(v) = max([0, min([100, v])])// Tenths of a percent are finer than a finger, and keep a report's numbers short.fn defect_report_tidy(v) = round(defect_report_clamp(v) * 10) / 10// The drawing's grid square a point is in: C4. A point on the far edge is in the last one.fn defect_report_grid(x, y, cols: int, rows: int) -> str { let c = min([cols - 1, floor(defect_report_clamp(x) * cols / 100)]) let r = min([rows - 1, floor(defect_report_clamp(y) * rows / 100)]) `${defect_report_letters[min([25, c])]}${r + 1}`}// The name of the smallest zone around a point, or null.fn defect_report_zone(zones, x, y) { if type_of(zones) != "list" { return null } let num = (v) => type_of(v) == "int" || type_of(v) == "float" let around = zones | filter((z) => type_of(z) == "map" && num(get(z, "x")) && num(get(z, "y")) && num(get(z, "w")) && num(get(z, "h")) && x >= z.x && x <= z.x + z.w && y >= z.y && y <= z.y + z.h) if len(around) == 0 { return null } str(get(sort_by(around, (z) => z.w * z.h)[0], "name") ?? "")}// What is still missing, in words.fn defect_report_missing(form: map, min_photos: int) -> list { let n = len(get(form, "photos") ?? []) (get(form, "severity") == null ? ["How serious it is"] : []) + (trim(str(get(form, "title") ?? "")) == "" ? ["What is wrong"] : []) + (get(form, "pin") == null ? ["Where it is on the drawing"] : []) + (n < min_photos ? [min_photos - n == 1 ? "1 more photo" : `${min_photos - n} more photos`] : [])}// A reference anyone can read over a phone: DR-20261102-1105.fn defect_report_ref(prefix: str, at: int, offset: int) -> str { let w = photo_evidence_when(at, offset) `${prefix}-${slice(w, 0, 4)}${slice(w, 5, 7)}${slice(w, 8, 10)}-${slice(w, 11, 13)}${slice(w, 14, 16)}`}// Where it is, in words.fn defect_report_where(drawing, pin, grid: str, zone) -> str { let plan = type_of(drawing) == "map" ? get(drawing, "label") : null let parts = [plan, pin != null ? `grid ${grid}` : null, zone] | filter((x) => x != null && x != "") join(parts, ", ")}// The report in words: one fact a line, the serious part first, for reading on a phone.fn defect_report_text(r: map) -> str { let line = (label, v) => v != null && str(v) != "" ? [`${label}: ${v}`] : [] let top = `${upper(str(r.severity_label ?? "Severity not set"))}: ${trim(str(r.title ?? ""))}` join([top] + line("Where", r.where) + line("Detail", trim(str(r.detail ?? ""))) + line("Photos", len(r.photos) > 0 ? len(r.photos) : null) + line("Job", r.job) + line("By", r.reporter) + line("Ref", r.ref), "\n")}component DefectReport(drawings: list = [], grid: list = [8, 6], severities: list = defect_report_levels, min_photos: int = 1, reporter: str? = null, job: str? = null, use_place: bool = true, prefix: str = "DR", title: str = "Report a defect", id: str = "ui-defect", on_send = null, class: str? = null) { style global defect_report_css state severity = null state heading = "" state detail = "" state plan = len(drawings) > 0 ? str(get(drawings[0], "id") ?? "") : "" state pin = null // { x, y } in percent of the drawing state zoom = 1 state photos = [] state sheet = null state press = null // a finger down on the drawing: { x, y, id } state drag = null // a finger on the pin: { sx, sy, id, moved, w, h } state said = "" state tried = false state report = null state sent = false state sending = false state online = true let drawing = drawings | find((d) => type_of(d) == "map" && str(get(d, "id") ?? "") == plan) let cols = len(grid) > 0 && type_of(grid[0]) == "int" && grid[0] > 0 ? grid[0] : 8 let rows = len(grid) > 1 && type_of(grid[1]) == "int" && grid[1] > 0 ? grid[1] : 6 let square = pin != null ? defect_report_grid(pin.x, pin.y, cols, rows) : "" let zones = drawing != null ? get(drawing, "zones") : null let zone = pin != null ? defect_report_zone(zones, pin.x, pin.y) : null let level = severities | find((s) => get(s, "id") == severity) let where = defect_report_where(drawing, pin, square, zone) let draft = { severity_label: level != null ? get(level, "label") : null, title: heading, detail: detail, where: where, photos: photos, job: job, reporter: reporter, ref: null } let missing = defect_report_missing({ severity: severity, title: heading, pin: pin, photos: photos }, min_photos) let list_zones = type_of(zones) == "list" ? zones | filter((z) => type_of(z) == "map" && get(z, "name") != null) : [] let put = (x, y) => { pin = { x: defect_report_tidy(x), y: defect_report_tidy(y) } tried = false } let flush = () => { if report != null && !sent && !sending && online && on_send != null { sending = true let r = js("Promise.resolve", on_send(report)) sending = false if r == false || is_err(r) { said = "It could not be sent yet. It is kept here and will be tried again." } else { sent = true said = "" } } } let send = () => { if len(missing) > 0 { tried = true said = "" } else { let at = now() let offset = 0 - js_new("Date").getTimezoneOffset() let ref = defect_report_ref(prefix, at, offset) let parts = set(draft, "ref", ref) report = { ref: ref, severity: severity, severity_label: parts.severity_label, title: trim(heading), detail: trim(detail), drawing: plan, x: pin.x, y: pin.y, grid: square, zone: zone, where: where, photos: photos, reporter: reporter, job: job, at: at, offset: offset, text: defect_report_text(parts) } said = !online && on_send != null ? "No connection. The report goes when there is one." : "" flush() } } let share = () => { let t = report != null ? report.text : defect_report_text(draft) let w = js("window") let n = js("navigator") if w.quilloNative != null { // iOS answers { shared, to } and Android true, so only a refusal is looked at. let r = js("quilloNative.share", t) if is_err(r) { said = "It could not be shared from here." } } else if n.share != null { let r = js("Promise.resolve", n.share({ text: t })) // A person closing the sheet is not a failure. if is_err(r) && !contains(str(r.error), "cancel") && !contains(str(r.error), "Abort") { said = "It could not be shared from here." } } else if n.clipboard != null { let r = js("Promise.resolve", n.clipboard.writeText(t)) said = is_err(r) ? "This browser would not copy it. Select the summary instead." : "Copied." } else { said = "Nothing here can share it. Select the summary instead." } } let again = () => { severity = null heading = "" detail = "" pin = null photos = [] report = null sent = false tried = false said = "" } <section class={ui_cls(["ui-defect", class])} @mount={(_el) => { let w = js("window") online = js("navigator").onLine != false let up = (_e = null) => { online = true flush() } let down = (_e = null) => { online = false } w.addEventListener("online", up) w.addEventListener("offline", down) () => { w.removeEventListener("online", up) w.removeEventListener("offline", down) } }}> <h3>{title}</h3> {!online && <p.ui-defect-note>No connection. The report is kept here and goes when there is one.</p>} <div.ui-defect-cols> <div.ui-defect-col> {len(drawings) > 1 && <fieldset.ui-defect-plans aria-label="Drawing"> {for d in drawings => <label class="ui-defect-plan" key={str(get(d, "id") ?? "")}> <input type="radio" name={`${id}-plan`} value={str(get(d, "id") ?? "")} checked={plan == str(get(d, "id") ?? "")} disabled={report != null} @change={() => { plan = str(get(d, "id") ?? "") pin = null }}/> {str(get(d, "label") ?? "")} </label>} </fieldset>} {drawing != null && <div.ui-defect-part> <h4>Where is it? Tap the drawing.</h4> <div.ui-defect-view> <div.ui-defect-sheet style={`width: ${zoom * 100}%`} @mount={(el) => { sheet = el }} @pointerdown={(e) => { if report == null && e.target.closest(".ui-defect-pin") == null { press = { x: e.clientX, y: e.clientY, id: e.pointerId } } }} @pointerup={(e) => { if press != null && e.pointerId == press.id { let far = abs(e.clientX - press.x) + abs(e.clientY - press.y) press = null // A finger that moved was scrolling, or changing its mind. if far < 10 { let r = sheet.getBoundingClientRect() put((e.clientX - r.left) * 100 / r.width, (e.clientY - r.top) * 100 / r.height) } } }} @pointercancel={() => { press = null }}> <img src={get(drawing, "src")} alt={`Drawing: ${str(get(drawing, "label") ?? "")}`} width={get(drawing, "width")} height={get(drawing, "height")}/> {pin != null && <span.ui-defect-spot style={`left: ${pin.x}%; top: ${pin.y}%`}> <button.ui-defect-pin type="button" aria-label={`Pin at ${where}. Arrow keys move it, Delete takes it off.`} disabled={report != null} @pointerdown={(e) => { e.preventDefault() e.target.setPointerCapture(e.pointerId) let r = sheet.getBoundingClientRect() drag = { sx: e.clientX, sy: e.clientY, id: e.pointerId, moved: false, w: r.width, h: r.height } e.target.dataset.drag = "1" }} @pointermove={(e) => { if drag != null && e.pointerId == drag.id { let dx = e.clientX - drag.sx let dy = e.clientY - drag.sy if drag.moved || abs(dx) + abs(dy) >= 8 { if !drag.moved { drag = set(drag, "moved", true) } // Not past the sheet's edges, however far the finger goes. let x = max([0 - pin.x * drag.w / 100, min([(100 - pin.x) * drag.w / 100, dx])]) let y = max([0 - pin.y * drag.h / 100, min([(100 - pin.y) * drag.h / 100, dy])]) e.target.style.transform = `translate(${x}px, ${y}px)` } } }} @pointerup={(e) => { if drag != null && e.pointerId == drag.id { let g = drag drag = null let el = e.target js("Reflect.deleteProperty", el.dataset, "drag") el.style.transform = "" if g.moved { put(pin.x + (e.clientX - g.sx) * 100 / g.w, pin.y + (e.clientY - g.sy) * 100 / g.h) } } }} @pointercancel={(e) => { // The system took the gesture: the pin goes back to where it was. drag = null let el = e.target js("Reflect.deleteProperty", el.dataset, "drag") el.style.transform = "" }} @keydown={(e) => { let step = e.shiftKey ? 5 : 1 let k = e.key if k == "ArrowLeft" || k == "ArrowRight" || k == "ArrowUp" || k == "ArrowDown" || k == "Delete" || k == "Backspace" { e.preventDefault() } if k == "ArrowLeft" { put(pin.x - step, pin.y) } if k == "ArrowRight" { put(pin.x + step, pin.y) } if k == "ArrowUp" { put(pin.x, pin.y - step) } if k == "ArrowDown" { put(pin.x, pin.y + step) } if k == "Delete" || k == "Backspace" { pin = null } }}> <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 22s7-7.2 7-12a7 7 0 1 0-14 0c0 4.800 7 12 7 12Zm0-9a3 3 0 1 1 0-6 3 3 0 0 1 0 6Z"/></svg> </button> </span>} </div> </div> <div.ui-defect-tools> <p.ui-defect-where role="status">{pin != null ? where : "No pin yet."}</p> <Button size="lg" variant="outline" disabled={zoom <= 1} label="Zoom out" on_click={() => { zoom = max([1, zoom - 1]) }}>−</Button> <Button size="lg" variant="outline" disabled={zoom >= 3} label="Zoom in" on_click={() => { zoom = min([3, zoom + 1]) }}>+</Button> {pin == null && <Button size="lg" variant="outline" disabled={report != null} on_click={() => { put(50, 50) }}>Middle of the drawing</Button>} {pin != null && <Button size="lg" variant="ghost" disabled={report != null} on_click={() => { pin = null }}>Take the pin off</Button>} </div> {len(list_zones) > 0 && <label class="ui-defect-label" for={`${id}-room`}>Or choose a room <select id={`${id}-room`} disabled={report != null} value={zone ?? ""} @change={(e) => { let z = list_zones | find((x) => str(x.name) == e.value) if z != null { put(z.x + z.w / 2, z.y + z.h / 2) } }}> <option value="">Choose</option> {for z in list_zones => <option key={str(z.name)} value={str(z.name)}>{str(z.name)}</option>} </select> </label>} </div>} </div> <div.ui-defect-col> <div.ui-defect-part> <h4>How serious?</h4> <fieldset.ui-defect-levels aria-label="How serious it is"> {for s in severities => <label class="ui-defect-level" key={str(get(s, "id") ?? "")}> <input type="radio" name={`${id}-severity`} value={str(get(s, "id") ?? "")} checked={severity == get(s, "id")} disabled={report != null} @change={() => { severity = get(s, "id") }}/> {str(get(s, "label") ?? "")} <small>{str(get(s, "hint") ?? "")}</small> </label>} </fieldset> </div> <label class="ui-defect-label" for={`${id}-title`}>What is wrong? <input id={`${id}-title`} name={`${id}-title`} autocomplete="off" enterkeyhint="next" disabled={report != null} value={heading} @input={(e) => { heading = e.value }}/> </label> <label class="ui-defect-label" for={`${id}-detail`}>More detail, if it helps <textarea id={`${id}-detail`} name={`${id}-detail`} rows="2" disabled={report != null} value={detail} @input={(e) => { detail = e.value }}></textarea> </label> <div.ui-defect-part> <PhotoEvidenceAdd label={len(photos) > 0 ? "Add another photo" : "Take photo"} tag={heading} job={job} use_place={use_place} disabled={report != null} on_add={(p) => { photos = photos + [p] }}/> <PhotoEvidenceThumbs photos={photos} on_remove={report == null ? (p) => { photos = photos | filter((x) => x.id != p.id) } : null}/> </div> <div.ui-defect-part> <h4>{report != null ? (sent ? "Sent" : (on_send != null ? "Waiting to send" : "Finished")) : "Summary"}</h4> <pre.ui-defect-sum>{report != null ? report.text : defect_report_text(draft)}</pre> </div> {tried && len(missing) > 0 && <ul.ui-defect-open role="alert">{for m in missing => <li key={m}>{m}</li>}</ul>} <p.ui-defect-said role="status">{said}</p> <div.ui-defect-acts> {report == null && <Button size="lg" on_click={send}>Send report</Button>} {report != null && on_send != null && !sent && online && <Button size="lg" variant="outline" loading={sending} on_click={flush}>Try again</Button>} <Button size="lg" variant="outline" on_click={share}>Share</Button> {report != null && <Button size="lg" variant="outline" on_click={again}>Report another</Button>} </div> </div> </div> </section>}Related
AssetLookupScan or type an asset tag to see its record, history and what to do, with near matches and a report for a tag nobody knows.3 views · 7 downloadsMeterReadingA round of meter readings on a big keypad, each checked against the last value and a sane range, with a photo on anything odd.4 views · 6 downloadsPhotoEvidencePhotos stamped with the time and place into the picture, with counts of what is still needed, tags, and a queue for when there is no connection.2 views · 5 downloadsStylusCanvasDraw with a pen by pressure and tilt, palm rejected, with undo, eraser, zoom, and an image or strokes out.3 views · 4 downloadsWorkOrderCardA job with status, priority, parts and steps, moved along with big buttons for gloves, that cannot be finished with required steps left.3 views · 6 downloadsActionSheetA list of actions rising from the bottom, with a destructive style and Cancel: swipe down or the phone's back gesture closes it; on a tablet, a card by what opened it.3 views · 9 downloads
Thread
Thread 0 posts
On DefectReport: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.