| @@ -50,12 +50,14 @@ Class BoardsController_Class | |||||
| slug = boards_Repository().UniqueSlug(GenerateSlug(boardName), 0) | slug = boards_Repository().UniqueSlug(GenerateSlug(boardName), 0) | ||||
| Dim board : Set board = New POBO_boards | 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 | boards_Repository().AddNew board | ||||
| @@ -117,6 +119,11 @@ Class BoardsController_Class | |||||
| """swim_lane_id"":" & cardItem.swim_lane_id & "," & _ | """swim_lane_id"":" & cardItem.swim_lane_id & "," & _ | ||||
| """job_number"":" & JsonStr(cardItem.job_number) & "," & _ | """job_number"":" & JsonStr(cardItem.job_number) & "," & _ | ||||
| """job_name"":" & JsonStr(cardItem.job_name) & "," & _ | """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 & "}" | """position"":" & cardItem.position & "}" | ||||
| firstCard = False | firstCard = False | ||||
| Loop | Loop | ||||
| @@ -168,10 +175,12 @@ Class BoardsController_Class | |||||
| Dim newSlug : newSlug = boards_Repository().UniqueSlug(GenerateSlug(newName), CLng(board.id)) | 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 | boards_Repository().Update board | ||||
| @@ -226,6 +235,24 @@ Class BoardsController_Class | |||||
| JsonStr = """" & v & """" | JsonStr = """" & v & """" | ||||
| End Function | 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 | End Class | ||||
| Dim BoardsController_Class__Singleton | Dim BoardsController_Class__Singleton | ||||
| @@ -37,22 +37,39 @@ Class CardsController_Class | |||||
| Dim username : username = GetCurrentUsername() | Dim username : username = GetCurrentUsername() | ||||
| Dim card : Set card = New POBO_cards | 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 | 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 & "," & _ | Response.Write "{""ok"":true,""id"":" & card.id & "," & _ | ||||
| """job_number"":" & JsonString(card.job_number) & "," & _ | """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) & "," & _ | |||||
| """column_id"":" & card.column_id & "," & _ | """column_id"":" & card.column_id & "," & _ | ||||
| """swim_lane_id"":" & card.swim_lane_id & "," & _ | """swim_lane_id"":" & card.swim_lane_id & "," & _ | ||||
| """position"":" & card.position & "}" | """position"":" & card.position & "}" | ||||
| @@ -77,14 +94,31 @@ Class CardsController_Class | |||||
| card.job_number = Trim(CStr(Request.Form("job_number"))) | card.job_number = Trim(CStr(Request.Form("job_number"))) | ||||
| card.job_name = Trim(CStr(Request.Form("job_name"))) | 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_at = Now() | ||||
| card.updated_by = GetCurrentUsername() | card.updated_by = GetCurrentUsername() | ||||
| On Error Resume Next | |||||
| cards_Repository().Update card | 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," & _ | Response.Write "{""ok"":true," & _ | ||||
| """job_number"":" & JsonString(card.job_number) & "," & _ | """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 | End Sub | ||||
| ' POST /cards/:id/move — form: column_id, swim_lane_id, position, [sibling_ids CSV for reorder] | ' 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 | End Function | ||||
| Private Function JsonString(s) | 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 Function | ||||
| End Class | End Class | ||||
| @@ -102,19 +102,42 @@ Class ColumnsController_Class | |||||
| End If | End If | ||||
| Dim rawJson : rawJson = GetRawJsonFromRequest() | 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 | Exit Sub | ||||
| End If | End If | ||||
| Dim username : username = GetCurrentUsername() | Dim username : username = GetCurrentUsername() | ||||
| Dim i, item | Dim i, item | ||||
| On Error Resume Next | |||||
| For i = 0 To parsed.Count - 1 | For i = 0 To parsed.Count - 1 | ||||
| Set item = parsed.Item(i) | Set item = parsed.Item(i) | ||||
| board_columns_Repository().UpdatePosition CLng(item.Item("id")), CLng(item.Item("position")), Now(), username | 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 | Next | ||||
| On Error GoTo 0 | |||||
| Set parser = Nothing | |||||
| Response.Write "{""ok"":true}" | Response.Write "{""ok"":true}" | ||||
| End Sub | End Sub | ||||
| @@ -102,19 +102,42 @@ Class SwimLanesController_Class | |||||
| End If | End If | ||||
| Dim rawJson : rawJson = GetRawJsonFromRequest() | 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 | Exit Sub | ||||
| End If | End If | ||||
| Dim username : username = GetCurrentUsername() | Dim username : username = GetCurrentUsername() | ||||
| Dim i, item | Dim i, item | ||||
| On Error Resume Next | |||||
| For i = 0 To parsed.Count - 1 | For i = 0 To parsed.Count - 1 | ||||
| Set item = parsed.Item(i) | Set item = parsed.Item(i) | ||||
| swim_lanes_Repository().UpdatePosition CLng(item.Item("id")), CLng(item.Item("position")), Now(), username | 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 | Next | ||||
| On Error GoTo 0 | |||||
| Set parser = Nothing | |||||
| Response.Write "{""ok"":true}" | Response.Write "{""ok"":true}" | ||||
| End Sub | End Sub | ||||
| @@ -5,20 +5,24 @@ Class POBO_boards | |||||
| Private p_id | Private p_id | ||||
| Private p_name | Private p_name | ||||
| Private p_slug | Private p_slug | ||||
| Private p_import_from_printstream | |||||
| Private p_printstream_job_name | |||||
| Private p_created_at | Private p_created_at | ||||
| Private p_created_by | Private p_created_by | ||||
| Private p_updated_at | Private p_updated_at | ||||
| Private p_updated_by | Private p_updated_by | ||||
| Private Sub Class_Initialize() | 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 | End Sub | ||||
| Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property | 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 Get slug() : slug = p_slug : End Property | ||||
| Public Property Let slug(v) : p_slug = CStr(v) : 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 Get created_at() : created_at = p_created_at : End Property | ||||
| Public Property Let created_at(v) : p_created_at = CDate(v) : 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_swim_lane_id | ||||
| Private p_job_number | Private p_job_number | ||||
| Private p_job_name | 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_position | ||||
| Private p_created_at | Private p_created_at | ||||
| Private p_created_by | Private p_created_by | ||||
| @@ -15,18 +20,23 @@ Class POBO_cards | |||||
| Private p_updated_by | Private p_updated_by | ||||
| Private Sub Class_Initialize() | 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 | End Sub | ||||
| Public Property Get PrimaryKey() : PrimaryKey = "id" : End Property | 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 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 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 Get position() : position = p_position : End Property | ||||
| Public Property Let position(v) : p_position = CDbl(v) : End Property | Public Property Let position(v) : p_position = CDbl(v) : End Property | ||||
| @@ -2,7 +2,7 @@ | |||||
| Class boards_Repository_Class | Class boards_Repository_Class | ||||
| Public Function FindByID(id) | 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)) | Dim rs : Set rs = DAL.Query(sql, Array(id)) | ||||
| If rs.EOF Then | If rs.EOF Then | ||||
| Err.Raise 1, "boards_Repository_Class", "Board not found with id = " & id | Err.Raise 1, "boards_Repository_Class", "Board not found with id = " & id | ||||
| @@ -13,7 +13,7 @@ Class boards_Repository_Class | |||||
| End Function | End Function | ||||
| Public Function FindBySlug(slug) | 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)) | Dim rs : Set rs = DAL.Query(sql, Array(slug)) | ||||
| If rs.EOF Then | If rs.EOF Then | ||||
| Set FindBySlug = Nothing | Set FindBySlug = Nothing | ||||
| @@ -24,7 +24,7 @@ Class boards_Repository_Class | |||||
| End Function | End Function | ||||
| Public Function GetAll() | 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 rs : Set rs = DAL.Query(sql, Empty) | ||||
| Dim list : Set list = New LinkedList_Class | Dim list : Set list = New LinkedList_Class | ||||
| Do Until rs.EOF | Do Until rs.EOF | ||||
| @@ -60,8 +60,8 @@ Class boards_Repository_Class | |||||
| End Function | End Function | ||||
| Public Sub AddNew(ByRef model) | 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) | Dim rsId : Set rsId = DAL.Query("SELECT @@IDENTITY AS NewID", Empty) | ||||
| If Not rsId.EOF Then | If Not rsId.EOF Then | ||||
| If Not IsNull(rsId(0)) Then model.id = rsId(0) | If Not IsNull(rsId(0)) Then model.id = rsId(0) | ||||
| @@ -70,8 +70,8 @@ Class boards_Repository_Class | |||||
| End Sub | End Sub | ||||
| Public Sub Update(model) | 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 | End Sub | ||||
| Public Sub Delete(id) | Public Sub Delete(id) | ||||
| @@ -2,7 +2,7 @@ | |||||
| Class cards_Repository_Class | Class cards_Repository_Class | ||||
| Private Function SelectBase() | 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 | End Function | ||||
| Public Function FindByID(id) | Public Function FindByID(id) | ||||
| @@ -52,8 +52,15 @@ Class cards_Repository_Class | |||||
| End Function | End Function | ||||
| Public Sub AddNew(ByRef model) | 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) | Dim rsId : Set rsId = DAL.Query("SELECT @@IDENTITY AS NewID", Empty) | ||||
| If Not rsId.EOF Then | If Not rsId.EOF Then | ||||
| If Not IsNull(rsId(0)) Then model.id = rsId(0) | If Not IsNull(rsId(0)) Then model.id = rsId(0) | ||||
| @@ -62,10 +69,21 @@ Class cards_Repository_Class | |||||
| End Sub | End Sub | ||||
| Public Sub Update(model) | 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 | End Sub | ||||
| Private Function QuantityIsBlank(v) | |||||
| QuantityIsBlank = (Len(Trim(CStr(v & ""))) = 0) | |||||
| End Function | |||||
| Public Sub Move(id, columnId, swimLaneId, position, updatedAt, updatedBy) | 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]=?" | 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) | 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> | <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 class="form-control bg-light text-muted" id="slug-preview" style="min-height:38px;"> </div> | ||||
| </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"> | <div class="d-flex gap-2"> | ||||
| <button type="submit" class="btn btn-primary">Create Board</button> | <button type="submit" class="btn btn-primary">Create Board</button> | ||||
| <a href="/boards" class="btn btn-outline-secondary">Cancel</a> | <a href="/boards" class="btn btn-outline-secondary">Cancel</a> | ||||
| @@ -31,11 +43,19 @@ | |||||
| <script> | <script> | ||||
| (function () { | (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 () { | 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) { | function slugify(s) { | ||||
| return s.toLowerCase() | return s.toLowerCase() | ||||
| .replace(/&/g, 'and') | .replace(/&/g, 'and') | ||||
| @@ -19,6 +19,20 @@ | |||||
| <label class="form-label text-muted small">Current Slug</label> | <label class="form-label text-muted small">Current Slug</label> | ||||
| <div class="form-control bg-light text-muted"><%= H(board.slug) %></div> | <div class="form-control bg-light text-muted"><%= H(board.slug) %></div> | ||||
| </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"> | <div class="d-flex gap-2"> | ||||
| <button type="submit" class="btn btn-primary">Save Changes</button> | <button type="submit" class="btn btn-primary">Save Changes</button> | ||||
| <a href="/board/<%= H(board.slug) %>" class="btn btn-outline-secondary">Cancel</a> | <a href="/board/<%= H(board.slug) %>" class="btn btn-outline-secondary">Cancel</a> | ||||
| @@ -39,3 +53,14 @@ | |||||
| </div> | </div> | ||||
| </div> | </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" /> | <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@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="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> | </head> | ||||
| <body class="kanban-page"> | <body class="kanban-page"> | ||||
| @@ -23,6 +26,20 @@ Response.CodePage = 65001 | |||||
| </a> | </a> | ||||
| <span class="navbar-brand mb-0 h5 kanban-board-title"><%= H(board.name) %></span> | <span class="navbar-brand mb-0 h5 kanban-board-title"><%= H(board.name) %></span> | ||||
| </div> | </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"> | <div class="d-flex align-items-center gap-2 board-header-actions"> | ||||
| <button class="btn btn-sm btn-outline-light" id="btn-add-card" | <button class="btn btn-sm btn-outline-light" id="btn-add-card" | ||||
| data-board-id="<%= board.id %>"> | data-board-id="<%= board.id %>"> | ||||
| @@ -60,6 +77,9 @@ Response.CodePage = 65001 | |||||
| <!-- Lane header --> | <!-- Lane header --> | ||||
| <div class="kanban-lane-header" data-lane-id="<%= vLaneItem.id %>"> | <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> | <span class="lane-label"><%= H(vLaneItem.name) %></span> | ||||
| </div> | </div> | ||||
| @@ -19,6 +19,28 @@ | |||||
| <label for="card-job-name" class="form-label">Job Name</label> | <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" /> | <input type="text" class="form-control" id="card-job-name" placeholder="e.g. Smith Residence" /> | ||||
| </div> | </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 id="card-modal-error" class="alert alert-danger d-none"></div> | ||||
| </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 { | body.kanban-page .navbar-brand { | ||||
| color: #f4f8ff !important; | color: #f4f8ff !important; | ||||
| font-family: "Fraunces", Georgia, serif; | |||||
| letter-spacing: -0.01em; | letter-spacing: -0.01em; | ||||
| } | } | ||||
| @@ -39,6 +38,33 @@ body.kanban-page .navbar-brand { | |||||
| flex-shrink: 0; | 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 { | .kanban-board-title { | ||||
| display: block; | display: block; | ||||
| min-width: 0; | min-width: 0; | ||||
| @@ -67,21 +93,27 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| width: 100%; | width: 100%; | ||||
| margin: 0 auto; | margin: 0 auto; | ||||
| height: calc(100vh - 65px); | height: calc(100vh - 65px); | ||||
| overflow: auto; | |||||
| overflow-x: auto; | |||||
| overflow-y: auto; | |||||
| padding: 0.9rem 1rem 1.1rem; | padding: 0.9rem 1rem 1.1rem; | ||||
| -webkit-overflow-scrolling: touch; | -webkit-overflow-scrolling: touch; | ||||
| scroll-behavior: smooth; | scroll-behavior: smooth; | ||||
| touch-action: pan-x pan-y; | touch-action: pan-x pan-y; | ||||
| overscroll-behavior: contain; | |||||
| scrollbar-width: thin; | |||||
| scrollbar-color: #8fb0e0 #dce8f8; | |||||
| scrollbar-gutter: stable; | |||||
| } | } | ||||
| .kanban-grid { | .kanban-grid { | ||||
| display: grid; | display: grid; | ||||
| min-width: max(75vw, max-content); | |||||
| width: max-content; | |||||
| min-width: 100%; | |||||
| border: 1px solid var(--line, #d9e3f5); | border: 1px solid var(--line, #d9e3f5); | ||||
| border-radius: 14px; | border-radius: 14px; | ||||
| background: rgba(255, 255, 255, 0.72); | background: rgba(255, 255, 255, 0.72); | ||||
| box-shadow: 0 12px 34px rgba(22, 48, 92, 0.12); | box-shadow: 0 12px 34px rgba(22, 48, 92, 0.12); | ||||
| overflow: hidden; | |||||
| overflow: clip; | |||||
| } | } | ||||
| /* Sticky corner and headers */ | /* Sticky corner and headers */ | ||||
| @@ -121,6 +153,9 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| left: 0; | left: 0; | ||||
| z-index: 20; | z-index: 20; | ||||
| min-width: 240px; | min-width: 240px; | ||||
| display: flex; | |||||
| align-items: center; | |||||
| gap: 0.42rem; | |||||
| padding: 0.85rem 0.82rem; | padding: 0.85rem 0.82rem; | ||||
| font-size: clamp(0.7rem, 0.14vw + 0.66rem, 0.78rem); | font-size: clamp(0.7rem, 0.14vw + 0.66rem, 0.78rem); | ||||
| font-weight: 700; | font-weight: 700; | ||||
| @@ -139,6 +174,33 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| overflow-wrap: anywhere; | 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 */ | /* Cells */ | ||||
| .kanban-cell { | .kanban-cell { | ||||
| min-width: 230px; | 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%); | 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 { | .kanban-cell.drag-over { | ||||
| background: linear-gradient(180deg, #e9f2ff 0%, #deecff 100%); | background: linear-gradient(180deg, #e9f2ff 0%, #deecff 100%); | ||||
| box-shadow: inset 0 0 0 2px rgba(19, 99, 223, 0.24); | 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); | 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 { | .card-job-number { | ||||
| display: inline-block; | display: inline-block; | ||||
| margin-bottom: 0.36rem; | |||||
| padding: 0.08rem 0.42rem; | padding: 0.08rem 0.42rem; | ||||
| border-radius: 999px; | border-radius: 999px; | ||||
| font-size: 0.66rem; | font-size: 0.66rem; | ||||
| @@ -198,12 +284,45 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| color: #0e4fae; | color: #0e4fae; | ||||
| background: #e7f0ff; | 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 */ | /* Settings panel */ | ||||
| @@ -294,25 +413,30 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| /* Scrollbars */ | /* Scrollbars */ | ||||
| .kanban-wrapper::-webkit-scrollbar, | .kanban-wrapper::-webkit-scrollbar, | ||||
| .settings-body::-webkit-scrollbar { | .settings-body::-webkit-scrollbar { | ||||
| width: 10px; | |||||
| height: 10px; | |||||
| width: 12px; | |||||
| height: 12px; | |||||
| } | } | ||||
| .kanban-wrapper::-webkit-scrollbar-track, | .kanban-wrapper::-webkit-scrollbar-track, | ||||
| .settings-body::-webkit-scrollbar-track { | .settings-body::-webkit-scrollbar-track { | ||||
| background: #eaf0fb; | |||||
| background: #dce8f8; | |||||
| border-radius: 999px; | |||||
| } | } | ||||
| .kanban-wrapper::-webkit-scrollbar-thumb, | .kanban-wrapper::-webkit-scrollbar-thumb, | ||||
| .settings-body::-webkit-scrollbar-thumb { | .settings-body::-webkit-scrollbar-thumb { | ||||
| background: #b8c9e6; | |||||
| background: #8fb0e0; | |||||
| border-radius: 999px; | border-radius: 999px; | ||||
| border: 2px solid #eaf0fb; | |||||
| border: 2px solid #dce8f8; | |||||
| } | } | ||||
| .kanban-wrapper::-webkit-scrollbar-thumb:hover, | .kanban-wrapper::-webkit-scrollbar-thumb:hover, | ||||
| .settings-body::-webkit-scrollbar-thumb:hover { | .settings-body::-webkit-scrollbar-thumb:hover { | ||||
| background: #97afd8; | |||||
| background: #5e8ecb; | |||||
| } | |||||
| .kanban-wrapper::-webkit-scrollbar-corner { | |||||
| background: #dce8f8; | |||||
| } | } | ||||
| /* Small screens */ | /* Small screens */ | ||||
| @@ -340,6 +464,11 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| gap: 0.35rem !important; | gap: 0.35rem !important; | ||||
| } | } | ||||
| .board-header-search { | |||||
| width: min(270px, 44vw); | |||||
| margin: 0 0.35rem; | |||||
| } | |||||
| .board-header-actions .btn { | .board-header-actions .btn { | ||||
| padding-left: 0.48rem; | padding-left: 0.48rem; | ||||
| padding-right: 0.48rem; | padding-right: 0.48rem; | ||||
| @@ -347,6 +476,25 @@ body.kanban-page .navbar .btn-outline-secondary:hover { | |||||
| } | } | ||||
| @media (max-width: 640px) { | @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 { | .kanban-settings-panel { | ||||
| width: 100vw; | width: 100vw; | ||||
| border-left: 0; | border-left: 0; | ||||
| @@ -3,6 +3,11 @@ | |||||
| 'use strict'; | 'use strict'; | ||||
| var boardId = KANBAN.boardId; | var boardId = KANBAN.boardId; | ||||
| var laneCollapseStorageKey = 'kanban_lane_collapsed_' + String(boardId); | |||||
| var collapsedLaneIds = loadCollapsedLaneIds(); | |||||
| var searchState = { | |||||
| query: '' | |||||
| }; | |||||
| var dragState = { | var dragState = { | ||||
| active: false, | active: false, | ||||
| x: 0, | x: 0, | ||||
| @@ -35,27 +40,123 @@ | |||||
| var grid = document.getElementById('kanban-grid'); | var grid = document.getElementById('kanban-grid'); | ||||
| var colHs = grid.querySelectorAll('.kanban-col-header'); | var colHs = grid.querySelectorAll('.kanban-col-header'); | ||||
| var cols = '240px'; | var cols = '240px'; | ||||
| colHs.forEach(function () { cols += ' 220px'; }); | |||||
| colHs.forEach(function () { cols += ' 230px'; }); | |||||
| grid.style.gridTemplateColumns = cols; | 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) { | function buildCardEl(card) { | ||||
| var div = document.createElement('div'); | var div = document.createElement('div'); | ||||
| div.className = 'kanban-card'; | div.className = 'kanban-card'; | ||||
| div.dataset.id = card.id; | div.dataset.id = card.id; | ||||
| div.dataset.columnId = card.column_id; | div.dataset.columnId = card.column_id; | ||||
| div.dataset.laneId = card.swim_lane_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 () { | 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; | return div; | ||||
| } | } | ||||
| @@ -69,6 +170,26 @@ | |||||
| cell.appendChild(buildCardEl(card)); | 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) { | function handleDragEnd(evt) { | ||||
| @@ -206,23 +327,31 @@ | |||||
| if (cell) { | if (cell) { | ||||
| cell.appendChild(buildCardEl(card)); | 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); }); | var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); }); | ||||
| if (card) { | 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 + '"]'); | 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) { | onCardDeleted: function (id) { | ||||
| KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); }); | KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); }); | ||||
| var el = document.querySelector('.kanban-card[data-id="' + id + '"]'); | var el = document.querySelector('.kanban-card[data-id="' + id + '"]'); | ||||
| if (el) el.remove(); | if (el) el.remove(); | ||||
| applyCardFilter(); | |||||
| }, | }, | ||||
| addColumn: function (col) { | addColumn: function (col) { | ||||
| var grid = document.getElementById('kanban-grid'); | var grid = document.getElementById('kanban-grid'); | ||||
| @@ -263,8 +392,13 @@ | |||||
| var lh = document.createElement('div'); | var lh = document.createElement('div'); | ||||
| lh.className = 'kanban-lane-header'; | lh.className = 'kanban-lane-header'; | ||||
| lh.dataset.laneId = lane.id; | 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); | grid.appendChild(lh); | ||||
| bindLaneHeaderToggle(lh); | |||||
| colHeaders.forEach(function (ch) { | colHeaders.forEach(function (ch) { | ||||
| var cell = document.createElement('div'); | var cell = document.createElement('div'); | ||||
| @@ -275,12 +409,20 @@ | |||||
| createCellSortable(cell); | createCellSortable(cell); | ||||
| }); | }); | ||||
| if (collapsedLaneIds[String(lane.id)]) { | |||||
| setLaneCollapsed(lane.id, true); | |||||
| } | |||||
| applyGridTemplate(); | applyGridTemplate(); | ||||
| }, | }, | ||||
| removeLane: function (laneId) { | removeLane: function (laneId) { | ||||
| document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove(); | document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove(); | ||||
| document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.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); }); | 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) { | renameColumn: function (colId, name) { | ||||
| var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label'); | var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label'); | ||||
| @@ -295,4 +437,6 @@ | |||||
| applyGridTemplate(); | applyGridTemplate(); | ||||
| renderCards(); | renderCards(); | ||||
| initSortables(); | initSortables(); | ||||
| initJobSearch(); | |||||
| initLaneHeaderToggles(); | |||||
| })(); | })(); | ||||
| @@ -14,6 +14,12 @@ | |||||
| var btnSave = document.getElementById('btn-save-card'); | var btnSave = document.getElementById('btn-save-card'); | ||||
| var btnDelete = document.getElementById('btn-delete-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; | var boardId = KANBAN.boardId; | ||||
| /* ── Helpers ─────────────────────────────────────────────── */ | /* ── Helpers ─────────────────────────────────────────────── */ | ||||
| @@ -44,6 +50,11 @@ | |||||
| laneIdEl.value = laneId || ''; | laneIdEl.value = laneId || ''; | ||||
| jobNumEl.value = ''; | jobNumEl.value = ''; | ||||
| jobNameEl.value = ''; | jobNameEl.value = ''; | ||||
| custNameEl.value = ''; | |||||
| delivDateEl.value = ''; | |||||
| qtyEl.value = ''; | |||||
| notesEl.value = ''; | |||||
| fullNoteEl.value = ''; | |||||
| btnDelete.classList.add('d-none'); | btnDelete.classList.add('d-none'); | ||||
| clearError(); | clearError(); | ||||
| bsModal.show(); | bsModal.show(); | ||||
| @@ -51,13 +62,18 @@ | |||||
| } | } | ||||
| /* ── Open for edit ───────────────────────────────────────── */ | /* ── 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'; | titleEl.textContent = 'Edit Card'; | ||||
| cardIdEl.value = id; | cardIdEl.value = id; | ||||
| colIdEl.value = colId; | colIdEl.value = colId; | ||||
| laneIdEl.value = laneId; | 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'); | btnDelete.classList.remove('d-none'); | ||||
| clearError(); | clearError(); | ||||
| bsModal.show(); | bsModal.show(); | ||||
| @@ -72,6 +88,11 @@ | |||||
| var laneId = laneIdEl.value; | var laneId = laneIdEl.value; | ||||
| var jNum = jobNumEl.value.trim(); | var jNum = jobNumEl.value.trim(); | ||||
| var jName = jobNameEl.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) { | if (!jNum && !jName) { | ||||
| showError('Enter at least a job number or job name.'); | showError('Enter at least a job number or job name.'); | ||||
| @@ -80,10 +101,10 @@ | |||||
| if (id) { | if (id) { | ||||
| // Update existing | // 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) { | if (res.ok) { | ||||
| bsModal.hide(); | bsModal.hide(); | ||||
| window.KanbanBoard.onCardUpdated(id, res.job_number, res.job_name); | |||||
| window.KanbanBoard.onCardUpdated(id, res); | |||||
| } else { | } else { | ||||
| showError(res.error || 'Save failed.'); | showError(res.error || 'Save failed.'); | ||||
| } | } | ||||
| @@ -95,11 +116,16 @@ | |||||
| return; | return; | ||||
| } | } | ||||
| post('/cards', { | post('/cards', { | ||||
| board_id: boardId, | |||||
| column_id: colId, | |||||
| board_id: boardId, | |||||
| column_id: colId, | |||||
| swim_lane_id: laneId, | 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) { | }, function (res) { | ||||
| if (res.ok) { | if (res.ok) { | ||||
| bsModal.hide(); | 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.