storing the date when a status was set

This commit is contained in:
orangecoding
2026-06-02 21:09:35 +02:00
parent d2978c14db
commit bbebc2a1a2
8 changed files with 133 additions and 24 deletions

View File

@@ -127,7 +127,7 @@ export function normalizeListListings(queryResult, { page, pageSize }) {
md += `| ID | Title | Address | Price | Size | Provider | Active | Status | Created | Job |\n`;
md += `|----|-------|---------|-------|------|----------|--------|--------|---------|-----|\n`;
for (const l of listings) {
md += `| ${cell(l.id)} | ${cell(l.title)} | ${cell(l.address)} | ${cell(l.price)} | ${cell(l.size)} | ${cell(l.provider)} | ${l.is_active ? 'yes' : 'no'} | ${cell(l.status)} | ${formatDate(l.created_at)} | ${cell(l.job_name)} |\n`;
md += `| ${cell(l.id)} | ${cell(l.title)} | ${cell(l.address)} | ${cell(l.price)} | ${cell(l.size)} | ${cell(l.provider)} | ${l.is_active ? 'yes' : 'no'} | ${cell(l.status?.status)} | ${formatDate(l.created_at)} | ${cell(l.job_name)} |\n`;
}
md += `\nUse **get_listing** with an ID for full details (description, link, image).\n`;
} else {
@@ -156,7 +156,10 @@ export function normalizeGetListing(listing) {
md += `- **Link:** ${listing.link || ''}\n`;
md += `- **Image:** ${listing.image_url || ''}\n`;
md += `- **Active:** ${listing.is_active ? 'yes' : 'no'}\n`;
md += `- **Status:** ${listing.status || ''}\n`;
md += `- **Status:** ${listing.status?.status || ''}\n`;
if (listing.status?.setAt) {
md += `- **Status set at:** ${formatDate(listing.status.setAt)}\n`;
}
md += `- **Created:** ${formatDate(listing.created_at)}\n`;
md += `- **Job:** ${listing.job_name || ''}\n`;
if (listing.latitude != null && listing.longitude != null) {

View File

@@ -3,10 +3,27 @@
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
*/
import { nullOrEmpty } from '../../utils.js';
import { nullOrEmpty, fromJson } from '../../utils.js';
import SqliteConnection from './SqliteConnection.js';
import { nanoid } from 'nanoid';
/**
* Parse the JSON `status` column of a listing row in place.
*
* The DB stores status as a JSON payload `{ status, setAt }` (or NULL).
* Consumers expect an object/null, so we normalize before returning.
*
* @param {Object|null|undefined} row - A raw row from the listings table.
* @returns {Object|null|undefined} The same row with `status` parsed.
*/
const parseListingStatus = (row) => {
if (row == null) return row;
if (typeof row.status === 'string') {
row.status = fromJson(row.status, null);
}
return row;
};
/**
* Return a list of known listing hashes for a given job and provider.
* Useful to de-duplicate before inserting new listings.
@@ -319,7 +336,9 @@ export const queryListings = ({
} else if (watchListFilter === false) {
whereParts.push('(wl.id IS NULL)');
}
// statusFilter: 'applied'|'rejected'|'accepted' -> equality; 'none' -> NULL
// statusFilter: 'applied'|'rejected'|'accepted' -> equality on JSON status field; 'none' -> NULL.
// The status column is a JSON payload `{ status, setAt }`, so we extract the inner
// status string for comparison instead of matching the raw text.
if (statusFilter === 'none') {
whereParts.push('(l.status IS NULL)');
} else if (
@@ -327,7 +346,7 @@ export const queryListings = ({
['applied', 'rejected', 'accepted'].includes(statusFilter.toLowerCase())
) {
params.statusValue = statusFilter.toLowerCase();
whereParts.push('(l.status = @statusValue)');
whereParts.push(`(json_extract(l.status, '$.status') = @statusValue)`);
}
// Time range filters (unix timestamps in milliseconds)
if (Number.isFinite(createdAfter) && createdAfter > 0) {
@@ -403,7 +422,7 @@ export const queryListings = ({
params,
);
return { totalNumber, page: safePage, result: rows };
return { totalNumber, page: safePage, result: rows.map(parseListingStatus) };
};
/**
@@ -638,7 +657,7 @@ export const getListingById = (id, userId = null, isAdmin = false) => {
if (!isAdmin) {
whereScoping = `AND (j.user_id = @userId OR EXISTS (SELECT 1 FROM json_each(j.shared_with_user) AS sw WHERE sw.value = @userId))`;
}
return (
return parseListingStatus(
SqliteConnection.query(
`SELECT l.*, j.name AS job_name, CASE WHEN wl.id IS NOT NULL THEN 1 ELSE 0 END AS isWatched
FROM listings l
@@ -646,7 +665,7 @@ export const getListingById = (id, userId = null, isAdmin = false) => {
LEFT JOIN watch_list wl ON wl.listing_id = l.id AND wl.user_id = @userId
WHERE l.id = @id AND l.manually_deleted = 0 ${whereScoping}`,
params,
)[0] || null
)[0] || null,
);
};
@@ -674,6 +693,10 @@ export const setListingNotes = (id, notes) => {
/**
* Set or clear the status of a single listing.
*
* The status column stores a JSON payload `{ status, setAt }` so consumers
* can show both the user's decision and when it was made. Passing `null`
* clears the column.
*
* @param {string} id - The listing ID.
* @param {('applied'|'rejected'|'accepted'|null)} status - New status, or null to clear.
* @returns {number} Number of rows affected (0 if listing not found).
@@ -685,9 +708,10 @@ export const setListingStatus = (id, status) => {
if (normalized != null && !allowed.includes(normalized)) {
throw new Error(`Invalid listing status: ${status}`);
}
const payload = normalized == null ? null : JSON.stringify({ status: normalized, setAt: Date.now() });
const res = SqliteConnection.execute(`UPDATE listings SET status = @status WHERE id = @id`, {
id,
status: normalized,
status: payload,
});
return res?.changes ?? 0;
};

View File

@@ -5,7 +5,7 @@
export function up(db) {
db.exec(`
ALTER TABLE listings ADD COLUMN status TEXT;
CREATE INDEX IF NOT EXISTS idx_listings_status ON listings (status);
ALTER TABLE listings ADD COLUMN status JSON;
CREATE INDEX IF NOT EXISTS idx_listings_status ON listings (json_extract(status, '$.status'));
`);
}