feat: Initial csv parsing for uploading rooms for a project. Need to style the upload form.
All checks were successful
CI / Linux Tests (push) Successful in 5m41s
All checks were successful
CI / Linux Tests (push) Successful in 5m41s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ tailwindcss
|
||||
.env
|
||||
.env*
|
||||
default.profraw
|
||||
rooms.csv
|
||||
|
||||
@@ -7,6 +7,7 @@ let package = Package(
|
||||
products: [
|
||||
.executable(name: "App", targets: ["App"]),
|
||||
.library(name: "AuthClient", targets: ["AuthClient"]),
|
||||
.library(name: "CSVParser", targets: ["CSVParser"]),
|
||||
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
|
||||
.library(name: "EnvVars", targets: ["EnvVars"]),
|
||||
.library(name: "FileClient", targets: ["FileClient"]),
|
||||
@@ -62,6 +63,20 @@ let package = Package(
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "CSVParser",
|
||||
dependencies: [
|
||||
.target(name: "ManualDCore"),
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "CSVParsingTests",
|
||||
dependencies: [
|
||||
.target(name: "CSVParser")
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "DatabaseClient",
|
||||
dependencies: [
|
||||
@@ -172,6 +187,7 @@ let package = Package(
|
||||
name: "ViewController",
|
||||
dependencies: [
|
||||
.target(name: "AuthClient"),
|
||||
.target(name: "CSVParser"),
|
||||
.target(name: "DatabaseClient"),
|
||||
.target(name: "PdfClient"),
|
||||
.target(name: "ProjectClient"),
|
||||
|
||||
43
Sources/CSVParser/Interface.swift
Normal file
43
Sources/CSVParser/Interface.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
|
||||
extension DependencyValues {
|
||||
public var csvParser: CSVParser {
|
||||
get { self[CSVParser.self] }
|
||||
set { self[CSVParser.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
@DependencyClient
|
||||
public struct CSVParser: Sendable {
|
||||
public var parseRooms: @Sendable (Room.CSV) async throws -> [Room.Create]
|
||||
}
|
||||
|
||||
extension CSVParser: DependencyKey {
|
||||
public static let testValue = Self()
|
||||
|
||||
public static let liveValue = Self(
|
||||
parseRooms: { csv in
|
||||
guard let string = String(data: csv.file, encoding: .utf8) else {
|
||||
throw CSVParsingError("Unreadable file data")
|
||||
}
|
||||
let rows = try RoomCSVParser().parse(string[...].utf8)
|
||||
let rooms = rows.reduce(into: [Room.Create]()) {
|
||||
if case .room(let room) = $1 {
|
||||
$0.append(room)
|
||||
}
|
||||
}
|
||||
return rooms
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
public struct CSVParsingError: Error {
|
||||
let reason: String
|
||||
|
||||
public init(_ reason: String) {
|
||||
self.reason = reason
|
||||
}
|
||||
}
|
||||
51
Sources/CSVParser/Internal/Room+parsing.swift
Normal file
51
Sources/CSVParser/Internal/Room+parsing.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import ManualDCore
|
||||
import Parsing
|
||||
|
||||
struct RoomCSVParser: Parser {
|
||||
var body: some Parser<Substring.UTF8View, [RoomRowType]> {
|
||||
Many {
|
||||
RoomRowParser()
|
||||
} separator: {
|
||||
"\n".utf8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RoomRowParser: Parser {
|
||||
|
||||
var body: some Parser<Substring.UTF8View, RoomRowType> {
|
||||
OneOf {
|
||||
RoomCreateParser().map { RoomRowType.room($0) }
|
||||
Prefix { $0 != UInt8(ascii: "\n") }
|
||||
.map(.string)
|
||||
.map { RoomRowType.header($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RoomRowType {
|
||||
case header(String)
|
||||
case room(Room.Create)
|
||||
}
|
||||
|
||||
struct RoomCreateParser: ParserPrinter {
|
||||
|
||||
var body: some ParserPrinter<Substring.UTF8View, Room.Create> {
|
||||
ParsePrint {
|
||||
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
||||
",".utf8
|
||||
Double.parser()
|
||||
",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
",".utf8
|
||||
Int.parser()
|
||||
}
|
||||
.map(.memberwise(Room.Create.init))
|
||||
}
|
||||
}
|
||||
@@ -89,8 +89,8 @@ public struct DatabaseClient: Sendable {
|
||||
|
||||
@DependencyClient
|
||||
public struct Rooms: Sendable {
|
||||
public var create: @Sendable (Room.Create) async throws -> Room
|
||||
public var createMany: @Sendable ([Room.Create]) async throws -> [Room]
|
||||
public var create: @Sendable (Project.ID, Room.Create) async throws -> Room
|
||||
public var createMany: @Sendable (Project.ID, [Room.Create]) async throws -> [Room]
|
||||
public var delete: @Sendable (Room.ID) async throws -> Void
|
||||
public var deleteRectangularSize:
|
||||
@Sendable (Room.ID, Room.RectangularSize.ID) async throws -> Room
|
||||
|
||||
@@ -10,14 +10,14 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
.init(
|
||||
create: { request in
|
||||
let model = try request.toModel()
|
||||
create: { projectID, request in
|
||||
let model = try request.toModel(projectID: projectID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
},
|
||||
createMany: { rooms in
|
||||
createMany: { projectID, rooms in
|
||||
try await rooms.asyncMap { request in
|
||||
let model = try request.toModel()
|
||||
let model = try request.toModel(projectID: projectID)
|
||||
try await model.validateAndSave(on: database)
|
||||
return try model.toDTO()
|
||||
}
|
||||
@@ -83,7 +83,7 @@ extension DatabaseClient.Rooms: TestDependencyKey {
|
||||
|
||||
extension Room.Create {
|
||||
|
||||
func toModel() throws -> RoomModel {
|
||||
func toModel(projectID: Project.ID) throws -> RoomModel {
|
||||
return .init(
|
||||
name: name,
|
||||
heatingLoad: heatingLoad,
|
||||
|
||||
@@ -96,8 +96,6 @@ public struct Room: Codable, Equatable, Identifiable, Sendable {
|
||||
extension Room {
|
||||
/// Represents the data required to create a new room for a project.
|
||||
public struct Create: Codable, Equatable, Sendable {
|
||||
/// The project this room is associated with.
|
||||
public let projectID: Project.ID
|
||||
/// A unique name for the room in the project.
|
||||
public let name: String
|
||||
/// The heating load required for the room (from Manual-J).
|
||||
@@ -114,14 +112,12 @@ extension Room {
|
||||
}
|
||||
|
||||
public init(
|
||||
projectID: Project.ID,
|
||||
name: String,
|
||||
heatingLoad: Double,
|
||||
coolingTotal: Double? = nil,
|
||||
coolingSensible: Double? = nil,
|
||||
registerCount: Int = 1
|
||||
) {
|
||||
self.projectID = projectID
|
||||
self.name = name
|
||||
self.heatingLoad = heatingLoad
|
||||
self.coolingTotal = coolingTotal
|
||||
@@ -130,6 +126,14 @@ extension Room {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CSV: Equatable, Sendable {
|
||||
public let file: Data
|
||||
|
||||
public init(file: Data) {
|
||||
self.file = file
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a rectangular size calculation that is stored in the
|
||||
/// database for a given room.
|
||||
///
|
||||
|
||||
@@ -188,6 +188,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
}
|
||||
|
||||
public enum RoomRoute: Equatable, Sendable {
|
||||
case csv(Room.CSV)
|
||||
case delete(id: Room.ID)
|
||||
case index
|
||||
case submit(Room.Create)
|
||||
@@ -197,6 +198,23 @@ extension SiteRoute.View.ProjectRoute {
|
||||
static let rootPath = "rooms"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.csv)) {
|
||||
Path {
|
||||
rootPath
|
||||
"csv"
|
||||
}
|
||||
Headers {
|
||||
Field("Content-Type") { "multipart/form-data" }
|
||||
}
|
||||
Method.post
|
||||
Body().map(.memberwise(Room.CSV.init))
|
||||
// Body {
|
||||
// FormData {
|
||||
//
|
||||
// }
|
||||
// .map(.memberwise(Room.CSV.init))
|
||||
// }
|
||||
}
|
||||
Route(.case(Self.delete)) {
|
||||
Path {
|
||||
rootPath
|
||||
@@ -215,7 +233,7 @@ extension SiteRoute.View.ProjectRoute {
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field("projectID") { Project.ID.parser() }
|
||||
// Field("projectID") { Project.ID.parser() }
|
||||
Field("name", .string)
|
||||
Field("heatingLoad") { Double.parser() }
|
||||
Optionally {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CSVParser
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Elementary
|
||||
@@ -284,10 +285,18 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
||||
on request: ViewController.Request,
|
||||
projectID: Project.ID
|
||||
) async -> AnySendableHTML {
|
||||
@Dependency(\.csvParser) var csvParser
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch self {
|
||||
|
||||
case .csv(let csv):
|
||||
return await roomsView(on: request, projectID: projectID) {
|
||||
let rooms = try await csvParser.parseRooms(csv)
|
||||
_ = try await database.rooms.createMany(projectID, rooms)
|
||||
}
|
||||
// return EmptyHTML()
|
||||
|
||||
case .delete(let id):
|
||||
return await ResultView {
|
||||
try await database.rooms.delete(id)
|
||||
@@ -298,7 +307,7 @@ extension SiteRoute.View.ProjectRoute.RoomRoute {
|
||||
|
||||
case .submit(let form):
|
||||
return await roomsView(on: request, projectID: projectID) {
|
||||
_ = try await database.rooms.create(form)
|
||||
_ = try await database.rooms.create(projectID, form)
|
||||
}
|
||||
|
||||
case .update(let id, let form):
|
||||
|
||||
@@ -47,8 +47,6 @@ struct RoomForm: HTML, Sendable {
|
||||
.hx.swap(.outerHTML)
|
||||
) {
|
||||
|
||||
input(.class("hidden"), .name("projectID"), .value("\(projectID)"))
|
||||
|
||||
if let id = room?.id {
|
||||
input(.class("hidden"), .name("id"), .value("\(id)"))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ struct RoomsView: HTML, Sendable {
|
||||
let rooms: [Room]
|
||||
let sensibleHeatRatio: Double?
|
||||
|
||||
private var csvRoute: String {
|
||||
SiteRoute.router.path(for: .view(.project(.detail(projectID, .rooms(.index)))))
|
||||
.appendingPath("csv")
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
div(.class("flex w-full flex-col")) {
|
||||
PageTitleRow {
|
||||
@@ -44,6 +49,18 @@ struct RoomsView: HTML, Sendable {
|
||||
.attributes(.class("border border-error"), when: sensibleHeatRatio == nil)
|
||||
}
|
||||
.attributes(.class("tooltip-open"), when: sensibleHeatRatio == nil)
|
||||
|
||||
Tooltip("Upload csv file", position: .left) {
|
||||
form(
|
||||
.hx.post(csvRoute),
|
||||
.hx.target("body"),
|
||||
.hx.swap(.outerHTML),
|
||||
.custom(name: "enctype", value: "multipart/form-data")
|
||||
) {
|
||||
input(.type(.file), .name("file"), .accept(".csv"))
|
||||
SubmitButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("flex items-end space-x-4 font-bold")) {
|
||||
|
||||
22
Tests/CSVParsingTests/CSVParsingTests.swift
Normal file
22
Tests/CSVParsingTests/CSVParsingTests.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import CSVParser
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct CSVParsingTests {
|
||||
|
||||
@Test
|
||||
func roomParsing() async throws {
|
||||
|
||||
let parser = CSVParser.liveValue
|
||||
|
||||
let input = """
|
||||
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
|
||||
Bed-1,12345,12345,,2
|
||||
Bed-2,1223,,1123,1
|
||||
"""
|
||||
let rooms = try await parser.parseRooms(.init(file: Data(input.utf8)))
|
||||
|
||||
#expect(rooms.count == 2)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,8 @@ struct ProjectTests {
|
||||
#expect(completed.frictionRate == true)
|
||||
|
||||
_ = try await database.rooms.create(
|
||||
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
)
|
||||
completed = try await database.projects.getCompletedSteps(project.id)
|
||||
#expect(completed.rooms == true)
|
||||
@@ -130,7 +131,8 @@ struct ProjectTests {
|
||||
.init(projectID: project.id, name: "Test", value: 0.2)
|
||||
)
|
||||
let room = try await database.rooms.create(
|
||||
.init(projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 12345, coolingTotal: 12345)
|
||||
)
|
||||
let supplyLength = try await database.equivalentLengths.create(
|
||||
.init(
|
||||
|
||||
@@ -15,7 +15,8 @@ struct RoomTests {
|
||||
@Dependency(\.database.rooms) var rooms
|
||||
|
||||
let room = try await rooms.create(
|
||||
.init(projectID: project.id, name: "Test", heatingLoad: 1234, coolingTotal: 1234)
|
||||
project.id,
|
||||
.init(name: "Test", heatingLoad: 1234, coolingTotal: 1234)
|
||||
)
|
||||
|
||||
let fetched = try await rooms.fetch(project.id)
|
||||
@@ -48,10 +49,13 @@ struct RoomTests {
|
||||
try await withTestUserAndProject { _, project in
|
||||
@Dependency(\.database.rooms) var rooms
|
||||
|
||||
let created = try await rooms.createMany([
|
||||
.init(projectID: project.id, name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
|
||||
.init(projectID: project.id, name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
|
||||
])
|
||||
let created = try await rooms.createMany(
|
||||
project.id,
|
||||
[
|
||||
.init(name: "Test 1", heatingLoad: 1234, coolingTotal: 1234),
|
||||
.init(name: "Test 2", heatingLoad: 1234, coolingTotal: 1234),
|
||||
]
|
||||
)
|
||||
|
||||
#expect(created.count == 2)
|
||||
#expect(created[0].name == "Test 1")
|
||||
@@ -85,7 +89,7 @@ struct RoomTests {
|
||||
@Test(
|
||||
arguments: [
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
@@ -93,7 +97,7 @@ struct RoomTests {
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: -12345,
|
||||
coolingTotal: 12344,
|
||||
@@ -101,7 +105,7 @@ struct RoomTests {
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: -12344,
|
||||
@@ -109,7 +113,7 @@ struct RoomTests {
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
@@ -117,7 +121,7 @@ struct RoomTests {
|
||||
registerCount: 1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: 12344,
|
||||
@@ -125,7 +129,7 @@ struct RoomTests {
|
||||
registerCount: -1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "",
|
||||
heatingLoad: -12345,
|
||||
coolingTotal: -12344,
|
||||
@@ -133,7 +137,7 @@ struct RoomTests {
|
||||
registerCount: -1
|
||||
),
|
||||
Room.Create(
|
||||
projectID: UUID(0),
|
||||
// projectID: UUID(0),
|
||||
name: "Test",
|
||||
heatingLoad: 12345,
|
||||
coolingTotal: nil,
|
||||
@@ -145,89 +149,12 @@ struct RoomTests {
|
||||
func validations(room: Room.Create) throws {
|
||||
#expect(throws: (any Error).self) {
|
||||
// do {
|
||||
try room.toModel().validate()
|
||||
try room.toModel(projectID: UUID(0)).validate()
|
||||
// } catch {
|
||||
// print("\(error)")
|
||||
// throw error
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func csvParsing() throws {
|
||||
let input = """
|
||||
Name,Heating Load,Cooling Total,Cooling Sensible,Register Count
|
||||
Bed-1,12345,12345,,2
|
||||
Bed-2,1223,,1123,1
|
||||
"""[...].utf8
|
||||
|
||||
let commaSeparator = ParsePrint {
|
||||
OneOf {
|
||||
",".utf8
|
||||
", ".utf8
|
||||
}
|
||||
}
|
||||
|
||||
let rowParser = ParsePrint {
|
||||
Prefix { $0 != UInt8(ascii: ",") }.map(.string)
|
||||
",".utf8
|
||||
Double.parser()
|
||||
Skip { commaSeparator }
|
||||
// ",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
Skip { commaSeparator }
|
||||
// ",".utf8
|
||||
Optionally {
|
||||
Double.parser()
|
||||
}
|
||||
Skip { commaSeparator }
|
||||
// ",".utf8
|
||||
Int.parser()
|
||||
}
|
||||
.map(.memberwise(Row.init))
|
||||
|
||||
let csvRowParser = OneOf {
|
||||
rowParser.map { CSVRowType.row($0) }
|
||||
Prefix { $0 != UInt8(ascii: "\n") }.map(.string).map { CSVRowType.header($0) }
|
||||
}
|
||||
|
||||
let rowsParser = Many {
|
||||
csvRowParser
|
||||
} separator: {
|
||||
"\n".utf8
|
||||
}
|
||||
|
||||
let rows = try rowsParser.parse(input)
|
||||
print("rows: \(rows)")
|
||||
#expect(rows.count == 3)
|
||||
#expect(rows.rows.count == 2)
|
||||
|
||||
print(String(try rowParser.print(rows.rows.first!))!)
|
||||
}
|
||||
}
|
||||
|
||||
enum CSVRowType {
|
||||
case header(String)
|
||||
case row(Row)
|
||||
}
|
||||
|
||||
struct Row {
|
||||
let name: String
|
||||
let heatingLoad: Double
|
||||
let coolingTotal: Double?
|
||||
let coolingSensible: Double?
|
||||
let registerCount: Int
|
||||
}
|
||||
|
||||
extension Array where Element == CSVRowType {
|
||||
var rows: [Row] {
|
||||
reduce(into: [Row]()) {
|
||||
if case .row(let row) = $1 {
|
||||
$0.append(row)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ struct TrunkSizeTests {
|
||||
@Dependency(\.database) var database
|
||||
|
||||
let room = try await database.rooms.create(
|
||||
project.id,
|
||||
.init(
|
||||
projectID: project.id, name: "Test", heatingLoad: 12345, coolingTotal: 12345,
|
||||
coolingSensible: nil, registerCount: 5)
|
||||
name: "Test", heatingLoad: 12345, coolingTotal: 12345,
|
||||
coolingSensible: nil, registerCount: 5
|
||||
)
|
||||
)
|
||||
|
||||
let trunk = try await database.trunkSizes.create(
|
||||
|
||||
@@ -63,6 +63,12 @@ p-6 w-full">
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tooltip tooltip-left" data-tip="Upload csv file">
|
||||
<form hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms/csv" hx-target="body" hx-swap="outerHTML" enctype="multipart/form-data">
|
||||
<input type="file" name="file" accept=".csv">
|
||||
<button class="btn btn-secondary" type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end space-x-4 font-bold">
|
||||
<span class="text-lg">Heating Total</span>
|
||||
@@ -151,7 +157,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000001.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000001" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000001">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Bed-1">
|
||||
@@ -204,7 +209,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000002.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000002" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000002">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Entry">
|
||||
@@ -257,7 +261,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000003.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000003" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000003">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Family Room">
|
||||
@@ -310,7 +313,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000004.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000004" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000004">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Kitchen">
|
||||
@@ -363,7 +365,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000005.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000005" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000005">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Living Room">
|
||||
@@ -416,7 +417,6 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm_00000000000000000000000000000006.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-patch="/projects/00000000-0000-0000-0000-000000000000/rooms/00000000-0000-0000-0000-000000000006" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
<input class="hidden" name="id" value="00000000-0000-0000-0000-000000000006">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="Master">
|
||||
@@ -441,8 +441,7 @@ p-6 w-full">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="roomForm.close()"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
<h1 class="text-3xl font-bold pb-6">Room</h1>
|
||||
<form class="grid grid-cols-1 gap-4" hx-post="/projects/00000000-0000-0000-0000-000000000000/rooms" hx-target="body" hx-swap="outerHTML">
|
||||
<input class="hidden" name="projectID" value="00000000-0000-0000-0000-000000000000">
|
||||
Name<label class="input w-full"><span class="label"></span>
|
||||
<label class="input w-full"><span class="label">Name</span>
|
||||
<input name="name" type="text" placeholder="Name" required autofocus value="">
|
||||
Heating Load</label><label class="input w-full"><span class="label"></span>
|
||||
<input name="heatingLoad" type="number" placeholder="1234" required min="0" value="">
|
||||
|
||||
Reference in New Issue
Block a user