Files
Kareer/src/qml/SettingsPage.qml
T
austinandClaude Opus 5 eb47a589cb Editable stage history, auto-ghosting, and an Add-form fix
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
2026-09-11 17:00:45 -05:00

221 lines
8.1 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 section, flat rows indented under the
// header, no card/border around them.
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Dialogs
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import io.github.toservetheking.Kareer
Kirigami.ScrollablePage {
id: root
title: i18nc("@title:window", "Preferences")
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 "Appearance".
bottomPadding: Kirigami.Units.gridUnit
ColumnLayout {
width: root.width
spacing: 0
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: i18nc("@title:group", "Appearance")
}
ColumnLayout {
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("@label:listbox", "Color scheme")
}
QQC2.ComboBox {
id: colorSchemeCombo
Layout.fillWidth: true
model: AppColorScheme.colorSchemesModel
textRole: "display"
delegate: QQC2.ItemDelegate {
id: schemeDelegate
required property var model
width: colorSchemeCombo.width
icon.source: "image://colorScheme/" + schemeDelegate.model.display
icon.color: "transparent"
text: schemeDelegate.model.display
highlighted: schemeDelegate.model.display === AppColorScheme.activeColorSchemeName
onClicked: {
AppColorScheme.activeColorSchemeName = schemeDelegate.model.display;
colorSchemeCombo.popup.close();
}
}
// Keep the closed-box label in sync without fighting the popup's own selection state.
displayText: AppColorScheme.activeColorSchemeName
}
}
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: i18nc("@title:group", "Database")
}
ColumnLayout {
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("@label", "Location")
}
QQC2.Label {
Layout.fillWidth: true
text: DatabaseLocation.path
wrapMode: Text.WrapAnywhere
opacity: 0.7
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Information
text: i18nc("@info", "Set by the --db option or the KAREER_DB_PATH environment variable, so it can't be changed here.")
visible: DatabaseLocation.overridden
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Error
text: DatabaseLocation.lastError
visible: text.length > 0
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Positive
text: DatabaseLocation.lastNotice
visible: text.length > 0
}
Flow {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
enabled: !DatabaseLocation.overridden
QQC2.Button {
icon.name: "folder-move"
text: i18nc("@action:button", "Move to…")
QQC2.ToolTip.text: i18nc("@info:tooltip", "Copy the database into another folder and use the copy from now on. The current file is left in place.")
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: moveFolderDialog.open()
}
QQC2.Button {
icon.name: "document-open"
text: i18nc("@action:button", "Open Existing…")
onClicked: openFileDialog.open()
}
QQC2.Button {
icon.name: "edit-reset"
text: i18nc("@action:button", "Use Default Location")
visible: DatabaseLocation.path !== DatabaseLocation.standardPath
QQC2.ToolTip.text: xi18nc("@info:tooltip", "Switch to <filename>%1</filename>. Nothing is copied.", DatabaseLocation.standardPath)
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
onClicked: DatabaseLocation.useDefault()
}
}
}
Kirigami.ListSectionHeader {
Layout.fillWidth: true
text: i18nc("@title:group", "Applications")
}
ColumnLayout {
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.Switch {
id: autoGhostSwitch
Layout.fillWidth: true
text: i18nc("@option:check", "Mark applications with no response as Ghosted")
checked: AutoGhost.enabled
onToggled: AutoGhost.enabled = checked
}
RowLayout {
Layout.fillWidth: true
enabled: autoGhostSwitch.checked
spacing: Kirigami.Units.smallSpacing
QQC2.Label {
text: i18nc("@label:spinbox Mark as ghosted after [N] days", "After")
}
QQC2.SpinBox {
from: 1
to: 365
value: AutoGhost.days
onValueModified: AutoGhost.days = value
}
QQC2.Label {
Layout.fillWidth: true
text: i18nc("@label:spinbox Mark as ghosted after [N] days", "days without any activity")
wrapMode: Text.Wrap
}
}
QQC2.Label {
Layout.fillWidth: true
text: i18nc("@info", "Only applications still at Applied are affected. The move is recorded in their history, and you can change the stage back at any time.")
wrapMode: Text.Wrap
font: Kirigami.Theme.smallFont
opacity: 0.7
}
}
}
Component.onCompleted: DatabaseLocation.clearMessages()
FolderDialog {
id: moveFolderDialog
title: i18nc("@title:window", "Move Database To")
onAccepted: DatabaseLocation.moveTo(selectedFolder)
}
FileDialog {
id: openFileDialog
title: i18nc("@title:window", "Open Database")
fileMode: FileDialog.OpenFile
nameFilters: [i18nc("@item:inlistbox", "Kareer databases (*.sqlite)"), i18nc("@item:inlistbox", "All files (*)")]
onAccepted: DatabaseLocation.useFile(selectedFile)
}
}