feat: Adds update path to equivalent length form / database / view routes.
This commit is contained in:
@@ -19,13 +19,12 @@
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--spacing: 0.25rem;
|
||||
--breakpoint-lg: 64rem;
|
||||
--container-lg: 32rem;
|
||||
--container-xl: 36rem;
|
||||
--container-4xl: 56rem;
|
||||
--container-7xl: 80rem;
|
||||
--text-xs: 0.75rem;
|
||||
--text-xs--line-height: calc(1 / 0.75);
|
||||
--text-sm: 0.875rem;
|
||||
--text-sm--line-height: calc(1.25 / 0.875);
|
||||
--text-lg: 1.125rem;
|
||||
--text-lg--line-height: calc(1.75 / 1.125);
|
||||
--text-xl: 1.25rem;
|
||||
--text-xl--line-height: calc(1.75 / 1.25);
|
||||
--text-2xl: 1.5rem;
|
||||
@@ -4235,6 +4234,12 @@
|
||||
.right-2 {
|
||||
right: calc(var(--spacing) * 2);
|
||||
}
|
||||
.right-4 {
|
||||
right: calc(var(--spacing) * 4);
|
||||
}
|
||||
.right-6 {
|
||||
right: calc(var(--spacing) * 6);
|
||||
}
|
||||
.dock-sm {
|
||||
@layer daisyui.l1.l2 {
|
||||
height: calc(0.25rem * 14);
|
||||
@@ -4292,6 +4297,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottom-4 {
|
||||
bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
.bottom-6 {
|
||||
bottom: calc(var(--spacing) * 6);
|
||||
}
|
||||
.join {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
@@ -4803,6 +4814,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
.timeline-end {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
grid-column-start: 1;
|
||||
@@ -5272,8 +5286,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
.mx-2 {
|
||||
margin-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
.file-input-ghost {
|
||||
@layer daisyui.l1.l2 {
|
||||
@@ -5615,6 +5629,9 @@
|
||||
.mt-6 {
|
||||
margin-top: calc(var(--spacing) * 6);
|
||||
}
|
||||
.mt-auto {
|
||||
margin-top: auto;
|
||||
}
|
||||
.breadcrumbs {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
max-width: 100%;
|
||||
@@ -6405,6 +6422,9 @@
|
||||
.h-\[1em\] {
|
||||
height: 1em;
|
||||
}
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -6539,21 +6559,9 @@
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
.w-screen {
|
||||
width: 100vw;
|
||||
}
|
||||
.max-w-\[280px\] {
|
||||
max-width: 280px;
|
||||
}
|
||||
.max-w-lg {
|
||||
max-width: var(--container-lg);
|
||||
}
|
||||
.max-w-screen-lg {
|
||||
max-width: var(--breakpoint-lg);
|
||||
}
|
||||
.max-w-xl {
|
||||
max-width: var(--container-xl);
|
||||
}
|
||||
.flex-none {
|
||||
flex: none;
|
||||
}
|
||||
@@ -6738,6 +6746,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
.grid-cols-5 {
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -6759,6 +6773,9 @@
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -7078,10 +7095,6 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 1px;
|
||||
}
|
||||
.border-t {
|
||||
border-top-style: var(--tw-border-style);
|
||||
border-top-width: 1px;
|
||||
}
|
||||
.border-r-2 {
|
||||
border-right-style: var(--tw-border-style);
|
||||
border-right-width: 2px;
|
||||
@@ -7202,6 +7215,12 @@
|
||||
.border-gray-200 {
|
||||
border-color: var(--color-gray-200);
|
||||
}
|
||||
.border-primary {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
.border-secondary {
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
.menu-active {
|
||||
:where(:not(ul, details, .menu-title, .btn))& {
|
||||
@layer daisyui.l1.l2 {
|
||||
@@ -7353,6 +7372,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.bg-base-100 {
|
||||
background-color: var(--color-base-100);
|
||||
}
|
||||
.bg-red-500 {
|
||||
background-color: var(--color-red-500);
|
||||
}
|
||||
@@ -7644,6 +7666,9 @@
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.p-6 {
|
||||
padding: calc(var(--spacing) * 6);
|
||||
}
|
||||
.menu-title {
|
||||
@layer daisyui.l1.l2.l3 {
|
||||
padding-inline: calc(0.25rem * 3);
|
||||
@@ -7770,6 +7795,9 @@
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
.py-1\.5 {
|
||||
padding-block: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -7796,6 +7824,9 @@
|
||||
.pe-2 {
|
||||
padding-inline-end: calc(var(--spacing) * 2);
|
||||
}
|
||||
.pt-6 {
|
||||
padding-top: calc(var(--spacing) * 6);
|
||||
}
|
||||
.pb-4 {
|
||||
padding-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
@@ -7854,6 +7885,14 @@
|
||||
font-size: var(--text-4xl);
|
||||
line-height: var(--tw-leading, var(--text-4xl--line-height));
|
||||
}
|
||||
.text-lg {
|
||||
font-size: var(--text-lg);
|
||||
line-height: var(--tw-leading, var(--text-lg--line-height));
|
||||
}
|
||||
.text-sm {
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||
}
|
||||
.text-xl {
|
||||
font-size: var(--text-xl);
|
||||
line-height: var(--tw-leading, var(--text-xl--line-height));
|
||||
@@ -8421,6 +8460,12 @@
|
||||
.text-info {
|
||||
color: var(--color-info);
|
||||
}
|
||||
.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.text-secondary {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.text-slate-900 {
|
||||
color: var(--color-slate-900);
|
||||
}
|
||||
@@ -8436,6 +8481,9 @@
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.tabular-nums {
|
||||
--tw-numeric-spacing: tabular-nums;
|
||||
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
|
||||
@@ -8495,6 +8543,10 @@
|
||||
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-sm {
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
@@ -9343,6 +9395,16 @@
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
}
|
||||
.md\:grid-cols-2 {
|
||||
@media (width >= 48rem) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.lg\:grid-cols-3 {
|
||||
@media (width >= 64rem) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.dark\:text-white {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color: var(--color-white);
|
||||
|
||||
@@ -119,20 +119,6 @@ private func siteHandler(
|
||||
@Dependency(\.apiController) var apiController
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
request.logger.debug("Site Handler: Route: \(route)")
|
||||
request.logger.debug("Content: \(request.content)")
|
||||
//
|
||||
// // HACK: Can't get arrays to decode currently.
|
||||
if let content = try? request.content.decode(
|
||||
SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo.self
|
||||
) {
|
||||
request.logger.debug("Site Handler: Got step two: \(content)")
|
||||
// return try await viewController.respond(
|
||||
// route: .project(.detail(content.projectID, .equivalentLength(.submit(.two(content))))),
|
||||
// request: request
|
||||
// )
|
||||
}
|
||||
|
||||
switch route {
|
||||
case .api(let route):
|
||||
return try await apiController.respond(route, request: request)
|
||||
|
||||
@@ -10,7 +10,9 @@ extension DatabaseClient {
|
||||
public var create: @Sendable (EffectiveLength.Create) async throws -> EffectiveLength
|
||||
public var delete: @Sendable (EffectiveLength.ID) async throws -> Void
|
||||
public var fetch: @Sendable (Project.ID) async throws -> [EffectiveLength]
|
||||
public var fetchMax: @Sendable (Project.ID) async throws -> EffectiveLength.MaxContainer
|
||||
public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength?
|
||||
public var update: @Sendable (EffectiveLength.Update) async throws -> EffectiveLength
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +39,34 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
|
||||
.all()
|
||||
.map { try $0.toDTO() }
|
||||
},
|
||||
fetchMax: { projectID in
|
||||
let effectiveLengths = try await EffectiveLengthModel.query(on: database)
|
||||
.with(\.$project)
|
||||
.filter(\.$project.$id, .equal, projectID)
|
||||
.all()
|
||||
.map { try $0.toDTO() }
|
||||
|
||||
return .init(
|
||||
supply: effectiveLengths.filter({ $0.type == .supply })
|
||||
.sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength })
|
||||
.first,
|
||||
return: effectiveLengths.filter({ $0.type == .return })
|
||||
.sorted(by: { $0.totalEquivalentLength > $1.totalEquivalentLength })
|
||||
.first
|
||||
)
|
||||
|
||||
},
|
||||
get: { id in
|
||||
try await EffectiveLengthModel.find(id, on: database).map { try $0.toDTO() }
|
||||
},
|
||||
update: { updates in
|
||||
guard let model = try await EffectiveLengthModel.find(updates.id, on: database) else {
|
||||
throw NotFoundError()
|
||||
}
|
||||
if try model.applyUpdates(updates) {
|
||||
try await model.save(on: database)
|
||||
}
|
||||
return try model.toDTO()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -155,4 +183,25 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
|
||||
updatedAt: updatedAt!
|
||||
)
|
||||
}
|
||||
|
||||
func applyUpdates(_ updates: EffectiveLength.Update) throws -> Bool {
|
||||
var hasUpdates = false
|
||||
if let name = updates.name, name != self.name {
|
||||
hasUpdates = true
|
||||
self.name = name
|
||||
}
|
||||
if let type = updates.type, type.rawValue != self.type {
|
||||
hasUpdates = true
|
||||
self.type = type.rawValue
|
||||
}
|
||||
if let straightLengths = updates.straightLengths, straightLengths != self.straightLengths {
|
||||
hasUpdates = true
|
||||
self.straightLengths = straightLengths
|
||||
}
|
||||
if let groups = updates.groups {
|
||||
hasUpdates = true
|
||||
self.groups = try JSONEncoder().encode(groups)
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,29 @@ extension EffectiveLength {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Update: Codable, Equatable, Sendable {
|
||||
|
||||
public let id: EffectiveLength.ID
|
||||
public let name: String?
|
||||
public let type: EffectiveLengthType?
|
||||
public let straightLengths: [Int]?
|
||||
public let groups: [Group]?
|
||||
|
||||
public init(
|
||||
id: EffectiveLength.ID,
|
||||
name: String? = nil,
|
||||
type: EffectiveLength.EffectiveLengthType? = nil,
|
||||
straightLengths: [Int]? = nil,
|
||||
groups: [EffectiveLength.Group]? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.straightLengths = straightLengths
|
||||
self.groups = groups
|
||||
}
|
||||
}
|
||||
|
||||
public enum EffectiveLengthType: String, CaseIterable, Codable, Sendable {
|
||||
case `return`
|
||||
case supply
|
||||
@@ -85,6 +108,32 @@ extension EffectiveLength {
|
||||
self.quantity = quantity
|
||||
}
|
||||
}
|
||||
|
||||
public struct MaxContainer: Codable, Equatable, Sendable {
|
||||
public let supply: EffectiveLength?
|
||||
public let `return`: EffectiveLength?
|
||||
|
||||
public init(supply: EffectiveLength? = nil, return: EffectiveLength? = nil) {
|
||||
self.supply = supply
|
||||
self.return = `return`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength {
|
||||
public var totalEquivalentLength: Double {
|
||||
straightLengths.reduce(into: 0.0) { $0 += Double($1) }
|
||||
+ groups.totalEquivalentLength
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == EffectiveLength.Group {
|
||||
|
||||
public var totalEquivalentLength: Double {
|
||||
reduce(into: 0.0) {
|
||||
$0 += ($1.value * Double($1.quantity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@@ -375,14 +375,23 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public enum EquivalentLengthRoute: Equatable, Sendable {
|
||||
case delete(id: EffectiveLength.ID)
|
||||
case field(FieldType, style: EffectiveLength.EffectiveLengthType? = nil)
|
||||
case form(dismiss: Bool = false)
|
||||
case index
|
||||
case submit(FormStep)
|
||||
case update(StepThree)
|
||||
|
||||
static let rootPath = "effective-lengths"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.delete(id:))) {
|
||||
Path {
|
||||
rootPath
|
||||
EffectiveLength.ID.parser()
|
||||
}
|
||||
Method.delete
|
||||
}
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
@@ -417,6 +426,44 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Method.post
|
||||
FormStep.router
|
||||
}
|
||||
Route(.case(Self.update)) {
|
||||
Path { rootPath }
|
||||
Method.patch
|
||||
Body {
|
||||
FormData {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
}
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
Field("straightLengths") {
|
||||
Int.parser()
|
||||
}
|
||||
}
|
||||
Many {
|
||||
Field("group[group]") {
|
||||
Int.parser()
|
||||
}
|
||||
}
|
||||
Many {
|
||||
Field("group[letter]", .string)
|
||||
}
|
||||
Many {
|
||||
Field("group[length]") {
|
||||
Int.parser()
|
||||
}
|
||||
|
||||
}
|
||||
Many {
|
||||
Field("group[quantity]") {
|
||||
Int.parser()
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(.memberwise(StepThree.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FormStep: Equatable, Sendable {
|
||||
|
||||
@@ -22,7 +22,7 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree {
|
||||
value: Double(groupLengths[n]),
|
||||
quantity: groupQuantities[n]
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
return groups
|
||||
|
||||
@@ -44,3 +44,21 @@ extension EffectiveLength.Create {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension EffectiveLength.Update {
|
||||
init(
|
||||
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
|
||||
projectID: Project.ID
|
||||
) throws {
|
||||
guard let id = form.id else {
|
||||
throw ValidationError("Id not found.")
|
||||
}
|
||||
self.init(
|
||||
id: id,
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
straightLengths: form.straightLengths,
|
||||
groups: form.groups
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +246,11 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
|
||||
case .delete(let id):
|
||||
try await database.effectiveLength.delete(id)
|
||||
return EmptyHTML()
|
||||
|
||||
case .index:
|
||||
return request.view {
|
||||
ProjectView(projectID: projectID, activeTab: .equivalentLength)
|
||||
@@ -262,16 +267,30 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
|
||||
return GroupField(style: style ?? .supply)
|
||||
}
|
||||
|
||||
case .update(let form):
|
||||
_ = try await database.effectiveLength.update(.init(form: form, projectID: projectID))
|
||||
return ProjectView(projectID: projectID, activeTab: .equivalentLength)
|
||||
|
||||
case .submit(let step):
|
||||
switch step {
|
||||
case .one(let stepOne):
|
||||
var effectiveLength: EffectiveLength? = nil
|
||||
if let id = stepOne.id {
|
||||
effectiveLength = try await database.effectiveLength.get(id)
|
||||
}
|
||||
return EffectiveLengthForm.StepTwo(
|
||||
projectID: projectID, stepOne: stepOne, effectiveLength: nil
|
||||
projectID: projectID,
|
||||
stepOne: stepOne,
|
||||
effectiveLength: effectiveLength
|
||||
)
|
||||
case .two(let stepTwo):
|
||||
request.logger.debug("ViewController: Got step two...")
|
||||
var effectiveLength: EffectiveLength? = nil
|
||||
if let id = stepTwo.id {
|
||||
effectiveLength = try await database.effectiveLength.get(id)
|
||||
}
|
||||
return EffectiveLengthForm.StepThree(
|
||||
projectID: projectID, effectiveLength: nil, stepTwo: stepTwo
|
||||
projectID: projectID, effectiveLength: effectiveLength, stepTwo: stepTwo
|
||||
)
|
||||
case .three(let stepThree):
|
||||
request.logger.debug("ViewController: Got step three: \(stepThree)")
|
||||
|
||||
@@ -11,12 +11,22 @@ import Styleguide
|
||||
|
||||
// TODO: Add back buttons / capability??
|
||||
|
||||
// TODO: Add patch / update capability
|
||||
|
||||
struct EffectiveLengthForm: HTML, Sendable {
|
||||
static let id = "equivalentLengthForm"
|
||||
|
||||
static func id(_ equivalentLength: EffectiveLength?) -> String {
|
||||
let base = "equivalentLengthForm"
|
||||
guard let equivalentLength else { return base }
|
||||
return "\(base)_\(equivalentLength.id.uuidString.replacing("-", with: ""))"
|
||||
}
|
||||
|
||||
let projectID: Project.ID
|
||||
let dismiss: Bool
|
||||
let type: EffectiveLength.EffectiveLengthType
|
||||
let effectiveLength: EffectiveLength?
|
||||
|
||||
var id: String { Self.id(effectiveLength) }
|
||||
|
||||
init(
|
||||
projectID: Project.ID,
|
||||
@@ -26,13 +36,27 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
self.projectID = projectID
|
||||
self.dismiss = dismiss
|
||||
self.type = type
|
||||
self.effectiveLength = nil
|
||||
}
|
||||
|
||||
init(
|
||||
effectiveLength: EffectiveLength
|
||||
) {
|
||||
self.dismiss = true
|
||||
self.type = effectiveLength.type
|
||||
self.projectID = effectiveLength.projectID
|
||||
self.effectiveLength = effectiveLength
|
||||
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
ModalForm(id: Self.id, dismiss: dismiss) {
|
||||
ModalForm(
|
||||
id: id,
|
||||
dismiss: dismiss
|
||||
) {
|
||||
h1(.class("text-2xl font-bold")) { "Effective Length" }
|
||||
div(.id("formStep")) {
|
||||
StepOne(projectID: projectID, effectiveLength: nil)
|
||||
div(.id("formStep_\(id)")) {
|
||||
StepOne(projectID: projectID, effectiveLength: effectiveLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +76,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
form(
|
||||
.class("space-y-4"),
|
||||
.hx.post(route),
|
||||
.hx.target("#formStep"),
|
||||
.hx.target("#formStep_\(EffectiveLengthForm.id(effectiveLength))"),
|
||||
.hx.swap(.innerHTML)
|
||||
) {
|
||||
if let id = effectiveLength?.id {
|
||||
@@ -87,7 +111,7 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
form(
|
||||
.class("space-y-4"),
|
||||
.hx.post(route),
|
||||
.hx.target("#formStep"),
|
||||
.hx.target("#formStep_\(EffectiveLengthForm.id(effectiveLength))"),
|
||||
.hx.swap(.innerHTML)
|
||||
) {
|
||||
if let id = effectiveLength?.id {
|
||||
@@ -109,8 +133,14 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
SVG(.circlePlus)
|
||||
}
|
||||
}
|
||||
div(.id("straightLengths")) {
|
||||
StraightLengthField()
|
||||
div(.id("straightLengths"), .class("space-y-4")) {
|
||||
if let effectiveLength {
|
||||
for length in effectiveLength.straightLengths {
|
||||
StraightLengthField(value: length)
|
||||
}
|
||||
} else {
|
||||
StraightLengthField()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -127,16 +157,23 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo
|
||||
|
||||
var route: String {
|
||||
let baseRoute = SiteRoute.View.router.path(
|
||||
for: .project(.detail(projectID, .equivalentLength(.index)))
|
||||
)
|
||||
return "\(baseRoute)/stepThree"
|
||||
if effectiveLength != nil {
|
||||
return SiteRoute.View.router.path(
|
||||
for: .project(.detail(projectID, .equivalentLength(.index))))
|
||||
} else {
|
||||
let baseRoute = SiteRoute.View.router.path(
|
||||
for: .project(.detail(projectID, .equivalentLength(.index)))
|
||||
)
|
||||
return "\(baseRoute)/stepThree"
|
||||
}
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
form(
|
||||
.class("space-y-4"),
|
||||
.hx.post(route),
|
||||
effectiveLength == nil
|
||||
? .hx.post(route)
|
||||
: .hx.patch(route),
|
||||
.hx.target("body"),
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
@@ -163,8 +200,23 @@ struct EffectiveLengthForm: HTML, Sendable {
|
||||
SVG(.circlePlus)
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("grid grid-cols-5 gap-2")) {
|
||||
Label("Group")
|
||||
Label("Letter")
|
||||
Label("Length")
|
||||
Label("Quantity")
|
||||
.attributes(.class("col-span-2"))
|
||||
}
|
||||
|
||||
div(.id("groups"), .class("space-y-4")) {
|
||||
GroupField(style: stepTwo.type)
|
||||
if let effectiveLength {
|
||||
for group in effectiveLength.groups {
|
||||
GroupField(style: effectiveLength.type, group: group)
|
||||
}
|
||||
} else {
|
||||
GroupField(style: stepTwo.type)
|
||||
}
|
||||
}
|
||||
Row {
|
||||
div {}
|
||||
@@ -188,30 +240,38 @@ struct StraightLengthField: HTML, Sendable {
|
||||
name: "straightLengths",
|
||||
placeholder: "Length"
|
||||
)
|
||||
.attributes(.type(.number), .min("0"), .autofocus, .required)
|
||||
.attributes(.type(.number), .min("0"), .autofocus, .required, .value(value))
|
||||
|
||||
TrashButton()
|
||||
.attributes(.data("remove", value: "true"))
|
||||
}
|
||||
.attributes(.hx.ext("remove"))
|
||||
.attributes(.hx.ext("remove"), .class("space-x-4"))
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupField: HTML, Sendable {
|
||||
|
||||
let style: EffectiveLength.EffectiveLengthType
|
||||
let group: EffectiveLength.Group?
|
||||
|
||||
init(style: EffectiveLength.EffectiveLengthType, group: EffectiveLength.Group? = nil) {
|
||||
self.style = style
|
||||
self.group = group
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
Row {
|
||||
div(.class("grid grid-cols-5 gap-2")) {
|
||||
GroupSelect(style: style)
|
||||
Input(name: "group[letter]", placeholder: "Letter")
|
||||
.attributes(.type(.text), .autofocus, .required)
|
||||
.attributes(.type(.text), .autofocus, .required, .value(group?.letter))
|
||||
Input(name: "group[length]", placeholder: "Length")
|
||||
.attributes(.type(.number), .min("0"), .required)
|
||||
.attributes(.type(.number), .min("0"), .required, .value(group?.value))
|
||||
Input(name: "group[quantity]", placeholder: "Quantity")
|
||||
.attributes(.type(.number), .min("1"), .value("1"), .required)
|
||||
TrashButton()
|
||||
.attributes(.data("remove", value: "true"))
|
||||
.attributes(.type(.number), .min("1"), .value("1"), .required, .value(group?.quantity ?? 1))
|
||||
div(.class("flex justify-end")) {
|
||||
TrashButton()
|
||||
.attributes(.data("remove", value: "true"), .class("mx-2"))
|
||||
}
|
||||
}
|
||||
.attributes(.class("space-x-2"), .hx.ext("remove"))
|
||||
}
|
||||
|
||||
@@ -3,32 +3,56 @@ import ElementaryHTMX
|
||||
import ManualDCore
|
||||
import Styleguide
|
||||
|
||||
// TODO: Group into grids of supply / return.
|
||||
|
||||
struct EffectiveLengthsView: HTML, Sendable {
|
||||
|
||||
let projectID: Project.ID
|
||||
let effectiveLengths: [EffectiveLength]
|
||||
|
||||
var supplies: [EffectiveLength] {
|
||||
effectiveLengths.filter({ $0.type == .supply })
|
||||
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
|
||||
}
|
||||
|
||||
var returns: [EffectiveLength] {
|
||||
effectiveLengths.filter({ $0.type == .return })
|
||||
.sorted { $0.totalEquivalentLength > $1.totalEquivalentLength }
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
div(
|
||||
.class("m-4")
|
||||
.class("m-4 space-y-4")
|
||||
) {
|
||||
Row {
|
||||
h1(.class("text-2xl font-bold")) { "Equivalent Lengths" }
|
||||
PlusButton()
|
||||
.attributes(.showModal(id: EffectiveLengthForm.id))
|
||||
.attributes(.showModal(id: EffectiveLengthForm.id(nil)))
|
||||
}
|
||||
.attributes(.class("pb-6"))
|
||||
|
||||
div(
|
||||
.id("effectiveLengths"),
|
||||
.class("space-y-6")
|
||||
) {
|
||||
for row in effectiveLengths {
|
||||
EffectiveLengthView(effectiveLength: row)
|
||||
EffectiveLengthForm(projectID: projectID, dismiss: true)
|
||||
|
||||
div {
|
||||
h2(.class("text-xl font-bold pb-4")) { "Supplies" }
|
||||
div(.class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")) {
|
||||
for (n, row) in supplies.enumerated() {
|
||||
EffectiveLengthView(effectiveLength: row)
|
||||
.attributes(.class(n == 0 ? "border-primary" : "border-gray-200"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
h2(.class("text-xl font-bold pb-4")) { "Returns" }
|
||||
div(.class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 space-x-4 space-y-4")) {
|
||||
for (n, row) in returns.enumerated() {
|
||||
EffectiveLengthView(effectiveLength: row)
|
||||
.attributes(.class(n == 0 ? "border-secondary" : "border-gray-200"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EffectiveLengthForm(projectID: projectID, dismiss: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,52 +66,84 @@ struct EffectiveLengthsView: HTML, Sendable {
|
||||
}
|
||||
|
||||
var groupsTotal: Double {
|
||||
effectiveLength.groups.reduce(into: 0) {
|
||||
$0 += ($1.value * Double($1.quantity))
|
||||
}
|
||||
effectiveLength.groups.totalEquivalentLength
|
||||
}
|
||||
|
||||
var id: String {
|
||||
return "effectiveLenghtCard_\(effectiveLength.id.uuidString.replacing("-", with: ""))"
|
||||
}
|
||||
|
||||
var body: some HTML<HTMLTag.div> {
|
||||
div(
|
||||
.class(
|
||||
"""
|
||||
border border-gray-200 rounded-lg shadow-lg p-4
|
||||
"""
|
||||
)
|
||||
.class("card h-full bg-base-100 shadow-sm border rounded-lg"),
|
||||
.id(id)
|
||||
) {
|
||||
Row {
|
||||
span(.class("text-xl font-bold")) { effectiveLength.name }
|
||||
}
|
||||
Row {
|
||||
div(.class("card-body")) {
|
||||
Row {
|
||||
h2 { effectiveLength.name }
|
||||
div(
|
||||
.class("space-x-4")
|
||||
) {
|
||||
span(.class("text-sm italic")) {
|
||||
"Total"
|
||||
}
|
||||
.attributes(.class("text-primary"), when: effectiveLength.type == .supply)
|
||||
.attributes(.class("text-secondary"), when: effectiveLength.type == .return)
|
||||
|
||||
Number(self.effectiveLength.totalEquivalentLength, digits: 0)
|
||||
.attributes(.class("badge badge-outline text-lg"))
|
||||
.attributes(
|
||||
.class("badge-primary"), when: effectiveLength.type == .supply
|
||||
)
|
||||
.attributes(
|
||||
.class("badge-secondary"), when: effectiveLength.type == .return
|
||||
)
|
||||
}
|
||||
}
|
||||
.attributes(.class("card-title pb-6"))
|
||||
|
||||
Label("Straight Lengths")
|
||||
}
|
||||
for length in effectiveLength.straightLengths {
|
||||
Row {
|
||||
div {}
|
||||
Number(length)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Label("Groups")
|
||||
Label("Equivalent Length")
|
||||
Label("Quantity")
|
||||
}
|
||||
.attributes(.class("border-b border-gray-200"))
|
||||
|
||||
for group in effectiveLength.groups {
|
||||
Row {
|
||||
span { "\(group.group)-\(group.letter)" }
|
||||
Number(group.value)
|
||||
Number(group.quantity)
|
||||
for length in effectiveLength.straightLengths {
|
||||
div(.class("flex justify-end")) {
|
||||
Number(length)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Label("Groups")
|
||||
Label("Equivalent Length")
|
||||
Label("Quantity")
|
||||
}
|
||||
.attributes(.class("border-b border-gray-200"))
|
||||
|
||||
for group in effectiveLength.groups {
|
||||
Row {
|
||||
span { "\(group.group)-\(group.letter)" }
|
||||
Number(group.value)
|
||||
Number(group.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("card-actions justify-end pt-6 space-y-4 mt-auto")) {
|
||||
// TODO: Delete.
|
||||
TrashButton()
|
||||
.attributes(
|
||||
.hx.delete(
|
||||
route: .project(
|
||||
.detail(
|
||||
effectiveLength.projectID, .equivalentLength(.delete(id: effectiveLength.id)))
|
||||
)),
|
||||
.hx.confirm("Are you sure?"),
|
||||
.hx.target("#\(id)"),
|
||||
.hx.swap(.outerHTML)
|
||||
)
|
||||
EditButton()
|
||||
.attributes(.showModal(id: EffectiveLengthForm.id(effectiveLength)))
|
||||
}
|
||||
|
||||
EffectiveLengthForm(effectiveLength: effectiveLength)
|
||||
}
|
||||
Row {
|
||||
Label("Total")
|
||||
Number(Double(straightLengthsTotal) + groupsTotal, digits: 0)
|
||||
.attributes(.class("text-xl font-bold"))
|
||||
}
|
||||
.attributes(.class("border-b border-t border-gray-200"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,11 @@ public struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable
|
||||
script(.src("/js/main.js")) {}
|
||||
link(.rel(.stylesheet), .href("/css/output.css"))
|
||||
link(.rel(.icon), .href("/images/favicon.ico"), .custom(name: "type", value: "image/x-icon"))
|
||||
HTMLRaw(
|
||||
"""
|
||||
<script src="https://unpkg.com/htmx-remove@latest"
|
||||
integrity="sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT"
|
||||
crossorigin="anonymous">
|
||||
</script>
|
||||
"""
|
||||
)
|
||||
script(
|
||||
.src("https://unpkg.com/htmx-remove@latest"),
|
||||
.crossorigin(.anonymous),
|
||||
.integrity("sha384-NwB2Xh66PNEYfVki0ao13UAFmdNtMIdBKZ8sNGRT6hKfCPaINuZ4ScxS6vVAycPT")
|
||||
) {}
|
||||
}
|
||||
|
||||
public var body: some HTML {
|
||||
|
||||
@@ -80,7 +80,6 @@ struct ProjectRouteTests {
|
||||
Optionally {
|
||||
Field("id", default: nil) { EffectiveLength.ID.parser() }
|
||||
}
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
Field("name", .string)
|
||||
Field("type") { EffectiveLength.EffectiveLengthType.parser() }
|
||||
Many {
|
||||
@@ -94,7 +93,7 @@ struct ProjectRouteTests {
|
||||
|
||||
var request = URLRequestData(
|
||||
body: .init(
|
||||
"projectID=15062A72-7AB5-4F15-9B1F-74A4BFA53CBB&name=Test&type=supply&straightLengths=20&straightLengths=10"
|
||||
"name=Test&type=supply&straightLengths=20&straightLengths=10"
|
||||
.utf8
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user