feat: Updates form routes and database routes to use id's in the url path.

This commit is contained in:
2026-01-09 09:25:37 -05:00
parent 9356ccb1c9
commit 30fddb9dce
27 changed files with 677 additions and 322 deletions

View File

@@ -5271,8 +5271,8 @@
.mx-2 { .mx-2 {
margin-inline: calc(var(--spacing) * 2); margin-inline: calc(var(--spacing) * 2);
} }
.mx-4 { .mx-auto {
margin-inline: calc(var(--spacing) * 4); margin-inline: auto;
} }
.file-input-ghost { .file-input-ghost {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
@@ -5569,6 +5569,15 @@
border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px); border-width: var(--border, 1px) 0 var(--border, 1px) var(--border, 1px);
} }
} }
.ms-4 {
margin-inline-start: calc(var(--spacing) * 4);
}
.ms-6 {
margin-inline-start: calc(var(--spacing) * 6);
}
.ms-8 {
margin-inline-start: calc(var(--spacing) * 8);
}
.me-4 { .me-4 {
margin-inline-end: calc(var(--spacing) * 4); margin-inline-end: calc(var(--spacing) * 4);
} }
@@ -6745,6 +6754,9 @@
.grid-cols-1 { .grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr)); grid-template-columns: repeat(1, minmax(0, 1fr));
} }
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid-cols-5 { .grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
@@ -6754,6 +6766,15 @@
.flex-wrap { .flex-wrap {
flex-wrap: wrap; flex-wrap: wrap;
} }
.items-baseline {
align-items: baseline;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.items-start { .items-start {
align-items: flex-start; align-items: flex-start;
} }
@@ -7793,6 +7814,9 @@
.ps-2 { .ps-2 {
padding-inline-start: calc(var(--spacing) * 2); padding-inline-start: calc(var(--spacing) * 2);
} }
.ps-8 {
padding-inline-start: calc(var(--spacing) * 8);
}
.file-input-xl { .file-input-xl {
@layer daisyui.l1.l2 { @layer daisyui.l1.l2 {
padding-inline-end: calc(0.25rem * 6); padding-inline-end: calc(0.25rem * 6);
@@ -9357,9 +9381,9 @@
outline-color: var(--color-indigo-600); outline-color: var(--color-indigo-600);
} }
} }
.md\:hidden { .md\:grid-cols-1 {
@media (width >= 48rem) { @media (width >= 48rem) {
display: none; grid-template-columns: repeat(1, minmax(0, 1fr));
} }
} }
.md\:grid-cols-2 { .md\:grid-cols-2 {
@@ -9420,9 +9444,9 @@
} }
} }
} }
.lg\:hidden { .lg\:grid-cols-2 {
@media (width >= 64rem) { @media (width >= 64rem) {
display: none; grid-template-columns: repeat(2, minmax(0, 1fr));
} }
} }
.lg\:grid-cols-3 { .lg\:grid-cols-3 {
@@ -9430,6 +9454,16 @@
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
.xl\:grid-cols-2 {
@media (width >= 80rem) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.xl\:grid-cols-3 {
@media (width >= 80rem) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.dark\:text-white { .dark\:text-white {
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color: var(--color-white); color: var(--color-white);

View File

@@ -12,6 +12,9 @@ extension DatabaseClient {
public var delete: @Sendable (ComponentPressureLoss.ID) async throws -> Void public var delete: @Sendable (ComponentPressureLoss.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> [ComponentPressureLoss] public var fetch: @Sendable (Project.ID) async throws -> [ComponentPressureLoss]
public var get: @Sendable (ComponentPressureLoss.ID) async throws -> ComponentPressureLoss? public var get: @Sendable (ComponentPressureLoss.ID) async throws -> ComponentPressureLoss?
public var update:
@Sendable (ComponentPressureLoss.ID, ComponentPressureLoss.Update) async throws ->
ComponentPressureLoss
} }
} }
@@ -43,6 +46,17 @@ extension DatabaseClient.ComponentLoss {
}, },
get: { id in get: { id in
try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() } try await ComponentLossModel.find(id, on: database).map { try $0.toDTO() }
},
update: { id, updates in
try updates.validate()
guard let model = try await ComponentLossModel.find(id, on: database) else {
throw NotFoundError()
}
model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database)
}
return try model.toDTO()
} }
) )
} }
@@ -68,6 +82,24 @@ extension ComponentPressureLoss.Create {
} }
} }
extension ComponentPressureLoss.Update {
func validate() throws(ValidationError) {
if let name {
guard !name.isEmpty else {
throw ValidationError("Component loss name should not be empty.")
}
}
if let value {
guard value > 0 else {
throw ValidationError("Component loss value should be greater than 0.")
}
guard value < 1.0 else {
throw ValidationError("Component loss value should be less than 1.0.")
}
}
}
}
extension ComponentPressureLoss { extension ComponentPressureLoss {
struct Migrate: AsyncMigration { struct Migrate: AsyncMigration {
let name = "CreateComponentLoss" let name = "CreateComponentLoss"
@@ -142,4 +174,13 @@ final class ComponentLossModel: Model, @unchecked Sendable {
updatedAt: updatedAt! updatedAt: updatedAt!
) )
} }
func applyUpdates(_ updates: ComponentPressureLoss.Update) {
if let name = updates.name, name != self.name {
self.name = name
}
if let value = updates.value, value != self.value {
self.value = value
}
}
} }

View File

@@ -12,7 +12,8 @@ extension DatabaseClient {
public var fetch: @Sendable (Project.ID) async throws -> [EffectiveLength] public var fetch: @Sendable (Project.ID) async throws -> [EffectiveLength]
public var fetchMax: @Sendable (Project.ID) async throws -> EffectiveLength.MaxContainer public var fetchMax: @Sendable (Project.ID) async throws -> EffectiveLength.MaxContainer
public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength? public var get: @Sendable (EffectiveLength.ID) async throws -> EffectiveLength?
public var update: @Sendable (EffectiveLength.Update) async throws -> EffectiveLength public var update:
@Sendable (EffectiveLength.ID, EffectiveLength.Update) async throws -> EffectiveLength
} }
} }
@@ -59,11 +60,12 @@ extension DatabaseClient.EffectiveLengthClient: TestDependencyKey {
get: { id in get: { id in
try await EffectiveLengthModel.find(id, on: database).map { try $0.toDTO() } try await EffectiveLengthModel.find(id, on: database).map { try $0.toDTO() }
}, },
update: { updates in update: { id, updates in
guard let model = try await EffectiveLengthModel.find(updates.id, on: database) else { guard let model = try await EffectiveLengthModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
if try model.applyUpdates(updates) { try model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database) try await model.save(on: database)
} }
return try model.toDTO() return try model.toDTO()
@@ -184,24 +186,18 @@ final class EffectiveLengthModel: Model, @unchecked Sendable {
) )
} }
func applyUpdates(_ updates: EffectiveLength.Update) throws -> Bool { func applyUpdates(_ updates: EffectiveLength.Update) throws {
var hasUpdates = false
if let name = updates.name, name != self.name { if let name = updates.name, name != self.name {
hasUpdates = true
self.name = name self.name = name
} }
if let type = updates.type, type.rawValue != self.type { if let type = updates.type, type.rawValue != self.type {
hasUpdates = true
self.type = type.rawValue self.type = type.rawValue
} }
if let straightLengths = updates.straightLengths, straightLengths != self.straightLengths { if let straightLengths = updates.straightLengths, straightLengths != self.straightLengths {
hasUpdates = true
self.straightLengths = straightLengths self.straightLengths = straightLengths
} }
if let groups = updates.groups { if let groups = updates.groups {
hasUpdates = true
self.groups = try JSONEncoder().encode(groups) self.groups = try JSONEncoder().encode(groups)
} }
return hasUpdates
} }
} }

View File

@@ -11,7 +11,8 @@ extension DatabaseClient {
public var delete: @Sendable (EquipmentInfo.ID) async throws -> Void public var delete: @Sendable (EquipmentInfo.ID) async throws -> Void
public var fetch: @Sendable (Project.ID) async throws -> EquipmentInfo? public var fetch: @Sendable (Project.ID) async throws -> EquipmentInfo?
public var get: @Sendable (EquipmentInfo.ID) async throws -> EquipmentInfo? public var get: @Sendable (EquipmentInfo.ID) async throws -> EquipmentInfo?
public var update: @Sendable (EquipmentInfo.Update) async throws -> EquipmentInfo public var update:
@Sendable (EquipmentInfo.ID, EquipmentInfo.Update) async throws -> EquipmentInfo
} }
} }
@@ -46,13 +47,15 @@ extension DatabaseClient.Equipment {
get: { id in get: { id in
try await EquipmentModel.find(id, on: database).map { try $0.toDTO() } try await EquipmentModel.find(id, on: database).map { try $0.toDTO() }
}, },
update: { request in update: { id, updates in
guard let model = try await EquipmentModel.find(request.id, on: database) else { guard let model = try await EquipmentModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
guard request.hasUpdates else { return try model.toDTO() } try updates.validate()
try model.applyUpdates(request) model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database) try await model.save(on: database)
}
return try model.toDTO() return try model.toDTO()
} }
) )
@@ -196,8 +199,7 @@ final class EquipmentModel: Model, @unchecked Sendable {
) )
} }
func applyUpdates(_ updates: EquipmentInfo.Update) throws { func applyUpdates(_ updates: EquipmentInfo.Update) {
try updates.validate()
if let staticPressure = updates.staticPressure { if let staticPressure = updates.staticPressure {
self.staticPressure = staticPressure self.staticPressure = staticPressure
} }

View File

@@ -13,7 +13,7 @@ extension DatabaseClient {
public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps public var getCompletedSteps: @Sendable (Project.ID) async throws -> Project.CompletedSteps
public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double? public var getSensibleHeatRatio: @Sendable (Project.ID) async throws -> Double?
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project> public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
public var update: @Sendable (Project.Update) async throws -> Project public var update: @Sendable (Project.ID, Project.Update) async throws -> Project
} }
} }
@@ -85,12 +85,13 @@ extension DatabaseClient.Projects: TestDependencyKey {
.paginate(request) .paginate(request)
.map { try $0.toDTO() } .map { try $0.toDTO() }
}, },
update: { updates in update: { id, updates in
guard let model = try await ProjectModel.find(updates.id, on: database) else { guard let model = try await ProjectModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate() try updates.validate()
if model.applyUpdates(updates) { model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database) try await model.save(on: database)
} }
return try model.toDTO() return try model.toDTO()
@@ -283,34 +284,26 @@ final class ProjectModel: Model, @unchecked Sendable {
) )
} }
func applyUpdates(_ updates: Project.Update) -> Bool { func applyUpdates(_ updates: Project.Update) {
var hasUpdates = false
if let name = updates.name, name != self.name { if let name = updates.name, name != self.name {
hasUpdates = true
self.name = name self.name = name
} }
if let streetAddress = updates.streetAddress, streetAddress != self.streetAddress { if let streetAddress = updates.streetAddress, streetAddress != self.streetAddress {
hasUpdates = true
self.streetAddress = streetAddress self.streetAddress = streetAddress
} }
if let city = updates.city, city != self.city { if let city = updates.city, city != self.city {
hasUpdates = true
self.city = city self.city = city
} }
if let state = updates.state, state != self.state { if let state = updates.state, state != self.state {
hasUpdates = true
self.state = state self.state = state
} }
if let zipCode = updates.zipCode, zipCode != self.zipCode { if let zipCode = updates.zipCode, zipCode != self.zipCode {
hasUpdates = true
self.zipCode = zipCode self.zipCode = zipCode
} }
if let sensibleHeatRatio = updates.sensibleHeatRatio, if let sensibleHeatRatio = updates.sensibleHeatRatio,
sensibleHeatRatio != self.sensibleHeatRatio sensibleHeatRatio != self.sensibleHeatRatio
{ {
hasUpdates = true
self.sensibleHeatRatio = sensibleHeatRatio self.sensibleHeatRatio = sensibleHeatRatio
} }
return hasUpdates
} }
} }

View File

@@ -11,7 +11,7 @@ extension DatabaseClient {
public var delete: @Sendable (Room.ID) async throws -> Void public var delete: @Sendable (Room.ID) async throws -> Void
public var get: @Sendable (Room.ID) async throws -> Room? public var get: @Sendable (Room.ID) async throws -> Room?
public var fetch: @Sendable (Project.ID) async throws -> [Room] public var fetch: @Sendable (Project.ID) async throws -> [Room]
public var update: @Sendable (Room.Update) async throws -> Room public var update: @Sendable (Room.ID, Room.Update) async throws -> Room
} }
} }
@@ -41,13 +41,14 @@ extension DatabaseClient.Rooms: TestDependencyKey {
.all() .all()
.map { try $0.toDTO() } .map { try $0.toDTO() }
}, },
update: { updates in update: { id, updates in
guard let model = try await RoomModel.find(updates.id, on: database) else { guard let model = try await RoomModel.find(id, on: database) else {
throw NotFoundError() throw NotFoundError()
} }
try updates.validate() try updates.validate()
if model.applyUpdates(updates) { model.applyUpdates(updates)
if model.hasChanges {
try await model.save(on: database) try await model.save(on: database)
} }
return try model.toDTO() return try model.toDTO()
@@ -218,30 +219,24 @@ final class RoomModel: Model, @unchecked Sendable {
) )
} }
func applyUpdates(_ updates: Room.Update) -> Bool { func applyUpdates(_ updates: Room.Update) {
var hasUpdates = false
if let name = updates.name, name != self.name { if let name = updates.name, name != self.name {
hasUpdates = true
self.name = name self.name = name
} }
if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad { if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad {
hasUpdates = true
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
} }
if let coolingTotal = updates.coolingTotal, coolingTotal != self.coolingTotal { if let coolingTotal = updates.coolingTotal, coolingTotal != self.coolingTotal {
hasUpdates = true
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal
} }
if let coolingSensible = updates.coolingSensible, coolingSensible != self.coolingSensible { if let coolingSensible = updates.coolingSensible, coolingSensible != self.coolingSensible {
hasUpdates = true
self.coolingSensible = coolingSensible self.coolingSensible = coolingSensible
} }
if let registerCount = updates.registerCount, registerCount != self.registerCount { if let registerCount = updates.registerCount, registerCount != self.registerCount {
hasUpdates = true
self.registerCount = registerCount self.registerCount = registerCount
} }
return hasUpdates
} }
} }

View File

@@ -52,6 +52,26 @@ extension ComponentPressureLoss {
] ]
} }
} }
public struct Update: Codable, Equatable, Sendable {
public let name: String?
public let value: Double?
public init(
name: String? = nil,
value: Double? = nil
) {
self.name = name
self.value = value
}
}
}
extension Array where Element == ComponentPressureLoss {
public var totalComponentPressureLoss: Double {
reduce(into: 0) { $0 += $1.value }
}
} }
public typealias ComponentPressureLosses = [String: Double] public typealias ComponentPressureLosses = [String: Double]

View File

@@ -63,20 +63,17 @@ extension EffectiveLength {
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
public let id: EffectiveLength.ID
public let name: String? public let name: String?
public let type: EffectiveLengthType? public let type: EffectiveLengthType?
public let straightLengths: [Int]? public let straightLengths: [Int]?
public let groups: [Group]? public let groups: [Group]?
public init( public init(
id: EffectiveLength.ID,
name: String? = nil, name: String? = nil,
type: EffectiveLength.EffectiveLengthType? = nil, type: EffectiveLength.EffectiveLengthType? = nil,
straightLengths: [Int]? = nil, straightLengths: [Int]? = nil,
groups: [EffectiveLength.Group]? = nil groups: [EffectiveLength.Group]? = nil
) { ) {
self.id = id
self.name = name self.name = name
self.type = type self.type = type
self.straightLengths = straightLengths self.straightLengths = straightLengths
@@ -113,6 +110,12 @@ extension EffectiveLength {
public let supply: EffectiveLength? public let supply: EffectiveLength?
public let `return`: EffectiveLength? public let `return`: EffectiveLength?
public var total: Double? {
guard let supply else { return nil }
guard let `return` else { return nil }
return supply.totalEquivalentLength + `return`.totalEquivalentLength
}
public init(supply: EffectiveLength? = nil, return: EffectiveLength? = nil) { public init(supply: EffectiveLength? = nil, return: EffectiveLength? = nil) {
self.supply = supply self.supply = supply
self.return = `return` self.return = `return`

View File

@@ -52,18 +52,15 @@ extension EquipmentInfo {
} }
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
public let id: EquipmentInfo.ID
public let staticPressure: Double? public let staticPressure: Double?
public let heatingCFM: Int? public let heatingCFM: Int?
public let coolingCFM: Int? public let coolingCFM: Int?
public init( public init(
id: EquipmentInfo.ID,
staticPressure: Double? = nil, staticPressure: Double? = nil,
heatingCFM: Int? = nil, heatingCFM: Int? = nil,
coolingCFM: Int? = nil coolingCFM: Int? = nil
) { ) {
self.id = id
self.staticPressure = staticPressure self.staticPressure = staticPressure
self.heatingCFM = heatingCFM self.heatingCFM = heatingCFM
self.coolingCFM = coolingCFM self.coolingCFM = coolingCFM

View File

@@ -79,7 +79,6 @@ extension Project {
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
public let id: Project.ID
public let name: String? public let name: String?
public let streetAddress: String? public let streetAddress: String?
public let city: String? public let city: String?
@@ -88,7 +87,6 @@ extension Project {
public let sensibleHeatRatio: Double? public let sensibleHeatRatio: Double?
public init( public init(
id: Project.ID,
name: String? = nil, name: String? = nil,
streetAddress: String? = nil, streetAddress: String? = nil,
city: String? = nil, city: String? = nil,
@@ -96,7 +94,6 @@ extension Project {
zipCode: String? = nil, zipCode: String? = nil,
sensibleHeatRatio: Double? = nil sensibleHeatRatio: Double? = nil
) { ) {
self.id = id
self.name = name self.name = name
self.streetAddress = streetAddress self.streetAddress = streetAddress
self.city = city self.city = city

View File

@@ -63,7 +63,6 @@ extension Room {
} }
public struct Update: Codable, Equatable, Sendable { public struct Update: Codable, Equatable, Sendable {
public let id: Room.ID
public let name: String? public let name: String?
public let heatingLoad: Double? public let heatingLoad: Double?
public let coolingTotal: Double? public let coolingTotal: Double?
@@ -71,14 +70,12 @@ extension Room {
public let registerCount: Int? public let registerCount: Int?
public init( public init(
id: Room.ID,
name: String? = nil, name: String? = nil,
heatingLoad: Double? = nil, heatingLoad: Double? = nil,
coolingTotal: Double? = nil, coolingTotal: Double? = nil,
coolingSensible: Double? = nil, coolingSensible: Double? = nil,
registerCount: Int? = nil registerCount: Int? = nil
) { ) {
self.id = id
self.name = name self.name = name
self.heatingLoad = heatingLoad self.heatingLoad = heatingLoad
self.coolingTotal = coolingTotal self.coolingTotal = coolingTotal

View File

@@ -40,7 +40,7 @@ extension SiteRoute.View {
case form(id: Project.ID? = nil, dismiss: Bool = false) case form(id: Project.ID? = nil, dismiss: Bool = false)
case index case index
case page(PageRequest) case page(PageRequest)
case update(Project.Update) case update(Project.ID, Project.Update)
public static func page(page: Int, per limit: Int) -> Self { public static func page(page: Int, per limit: Int) -> Self {
.page(.init(page: page, per: limit)) .page(.init(page: page, per: limit))
@@ -112,11 +112,13 @@ extension SiteRoute.View {
.map(.memberwise(PageRequest.init)) .map(.memberwise(PageRequest.init))
} }
Route(.case(Self.update)) { Route(.case(Self.update)) {
Path { rootPath } Path {
rootPath
Project.ID.parser()
}
Method.patch Method.patch
Body { Body {
FormData { FormData {
Field("id") { Project.ID.parser() }
Optionally { Optionally {
Field("name", .string) Field("name", .string)
} }
@@ -149,6 +151,7 @@ extension SiteRoute.View.ProjectRoute {
public enum DetailRoute: Equatable, Sendable { public enum DetailRoute: Equatable, Sendable {
case index(tab: Tab = .default) case index(tab: Tab = .default)
case componentLoss(ComponentLossRoute)
case equipment(EquipmentInfoRoute) case equipment(EquipmentInfoRoute)
case equivalentLength(EquivalentLengthRoute) case equivalentLength(EquivalentLengthRoute)
case frictionRate(FrictionRateRoute) case frictionRate(FrictionRateRoute)
@@ -163,6 +166,9 @@ extension SiteRoute.View.ProjectRoute {
} }
} }
} }
Route(.case(Self.componentLoss)) {
ComponentLossRoute.router
}
Route(.case(Self.equipment)) { Route(.case(Self.equipment)) {
EquipmentInfoRoute.router EquipmentInfoRoute.router
} }
@@ -193,7 +199,7 @@ extension SiteRoute.View.ProjectRoute {
case form(id: Room.ID? = nil, dismiss: Bool = false) case form(id: Room.ID? = nil, dismiss: Bool = false)
case index case index
case submit(Room.Create) case submit(Room.Create)
case update(Room.Update) case update(Room.ID, Room.Update)
case updateSensibleHeatRatio(SHRUpdate) case updateSensibleHeatRatio(SHRUpdate)
static let rootPath = "rooms" static let rootPath = "rooms"
@@ -243,11 +249,13 @@ extension SiteRoute.View.ProjectRoute {
} }
} }
Route(.case(Self.update)) { Route(.case(Self.update)) {
Path { rootPath } Path {
rootPath
Room.ID.parser()
}
Method.patch Method.patch
Body { Body {
FormData { FormData {
Field("id") { Room.ID.parser() }
Optionally { Optionally {
Field("name", .string) Field("name", .string)
} }
@@ -291,6 +299,59 @@ extension SiteRoute.View.ProjectRoute {
} }
} }
public enum ComponentLossRoute: Equatable, Sendable {
case index
case delete(ComponentPressureLoss.ID)
case submit(ComponentPressureLoss.Create)
case update(ComponentPressureLoss.ID, ComponentPressureLoss.Update)
static let rootPath = "component-loss"
static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.delete)) {
Path {
rootPath
ComponentPressureLoss.ID.parser()
}
Method.delete
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field("projectID") { Project.ID.parser() }
Field("name", .string)
Field("value") { Double.parser() }
}
.map(.memberwise(ComponentPressureLoss.Create.init))
}
}
Route(.case(Self.update)) {
Path {
rootPath
ComponentPressureLoss.ID.parser()
}
Method.patch
Body {
FormData {
Optionally {
Field("name", .string)
}
Optionally {
Field("value") { Double.parser() }
}
}
.map(.memberwise(ComponentPressureLoss.Update.init))
}
}
}
}
public enum FrictionRateRoute: Equatable, Sendable { public enum FrictionRateRoute: Equatable, Sendable {
case index case index
// TODO: Remove form or move equipment / component losses routes here. // TODO: Remove form or move equipment / component losses routes here.
@@ -326,7 +387,7 @@ extension SiteRoute.View.ProjectRoute {
case index case index
case form(dismiss: Bool) case form(dismiss: Bool)
case submit(EquipmentInfo.Create) case submit(EquipmentInfo.Create)
case update(EquipmentInfo.Update) case update(EquipmentInfo.ID, EquipmentInfo.Update)
static let rootPath = "equipment" static let rootPath = "equipment"
@@ -359,11 +420,13 @@ extension SiteRoute.View.ProjectRoute {
} }
} }
Route(.case(Self.update)) { Route(.case(Self.update)) {
Path { rootPath } Path {
rootPath
EquipmentInfo.ID.parser()
}
Method.patch Method.patch
Body { Body {
FormData { FormData {
Field("id") { EquipmentInfo.ID.parser() }
Optionally { Optionally {
Field("staticPressure", default: nil) { Double.parser() } Field("staticPressure", default: nil) { Double.parser() }
} }
@@ -386,7 +449,7 @@ extension SiteRoute.View.ProjectRoute {
case form(dismiss: Bool = false) case form(dismiss: Bool = false)
case index case index
case submit(FormStep) case submit(FormStep)
case update(StepThree) case update(EffectiveLength.ID, StepThree)
static let rootPath = "effective-lengths" static let rootPath = "effective-lengths"
@@ -433,7 +496,10 @@ extension SiteRoute.View.ProjectRoute {
FormStep.router FormStep.router
} }
Route(.case(Self.update)) { Route(.case(Self.update)) {
Path { rootPath } Path {
rootPath
EffectiveLength.ID.parser()
}
Method.patch Method.patch
Body { Body {
FormData { FormData {

View File

@@ -50,11 +50,7 @@ extension EffectiveLength.Update {
form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree, form: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepThree,
projectID: Project.ID projectID: Project.ID
) throws { ) throws {
guard let id = form.id else {
throw ValidationError("Id not found.")
}
self.init( self.init(
id: id,
name: form.name, name: form.name,
type: form.type, type: form.type,
straightLengths: form.straightLengths, straightLengths: form.straightLengths,

View File

@@ -0,0 +1,20 @@
import Foundation
extension String {
func appendingPath(_ string: String) -> Self {
guard string.starts(with: "/") else {
return self.appending("/\(string)")
}
return self.appending(string)
}
func appendingPath(_ id: UUID?) -> Self {
guard let id else { return self }
return appendingPath(id.uuidString)
}
func appendingPath(_ id: UUID) -> Self {
return appendingPath(id.uuidString)
}
}

View File

@@ -0,0 +1,7 @@
import Foundation
extension UUID {
var idString: String {
uuidString.replacing("-", with: "")
}
}

View File

@@ -105,8 +105,8 @@ extension SiteRoute.View.ProjectRoute {
try await database.projects.delete(id) try await database.projects.delete(id)
return EmptyHTML() return EmptyHTML()
case .update(let form): case .update(let id, let form):
let project = try await database.projects.update(form) let project = try await database.projects.update(id, form)
return ProjectView(projectID: project.id, activeTab: .project) return ProjectView(projectID: project.id, activeTab: .project)
case .detail(let projectID, let route): case .detail(let projectID, let route):
@@ -115,6 +115,8 @@ extension SiteRoute.View.ProjectRoute {
return request.view { return request.view {
ProjectView(projectID: projectID, activeTab: tab) ProjectView(projectID: projectID, activeTab: tab)
} }
case .componentLoss(let route):
return try await route.renderView(on: request, projectID: projectID)
case .equipment(let route): case .equipment(let route):
return try await route.renderView(on: request, projectID: projectID) return try await route.renderView(on: request, projectID: projectID)
case .equivalentLength(let route): case .equivalentLength(let route):
@@ -147,8 +149,8 @@ extension SiteRoute.View.ProjectRoute.EquipmentInfoRoute {
case .submit(let form): case .submit(let form):
let equipment = try await database.equipment.create(form) let equipment = try await database.equipment.create(form)
return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
case .update(let updates): case .update(let id, let updates):
let equipment = try await database.equipment.update(updates) let equipment = try await database.equipment.update(id, updates)
return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID) return EquipmentInfoView(equipmentInfo: equipment, projectID: projectID)
} }
} }
@@ -187,13 +189,14 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
ProjectView(projectID: projectID, activeTab: .rooms) ProjectView(projectID: projectID, activeTab: .rooms)
} }
case .update(let form): case .update(let id, let form):
let _ = try await database.rooms.update(form) let _ = try await database.rooms.update(id, form)
return ProjectView(projectID: projectID, activeTab: .rooms) return ProjectView(projectID: projectID, activeTab: .rooms)
case .updateSensibleHeatRatio(let form): case .updateSensibleHeatRatio(let form):
let _ = try await database.projects.update( let _ = try await database.projects.update(
.init(id: form.projectID, sensibleHeatRatio: form.sensibleHeatRatio) form.projectID,
.init(sensibleHeatRatio: form.sensibleHeatRatio)
) )
return request.view { return request.view {
ProjectView(projectID: projectID, activeTab: .rooms) ProjectView(projectID: projectID, activeTab: .rooms)
@@ -210,8 +213,8 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
switch self { switch self {
case .index: case .index:
let equipment = try await database.equipment.fetch(projectID) // let equipment = try await database.equipment.fetch(projectID)
let componentLosses = try await database.componentLoss.fetch(projectID) // let componentLosses = try await database.componentLoss.fetch(projectID)
return request.view { return request.view {
ProjectView(projectID: projectID, activeTab: .frictionRate) ProjectView(projectID: projectID, activeTab: .frictionRate)
@@ -224,12 +227,36 @@ extension SiteRoute.View.ProjectRoute.FrictionRateRoute {
return div { "REMOVE ME!" } return div { "REMOVE ME!" }
// return EquipmentForm(dismiss: dismiss, projectID: projectID) // return EquipmentForm(dismiss: dismiss, projectID: projectID)
case .componentPressureLoss: case .componentPressureLoss:
return ComponentLossForm(dismiss: dismiss, projectID: projectID) return ComponentLossForm(dismiss: dismiss, projectID: projectID, componentLoss: nil)
} }
} }
} }
} }
extension SiteRoute.View.ProjectRoute.ComponentLossRoute {
func renderView(
on request: ViewController.Request,
projectID: Project.ID
) async throws -> AnySendableHTML {
@Dependency(\.database) var database
switch self {
case .index:
return EmptyHTML()
case .delete(let id):
_ = try await database.componentLoss.delete(id)
return EmptyHTML()
case .submit(let form):
_ = try await database.componentLoss.create(form)
return ProjectView(projectID: projectID, activeTab: .frictionRate)
case .update(let id, let form):
_ = try await database.componentLoss.update(id, form)
return ProjectView(projectID: projectID, activeTab: .frictionRate)
}
}
}
extension SiteRoute.View.ProjectRoute.FrictionRateRoute.FormType { extension SiteRoute.View.ProjectRoute.FrictionRateRoute.FormType {
var id: String { var id: String {
switch self { switch self {
@@ -271,8 +298,8 @@ extension SiteRoute.View.ProjectRoute.EquivalentLengthRoute {
return GroupField(style: style ?? .supply) return GroupField(style: style ?? .supply)
} }
case .update(let form): case .update(let id, let form):
_ = try await database.effectiveLength.update(.init(form: form, projectID: projectID)) _ = try await database.effectiveLength.update(id, .init(form: form, projectID: projectID))
return ProjectView(projectID: projectID, activeTab: .equivalentLength) return ProjectView(projectID: projectID, activeTab: .equivalentLength)
case .submit(let step): case .submit(let step):

View File

@@ -4,38 +4,62 @@ import ManualDCore
import Styleguide import Styleguide
struct ComponentLossForm: HTML, Sendable { struct ComponentLossForm: HTML, Sendable {
static func id(_ componentLoss: ComponentPressureLoss? = nil) -> String {
let base = "componentLossForm"
guard let componentLoss else { return base }
return "\(base)_\(componentLoss.id.idString)"
}
let dismiss: Bool let dismiss: Bool
let projectID: Project.ID let projectID: Project.ID
let componentLoss: ComponentPressureLoss?
var route: String {
SiteRoute.View.router.path(
for: .project(.detail(projectID, .componentLoss(.index)))
)
.appendingPath(componentLoss?.id)
// if let componentLoss {
// return baseRoute.appending("/\(componentLoss.id)")
// }
// return baseRoute
}
var body: some HTML { var body: some HTML {
ModalForm(id: "componentLossForm", dismiss: dismiss) { ModalForm(id: Self.id(componentLoss), dismiss: dismiss) {
h1(.class("text-2xl font-bold")) { "Component Loss" } h1(.class("text-2xl font-bold")) { "Component Loss" }
form(.class("space-y-4 p-4")) { form(
.class("space-y-4 p-4"),
componentLoss == nil
? .hx.post(route)
: .hx.patch(route),
.hx.target("body"),
.hx.swap(.outerHTML)
) {
if let componentLoss {
input(.class("hidden"), .name("id"), .value("\(componentLoss.id)"))
}
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
div { div {
label(.for("name")) { "Name" } label(.for("name")) { "Name" }
Input(id: "name", placeholder: "Name") Input(id: "name", placeholder: "Name")
.attributes(.type(.text), .required, .autofocus) .attributes(.type(.text), .required, .autofocus, .value(componentLoss?.name))
} }
div { div {
label(.for("value")) { "Value" } label(.for("value")) { "Value" }
Input(id: "name", placeholder: "Pressure loss") Input(id: "value", placeholder: "Pressure loss")
.attributes(.type(.number), .min("0"), .max("1"), .step("0.1"), .required)
}
Row {
div {}
div {
CancelButton()
.attributes( .attributes(
.hx.get( .type(.number), .min("0.03"), .max("1.0"), .step("0.1"), .required,
route: .project( .value(componentLoss?.value)
.detail(projectID, .frictionRate(.form(.componentPressureLoss, dismiss: true))))
),
.hx.target("#componentLossForm"),
.hx.swap(.outerHTML)
) )
}
SubmitButton() SubmitButton()
} .attributes(.class("btn-block"))
}
} }
} }
} }

View File

@@ -23,33 +23,71 @@ struct ComponentPressureLossesView: HTML, Sendable {
) )
) { ) {
Row { Row {
div(.class("flex space-x-4 items-center")) {
h1(.class("text-2xl font-bold")) { "Component Pressure Losses" } h1(.class("text-2xl font-bold")) { "Component Pressure Losses" }
div(.class("flex text-primary space-x-2 items-baseline")) {
Number(total)
.attributes(.class("text-xl font-bold badge badge-outline badge-primary"))
span(.class("text-sm italic")) { "Total" }
}
}
PlusButton() PlusButton()
.attributes( .attributes(
.hx.get( .showModal(id: ComponentLossForm.id())
route: .project(
.detail(projectID, .frictionRate(.form(.componentPressureLoss, dismiss: false))))
),
.hx.target("#componentLossForm"),
.hx.swap(.outerHTML)
) )
} }
table(.class("table table-zebra")) {
thead {
tr(.class("text-xl font-bold")) {
th { "Name" }
th { "Value" }
th {}
}
}
tbody {
for row in componentPressureLosses { for row in componentPressureLosses {
Row { TableRow(row: row)
Label { row.name }
Number(row.value)
} }
.attributes(.class("border-b border-gray-200"))
} }
Row {
Label { "Total" }
Number(total)
.attributes(.class("text-xl font-bold"))
} }
} }
ComponentLossForm(dismiss: true, projectID: projectID) ComponentLossForm(dismiss: true, projectID: projectID, componentLoss: nil)
} }
struct TableRow: HTML, Sendable {
let row: ComponentPressureLoss
var body: some HTML<HTMLTag.tr> {
tr(.class("text-lg")) {
td { row.name }
td { Number(row.value) }
td {
div(.class("flex join items-end justify-end mx-auto")) {
TrashButton()
.attributes(
.class("join-item"),
.hx.delete(
route: .project(
.detail(row.projectID, .componentLoss(.delete(row.id)))
)
),
.hx.target("body"),
.hx.swap(.outerHTML),
.hx.confirm("Are your sure?")
)
EditButton()
.attributes(
.class("join-item"),
.showModal(id: ComponentLossForm.id(row))
)
}
ComponentLossForm(dismiss: true, projectID: row.projectID, componentLoss: row)
}
}
}
}
} }

View File

@@ -157,14 +157,14 @@ struct EffectiveLengthForm: HTML, Sendable {
let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo let stepTwo: SiteRoute.View.ProjectRoute.EquivalentLengthRoute.StepTwo
var route: String { var route: String {
if effectiveLength != nil {
return SiteRoute.View.router.path(
for: .project(.detail(projectID, .equivalentLength(.index))))
} else {
let baseRoute = SiteRoute.View.router.path( let baseRoute = SiteRoute.View.router.path(
for: .project(.detail(projectID, .equivalentLength(.index))) for: .project(.detail(projectID, .equivalentLength(.index)))
) )
return "\(baseRoute)/stepThree"
if let effectiveLength {
return baseRoute.appendingPath(effectiveLength.id)
} else {
return baseRoute.appendingPath("stepThree")
} }
} }

View File

@@ -126,20 +126,28 @@ struct EffectiveLengthsView: HTML, Sendable {
} }
div(.class("card-actions justify-end pt-6 space-y-4 mt-auto")) { div(.class("card-actions justify-end pt-6 space-y-4 mt-auto")) {
// TODO: Delete. div(.class("join")) {
TrashButton() TrashButton()
.attributes( .attributes(
.class("join-item"),
.hx.delete( .hx.delete(
route: .project( route: .project(
.detail( .detail(
effectiveLength.projectID, .equivalentLength(.delete(id: effectiveLength.id))) effectiveLength.projectID,
)), .equivalentLength(.delete(id: effectiveLength.id))
)
)
),
.hx.confirm("Are you sure?"), .hx.confirm("Are you sure?"),
.hx.target("#\(id)"), .hx.target("#\(id)"),
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
) )
EditButton() EditButton()
.attributes(.showModal(id: EffectiveLengthForm.id(effectiveLength))) .attributes(
.class("join-item"),
.showModal(id: EffectiveLengthForm.id(effectiveLength))
)
}
} }
EffectiveLengthForm(effectiveLength: effectiveLength) EffectiveLengthForm(effectiveLength: effectiveLength)

View File

@@ -18,18 +18,11 @@ struct EquipmentInfoForm: HTML, Sendable {
return "\(staticPressure)" return "\(staticPressure)"
} }
var heatingCFM: String { var route: String {
guard let heatingCFM = equipmentInfo?.heatingCFM else { SiteRoute.View.router.path(
return "" for: .project(.detail(projectID, .equipment(.index)))
} )
return "\(heatingCFM)" .appendingPath(equipmentInfo?.id)
}
var coolingCFM: String {
guard let heatingCFM = equipmentInfo?.heatingCFM else {
return ""
}
return "\(heatingCFM)"
} }
var body: some HTML { var body: some HTML {
@@ -38,8 +31,8 @@ struct EquipmentInfoForm: HTML, Sendable {
form( form(
.class("space-y-4 p-4"), .class("space-y-4 p-4"),
equipmentInfo != nil equipmentInfo != nil
? .hx.patch(route: .project(.detail(projectID, .equipment(.index)))) ? .hx.patch(route)
: .hx.post(route: .project(.detail(projectID, .equipment(.index)))), : .hx.post(route),
.hx.target("#equipmentInfo"), .hx.target("#equipmentInfo"),
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
) { ) {
@@ -59,12 +52,12 @@ struct EquipmentInfoForm: HTML, Sendable {
div { div {
label(.for("heatingCFM")) { "Heating CFM" } label(.for("heatingCFM")) { "Heating CFM" }
Input(id: "heatingCFM", placeholder: "CFM") Input(id: "heatingCFM", placeholder: "CFM")
.attributes(.type(.number), .min("0"), .value(heatingCFM)) .attributes(.type(.number), .min("0"), .value(equipmentInfo?.heatingCFM))
} }
div { div {
label(.for("coolingCFM")) { "Cooling CFM" } label(.for("coolingCFM")) { "Cooling CFM" }
Input(id: "coolingCFM", placeholder: "CFM") Input(id: "coolingCFM", placeholder: "CFM")
.attributes(.type(.number), .min("0"), .value(coolingCFM)) .attributes(.type(.number), .min("0"), .value(equipmentInfo?.coolingCFM))
} }
div { div {
SubmitButton(title: "Save") SubmitButton(title: "Save")

View File

@@ -23,24 +23,28 @@ struct EquipmentInfoView: HTML, Sendable {
if let equipmentInfo { if let equipmentInfo {
Row { table(.class("table table-zebra")) {
Label { "Static Pressure" } thead {
Number(equipmentInfo.staticPressure) tr {
th { Label("Name") }
th { Label("Value") }
}
}
tbody(.class("text-lg")) {
tr {
td { "Static Pressure" }
td { Number(equipmentInfo.staticPressure) }
}
tr {
td { "Heating CFM" }
td { Number(equipmentInfo.heatingCFM) }
}
tr {
td { "Cooling CFM" }
td { Number(equipmentInfo.coolingCFM) }
} }
.attributes(.class("border-b border-gray-200"))
Row {
Label { "Heating CFM" }
Number(equipmentInfo.heatingCFM)
} }
.attributes(.class("border-b border-gray-200"))
Row {
Label { "Cooling CFM" }
Number(equipmentInfo.coolingCFM)
} }
.attributes(.class("border-b border-gray-200"))
} }
EquipmentInfoForm( EquipmentInfoForm(
dismiss: true, projectID: projectID, equipmentInfo: equipmentInfo dismiss: true, projectID: projectID, equipmentInfo: equipmentInfo

View File

@@ -6,15 +6,95 @@ struct FrictionRateView: HTML, Sendable {
let equipmentInfo: EquipmentInfo? let equipmentInfo: EquipmentInfo?
let componentLosses: [ComponentPressureLoss] let componentLosses: [ComponentPressureLoss]
let equivalentLengths: EffectiveLength.MaxContainer
let projectID: Project.ID let projectID: Project.ID
var availableStaticPressure: Double? {
guard let staticPressure = equipmentInfo?.staticPressure else {
return nil
}
return staticPressure - componentLosses.totalComponentPressureLoss
}
var frictionRateDesignValue: Double? {
guard let availableStaticPressure, let tel = equivalentLengths.total else {
return nil
}
return (((availableStaticPressure * 100) / tel) * 100) / 100
}
var badgeColor: String {
let base = "badge-primary"
guard let frictionRateDesignValue else { return base }
if frictionRateDesignValue >= 0.18 || frictionRateDesignValue <= 0.02 {
return "badge-error"
}
return base
}
var showHighErrors: Bool {
guard let frictionRateDesignValue else { return false }
return frictionRateDesignValue >= 0.18
}
var showLowErrors: Bool {
guard let frictionRateDesignValue else { return false }
return frictionRateDesignValue <= 0.02
}
var body: some HTML { var body: some HTML {
div(.class("p-4 space-y-6")) { div(.class("p-4 space-y-6")) {
h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" } h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" }
div(.class("flex space-x-4")) {
Label("Available Static Pressure")
if let availableStaticPressure {
Number(availableStaticPressure, digits: 2)
.attributes(.class("badge badge-lg badge-outline font-bold ms-4"))
}
}
div(.class("flex space-x-4")) {
if let frictionRateDesignValue {
Label("Friction Rate Design Value")
Number(frictionRateDesignValue, digits: 2)
.attributes(.class("badge badge-lg badge-outline \(badgeColor) font-bold"))
}
}
div(.class("text-error italic")) {
p {
"No component pressures losses"
}
.attributes(.class("hidden"), when: componentLosses.totalComponentPressureLoss > 0)
p {
"Calculated friction rate is below 0.02. The fan may not deliver the required CFM."
br()
" * Increase the blower speed"
br()
" * Increase the blower size"
br()
" * Decrease the Total Effective Length (TEL)"
}
.attributes(.class("hidden"), when: !showLowErrors)
p {
"Calculated friction rate is above 0.18. The fan may deliver too many CFM."
br()
" * Decrease the blower speed"
br()
" * Decreae the blower size"
br()
" * Increase the Total Effective Length (TEL)"
}
.attributes(.class("hidden"), when: !showHighErrors)
}
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
EquipmentInfoView(equipmentInfo: equipmentInfo, projectID: projectID) EquipmentInfoView(equipmentInfo: equipmentInfo, projectID: projectID)
ComponentPressureLossesView( ComponentPressureLossesView(
componentPressureLosses: componentLosses, projectID: projectID componentPressureLosses: componentLosses, projectID: projectID
) )
} }
} }
}
} }

View File

@@ -18,14 +18,19 @@ struct ProjectForm: HTML, Sendable {
self.project = project self.project = project
} }
var route: String {
SiteRoute.View.router.path(for: .project(.index))
.appendingPath(project?.id)
}
var body: some HTML { var body: some HTML {
ModalForm(id: Self.id, dismiss: dismiss) { ModalForm(id: Self.id, dismiss: dismiss) {
h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" }
form( form(
.class("space-y-4 p-4"), .class("space-y-4 p-4"),
project == nil project == nil
? .hx.post(route: .project(.index)) ? .hx.post(route)
: .hx.patch(route: .project(.index)), : .hx.patch(route),
.hx.target("body"), .hx.target("body"),
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
) { ) {

View File

@@ -5,8 +5,6 @@ import ElementaryHTMX
import ManualDCore import ManualDCore
import Styleguide import Styleguide
// TODO: Make view async and load based on the active tab.
struct ProjectView: HTML, Sendable { struct ProjectView: HTML, Sendable {
@Dependency(\.database) var database @Dependency(\.database) var database
@@ -58,7 +56,10 @@ struct ProjectView: HTML, Sendable {
case .frictionRate: case .frictionRate:
try await FrictionRateView( try await FrictionRateView(
equipmentInfo: database.equipment.fetch(projectID), equipmentInfo: database.equipment.fetch(projectID),
componentLosses: database.componentLoss.fetch(projectID), projectID: projectID) componentLosses: database.componentLoss.fetch(projectID),
equivalentLengths: database.effectiveLength.fetchMax(projectID),
projectID: projectID
)
case .ductSizing: case .ductSizing:
div { "FIX ME!" } div { "FIX ME!" }
@@ -75,8 +76,9 @@ struct ProjectView: HTML, Sendable {
} }
} }
// TODO: Update to use DaisyUI drawer. extension ProjectView {
struct Sidebar: HTML {
struct Sidebar: HTML {
let active: SiteRoute.View.ProjectRoute.DetailRoute.Tab let active: SiteRoute.View.ProjectRoute.DetailRoute.Tab
let projectID: Project.ID let projectID: Project.ID
@@ -192,18 +194,17 @@ struct Sidebar: HTML {
href: String, href: String,
isComplete: Bool, isComplete: Bool,
hideIsComplete: Bool = false hideIsComplete: Bool = false
) -> some HTML<HTMLTag.div> { ) -> some HTML<HTMLTag.a> {
div(
.class(
"w-full is-drawer-close:tooltip is-drawer-close:tooltip-right"
),
.data("tip", value: title)
) {
a( a(
.class( .class(
"flex btn btn-soft btn-square btn-block is-drawer-open:justify-between is-drawer-close:items-center" """
flex w-full btn btn-soft btn-square btn-block
is-drawer-open:justify-between is-drawer-close:items-center
is-drawer-close:tooltip is-drawer-close:tooltip-right
"""
), ),
.href(href) .href(href),
.data("tip", value: title)
) { ) {
div(.class("flex is-drawer-open:space-x-4")) { div(.class("flex is-drawer-open:space-x-4")) {
SVG(icon) SVG(icon)
@@ -226,7 +227,6 @@ struct Sidebar: HTML {
.attributes(.class("is-drawer-close:text-green-400"), when: isComplete) .attributes(.class("is-drawer-close:text-green-400"), when: isComplete)
.attributes(.class("is-drawer-close:text-error"), when: !isComplete && !hideIsComplete) .attributes(.class("is-drawer-close:text-error"), when: !isComplete && !hideIsComplete)
} }
}
private func row( private func row(
title: String, title: String,
@@ -234,10 +234,11 @@ struct Sidebar: HTML {
route: SiteRoute.View, route: SiteRoute.View,
isComplete: Bool, isComplete: Bool,
hideIsComplete: Bool = false hideIsComplete: Bool = false
) -> some HTML<HTMLTag.div> { ) -> some HTML<HTMLTag.a> {
row( row(
title: title, icon: icon, href: SiteRoute.View.router.path(for: route), title: title, icon: icon, href: SiteRoute.View.router.path(for: route),
isComplete: isComplete, hideIsComplete: hideIsComplete isComplete: isComplete, hideIsComplete: hideIsComplete
) )
} }
}
} }

View File

@@ -27,6 +27,13 @@ struct RoomForm: HTML, Sendable {
self.room = room self.room = room
} }
var route: String {
SiteRoute.View.router.path(
for: .project(.detail(projectID, .rooms(.index)))
)
.appendingPath(room?.id)
}
var body: some HTML { var body: some HTML {
ModalForm(id: id, dismiss: dismiss) { ModalForm(id: id, dismiss: dismiss) {
h1(.class("text-3xl font-bold pb-6")) { "Room" } h1(.class("text-3xl font-bold pb-6")) { "Room" }
@@ -34,8 +41,8 @@ struct RoomForm: HTML, Sendable {
.class("modal-backdrop"), .class("modal-backdrop"),
.init(name: "method", value: "dialog"), .init(name: "method", value: "dialog"),
room == nil room == nil
? .hx.post(route: .project(.detail(projectID, .rooms(.index)))) ? .hx.post(route)
: .hx.patch(route: .project(.detail(projectID, .rooms(.index)))), : .hx.patch(route),
.hx.target("body"), .hx.target("body"),
.hx.swap(.outerHTML) .hx.swap(.outerHTML)
) { ) {

View File

@@ -121,9 +121,11 @@ struct RoomsView: HTML, Sendable {
Number(room.registerCount) Number(room.registerCount)
} }
td { td {
div(.class("flex justify-end space-x-6")) { div(.class("flex justify-end")) {
div(.class("join")) {
TrashButton() TrashButton()
.attributes( .attributes(
.class("join-item"),
.hx.delete( .hx.delete(
route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))), route: .project(.detail(room.projectID, .rooms(.delete(id: room.id))))),
.hx.target("closest tr"), .hx.target("closest tr"),
@@ -131,9 +133,11 @@ struct RoomsView: HTML, Sendable {
) )
EditButton() EditButton()
.attributes( .attributes(
.class("join-item"),
.showModal(id: "roomForm_\(room.name)") .showModal(id: "roomForm_\(room.name)")
) )
} }
}
RoomForm( RoomForm(
id: "roomForm_\(room.name)", id: "roomForm_\(room.name)",
dismiss: true, dismiss: true,