Stage history editor: - The edit form's Pipeline section is now the application's history: one row per step (stage + date), Add Step and Remove Step, kept in date order. The last step is the current stage and the first step's date the date applied, replacing the separate Stage and Date Applied fields. An application logged after the fact (applied, interviewed, rejected) keeps its whole path. - JobsDatabase::replaceStageHistory() rewrites a job's history in one transaction (sorted, repeats collapsed, stage and date applied kept in step); StageHistoryModel backs the section; JobsModel::addJob() now returns the new id so a new application's history can be saved. - `kareer history <id> [Stage=YYYY-MM-DD ...]` shows or replaces it. Auto-ghosting: - Applications still at Applied with no activity (the later of the date applied and the last stage change) for more than 30 days move to Ghosted, recorded like any stage change. Runs at startup for GUI and CLI, after switching databases, and shortly after the setting changes. - Preferences gains an Applications section: on/off and the number of days (kareerrc [AutoGhost]). The GUI shows a passive notification; the CLI notes it on stderr so --json output stays clean. Fixes: - The Add Application form pre-filled empty text fields with the word "undefined" (typing "a" gave "undefineda"): fields with no value reached QML as undefined. Every field now gets a typed default. - Embed the app icon as the window icon fallback, so the window and About page show it when running uninstalled. Tests: history replacement and the after-the-fact Allstate case, the ghosting rules (fresh/stale/logged-late/reopened/threshold), and empty new-form fields. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01BxDf7HqD1xPnsZP8wt3NTk
397 lines
18 KiB
QML
397 lines
18 KiB
QML
/*
|
|
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
|
|
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
pragma ComponentBehavior: Bound
|
|
|
|
// Styled after KDE System Settings' Audio page (plasma-pa's kcm/ui/main.qml):
|
|
// Kirigami.ListSectionHeader per category, flat rows indented under the
|
|
// header and separated by Kirigami.Separator, no card/border around them.
|
|
import QtQuick
|
|
import QtQuick.Controls as QQC2
|
|
import QtQuick.Layouts
|
|
import Qt.labs.qmlmodels
|
|
import org.kde.kirigami as Kirigami
|
|
import org.kde.kitemmodels as KItemModels
|
|
import org.kde.kirigamiaddons.dateandtime as DateTime
|
|
import io.github.toservetheking.Kareer
|
|
|
|
Kirigami.ScrollablePage {
|
|
id: root
|
|
|
|
required property JobsModel jobsModel
|
|
property int editingJobId: -1
|
|
|
|
title: root.editingJobId < 0 ? i18nc("@title", "Add Application") : i18nc("@title", "Edit Application")
|
|
|
|
signal done()
|
|
|
|
leftPadding: 0
|
|
rightPadding: 0
|
|
// No topPadding: Kirigami.ListSectionHeader (the first thing on the
|
|
// page) already carries its own top padding, and stacking ours on top
|
|
// of that left an oversized gap above "Company", the first category.
|
|
bottomPadding: Kirigami.Units.gridUnit
|
|
|
|
JobEditModel {
|
|
id: editModel
|
|
jobsModel: root.jobsModel
|
|
editingJobId: root.editingJobId
|
|
}
|
|
|
|
actions: [
|
|
Kirigami.Action {
|
|
text: i18nc("@action:button", "Cancel")
|
|
icon.name: "dialog-cancel"
|
|
onTriggered: root.done()
|
|
},
|
|
Kirigami.Action {
|
|
text: i18nc("@action:button", "Delete")
|
|
icon.name: "edit-delete"
|
|
visible: root.editingJobId >= 0
|
|
onTriggered: deleteConfirmDialog.open()
|
|
},
|
|
Kirigami.Action {
|
|
text: i18nc("@action:button", "Save")
|
|
icon.name: "document-save"
|
|
// On failure, editModel.lastError is set and shown by errorLabel.
|
|
onTriggered: {
|
|
if (editModel.save()) {
|
|
root.done();
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
ColumnLayout {
|
|
spacing: 0
|
|
|
|
Kirigami.InlineMessage {
|
|
id: errorLabel
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
|
type: Kirigami.MessageType.Error
|
|
text: editModel.lastError
|
|
visible: text.length > 0
|
|
}
|
|
|
|
Repeater {
|
|
model: editModel.categories
|
|
|
|
delegate: ColumnLayout {
|
|
id: section
|
|
|
|
required property var modelData
|
|
|
|
Layout.fillWidth: true
|
|
spacing: 0
|
|
|
|
Kirigami.ListSectionHeader {
|
|
Layout.fillWidth: true
|
|
text: section.modelData.title
|
|
}
|
|
|
|
KItemModels.KSortFilterProxyModel {
|
|
id: catModel
|
|
sourceModel: editModel
|
|
filterRoleName: "categoryId"
|
|
// No category id is a prefix of another, so this is an exact match.
|
|
filterString: section.modelData.id
|
|
}
|
|
|
|
// The Pipeline category has no plain fields: it is the
|
|
// application's stage history. The last step is its current
|
|
// stage, the first step's date its date applied.
|
|
ColumnLayout {
|
|
visible: section.modelData.id === "pipeline"
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: i18nc("@info", "Every stage this application went through, oldest first. The first date is when you applied, and the last stage is where it stands now.")
|
|
wrapMode: Text.Wrap
|
|
font: Kirigami.Theme.smallFont
|
|
opacity: 0.7
|
|
}
|
|
|
|
Repeater {
|
|
model: section.modelData.id === "pipeline" ? editModel.history : null
|
|
|
|
delegate: RowLayout {
|
|
id: stepRow
|
|
required property int index
|
|
required property string stage
|
|
required property var date
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.ComboBox {
|
|
Layout.fillWidth: true
|
|
model: editModel.history.stages
|
|
currentIndex: editModel.history.stages.indexOf(stepRow.stage)
|
|
onActivated: editModel.history.setStage(stepRow.index, currentText)
|
|
Accessible.name: i18nc("@label:listbox", "Stage")
|
|
}
|
|
QQC2.Button {
|
|
text: Qt.formatDate(stepRow.date, Qt.ISODate)
|
|
icon.name: "view-calendar-day"
|
|
Accessible.name: i18nc("@action:button", "Date of this stage")
|
|
onClicked: {
|
|
stepDatePopup.value = stepRow.date;
|
|
stepDatePopup.open();
|
|
}
|
|
|
|
DateTime.DatePopup {
|
|
id: stepDatePopup
|
|
onAccepted: editModel.history.setDate(stepRow.index, value)
|
|
}
|
|
}
|
|
QQC2.ToolButton {
|
|
icon.name: "list-remove"
|
|
text: i18nc("@action:button", "Remove Step")
|
|
display: QQC2.AbstractButton.IconOnly
|
|
enabled: editModel.history.count > 1
|
|
QQC2.ToolTip.text: text
|
|
QQC2.ToolTip.visible: hovered
|
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
|
onClicked: editModel.history.removeStep(stepRow.index)
|
|
}
|
|
}
|
|
}
|
|
|
|
QQC2.Button {
|
|
icon.name: "list-add"
|
|
text: i18nc("@action:button", "Add Step")
|
|
onClicked: editModel.history.appendStep()
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
visible: rowRepeater.count > 0
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Kirigami.Units.largeSpacing
|
|
Layout.rightMargin: Kirigami.Units.largeSpacing
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
|
spacing: Kirigami.Units.largeSpacing
|
|
|
|
Repeater {
|
|
id: rowRepeater
|
|
model: catModel
|
|
|
|
delegate: DelegateChooser {
|
|
role: "rowType"
|
|
|
|
// Text
|
|
DelegateChoice {
|
|
roleValue: 0
|
|
delegate: ColumnLayout {
|
|
id: textD
|
|
required property int index
|
|
required property string label
|
|
required property string value
|
|
required property string placeholder
|
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: textD.label
|
|
}
|
|
QQC2.TextField {
|
|
Layout.fillWidth: true
|
|
text: textD.value
|
|
placeholderText: textD.placeholder
|
|
onTextEdited: editModel.setValue(textD.sourceRow, text)
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: textD.index !== rowRepeater.count - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Combo box
|
|
DelegateChoice {
|
|
roleValue: 1
|
|
delegate: ColumnLayout {
|
|
id: comboD
|
|
required property int index
|
|
required property string label
|
|
required property string value
|
|
required property var comboOptions
|
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: comboD.label
|
|
}
|
|
QQC2.ComboBox {
|
|
Layout.fillWidth: true
|
|
model: comboD.comboOptions
|
|
currentIndex: comboD.comboOptions.indexOf(comboD.value)
|
|
onActivated: editModel.setValue(comboD.sourceRow, currentText)
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: comboD.index !== rowRepeater.count - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spin box
|
|
DelegateChoice {
|
|
roleValue: 2
|
|
delegate: ColumnLayout {
|
|
id: spinD
|
|
required property int index
|
|
required property string label
|
|
required property var model
|
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: spinD.label
|
|
}
|
|
QQC2.SpinBox {
|
|
id: spinBox
|
|
Layout.fillWidth: true
|
|
from: spinD.model.spinMin
|
|
to: spinD.model.spinMax
|
|
stepSize: 1000
|
|
// Live binding so values loaded after the delegate is
|
|
// created still show up. Writing back only on
|
|
// valueModified (user edits) avoids a binding loop.
|
|
value: spinD.model.value
|
|
onValueModified: editModel.setValue(spinD.sourceRow, value)
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: spinD.index !== rowRepeater.count - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Date
|
|
DelegateChoice {
|
|
roleValue: 3
|
|
delegate: ColumnLayout {
|
|
id: dateD
|
|
required property int index
|
|
required property string label
|
|
required property var value
|
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: dateD.label
|
|
}
|
|
QQC2.Button {
|
|
Layout.fillWidth: true
|
|
text: Qt.formatDate(dateD.value, Qt.ISODate)
|
|
icon.name: "view-calendar-day"
|
|
onClicked: {
|
|
datePopup.value = dateD.value;
|
|
datePopup.open();
|
|
}
|
|
|
|
DateTime.DatePopup {
|
|
id: datePopup
|
|
onAccepted: editModel.setValue(dateD.sourceRow, value)
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: dateD.index !== rowRepeater.count - 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Text area
|
|
DelegateChoice {
|
|
roleValue: 4
|
|
delegate: ColumnLayout {
|
|
id: areaD
|
|
required property int index
|
|
required property string label
|
|
required property string value
|
|
readonly property int sourceRow: catModel.mapToSource(catModel.index(index, 0)).row
|
|
Layout.fillWidth: true
|
|
spacing: Kirigami.Units.smallSpacing
|
|
|
|
QQC2.Label {
|
|
Layout.fillWidth: true
|
|
text: areaD.label
|
|
}
|
|
QQC2.TextArea {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
|
|
wrapMode: TextEdit.Wrap
|
|
text: areaD.value
|
|
// TextArea has no textEdited signal; skip the echo from
|
|
// the model-driven binding and only write real edits.
|
|
onTextChanged: {
|
|
if (text !== areaD.value) {
|
|
editModel.setValue(areaD.sourceRow, text);
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.Separator {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
|
visible: areaD.index !== rowRepeater.count - 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Kirigami.PromptDialog {
|
|
id: deleteConfirmDialog
|
|
title: i18nc("@title", "Delete Application")
|
|
subtitle: i18nc("@info", "Are you sure you want to delete this application? This cannot be undone.")
|
|
standardButtons: QQC2.Dialog.Cancel
|
|
|
|
customFooterActions: [
|
|
Kirigami.Action {
|
|
text: i18nc("@action:button", "Delete")
|
|
icon.name: "edit-delete"
|
|
onTriggered: {
|
|
editModel.deleteJob();
|
|
deleteConfirmDialog.close();
|
|
root.done();
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|