Files
Kareer/src/jobsdatabase.h
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

119 lines
4.2 KiB
C++

/*
SPDX-FileCopyrightText: 2026 ToServeTheKing <[email protected]>
SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "job.h"
#include <QList>
#include <QString>
#include <optional>
class QSqlDatabase;
/**
* SQLite-backed storage for job applications and their stage history.
*
* Every JobsDatabase instance owns its own named QSqlDatabase connection
* (Qt's SQL connections are identified by name, not by object identity),
* so multiple instances - e.g. in tests - never collide.
*/
class JobsDatabase
{
public:
JobsDatabase();
explicit JobsDatabase(const QString &path);
~JobsDatabase();
JobsDatabase(const JobsDatabase &) = delete;
JobsDatabase &operator=(const JobsDatabase &) = delete;
/// The database file a default-constructed instance opens. Resolved in
/// order: the --db override, the KAREER_DB_PATH environment variable
/// (used by autotests), the path configured in kareerrc, and finally
/// $XDG_DATA_HOME/kareer/kareer.sqlite.
static QString defaultPath();
/// $XDG_DATA_HOME/kareer/kareer.sqlite, ignoring every override.
static QString standardPath();
/// Set from the --db command-line option; wins over everything else.
static void setPathOverride(const QString &path);
/// The user's chosen location (kareerrc); empty means standardPath().
static void setConfiguredPath(const QString &path);
static QString configuredPath();
/// True when --db or KAREER_DB_PATH decides the path, so the configured
/// location has no effect.
static bool hasForcedPath();
/// While true (GUI first run, before the user has picked a location), a
/// default-constructed instance opens nothing and reports an error.
static void setSelectionPending(bool pending);
static bool selectionPending();
/// The file this instance opened, or empty if it opened nothing.
QString path() const;
/// Reopens against defaultPath() if that no longer matches path() (the
/// user picked another location). Returns true if it reopened.
bool reopenIfPathChanged();
/// Performs a harmless write (rewrites the header's user_version), so
/// callers can tell a read-only file apart from a usable one.
bool checkWritable();
bool isOpen() const;
QString lastError() const;
QList<Job> allJobs() const;
std::optional<Job> jobById(int id) const;
/// Inserts a new job. On success, job.id/createdAt/updatedAt are filled
/// in and an initial Start -> job.stage transition is recorded.
bool addJob(Job &job);
/// Updates every field except stage (use setStage for that, so every
/// stage change is captured in the history).
bool updateJob(const Job &job);
/// Moves a job to newStage, recording the transition. A no-op (but still
/// successful) if the job is already in newStage.
bool setStage(int id, const QString &newStage);
bool deleteJob(int id);
/// The steps one application went through, oldest first.
QList<StageStep> stageHistory(int jobId) const;
/// Rewrites an application's whole history in one transaction (used by
/// the history editor, e.g. to log Applied -> Interview -> Rejected after
/// the fact). Steps are sorted by time (stably), consecutive repeats of a
/// stage collapse into the first, the job's stage becomes the last step's
/// and its date applied the first step's date. Fails, changing nothing,
/// if there are no steps, a stage is unknown, or a time is invalid.
bool replaceStageHistory(int jobId, QList<StageStep> steps);
/// Moves every application still at Applied whose last activity (the
/// later of its date applied and its last stage change) is more than
/// `days` days before `now` to Ghosted, recorded through setStage() like
/// any other move. Returns how many were moved; does nothing if days <= 0.
int ghostStaleApplications(int days, const QDateTime &now = QDateTime::currentDateTimeUtc());
QList<StageTransition> stageTransitions() const;
private:
void init(const QString &path);
void close();
bool migrate();
Job jobFromQuery(class QSqlQuery &query) const;
QString m_connectionName;
QString m_path;
QString m_lastError;
};