| @@ -50,12 +50,14 @@ Class BoardsController_Class | |||
| slug = boards_Repository().UniqueSlug(GenerateSlug(boardName), 0) | |||
| Dim board : Set board = New POBO_boards | |||
| board.name = boardName | |||
| board.slug = slug | |||
| board.created_at = Now() | |||
| board.created_by = currentUsername | |||
| board.updated_at = Now() | |||
| board.updated_by = currentUsername | |||
| board.name = boardName | |||
| board.slug = slug | |||
| board.import_from_printstream = (Request.Form("import_from_printstream") = "on") | |||
| board.printstream_job_name = Trim(CStr(Request.Form("printstream_job_name"))) | |||
| board.created_at = Now() | |||
| board.created_by = currentUsername | |||
| board.updated_at = Now() | |||
| board.updated_by = currentUsername | |||
| boards_Repository().AddNew board | |||
| @@ -117,6 +119,11 @@ Class BoardsController_Class | |||
| """swim_lane_id"":" & cardItem.swim_lane_id & "," & _ | |||
| """job_number"":" & JsonStr(cardItem.job_number) & "," & _ | |||
| """job_name"":" & JsonStr(cardItem.job_name) & "," & _ | |||
| """customer_name"":" & JsonStr(cardItem.customer_name) & "," & _ | |||
| """delivery_date"":" & JsonDateStr(cardItem.delivery_date) & "," & _ | |||
| """quantity"":" & JsonStr(cardItem.quantity) & "," & _ | |||
| """notes"":" & JsonStr(cardItem.notes) & "," & _ | |||
| """full_note"":" & JsonStr(cardItem.full_note) & "," & _ | |||
| """position"":" & cardItem.position & "}" | |||
| firstCard = False | |||
| Loop | |||
| @@ -168,10 +175,12 @@ Class BoardsController_Class | |||
| Dim newSlug : newSlug = boards_Repository().UniqueSlug(GenerateSlug(newName), CLng(board.id)) | |||
| board.name = newName | |||
| board.slug = newSlug | |||
| board.updated_at = Now() | |||
| board.updated_by = GetCurrentUsername() | |||
| board.name = newName | |||
| board.slug = newSlug | |||
| board.import_from_printstream = (Request.Form("import_from_printstream") = "on") | |||
| board.printstream_job_name = Trim(CStr(Request.Form("printstream_job_name"))) | |||
| board.updated_at = Now() | |||
| board.updated_by = GetCurrentUsername() | |||
| boards_Repository().Update board | |||
| @@ -226,6 +235,24 @@ Class BoardsController_Class | |||
| JsonStr = """" & v & """" | |||
| End Function | |||
| Private Function JsonDateStr(d) | |||
| If IsNull(d) Or IsEmpty(d) Then | |||
| JsonDateStr = "null" | |||
| Exit Function | |||
| End If | |||
| On Error Resume Next | |||
| Dim dt, s | |||
| dt = CDate(d) | |||
| s = Year(dt) & "-" & Right("0" & Month(dt), 2) & "-" & Right("0" & Day(dt), 2) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| JsonDateStr = "null" | |||
| Else | |||
| JsonDateStr = """" & s & """" | |||
| End If | |||
| On Error GoTo 0 | |||
| End Function | |||
| End Class | |||
| Dim BoardsController_Class__Singleton | |||
| @@ -37,22 +37,39 @@ Class CardsController_Class | |||
| Dim username : username = GetCurrentUsername() | |||
| Dim card : Set card = New POBO_cards | |||
| card.board_id = boardId | |||
| card.column_id = columnId | |||
| card.swim_lane_id = swimLaneId | |||
| card.job_number = jobNum | |||
| card.job_name = jobName | |||
| card.position = nextPos | |||
| card.created_at = Now() | |||
| card.created_by = username | |||
| card.updated_at = Now() | |||
| card.updated_by = username | |||
| card.board_id = boardId | |||
| card.column_id = columnId | |||
| card.swim_lane_id = swimLaneId | |||
| card.job_number = jobNum | |||
| card.job_name = jobName | |||
| card.customer_name = Trim(CStr(Request.Form("customer_name") & "")) | |||
| card.delivery_date = Trim(CStr(Request.Form("delivery_date") & "")) | |||
| card.quantity = Trim(CStr(Request.Form("quantity") & "")) | |||
| card.notes = Trim(CStr(Request.Form("notes") & "")) | |||
| card.full_note = CStr(Request.Form("full_note") & "") | |||
| card.position = nextPos | |||
| card.created_at = Now() | |||
| card.created_by = username | |||
| card.updated_at = Now() | |||
| card.updated_by = username | |||
| On Error Resume Next | |||
| cards_Repository().AddNew card | |||
| If Err.Number <> 0 Then | |||
| Response.Write "{""ok"":false,""error"":" & JsonString(Err.Description) & "}" | |||
| Err.Clear | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| Response.Write "{""ok"":true,""id"":" & card.id & "," & _ | |||
| """job_number"":" & JsonString(card.job_number) & "," & _ | |||
| """job_name"":" & JsonString(card.job_name) & "," & _ | |||
| """customer_name"":" & JsonString(card.customer_name) & "," & _ | |||
| """delivery_date"":" & JsonDateStr(card.delivery_date) & "," & _ | |||
| """quantity"":" & JsonString(card.quantity) & "," & _ | |||
| """notes"":" & JsonString(card.notes) & "," & _ | |||
| """full_note"":" & JsonString(card.full_note) & "," & _ | |||
| """column_id"":" & card.column_id & "," & _ | |||
| """swim_lane_id"":" & card.swim_lane_id & "," & _ | |||
| """position"":" & card.position & "}" | |||
| @@ -77,14 +94,31 @@ Class CardsController_Class | |||
| card.job_number = Trim(CStr(Request.Form("job_number"))) | |||
| card.job_name = Trim(CStr(Request.Form("job_name"))) | |||
| card.customer_name = Trim(CStr(Request.Form("customer_name") & "")) | |||
| card.delivery_date = Trim(CStr(Request.Form("delivery_date") & "")) | |||
| card.quantity = Trim(CStr(Request.Form("quantity") & "")) | |||
| card.notes = Trim(CStr(Request.Form("notes") & "")) | |||
| card.full_note = CStr(Request.Form("full_note") & "") | |||
| card.updated_at = Now() | |||
| card.updated_by = GetCurrentUsername() | |||
| On Error Resume Next | |||
| cards_Repository().Update card | |||
| If Err.Number <> 0 Then | |||
| Response.Write "{""ok"":false,""error"":" & JsonString(Err.Description) & "}" | |||
| Err.Clear | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| Response.Write "{""ok"":true," & _ | |||
| """job_number"":" & JsonString(card.job_number) & "," & _ | |||
| """job_name"":" & JsonString(card.job_name) & "}" | |||
| """job_name"":" & JsonString(card.job_name) & "," & _ | |||
| """customer_name"":" & JsonString(card.customer_name) & "," & _ | |||
| """delivery_date"":" & JsonDateStr(card.delivery_date) & "," & _ | |||
| """quantity"":" & JsonString(card.quantity) & "," & _ | |||
| """notes"":" & JsonString(card.notes) & "," & _ | |||
| """full_note"":" & JsonString(card.full_note) & "}" | |||
| End Sub | |||
| ' POST /cards/:id/move — form: column_id, swim_lane_id, position, [sibling_ids CSV for reorder] | |||
| @@ -147,7 +181,31 @@ Class CardsController_Class | |||
| End Function | |||
| Private Function JsonString(s) | |||
| JsonString = """" & Replace(Replace(CStr(s), "\", "\\"), """", "\""") & """" | |||
| Dim v : v = CStr(s & "") | |||
| v = Replace(v, "\", "\\") | |||
| v = Replace(v, """", "\""") | |||
| v = Replace(v, vbCrLf, "\n") | |||
| v = Replace(v, vbLf, "\n") | |||
| v = Replace(v, vbCr, "\n") | |||
| JsonString = """" & v & """" | |||
| End Function | |||
| Private Function JsonDateStr(d) | |||
| If IsNull(d) Or IsEmpty(d) Then | |||
| JsonDateStr = "null" | |||
| Exit Function | |||
| End If | |||
| On Error Resume Next | |||
| Dim dt, s | |||
| dt = CDate(d) | |||
| s = Year(dt) & "-" & Right("0" & Month(dt), 2) & "-" & Right("0" & Day(dt), 2) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| JsonDateStr = "null" | |||
| Else | |||
| JsonDateStr = """" & s & """" | |||
| End If | |||
| On Error GoTo 0 | |||
| End Function | |||
| End Class | |||
| @@ -102,19 +102,42 @@ Class ColumnsController_Class | |||
| End If | |||
| Dim rawJson : rawJson = GetRawJsonFromRequest() | |||
| Dim parsed : Set parsed = JSON.parse(rawJson) | |||
| Dim parser : Set parser = New aspJSON | |||
| Dim parsed | |||
| If IsNull(parsed) Or IsEmpty(parsed) Then | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON""}" | |||
| On Error Resume Next | |||
| parser.loadJSON rawJson | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON payload""}" | |||
| Exit Sub | |||
| End If | |||
| Set parsed = parser.data | |||
| On Error GoTo 0 | |||
| If parsed Is Nothing Or parsed.Count = 0 Then | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON payload""}" | |||
| Exit Sub | |||
| End If | |||
| Dim username : username = GetCurrentUsername() | |||
| Dim i, item | |||
| On Error Resume Next | |||
| For i = 0 To parsed.Count - 1 | |||
| Set item = parsed.Item(i) | |||
| board_columns_Repository().UpdatePosition CLng(item.Item("id")), CLng(item.Item("position")), Now(), username | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid reorder item at index " & i & """}" | |||
| Exit Sub | |||
| End If | |||
| Next | |||
| On Error GoTo 0 | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":true}" | |||
| End Sub | |||
| @@ -102,19 +102,42 @@ Class SwimLanesController_Class | |||
| End If | |||
| Dim rawJson : rawJson = GetRawJsonFromRequest() | |||
| Dim parsed : Set parsed = JSON.parse(rawJson) | |||
| Dim parser : Set parser = New aspJSON | |||
| Dim parsed | |||
| If IsNull(parsed) Or IsEmpty(parsed) Then | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON""}" | |||
| On Error Resume Next | |||
| parser.loadJSON rawJson | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON payload""}" | |||
| Exit Sub | |||
| End If | |||
| Set parsed = parser.data | |||
| On Error GoTo 0 | |||
| If parsed Is Nothing Or parsed.Count = 0 Then | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid JSON payload""}" | |||
| Exit Sub | |||
| End If | |||
| Dim username : username = GetCurrentUsername() | |||
| Dim i, item | |||
| On Error Resume Next | |||
| For i = 0 To parsed.Count - 1 | |||
| Set item = parsed.Item(i) | |||
| swim_lanes_Repository().UpdatePosition CLng(item.Item("id")), CLng(item.Item("position")), Now(), username | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":false,""error"":""Invalid reorder item at index " & i & """}" | |||
| Exit Sub | |||
| End If | |||
| Next | |||
| On Error GoTo 0 | |||
| Set parser = Nothing | |||
| Response.Write "{""ok"":true}" | |||
| End Sub | |||
| @@ -5,20 +5,24 @@ Class POBO_boards | |||
| Private p_id | |||
| Private p_name | |||
| Private p_slug | |||
| Private p_import_from_printstream | |||
| Private p_printstream_job_name | |||
| Private p_created_at | |||
| Private p_created_by | |||
| Private p_updated_at | |||
| Private p_updated_by | |||
| Private Sub Class_Initialize() | |||
| p_id = 0 | |||
| p_name = "" | |||
| p_slug = "" | |||
| p_created_at = #1/1/1970# | |||
| p_created_by = "" | |||
| p_updated_at = #1/1/1970# | |||
| p_updated_by = "" | |||
| Properties = Array("id","name","slug","created_at","created_by","updated_at","updated_by") | |||
| p_id = 0 | |||
| p_name = "" | |||
| p_slug = "" | |||
| p_import_from_printstream = False | |||
| p_printstream_job_name = "" | |||
| p_created_at = #1/1/1970# | |||
| p_created_by = "" | |||
| p_updated_at = #1/1/1970# | |||
| p_updated_by = "" | |||
| Properties = Array("id","name","slug","import_from_printstream","printstream_job_name","created_at","created_by","updated_at","updated_by") | |||
| End Sub | |||
| Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property | |||
| @@ -33,6 +37,12 @@ Class POBO_boards | |||
| Public Property Get slug() : slug = p_slug : End Property | |||
| Public Property Let slug(v) : p_slug = CStr(v) : End Property | |||
| Public Property Get import_from_printstream() : import_from_printstream = p_import_from_printstream : End Property | |||
| Public Property Let import_from_printstream(v) : p_import_from_printstream = CBool(v) : End Property | |||
| Public Property Get printstream_job_name() : printstream_job_name = p_printstream_job_name : End Property | |||
| Public Property Let printstream_job_name(v) : p_printstream_job_name = CStr(v) : End Property | |||
| Public Property Get created_at() : created_at = p_created_at : End Property | |||
| Public Property Let created_at(v) : p_created_at = CDate(v) : End Property | |||
| @@ -8,6 +8,11 @@ Class POBO_cards | |||
| Private p_swim_lane_id | |||
| Private p_job_number | |||
| Private p_job_name | |||
| Private p_customer_name | |||
| Private p_delivery_date | |||
| Private p_quantity | |||
| Private p_notes | |||
| Private p_full_note | |||
| Private p_position | |||
| Private p_created_at | |||
| Private p_created_by | |||
| @@ -15,18 +20,23 @@ Class POBO_cards | |||
| Private p_updated_by | |||
| Private Sub Class_Initialize() | |||
| p_id = 0 | |||
| p_board_id = 0 | |||
| p_column_id = 0 | |||
| p_swim_lane_id = 0 | |||
| p_job_number = "" | |||
| p_job_name = "" | |||
| p_position = 0 | |||
| p_created_at = #1/1/1970# | |||
| p_created_by = "" | |||
| p_updated_at = #1/1/1970# | |||
| p_updated_by = "" | |||
| Properties = Array("id","board_id","column_id","swim_lane_id","job_number","job_name","position","created_at","created_by","updated_at","updated_by") | |||
| p_id = 0 | |||
| p_board_id = 0 | |||
| p_column_id = 0 | |||
| p_swim_lane_id = 0 | |||
| p_job_number = "" | |||
| p_job_name = "" | |||
| p_customer_name = "" | |||
| p_delivery_date = Null | |||
| p_quantity = "" | |||
| p_notes = "" | |||
| p_full_note = "" | |||
| p_position = 0 | |||
| p_created_at = #1/1/1970# | |||
| p_created_by = "" | |||
| p_updated_at = #1/1/1970# | |||
| p_updated_by = "" | |||
| Properties = Array("id","board_id","column_id","swim_lane_id","job_number","job_name","customer_name","delivery_date","quantity","notes","full_note","position","created_at","created_by","updated_at","updated_by") | |||
| End Sub | |||
| Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property | |||
| @@ -50,6 +60,23 @@ Class POBO_cards | |||
| Public Property Get job_name() : job_name = p_job_name : End Property | |||
| Public Property Let job_name(v) : p_job_name = CStr(v) : End Property | |||
| Public Property Get customer_name() : customer_name = p_customer_name : End Property | |||
| Public Property Let customer_name(v) : p_customer_name = CStr(v & "") : End Property | |||
| Public Property Get delivery_date() : delivery_date = p_delivery_date : End Property | |||
| Public Property Let delivery_date(v) | |||
| If IsDate(v) Then p_delivery_date = CDate(v) Else p_delivery_date = Null | |||
| End Property | |||
| Public Property Get quantity() : quantity = p_quantity : End Property | |||
| Public Property Let quantity(v) : p_quantity = CStr(v & "") : End Property | |||
| Public Property Get notes() : notes = p_notes : End Property | |||
| Public Property Let notes(v) : p_notes = CStr(v & "") : End Property | |||
| Public Property Get full_note() : full_note = p_full_note : End Property | |||
| Public Property Let full_note(v) : p_full_note = CStr(v & "") : End Property | |||
| Public Property Get position() : position = p_position : End Property | |||
| Public Property Let position(v) : p_position = CDbl(v) : End Property | |||
| @@ -2,7 +2,7 @@ | |||
| Class boards_Repository_Class | |||
| Public Function FindByID(id) | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] WHERE [id] = ?" | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[import_from_printstream],[printstream_job_name],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] WHERE [id] = ?" | |||
| Dim rs : Set rs = DAL.Query(sql, Array(id)) | |||
| If rs.EOF Then | |||
| Err.Raise 1, "boards_Repository_Class", "Board not found with id = " & id | |||
| @@ -13,7 +13,7 @@ Class boards_Repository_Class | |||
| End Function | |||
| Public Function FindBySlug(slug) | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] WHERE [slug] = ?" | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[import_from_printstream],[printstream_job_name],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] WHERE [slug] = ?" | |||
| Dim rs : Set rs = DAL.Query(sql, Array(slug)) | |||
| If rs.EOF Then | |||
| Set FindBySlug = Nothing | |||
| @@ -24,7 +24,7 @@ Class boards_Repository_Class | |||
| End Function | |||
| Public Function GetAll() | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] ORDER BY [name] ASC" | |||
| Dim sql : sql = "SELECT [id],[name],[slug],[import_from_printstream],[printstream_job_name],[created_at],[created_by],[updated_at],[updated_by] FROM [boards] ORDER BY [name] ASC" | |||
| Dim rs : Set rs = DAL.Query(sql, Empty) | |||
| Dim list : Set list = New LinkedList_Class | |||
| Do Until rs.EOF | |||
| @@ -60,8 +60,8 @@ Class boards_Repository_Class | |||
| End Function | |||
| Public Sub AddNew(ByRef model) | |||
| Dim sql : sql = "INSERT INTO [boards] ([name],[slug],[created_at],[created_by],[updated_at],[updated_by]) VALUES (?,?,?,?,?,?)" | |||
| DAL.Execute sql, Array(model.name, model.slug, model.created_at, model.created_by, model.updated_at, model.updated_by) | |||
| Dim sql : sql = "INSERT INTO [boards] ([name],[slug],[import_from_printstream],[printstream_job_name],[created_at],[created_by],[updated_at],[updated_by]) VALUES (?,?,?,?,?,?,?,?)" | |||
| DAL.Execute sql, Array(model.name, model.slug, model.import_from_printstream, model.printstream_job_name, model.created_at, model.created_by, model.updated_at, model.updated_by) | |||
| Dim rsId : Set rsId = DAL.Query("SELECT @@IDENTITY AS NewID", Empty) | |||
| If Not rsId.EOF Then | |||
| If Not IsNull(rsId(0)) Then model.id = rsId(0) | |||
| @@ -70,8 +70,8 @@ Class boards_Repository_Class | |||
| End Sub | |||
| Public Sub Update(model) | |||
| Dim sql : sql = "UPDATE [boards] SET [name]=?,[slug]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| DAL.Execute sql, Array(model.name, model.slug, model.updated_at, model.updated_by, model.id) | |||
| Dim sql : sql = "UPDATE [boards] SET [name]=?,[slug]=?,[import_from_printstream]=?,[printstream_job_name]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| DAL.Execute sql, Array(model.name, model.slug, model.import_from_printstream, model.printstream_job_name, model.updated_at, model.updated_by, model.id) | |||
| End Sub | |||
| Public Sub Delete(id) | |||
| @@ -2,7 +2,7 @@ | |||
| Class cards_Repository_Class | |||
| Private Function SelectBase() | |||
| SelectBase = "SELECT [id],[board_id],[column_id],[swim_lane_id],[job_number],[job_name],[position],[created_at],[created_by],[updated_at],[updated_by] FROM [cards]" | |||
| SelectBase = "SELECT [id],[board_id],[column_id],[swim_lane_id],[job_number],[job_name],[customer_name],[delivery_date],[quantity],[notes],[full_note],[position],[created_at],[created_by],[updated_at],[updated_by] FROM [cards]" | |||
| End Function | |||
| Public Function FindByID(id) | |||
| @@ -52,8 +52,15 @@ Class cards_Repository_Class | |||
| End Function | |||
| Public Sub AddNew(ByRef model) | |||
| Dim sql : sql = "INSERT INTO [cards] ([board_id],[column_id],[swim_lane_id],[job_number],[job_name],[position],[created_at],[created_by],[updated_at],[updated_by]) VALUES (?,?,?,?,?,?,?,?,?,?)" | |||
| DAL.Execute sql, Array(model.board_id, model.column_id, model.swim_lane_id, model.job_number, model.job_name, model.position, model.created_at, model.created_by, model.updated_at, model.updated_by) | |||
| Dim sql, params | |||
| If QuantityIsBlank(model.quantity) Then | |||
| sql = "INSERT INTO [cards] ([board_id],[column_id],[swim_lane_id],[job_number],[job_name],[customer_name],[delivery_date],[quantity],[notes],[full_note],[position],[created_at],[created_by],[updated_at],[updated_by]) VALUES (?,?,?,?,?,?,?,NULL,?,?,?,?,?,?,?)" | |||
| params = Array(model.board_id, model.column_id, model.swim_lane_id, model.job_number, model.job_name, model.customer_name, model.delivery_date, model.notes, model.full_note, model.position, model.created_at, model.created_by, model.updated_at, model.updated_by) | |||
| Else | |||
| sql = "INSERT INTO [cards] ([board_id],[column_id],[swim_lane_id],[job_number],[job_name],[customer_name],[delivery_date],[quantity],[notes],[full_note],[position],[created_at],[created_by],[updated_at],[updated_by]) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" | |||
| params = Array(model.board_id, model.column_id, model.swim_lane_id, model.job_number, model.job_name, model.customer_name, model.delivery_date, model.quantity, model.notes, model.full_note, model.position, model.created_at, model.created_by, model.updated_at, model.updated_by) | |||
| End If | |||
| DAL.Execute sql, params | |||
| Dim rsId : Set rsId = DAL.Query("SELECT @@IDENTITY AS NewID", Empty) | |||
| If Not rsId.EOF Then | |||
| If Not IsNull(rsId(0)) Then model.id = rsId(0) | |||
| @@ -62,10 +69,21 @@ Class cards_Repository_Class | |||
| End Sub | |||
| Public Sub Update(model) | |||
| Dim sql : sql = "UPDATE [cards] SET [job_number]=?,[job_name]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| DAL.Execute sql, Array(model.job_number, model.job_name, model.updated_at, model.updated_by, model.id) | |||
| Dim sql, params | |||
| If QuantityIsBlank(model.quantity) Then | |||
| sql = "UPDATE [cards] SET [job_number]=?,[job_name]=?,[customer_name]=?,[delivery_date]=?,[quantity]=NULL,[notes]=?,[full_note]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| params = Array(model.job_number, model.job_name, model.customer_name, model.delivery_date, model.notes, model.full_note, model.updated_at, model.updated_by, model.id) | |||
| Else | |||
| sql = "UPDATE [cards] SET [job_number]=?,[job_name]=?,[customer_name]=?,[delivery_date]=?,[quantity]=?,[notes]=?,[full_note]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| params = Array(model.job_number, model.job_name, model.customer_name, model.delivery_date, model.quantity, model.notes, model.full_note, model.updated_at, model.updated_by, model.id) | |||
| End If | |||
| DAL.Execute sql, params | |||
| End Sub | |||
| Private Function QuantityIsBlank(v) | |||
| QuantityIsBlank = (Len(Trim(CStr(v & ""))) = 0) | |||
| End Function | |||
| Public Sub Move(id, columnId, swimLaneId, position, updatedAt, updatedBy) | |||
| Dim sql : sql = "UPDATE [cards] SET [column_id]=?,[swim_lane_id]=?,[position]=?,[updated_at]=?,[updated_by]=? WHERE [id]=?" | |||
| DAL.Execute sql, Array(columnId, swimLaneId, position, updatedAt, updatedBy, id) | |||
| @@ -19,6 +19,18 @@ | |||
| <label class="form-label text-muted small">URL Slug <span class="text-secondary">(auto-generated)</span></label> | |||
| <div class="form-control bg-light text-muted" id="slug-preview" style="min-height:38px;"> </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <div class="form-check"> | |||
| <input class="form-check-input" type="checkbox" id="import_from_printstream" | |||
| name="import_from_printstream" /> | |||
| <label class="form-check-label" for="import_from_printstream">Import Jobs from Printstream</label> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3" id="printstream-job-name-group" style="display:none;"> | |||
| <label for="printstream_job_name" class="form-label">Job Name to Import</label> | |||
| <textarea class="form-control" id="printstream_job_name" name="printstream_job_name" | |||
| rows="4" placeholder="Enter job name(s) to import"></textarea> | |||
| </div> | |||
| <div class="d-flex gap-2"> | |||
| <button type="submit" class="btn btn-primary">Create Board</button> | |||
| <a href="/boards" class="btn btn-outline-secondary">Cancel</a> | |||
| @@ -31,11 +43,19 @@ | |||
| <script> | |||
| (function () { | |||
| var nameEl = document.getElementById('name'); | |||
| var preview = document.getElementById('slug-preview'); | |||
| var nameEl = document.getElementById('name'); | |||
| var preview = document.getElementById('slug-preview'); | |||
| var chk = document.getElementById('import_from_printstream'); | |||
| var jobGroup = document.getElementById('printstream-job-name-group'); | |||
| nameEl.addEventListener('input', function () { | |||
| preview.textContent = slugify(nameEl.value) || ' '; | |||
| preview.textContent = slugify(nameEl.value) || ' '; | |||
| }); | |||
| chk.addEventListener('change', function () { | |||
| jobGroup.style.display = this.checked ? '' : 'none'; | |||
| }); | |||
| function slugify(s) { | |||
| return s.toLowerCase() | |||
| .replace(/&/g, 'and') | |||
| @@ -19,6 +19,20 @@ | |||
| <label class="form-label text-muted small">Current Slug</label> | |||
| <div class="form-control bg-light text-muted"><%= H(board.slug) %></div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <div class="form-check"> | |||
| <input class="form-check-input" type="checkbox" id="import_from_printstream" | |||
| name="import_from_printstream" | |||
| <%= IIf(board.import_from_printstream, "checked", "") %> /> | |||
| <label class="form-check-label" for="import_from_printstream">Import Jobs from Printstream</label> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3" id="printstream-job-name-group" | |||
| style="<%= IIf(board.import_from_printstream, "", "display:none;") %>"> | |||
| <label for="printstream_job_name" class="form-label">Job Name to Import</label> | |||
| <textarea class="form-control" id="printstream_job_name" name="printstream_job_name" | |||
| rows="4" placeholder="Enter job name(s) to import"><%= H(board.printstream_job_name) %></textarea> | |||
| </div> | |||
| <div class="d-flex gap-2"> | |||
| <button type="submit" class="btn btn-primary">Save Changes</button> | |||
| <a href="/board/<%= H(board.slug) %>" class="btn btn-outline-secondary">Cancel</a> | |||
| @@ -39,3 +53,14 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <script> | |||
| (function () { | |||
| var chk = document.getElementById('import_from_printstream'); | |||
| var jobGroup = document.getElementById('printstream-job-name-group'); | |||
| chk.addEventListener('change', function () { | |||
| jobGroup.style.display = this.checked ? '' : 'none'; | |||
| }); | |||
| })(); | |||
| </script> | |||
| @@ -10,8 +10,11 @@ Response.CodePage = 65001 | |||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |||
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> | |||
| <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet" /> | |||
| <link href="/css/site.css?v=20260423a" rel="stylesheet" /> | |||
| <link href="/css/kanban.css?v=20260423a" rel="stylesheet" /> | |||
| <link rel="preconnect" href="https://fonts.googleapis.com" /> | |||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | |||
| <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600&display=swap" rel="stylesheet" /> | |||
| <link href="/css/site.css?v=20260423b" rel="stylesheet" /> | |||
| <link href="/css/kanban.css?v=20260423b" rel="stylesheet" /> | |||
| </head> | |||
| <body class="kanban-page"> | |||
| @@ -23,6 +26,20 @@ Response.CodePage = 65001 | |||
| </a> | |||
| <span class="navbar-brand mb-0 h5 kanban-board-title"><%= H(board.name) %></span> | |||
| </div> | |||
| <div class="board-header-search"> | |||
| <label for="job-search-input" class="visually-hidden">Search jobs</label> | |||
| <div class="input-group input-group-sm"> | |||
| <span class="input-group-text"> | |||
| <i class="bi bi-search"></i> | |||
| </span> | |||
| <input | |||
| type="search" | |||
| id="job-search-input" | |||
| class="form-control" | |||
| placeholder="Search job #, name, customer..." | |||
| autocomplete="off" /> | |||
| </div> | |||
| </div> | |||
| <div class="d-flex align-items-center gap-2 board-header-actions"> | |||
| <button class="btn btn-sm btn-outline-light" id="btn-add-card" | |||
| data-board-id="<%= board.id %>"> | |||
| @@ -60,6 +77,9 @@ Response.CodePage = 65001 | |||
| <!-- Lane header --> | |||
| <div class="kanban-lane-header" data-lane-id="<%= vLaneItem.id %>"> | |||
| <button type="button" class="lane-toggle" title="Collapse or expand swim lane" aria-label="Collapse or expand swim lane" aria-expanded="true"> | |||
| <i class="bi bi-chevron-down" aria-hidden="true"></i> | |||
| </button> | |||
| <span class="lane-label"><%= H(vLaneItem.name) %></span> | |||
| </div> | |||
| @@ -19,6 +19,28 @@ | |||
| <label for="card-job-name" class="form-label">Job Name</label> | |||
| <input type="text" class="form-control" id="card-job-name" placeholder="e.g. Smith Residence" /> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label for="card-customer-name" class="form-label">Customer</label> | |||
| <input type="text" class="form-control" id="card-customer-name" /> | |||
| </div> | |||
| <div class="row g-2 mb-3"> | |||
| <div class="col"> | |||
| <label for="card-delivery-date" class="form-label">Delivery Date</label> | |||
| <input type="date" class="form-control" id="card-delivery-date" /> | |||
| </div> | |||
| <div class="col"> | |||
| <label for="card-quantity" class="form-label">Quantity</label> | |||
| <input type="text" class="form-control" id="card-quantity" /> | |||
| </div> | |||
| </div> | |||
| <div class="mb-3"> | |||
| <label for="card-notes" class="form-label">Notes</label> | |||
| <textarea class="form-control" id="card-notes" rows="3"></textarea> | |||
| </div> | |||
| <div class="mb-3" id="card-full-note-wrap"> | |||
| <label for="card-full-note" class="form-label">PrintStream Notes</label> | |||
| <textarea class="form-control" id="card-full-note" rows="4" readonly></textarea> | |||
| </div> | |||
| <div id="card-modal-error" class="alert alert-danger d-none"></div> | |||
| </div> | |||
| @@ -0,0 +1,15 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' MIGRATION: add_printstream_to_boards | |||
| '======================================================================================================================= | |||
| Sub Migration_Up(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [boards] ADD COLUMN [import_from_printstream] YESNO" | |||
| migration.ExecuteSQL "ALTER TABLE [boards] ADD COLUMN [printstream_job_name] MEMO" | |||
| End Sub | |||
| Sub Migration_Down(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [boards] DROP COLUMN [printstream_job_name]" | |||
| migration.ExecuteSQL "ALTER TABLE [boards] DROP COLUMN [import_from_printstream]" | |||
| End Sub | |||
| %> | |||
| @@ -0,0 +1,19 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' MIGRATION: add_printstream_fields_to_cards | |||
| '======================================================================================================================= | |||
| Sub Migration_Up(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [cards] ADD COLUMN [customer_name] VARCHAR(255)" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] ADD COLUMN [delivery_date] DATETIME" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] ADD COLUMN [quantity] VARCHAR(50)" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] ADD COLUMN [notes] MEMO" | |||
| End Sub | |||
| Sub Migration_Down(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [cards] DROP COLUMN [notes]" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] DROP COLUMN [quantity]" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] DROP COLUMN [delivery_date]" | |||
| migration.ExecuteSQL "ALTER TABLE [cards] DROP COLUMN [customer_name]" | |||
| End Sub | |||
| %> | |||
| @@ -0,0 +1,13 @@ | |||
| <% | |||
| '======================================================================================================================= | |||
| ' MIGRATION: add_full_note_to_cards | |||
| '======================================================================================================================= | |||
| Sub Migration_Up(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [cards] ADD COLUMN [full_note] MEMO" | |||
| End Sub | |||
| Sub Migration_Down(migration) | |||
| migration.ExecuteSQL "ALTER TABLE [cards] DROP COLUMN [full_note]" | |||
| End Sub | |||
| %> | |||
| @@ -27,7 +27,6 @@ body.kanban-page .navbar { | |||
| body.kanban-page .navbar-brand { | |||
| color: #f4f8ff !important; | |||
| font-family: "Fraunces", Georgia, serif; | |||
| letter-spacing: -0.01em; | |||
| } | |||
| @@ -39,6 +38,33 @@ body.kanban-page .navbar-brand { | |||
| flex-shrink: 0; | |||
| } | |||
| .board-header-search { | |||
| width: min(440px, 36vw); | |||
| margin: 0 0.75rem; | |||
| } | |||
| .board-header-search .input-group-text { | |||
| border-color: rgba(214, 229, 250, 0.7); | |||
| background: rgba(255, 255, 255, 0.15); | |||
| color: #eff6ff; | |||
| } | |||
| .board-header-search .form-control { | |||
| border-color: rgba(214, 229, 250, 0.7); | |||
| background: rgba(255, 255, 255, 0.17); | |||
| color: #f5f9ff; | |||
| } | |||
| .board-header-search .form-control::placeholder { | |||
| color: rgba(235, 243, 255, 0.72); | |||
| } | |||
| .board-header-search .form-control:focus { | |||
| border-color: rgba(235, 245, 255, 0.95); | |||
| box-shadow: 0 0 0 0.2rem rgba(157, 194, 245, 0.25); | |||
| background: rgba(255, 255, 255, 0.23); | |||
| } | |||
| .kanban-board-title { | |||
| display: block; | |||
| min-width: 0; | |||
| @@ -67,21 +93,27 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| width: 100%; | |||
| margin: 0 auto; | |||
| height: calc(100vh - 65px); | |||
| overflow: auto; | |||
| overflow-x: auto; | |||
| overflow-y: auto; | |||
| padding: 0.9rem 1rem 1.1rem; | |||
| -webkit-overflow-scrolling: touch; | |||
| scroll-behavior: smooth; | |||
| touch-action: pan-x pan-y; | |||
| overscroll-behavior: contain; | |||
| scrollbar-width: thin; | |||
| scrollbar-color: #8fb0e0 #dce8f8; | |||
| scrollbar-gutter: stable; | |||
| } | |||
| .kanban-grid { | |||
| display: grid; | |||
| min-width: max(75vw, max-content); | |||
| width: max-content; | |||
| min-width: 100%; | |||
| border: 1px solid var(--line, #d9e3f5); | |||
| border-radius: 14px; | |||
| background: rgba(255, 255, 255, 0.72); | |||
| box-shadow: 0 12px 34px rgba(22, 48, 92, 0.12); | |||
| overflow: hidden; | |||
| overflow: clip; | |||
| } | |||
| /* Sticky corner and headers */ | |||
| @@ -121,6 +153,9 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| left: 0; | |||
| z-index: 20; | |||
| min-width: 240px; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 0.42rem; | |||
| padding: 0.85rem 0.82rem; | |||
| font-size: clamp(0.7rem, 0.14vw + 0.66rem, 0.78rem); | |||
| font-weight: 700; | |||
| @@ -139,6 +174,33 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| overflow-wrap: anywhere; | |||
| } | |||
| .kanban-lane-header .lane-toggle { | |||
| display: inline-flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 1.34rem; | |||
| height: 1.34rem; | |||
| padding: 0; | |||
| border: 0; | |||
| border-radius: 999px; | |||
| background: rgba(26, 74, 145, 0.12); | |||
| color: #1f4f96; | |||
| cursor: pointer; | |||
| flex: 0 0 auto; | |||
| } | |||
| .kanban-lane-header .lane-toggle:hover { | |||
| background: rgba(26, 74, 145, 0.22); | |||
| } | |||
| .kanban-lane-header .lane-toggle i { | |||
| transition: transform 140ms ease; | |||
| } | |||
| .kanban-lane-header.lane-collapsed .lane-toggle i { | |||
| transform: rotate(-90deg); | |||
| } | |||
| /* Cells */ | |||
| .kanban-cell { | |||
| min-width: 230px; | |||
| @@ -154,6 +216,20 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| linear-gradient(180deg, rgba(255, 255, 255, 0.88) 0%, rgba(244, 248, 255, 0.93) 100%); | |||
| } | |||
| .kanban-cell.lane-collapsed { | |||
| min-height: 0; | |||
| max-height: 0; | |||
| padding-top: 0; | |||
| padding-bottom: 0; | |||
| border-top-color: transparent; | |||
| overflow: hidden; | |||
| pointer-events: none; | |||
| } | |||
| .kanban-cell.lane-collapsed .kanban-card { | |||
| display: none !important; | |||
| } | |||
| .kanban-cell.drag-over { | |||
| background: linear-gradient(180deg, #e9f2ff 0%, #deecff 100%); | |||
| box-shadow: inset 0 0 0 2px rgba(19, 99, 223, 0.24); | |||
| @@ -187,9 +263,19 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| box-shadow: 0 14px 30px rgba(17, 46, 94, 0.22); | |||
| } | |||
| .kanban-card-hidden { | |||
| display: none !important; | |||
| } | |||
| .card-headline { | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 0.38rem; | |||
| min-width: 0; | |||
| } | |||
| .card-job-number { | |||
| display: inline-block; | |||
| margin-bottom: 0.36rem; | |||
| padding: 0.08rem 0.42rem; | |||
| border-radius: 999px; | |||
| font-size: 0.66rem; | |||
| @@ -198,12 +284,45 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| text-transform: uppercase; | |||
| color: #0e4fae; | |||
| background: #e7f0ff; | |||
| flex: 0 0 auto; | |||
| } | |||
| .card-customer { | |||
| font-size: 0.78rem; | |||
| font-weight: 600; | |||
| color: #2b4a80; | |||
| min-width: 0; | |||
| white-space: nowrap; | |||
| overflow: hidden; | |||
| text-overflow: ellipsis; | |||
| } | |||
| .card-job-name { | |||
| color: #1f2b43; | |||
| font-size: 0.86rem; | |||
| line-height: 1.32; | |||
| .card-meta { | |||
| display: flex; | |||
| flex-wrap: wrap; | |||
| gap: 0.5rem; | |||
| margin-top: 0.25rem; | |||
| } | |||
| .card-meta-label { | |||
| font-size: 0.63rem; | |||
| font-weight: 700; | |||
| color: #7a90b2; | |||
| text-transform: uppercase; | |||
| letter-spacing: 0.05em; | |||
| } | |||
| .card-delivery, | |||
| .card-qty { | |||
| font-size: 0.73rem; | |||
| color: #3a5080; | |||
| } | |||
| .card-notes { | |||
| margin-top: 0.22rem; | |||
| font-size: 0.71rem; | |||
| color: #647899; | |||
| line-height: 1.3; | |||
| } | |||
| /* Settings panel */ | |||
| @@ -294,25 +413,30 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| /* Scrollbars */ | |||
| .kanban-wrapper::-webkit-scrollbar, | |||
| .settings-body::-webkit-scrollbar { | |||
| width: 10px; | |||
| height: 10px; | |||
| width: 12px; | |||
| height: 12px; | |||
| } | |||
| .kanban-wrapper::-webkit-scrollbar-track, | |||
| .settings-body::-webkit-scrollbar-track { | |||
| background: #eaf0fb; | |||
| background: #dce8f8; | |||
| border-radius: 999px; | |||
| } | |||
| .kanban-wrapper::-webkit-scrollbar-thumb, | |||
| .settings-body::-webkit-scrollbar-thumb { | |||
| background: #b8c9e6; | |||
| background: #8fb0e0; | |||
| border-radius: 999px; | |||
| border: 2px solid #eaf0fb; | |||
| border: 2px solid #dce8f8; | |||
| } | |||
| .kanban-wrapper::-webkit-scrollbar-thumb:hover, | |||
| .settings-body::-webkit-scrollbar-thumb:hover { | |||
| background: #97afd8; | |||
| background: #5e8ecb; | |||
| } | |||
| .kanban-wrapper::-webkit-scrollbar-corner { | |||
| background: #dce8f8; | |||
| } | |||
| /* Small screens */ | |||
| @@ -340,6 +464,11 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| gap: 0.35rem !important; | |||
| } | |||
| .board-header-search { | |||
| width: min(270px, 44vw); | |||
| margin: 0 0.35rem; | |||
| } | |||
| .board-header-actions .btn { | |||
| padding-left: 0.48rem; | |||
| padding-right: 0.48rem; | |||
| @@ -347,6 +476,25 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||
| } | |||
| @media (max-width: 640px) { | |||
| .board-header-search { | |||
| width: 100%; | |||
| margin: 0.5rem 0 0; | |||
| order: 3; | |||
| } | |||
| body.kanban-page .navbar { | |||
| flex-wrap: wrap; | |||
| align-items: flex-start !important; | |||
| } | |||
| .board-header-main { | |||
| flex: 1 1 auto; | |||
| } | |||
| .board-header-actions { | |||
| flex: 0 0 auto; | |||
| } | |||
| .kanban-settings-panel { | |||
| width: 100vw; | |||
| border-left: 0; | |||
| @@ -3,6 +3,11 @@ | |||
| 'use strict'; | |||
| var boardId = KANBAN.boardId; | |||
| var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId); | |||
| var collapsedLaneIds = loadCollapsedLaneIds(); | |||
| var searchState = { | |||
| query: '' | |||
| }; | |||
| var dragState = { | |||
| active: false, | |||
| x: 0, | |||
| @@ -35,27 +40,123 @@ | |||
| var grid = document.getElementById('kanban-grid'); | |||
| var colHs = grid.querySelectorAll('.kanban-col-header'); | |||
| var cols = '240px'; | |||
| colHs.forEach(function () { cols += ' 220px'; }); | |||
| colHs.forEach(function () { cols += ' 230px'; }); | |||
| grid.style.gridTemplateColumns = cols; | |||
| } | |||
| function loadCollapsedLaneIds() { | |||
| var laneMap = {}; | |||
| try { | |||
| var raw = window.localStorage.getItem(laneCollapseStorageKey); | |||
| if (!raw) return laneMap; | |||
| var arr = JSON.parse(raw); | |||
| if (!Array.isArray(arr)) return laneMap; | |||
| arr.forEach(function (laneId) { | |||
| laneMap[String(laneId)] = true; | |||
| }); | |||
| } catch (e) { | |||
| console.warn('Failed to load lane collapse state', e); | |||
| } | |||
| return laneMap; | |||
| } | |||
| function saveCollapsedLaneIds() { | |||
| try { | |||
| window.localStorage.setItem(laneCollapseStorageKey, JSON.stringify(Object.keys(collapsedLaneIds))); | |||
| } catch (e) { | |||
| console.warn('Failed to save lane collapse state', e); | |||
| } | |||
| } | |||
| function setLaneCollapsed(laneId, isCollapsed) { | |||
| var laneKey = String(laneId); | |||
| var header = document.querySelector('.kanban-lane-header[data-lane-id="' + laneKey + '"]'); | |||
| if (!header) return; | |||
| var laneCells = document.querySelectorAll('.kanban-cell[data-lane-id="' + laneKey + '"]'); | |||
| header.classList.toggle('lane-collapsed', isCollapsed); | |||
| laneCells.forEach(function (cell) { | |||
| cell.classList.toggle('lane-collapsed', isCollapsed); | |||
| }); | |||
| var toggleBtn = header.querySelector('.lane-toggle'); | |||
| if (toggleBtn) { | |||
| toggleBtn.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true'); | |||
| toggleBtn.title = isCollapsed ? 'Expand swim lane' : 'Collapse swim lane'; | |||
| toggleBtn.setAttribute('aria-label', toggleBtn.title); | |||
| } | |||
| if (isCollapsed) { | |||
| collapsedLaneIds[laneKey] = true; | |||
| } else { | |||
| delete collapsedLaneIds[laneKey]; | |||
| } | |||
| saveCollapsedLaneIds(); | |||
| } | |||
| function toggleLaneCollapsed(laneId) { | |||
| var laneKey = String(laneId); | |||
| setLaneCollapsed(laneKey, !collapsedLaneIds[laneKey]); | |||
| } | |||
| function bindLaneHeaderToggle(headerEl) { | |||
| if (!headerEl) return; | |||
| var toggleBtn = headerEl.querySelector('.lane-toggle'); | |||
| if (!toggleBtn) return; | |||
| if (!toggleBtn.dataset.boundToggle) { | |||
| toggleBtn.addEventListener('click', function (evt) { | |||
| evt.preventDefault(); | |||
| evt.stopPropagation(); | |||
| toggleLaneCollapsed(headerEl.dataset.laneId); | |||
| }); | |||
| toggleBtn.dataset.boundToggle = '1'; | |||
| } | |||
| } | |||
| function initLaneHeaderToggles() { | |||
| document.querySelectorAll('.kanban-lane-header').forEach(function (headerEl) { | |||
| bindLaneHeaderToggle(headerEl); | |||
| if (collapsedLaneIds[String(headerEl.dataset.laneId)]) { | |||
| setLaneCollapsed(headerEl.dataset.laneId, true); | |||
| } | |||
| }); | |||
| } | |||
| function cardBodyHtml(card) { | |||
| var html = '<div class="card-headline">' + | |||
| '<span class="card-job-number">' + esc(card.job_number || '') + '</span>'; | |||
| if (card.customer_name) { | |||
| html += '<span class="card-customer">' + esc(card.customer_name) + '</span>'; | |||
| } | |||
| html += '</div>'; | |||
| return html; | |||
| } | |||
| function buildCardSearchText(card) { | |||
| return [ | |||
| card.job_number || '', | |||
| card.job_name || '', | |||
| card.customer_name || '', | |||
| card.notes || '' | |||
| ].join(' ').toLowerCase(); | |||
| } | |||
| function buildCardEl(card) { | |||
| var div = document.createElement('div'); | |||
| div.className = 'kanban-card'; | |||
| div.dataset.id = card.id; | |||
| div.dataset.columnId = card.column_id; | |||
| div.dataset.laneId = card.swim_lane_id; | |||
| div.innerHTML = | |||
| '<div class="card-job-number">' + esc(card.job_number || '') + '</div>' + | |||
| '<div class="card-job-name">' + esc(card.job_name || '') + '</div>'; | |||
| div.dataset.searchText = buildCardSearchText(card); | |||
| div.innerHTML = cardBodyHtml(card); | |||
| div.addEventListener('click', function () { | |||
| window.KanbanModal.openEdit( | |||
| card.id, | |||
| card.column_id, | |||
| card.swim_lane_id, | |||
| card.job_number, | |||
| card.job_name | |||
| ); | |||
| var c = KANBAN.cards.find(function (x) { return String(x.id) === String(div.dataset.id); }); | |||
| if (!c) return; | |||
| window.KanbanModal.openEdit(c.id, c.column_id, c.swim_lane_id, c.job_number, c.job_name, c.customer_name, c.delivery_date, c.quantity, c.notes, c.full_note); | |||
| }); | |||
| return div; | |||
| } | |||
| @@ -69,6 +170,26 @@ | |||
| cell.appendChild(buildCardEl(card)); | |||
| } | |||
| }); | |||
| applyCardFilter(); | |||
| } | |||
| function applyCardFilter() { | |||
| var activeQuery = searchState.query; | |||
| document.querySelectorAll('.kanban-card').forEach(function (el) { | |||
| var searchableText = (el.dataset.searchText || '').toLowerCase(); | |||
| var isMatch = activeQuery === '' || searchableText.indexOf(activeQuery) > -1; | |||
| el.classList.toggle('kanban-card-hidden', !isMatch); | |||
| }); | |||
| } | |||
| function initJobSearch() { | |||
| var searchInput = document.getElementById('job-search-input'); | |||
| if (!searchInput) return; | |||
| searchInput.addEventListener('input', function () { | |||
| searchState.query = String(searchInput.value || '').toLowerCase().trim(); | |||
| applyCardFilter(); | |||
| }); | |||
| } | |||
| function handleDragEnd(evt) { | |||
| @@ -206,23 +327,31 @@ | |||
| if (cell) { | |||
| cell.appendChild(buildCardEl(card)); | |||
| } | |||
| applyCardFilter(); | |||
| }, | |||
| onCardUpdated: function (id, jobNumber, jobName) { | |||
| onCardUpdated: function (id, data) { | |||
| var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); }); | |||
| if (card) { | |||
| card.job_number = jobNumber; | |||
| card.job_name = jobName; | |||
| card.job_number = data.job_number || ''; | |||
| card.job_name = data.job_name || ''; | |||
| card.customer_name = data.customer_name || ''; | |||
| card.delivery_date = data.delivery_date || null; | |||
| card.quantity = data.quantity || ''; | |||
| card.notes = data.notes || ''; | |||
| card.full_note = data.full_note !== undefined ? data.full_note : (card.full_note || ''); | |||
| } | |||
| var el = document.querySelector('.kanban-card[data-id="' + id + '"]'); | |||
| if (el) { | |||
| el.querySelector('.card-job-number').textContent = jobNumber; | |||
| el.querySelector('.card-job-name').textContent = jobName; | |||
| if (el && card) { | |||
| el.innerHTML = cardBodyHtml(card); | |||
| el.dataset.searchText = buildCardSearchText(card); | |||
| } | |||
| applyCardFilter(); | |||
| }, | |||
| onCardDeleted: function (id) { | |||
| KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); }); | |||
| var el = document.querySelector('.kanban-card[data-id="' + id + '"]'); | |||
| if (el) el.remove(); | |||
| applyCardFilter(); | |||
| }, | |||
| addColumn: function (col) { | |||
| var grid = document.getElementById('kanban-grid'); | |||
| @@ -263,8 +392,13 @@ | |||
| var lh = document.createElement('div'); | |||
| lh.className = 'kanban-lane-header'; | |||
| lh.dataset.laneId = lane.id; | |||
| lh.innerHTML = '<span class="lane-label">' + esc(lane.name) + '</span>'; | |||
| lh.innerHTML = | |||
| '<button type="button" class="lane-toggle" title="Collapse swim lane" aria-label="Collapse swim lane" aria-expanded="true">' + | |||
| '<i class="bi bi-chevron-down" aria-hidden="true"></i>' + | |||
| '</button>' + | |||
| '<span class="lane-label">' + esc(lane.name) + '</span>'; | |||
| grid.appendChild(lh); | |||
| bindLaneHeaderToggle(lh); | |||
| colHeaders.forEach(function (ch) { | |||
| var cell = document.createElement('div'); | |||
| @@ -275,12 +409,20 @@ | |||
| createCellSortable(cell); | |||
| }); | |||
| if (collapsedLaneIds[String(lane.id)]) { | |||
| setLaneCollapsed(lane.id, true); | |||
| } | |||
| applyGridTemplate(); | |||
| }, | |||
| removeLane: function (laneId) { | |||
| document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove(); | |||
| document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); }); | |||
| KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); }); | |||
| if (collapsedLaneIds[String(laneId)]) { | |||
| delete collapsedLaneIds[String(laneId)]; | |||
| saveCollapsedLaneIds(); | |||
| } | |||
| }, | |||
| renameColumn: function (colId, name) { | |||
| var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label'); | |||
| @@ -295,4 +437,6 @@ | |||
| applyGridTemplate(); | |||
| renderCards(); | |||
| initSortables(); | |||
| initJobSearch(); | |||
| initLaneHeaderToggles(); | |||
| })(); | |||
| @@ -14,6 +14,12 @@ | |||
| var btnSave = document.getElementById('btn-save-card'); | |||
| var btnDelete = document.getElementById('btn-delete-card'); | |||
| var custNameEl = document.getElementById('card-customer-name'); | |||
| var delivDateEl = document.getElementById('card-delivery-date'); | |||
| var qtyEl = document.getElementById('card-quantity'); | |||
| var notesEl = document.getElementById('card-notes'); | |||
| var fullNoteEl = document.getElementById('card-full-note'); | |||
| var boardId = KANBAN.boardId; | |||
| /* ── Helpers ─────────────────────────────────────────────── */ | |||
| @@ -44,6 +50,11 @@ | |||
| laneIdEl.value = laneId || ''; | |||
| jobNumEl.value = ''; | |||
| jobNameEl.value = ''; | |||
| custNameEl.value = ''; | |||
| delivDateEl.value = ''; | |||
| qtyEl.value = ''; | |||
| notesEl.value = ''; | |||
| fullNoteEl.value = ''; | |||
| btnDelete.classList.add('d-none'); | |||
| clearError(); | |||
| bsModal.show(); | |||
| @@ -51,13 +62,18 @@ | |||
| } | |||
| /* ── Open for edit ───────────────────────────────────────── */ | |||
| function openEdit(id, colId, laneId, jobNum, jobName) { | |||
| function openEdit(id, colId, laneId, jobNum, jobName, custName, delivDate, qty, notes, fullNote) { | |||
| titleEl.textContent = 'Edit Card'; | |||
| cardIdEl.value = id; | |||
| colIdEl.value = colId; | |||
| laneIdEl.value = laneId; | |||
| jobNumEl.value = jobNum || ''; | |||
| jobNameEl.value = jobName || ''; | |||
| jobNumEl.value = jobNum || ''; | |||
| jobNameEl.value = jobName || ''; | |||
| custNameEl.value = custName || ''; | |||
| delivDateEl.value = delivDate || ''; | |||
| qtyEl.value = qty || ''; | |||
| notesEl.value = notes || ''; | |||
| fullNoteEl.value = fullNote || ''; | |||
| btnDelete.classList.remove('d-none'); | |||
| clearError(); | |||
| bsModal.show(); | |||
| @@ -72,6 +88,11 @@ | |||
| var laneId = laneIdEl.value; | |||
| var jNum = jobNumEl.value.trim(); | |||
| var jName = jobNameEl.value.trim(); | |||
| var cust = custNameEl.value.trim(); | |||
| var dDate = delivDateEl.value; | |||
| var qty = qtyEl.value.trim(); | |||
| var notes = notesEl.value.trim(); | |||
| var fullNote = fullNoteEl.value; | |||
| if (!jNum && !jName) { | |||
| showError('Enter at least a job number or job name.'); | |||
| @@ -80,10 +101,10 @@ | |||
| if (id) { | |||
| // Update existing | |||
| post('/cards/' + id, { job_number: jNum, job_name: jName }, function (res) { | |||
| post('/cards/' + id, { job_number: jNum, job_name: jName, customer_name: cust, delivery_date: dDate, quantity: qty, notes: notes, full_note: fullNote }, function (res) { | |||
| if (res.ok) { | |||
| bsModal.hide(); | |||
| window.KanbanBoard.onCardUpdated(id, res.job_number, res.job_name); | |||
| window.KanbanBoard.onCardUpdated(id, res); | |||
| } else { | |||
| showError(res.error || 'Save failed.'); | |||
| } | |||
| @@ -95,11 +116,16 @@ | |||
| return; | |||
| } | |||
| post('/cards', { | |||
| board_id: boardId, | |||
| column_id: colId, | |||
| board_id: boardId, | |||
| column_id: colId, | |||
| swim_lane_id: laneId, | |||
| job_number: jNum, | |||
| job_name: jName | |||
| job_number: jNum, | |||
| job_name: jName, | |||
| customer_name: cust, | |||
| delivery_date: dDate, | |||
| quantity: qty, | |||
| notes: notes, | |||
| full_note: fullNote | |||
| }, function (res) { | |||
| if (res.ok) { | |||
| bsModal.hide(); | |||
| @@ -1,90 +0,0 @@ | |||
| ' migrate_isbusiness_to_households.vbs | |||
| ' Moves IsBusiness from HouseholderNames to Households. | |||
| ' | |||
| ' Usage: | |||
| ' cscript //nologo scripts\migrate_isbusiness_to_households.vbs "C:\path\to\myAccessFile.accdb" | |||
| ' | |||
| ' What it does: | |||
| ' 1) Adds Households.IsBusiness (SMALLINT) if missing | |||
| ' 2) Copies data: sets Households.IsBusiness=1 if any related HouseholderNames.IsBusiness<>0 | |||
| ' 3) Sets NULLs to 0 | |||
| ' 4) Drops HouseholderNames.IsBusiness if present | |||
| ' | |||
| Option Explicit | |||
| Dim dbPath | |||
| If WScript.Arguments.Count < 1 Then | |||
| WScript.Echo "ERROR: missing db path." | |||
| WScript.Echo "Usage: cscript //nologo scripts\migrate_isbusiness_to_households.vbs ""C:\path\to\db.accdb""" | |||
| WScript.Quit 1 | |||
| End If | |||
| dbPath = WScript.Arguments(0) | |||
| Dim conn | |||
| Set conn = CreateObject("ADODB.Connection") | |||
| conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & dbPath & ";Persist Security Info=False;" | |||
| On Error Resume Next | |||
| If Not ColumnExists(conn, "Households", "IsBusiness") Then | |||
| Exec conn, "ALTER TABLE [Households] ADD COLUMN [IsBusiness] SMALLINT" | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "ERROR adding Households.IsBusiness: " & Err.Description | |||
| WScript.Quit 1 | |||
| End If | |||
| WScript.Echo "Added Households.IsBusiness" | |||
| Else | |||
| WScript.Echo "Households.IsBusiness already exists" | |||
| End If | |||
| ' Copy data (only if the old column exists) | |||
| If ColumnExists(conn, "HouseholderNames", "IsBusiness") Then | |||
| ' Normalize all existing households first so the column is never left NULL. | |||
| Exec conn, "UPDATE [Households] SET [IsBusiness]=0" | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "ERROR initializing Households.IsBusiness: " & Err.Description | |||
| WScript.Quit 1 | |||
| End If | |||
| ' Promote households to business when any related name was previously marked as a business. | |||
| Exec conn, "UPDATE [Households] SET [IsBusiness]=1 WHERE [Id] IN (SELECT [HouseholdId] FROM [HouseholderNames] WHERE [IsBusiness]<>0)" | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "ERROR copying IsBusiness to Households: " & Err.Description | |||
| WScript.Quit 1 | |||
| End If | |||
| WScript.Echo "Copied IsBusiness values to Households" | |||
| Exec conn, "ALTER TABLE [HouseholderNames] DROP COLUMN [IsBusiness]" | |||
| If Err.Number <> 0 Then | |||
| WScript.Echo "ERROR dropping HouseholderNames.IsBusiness: " & Err.Description | |||
| WScript.Quit 1 | |||
| End If | |||
| WScript.Echo "Dropped HouseholderNames.IsBusiness" | |||
| Else | |||
| WScript.Echo "HouseholderNames.IsBusiness does not exist; nothing to drop" | |||
| End If | |||
| conn.Close | |||
| Set conn = Nothing | |||
| WScript.Echo "Done." | |||
| ' --- helpers --- | |||
| Sub Exec(c, sql) | |||
| Err.Clear | |||
| c.Execute sql | |||
| End Sub | |||
| Function ColumnExists(c, tableName, colName) | |||
| Dim rs | |||
| ColumnExists = False | |||
| Err.Clear | |||
| Set rs = c.OpenSchema(4, Array(Empty, Empty, tableName, colName)) ' adSchemaColumns=4 | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| Exit Function | |||
| End If | |||
| If Not rs.EOF Then ColumnExists = True | |||
| rs.Close | |||
| Set rs = Nothing | |||
| End Function | |||
Powered by TurnKey Linux.