Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ follow semantic versioning; release dates are ISO 8601.

## v2.2.3 — Planned

### Public API

- **A structured proposal document model.** The proposal family's data layer knew one
shape — a titled run of prose sections with a flat timeline and pricing list — which
cannot carry the structured business proposal: brand marks, an authored multi-line
title, an at-a-glance fact card, goal cells, a numbered scope list, authored
deliverable columns, a phase grid with its own headers, priced rows with a
`Role` (`NONE` / `SUBTOTAL` / `OPTIONAL`) and a total band, and a signing card.
`templates.data.proposal` now carries that second model —
`StructuredProposalData` (+ its section records) wrapped by
`StructuredProposalDocumentSpec` — alongside the narrative one; a preset consumes
the model whose shape it renders. Every component normalizes `null` to its empty
form and freezes its collections, matching the family's existing records.

### Templates

- **Monogram Sidebar draws the employer.** Its experience entries rendered the position,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The signing card that closes a structured proposal: an icon-badged
* heading, the acceptance statement, and the labelled signature fields.
*
* @param heading the section heading
* @param icon the icon token of the section badge
* @param statement the acceptance statement paragraph
* @param fields the signature field labels, in order (e.g. name, date,
* signature)
*/
public record ProposalAcceptance(
String heading,
String icon,
String statement,
List<String> fields) {

/**
* Normalizes optional fields and freezes the field list.
*/
public ProposalAcceptance {
heading = Objects.requireNonNullElse(heading, "");
icon = Objects.requireNonNullElse(icon, "");
statement = Objects.requireNonNullElse(statement, "");
fields = List.copyOf(Objects.requireNonNullElse(fields, List.of()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.Objects;

/**
* The sender's brand marks for a structured proposal: the logo monogram,
* the two wordmark lines, the tracked document label, and the footer line.
*
* <p>These are display strings, not identity data — the narrative model's
* {@link ProposalParty} carries the sender's address and contact details;
* this record carries only what the page chrome draws.</p>
*
* @param monogram the one- or two-letter logo mark
* @param nameLine1 first wordmark line
* @param nameLine2 second wordmark line
* @param documentLabel the document label, set in tracked capitals
* @param website the website line shown in the footer
* @param footerName the brand name shown in the footer
*/
public record ProposalBrand(
String monogram,
String nameLine1,
String nameLine2,
String documentLabel,
String website,
String footerName) {

/**
* Normalizes optional fields to empty strings.
*/
public ProposalBrand {
monogram = Objects.requireNonNullElse(monogram, "");
nameLine1 = Objects.requireNonNullElse(nameLine1, "");
nameLine2 = Objects.requireNonNullElse(nameLine2, "");
documentLabel = Objects.requireNonNullElse(documentLabel, "");
website = Objects.requireNonNullElse(website, "");
footerName = Objects.requireNonNullElse(footerName, "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The deliverables band of a structured proposal: an icon-badged heading and
* two authored bullet columns.
*
* <p>The left/right split is authored rather than computed so it can be
* tuned per proposal — moving one long bullet between columns is a data
* revision, not a layout change.</p>
*
* @param heading the section heading
* @param icon the icon token of the section badge
* @param leftColumn the bullets of the left column, in order
* @param rightColumn the bullets of the right column, in order
*/
public record ProposalDeliverables(
String heading,
String icon,
List<String> leftColumn,
List<String> rightColumn) {

/**
* Normalizes optional fields and freezes both columns.
*/
public ProposalDeliverables {
heading = Objects.requireNonNullElse(heading, "");
icon = Objects.requireNonNullElse(icon, "");
leftColumn = List.copyOf(Objects.requireNonNullElse(leftColumn, List.of()));
rightColumn = List.copyOf(Objects.requireNonNullElse(rightColumn, List.of()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The at-a-glance fact card of a structured proposal.
*
* @param heading the card heading
* @param facts the fact rows, in order
*/
public record ProposalGlance(String heading, List<Fact> facts) {

/**
* Normalizes optional fields and freezes the fact list.
*/
public ProposalGlance {
heading = Objects.requireNonNullElse(heading, "");
facts = List.copyOf(Objects.requireNonNullElse(facts, List.of()));
}

/**
* One fact row: an icon, a quiet label, the value line, and an optional
* parenthetical note.
*
* @param icon the icon token of the fact
* @param label the quiet label above the value
* @param value the emphasized value line
* @param note the optional second value line; empty when the row has none
*/
public record Fact(String icon, String label, String value, String note) {

/**
* Normalizes optional fields to empty strings.
*/
public Fact {
icon = Objects.requireNonNullElse(icon, "");
label = Objects.requireNonNullElse(label, "");
value = Objects.requireNonNullElse(value, "");
note = Objects.requireNonNullElse(note, "");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The project-goals band of a structured proposal: an icon-badged heading
* and a horizontal run of icon-beside-text goal cells.
*
* @param heading the section heading
* @param icon the icon token of the section badge
* @param items the goal cells, in order
*/
public record ProposalGoals(String heading, String icon, List<Goal> items) {

/**
* Normalizes optional fields and freezes the goal list.
*/
public ProposalGoals {
heading = Objects.requireNonNullElse(heading, "");
icon = Objects.requireNonNullElse(icon, "");
items = List.copyOf(Objects.requireNonNullElse(items, List.of()));
}

/**
* One goal cell.
*
* @param icon the icon token of the goal
* @param text the goal statement
*/
public record Goal(String icon, String text) {

/**
* Normalizes optional fields to empty strings.
*/
public Goal {
icon = Objects.requireNonNullElse(icon, "");
text = Objects.requireNonNullElse(text, "");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The priced block of a structured proposal: an icon-badged heading, the
* authored table headers, the priced rows with their roles, and the total
* band.
*
* <p>Distinct from the narrative model's {@link ProposalPricingRow}: a row
* here names its role through {@link Role} — presets are expected to set
* subtotal and optional rows apart — and the grand total is its own
* labelled band rather than the last row of the list.</p>
*
* @param heading the section heading
* @param icon the icon token of the section badge
* @param itemHeader the header label of the item column
* @param amountHeader the header label of the amount column
* @param rows the priced rows, in order
* @param totalLabel the label of the total band
* @param totalAmount the amount of the total band
*/
public record ProposalInvestment(
String heading,
String icon,
String itemHeader,
String amountHeader,
List<Row> rows,
String totalLabel,
String totalAmount) {

/**
* Normalizes optional fields and freezes the row list.
*/
public ProposalInvestment {
heading = Objects.requireNonNullElse(heading, "");
icon = Objects.requireNonNullElse(icon, "");
itemHeader = Objects.requireNonNullElse(itemHeader, "");
amountHeader = Objects.requireNonNullElse(amountHeader, "");
rows = List.copyOf(Objects.requireNonNullElse(rows, List.of()));
totalLabel = Objects.requireNonNullElse(totalLabel, "");
totalAmount = Objects.requireNonNullElse(totalAmount, "");
}

/**
* One priced row.
*
* @param label the row label
* @param amount the row amount text
* @param role the row's role; {@code null} normalizes to
* {@link Role#NONE}
*/
public record Row(String label, String amount, Role role) {

/**
* Normalizes optional fields.
*/
public Row {
label = Objects.requireNonNullElse(label, "");
amount = Objects.requireNonNullElse(amount, "");
role = Objects.requireNonNullElse(role, Role.NONE);
}
}

/**
* The role of a priced row — named for what the row is, not for how it
* looks, so a theme change moves the styling without touching the data.
*/
public enum Role {
/** An ordinary priced row. */
NONE,
/** A subtotal row; presets are expected to set it apart. */
SUBTOTAL,
/** An optional line item; presets are expected to mark its amount. */
OPTIONAL
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.Objects;

/**
* The prepared-for / prepared-by / date line under a structured proposal's
* title.
*
* @param preparedFor the client the proposal is prepared for
* @param preparedBy the sender the proposal is prepared by
* @param date the proposal date text
*/
public record ProposalMetaLine(String preparedFor, String preparedBy, String date) {

/**
* Normalizes optional fields to empty strings.
*/
public ProposalMetaLine {
preparedFor = Objects.requireNonNullElse(preparedFor, "");
preparedBy = Objects.requireNonNullElse(preparedBy, "");
date = Objects.requireNonNullElse(date, "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.demcha.compose.document.templates.data.proposal;

import java.util.List;
import java.util.Objects;

/**
* The phase grid of a structured proposal: an icon-badged heading, the
* authored column headers, and one row per phase.
*
* <p>Distinct from the narrative model's {@link ProposalTimelineItem}: a
* phase carries a number, a focus column and an output column, and the grid
* owns its header labels — the narrative timeline is a phase/duration/details
* triple with preset-owned headers.</p>
*
* <p>Unlike {@link ProposalInvestment}, whose two columns are fixed by the
* concept and therefore named fields, the phase grid's column set belongs
* to the preset that draws it — so the headers stay an authored list in
* column order. A preset validates that the list carries one label per
* column it renders; the data layer does not fix the count.</p>
*
* @param heading the section heading
* @param icon the icon token of the section badge
* @param columnHeaders the grid's header labels, one per rendered column,
* in column order
* @param phases the phase rows, in order
*/
public record ProposalPhaseGrid(
String heading,
String icon,
List<String> columnHeaders,
List<Phase> phases) {

/**
* Normalizes optional fields and freezes both lists.
*/
public ProposalPhaseGrid {
heading = Objects.requireNonNullElse(heading, "");
icon = Objects.requireNonNullElse(icon, "");
columnHeaders = List.copyOf(Objects.requireNonNullElse(columnHeaders, List.of()));
phases = List.copyOf(Objects.requireNonNullElse(phases, List.of()));
}

/**
* One phase row of the grid.
*
* @param number the phase number, as authored (e.g. {@code "1"})
* @param name the phase name
* @param focus what the phase concentrates on
* @param duration the phase duration text
* @param output what the phase delivers
*/
public record Phase(
String number,
String name,
String focus,
String duration,
String output) {

/**
* Normalizes optional fields to empty strings.
*/
public Phase {
number = Objects.requireNonNullElse(number, "");
name = Objects.requireNonNullElse(name, "");
focus = Objects.requireNonNullElse(focus, "");
duration = Objects.requireNonNullElse(duration, "");
output = Objects.requireNonNullElse(output, "");
}
}
}
Loading
Loading