06/23 Fix inspection layout display and exported PDF file (sent via email)
This commit is contained in:
@@ -29,17 +29,13 @@ private let kFtrH: CGFloat = 39.6 // footer area height
|
|||||||
private let kTopY: CGFloat = kHdrH + 14 // first content Y on a fresh page
|
private let kTopY: CGFloat = kHdrH + 14 // first content Y on a fresh page
|
||||||
private let kBotY: CGFloat = kH - kFtrH // lowest Y before footer
|
private let kBotY: CGFloat = kH - kFtrH // lowest Y before footer
|
||||||
|
|
||||||
// Image compression — keeps the PDF (and thus the email attachment) small
|
/// Downscale to 700px on the longer side and re-encode as JPEG quality 0.55,
|
||||||
// regardless of source photo resolution. Same values as IssuePDFGenerator.
|
/// returning a fresh UIImage built from the compressed bytes. Applied to
|
||||||
private let kImgMaxPx: CGFloat = 700
|
/// every photo before it's embedded in the PDF — keeps file size minimal.
|
||||||
private let kImgJPEGQuality: CGFloat = 0.55
|
|
||||||
|
|
||||||
/// Downscale to kImgMaxPx on the longer side and re-encode as JPEG at
|
|
||||||
/// kImgJPEGQuality, returning a fresh UIImage built from the compressed
|
|
||||||
/// bytes. Applied to every photo before it's embedded in the PDF — this is
|
|
||||||
/// the main lever for keeping file size minimal.
|
|
||||||
/// nonisolated: called from inside a TaskGroup (concurrent), not @MainActor.
|
/// nonisolated: called from inside a TaskGroup (concurrent), not @MainActor.
|
||||||
private nonisolated func compress(_ image: UIImage) -> UIImage? {
|
private nonisolated func compress(_ image: UIImage) -> UIImage? {
|
||||||
|
let kImgMaxPx: CGFloat = 700
|
||||||
|
let kImgJPEGQuality: CGFloat = 0.55
|
||||||
let size = image.size
|
let size = image.size
|
||||||
guard size.width > 0, size.height > 0 else { return nil }
|
guard size.width > 0, size.height > 0 else { return nil }
|
||||||
let scale = min(kImgMaxPx / size.width, kImgMaxPx / size.height, 1.0)
|
let scale = min(kImgMaxPx / size.width, kImgMaxPx / size.height, 1.0)
|
||||||
@@ -412,26 +408,48 @@ private func drawFormFields(_ pctx: PDFContext,
|
|||||||
let baseRowH: CGFloat = 28
|
let baseRowH: CGFloat = 28
|
||||||
let photoRowH: CGFloat = 92 // taller row to fit an embedded photo
|
let photoRowH: CGFloat = 92 // taller row to fit an embedded photo
|
||||||
|
|
||||||
// Group by original row
|
// Sort fields by (row, col) before grouping — mirrors the web PDF generator:
|
||||||
|
// form_fields = sorted(schema, key=lambda f: (f['row'], f['col']))
|
||||||
|
// Without this, schema fields arrive in JSON-array/editor-insertion order,
|
||||||
|
// which can place section headers after the data rows they belong to.
|
||||||
|
let sortedSchema = schema.sorted {
|
||||||
|
let r0 = $0["row"] as? Int ?? 0, r1 = $1["row"] as? Int ?? 0
|
||||||
|
if r0 != r1 { return r0 < r1 }
|
||||||
|
let c0 = $0["col"] as? Int ?? 0, c1 = $1["col"] as? Int ?? 0
|
||||||
|
return c0 < c1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group by original row, tracking which section header precedes each new row.
|
||||||
|
// Uses a queue (not a single pendingSec variable) so that when a section
|
||||||
|
// field is encountered but the next few fields are on rows already in
|
||||||
|
// rowGroups, the section label is not silently dropped or overwritten by the
|
||||||
|
// next section before it was consumed.
|
||||||
var rowGroups: [Int: [[String: Any]]] = [:]
|
var rowGroups: [Int: [[String: Any]]] = [:]
|
||||||
var rowOrder: [Int] = []
|
var rowOrder: [Int] = []
|
||||||
var pendingSec: String? = nil
|
var sectionQueue: [String] = [] // pending section labels, in schema order
|
||||||
var secForRow: [Int: String] = [:]
|
var secForRow: [Int: String] = [:]
|
||||||
|
|
||||||
for f in schema {
|
for f in sortedSchema {
|
||||||
let ftype = f["type"] as? String ?? ""
|
let ftype = f["type"] as? String ?? ""
|
||||||
if skipTypes.contains(ftype) { continue }
|
if skipTypes.contains(ftype) { continue }
|
||||||
if ftype == "section" { pendingSec = f["label"] as? String ?? ""; continue }
|
if ftype == "section" {
|
||||||
|
sectionQueue.append(f["label"] as? String ?? "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
let row = f["row"] as? Int ?? 1
|
let row = f["row"] as? Int ?? 1
|
||||||
if rowGroups[row] == nil {
|
if rowGroups[row] == nil {
|
||||||
rowOrder.append(row)
|
rowOrder.append(row)
|
||||||
rowGroups[row] = []
|
rowGroups[row] = []
|
||||||
if let s = pendingSec { secForRow[row] = s; pendingSec = nil }
|
// Assign the oldest pending section label to this new row.
|
||||||
|
if !sectionQueue.isEmpty {
|
||||||
|
secForRow[row] = sectionQueue.removeFirst()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rowGroups[row]!.append(f)
|
rowGroups[row]!.append(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
for origRow in rowOrder.sorted() {
|
// Iterate in rowOrder (which is now schema-sorted insertion order = row order)
|
||||||
|
for origRow in rowOrder {
|
||||||
guard let fields = rowGroups[origRow] else { continue }
|
guard let fields = rowGroups[origRow] else { continue }
|
||||||
|
|
||||||
// Section banner
|
// Section banner
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ struct ExecuteInspectionView: View {
|
|||||||
let existingFormData = inspection.formData
|
let existingFormData = inspection.formData
|
||||||
var data: [String: Any] = [:]
|
var data: [String: Any] = [:]
|
||||||
for (k, v) in formValues {
|
for (k, v) in formValues {
|
||||||
if let s = v as? String, s.hasPrefix("local://"),
|
if v.hasPrefix("local://"),
|
||||||
let saved = existingFormData[k] as? String, saved.hasPrefix("uploads/") {
|
let saved = existingFormData[k] as? String, saved.hasPrefix("uploads/") {
|
||||||
data[k] = saved
|
data[k] = saved
|
||||||
} else {
|
} else {
|
||||||
@@ -532,7 +532,7 @@ struct ExecuteInspectionView: View {
|
|||||||
let existingFormData = inspection.formData
|
let existingFormData = inspection.formData
|
||||||
var data: [String: Any] = [:]
|
var data: [String: Any] = [:]
|
||||||
for (k, v) in formValues {
|
for (k, v) in formValues {
|
||||||
if let s = v as? String, s.hasPrefix("local://"),
|
if v.hasPrefix("local://"),
|
||||||
let saved = existingFormData[k] as? String, saved.hasPrefix("uploads/") {
|
let saved = existingFormData[k] as? String, saved.hasPrefix("uploads/") {
|
||||||
// processPhotoQueue already uploaded this photo — keep the server path
|
// processPhotoQueue already uploaded this photo — keep the server path
|
||||||
data[k] = saved
|
data[k] = saved
|
||||||
|
|||||||
@@ -678,9 +678,17 @@ struct ReadOnlyGridFormView: View {
|
|||||||
let skipTypes: Set<String> = ["label", "section",
|
let skipTypes: Set<String> = ["label", "section",
|
||||||
"button_submit", "button_print", "button_email"]
|
"button_submit", "button_print", "button_email"]
|
||||||
|
|
||||||
|
// Sort schema by (row, col) once — same as web PDF and InspectionPDFGenerator.
|
||||||
|
let sortedSchema = schema.sorted {
|
||||||
|
let r0 = $0["row"] as? Int ?? 0, r1 = $1["row"] as? Int ?? 0
|
||||||
|
if r0 != r1 { return r0 < r1 }
|
||||||
|
let c0 = $0["col"] as? Int ?? 0, c1 = $1["col"] as? Int ?? 0
|
||||||
|
return c0 < c1
|
||||||
|
}
|
||||||
|
|
||||||
// Pass 1 — answered data field IDs
|
// Pass 1 — answered data field IDs
|
||||||
var answeredIds = Set<String>()
|
var answeredIds = Set<String>()
|
||||||
for f in schema {
|
for f in sortedSchema {
|
||||||
guard let ftype = f["type"] as? String, !skipTypes.contains(ftype) else { continue }
|
guard let ftype = f["type"] as? String, !skipTypes.contains(ftype) else { continue }
|
||||||
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
||||||
let val = formValues[fid] ?? ""
|
let val = formValues[fid] ?? ""
|
||||||
@@ -691,7 +699,7 @@ struct ReadOnlyGridFormView: View {
|
|||||||
// Pass 2 — label IDs that immediately precede an answered field
|
// Pass 2 — label IDs that immediately precede an answered field
|
||||||
var visibleLabelIds = Set<String>()
|
var visibleLabelIds = Set<String>()
|
||||||
var lbuf: [String] = []
|
var lbuf: [String] = []
|
||||||
for f in schema {
|
for f in sortedSchema {
|
||||||
let ftype = f["type"] as? String ?? ""
|
let ftype = f["type"] as? String ?? ""
|
||||||
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
||||||
if ftype == "label" {
|
if ftype == "label" {
|
||||||
@@ -705,7 +713,7 @@ struct ReadOnlyGridFormView: View {
|
|||||||
// Pass 3 — section IDs that precede at least one answered field
|
// Pass 3 — section IDs that precede at least one answered field
|
||||||
var visibleSectionIds = Set<String>()
|
var visibleSectionIds = Set<String>()
|
||||||
var pendingSecId: String? = nil
|
var pendingSecId: String? = nil
|
||||||
for f in schema {
|
for f in sortedSchema {
|
||||||
let ftype = f["type"] as? String ?? ""
|
let ftype = f["type"] as? String ?? ""
|
||||||
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
let fid = f["id"] as? String ?? (f["id"] as? Int).map(String.init) ?? ""
|
||||||
if ftype == "section" {
|
if ftype == "section" {
|
||||||
@@ -715,18 +723,20 @@ struct ReadOnlyGridFormView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 4 — group fields by original row, keep schema order
|
// Pass 4 — group fields by original row, keep schema order.
|
||||||
|
// sortedSchema already computed above — reuse it.
|
||||||
var rowGroups: [Int: [[String: Any]]] = [:]
|
var rowGroups: [Int: [[String: Any]]] = [:]
|
||||||
var rowOrder: [Int] = []
|
var rowOrder: [Int] = []
|
||||||
for f in schema {
|
for f in sortedSchema {
|
||||||
let row = f["row"] as? Int ?? 1
|
let row = f["row"] as? Int ?? 1
|
||||||
if rowGroups[row] == nil { rowOrder.append(row); rowGroups[row] = [] }
|
if rowGroups[row] == nil { rowOrder.append(row); rowGroups[row] = [] }
|
||||||
rowGroups[row]!.append(f)
|
rowGroups[row]!.append(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 5 — for each row, collect visible fields; skip rows with none
|
// Pass 5 — for each row, collect visible fields; skip rows with none.
|
||||||
|
// Iterate in rowOrder (insertion order of sorted schema = row order).
|
||||||
var result: [RowGroup] = []
|
var result: [RowGroup] = []
|
||||||
for origRow in rowOrder.sorted() {
|
for origRow in rowOrder {
|
||||||
guard let group = rowGroups[origRow] else { continue }
|
guard let group = rowGroups[origRow] else { continue }
|
||||||
var visibleInRow: [[String: Any]] = []
|
var visibleInRow: [[String: Any]] = []
|
||||||
for f in group {
|
for f in group {
|
||||||
|
|||||||
Reference in New Issue
Block a user