22#ifdef PACS_WITH_DATABASE_SYSTEM
24#include <database/query_builder.h>
28using kcenon::common::make_error;
29using kcenon::common::ok;
33auto get_string(
const database_row& row,
const std::string& key) -> std::string {
34 auto it = row.find(key);
35 return (it != row.end()) ? it->second : std::string{};
38auto get_int64(
const database_row& row,
const std::string& key) -> int64_t {
39 auto it = row.find(key);
40 if (it == row.end() || it->second.empty()) {
45 return std::stoll(it->second);
54 : base_repository(std::
move(db),
"worklist",
"worklist_pk") {}
56auto worklist_repository::to_like_pattern(std::string_view pattern)
59 result.reserve(pattern.size());
61 for (
char c : pattern) {
64 }
else if (c ==
'?') {
66 }
else if (c ==
'%' || c ==
'_') {
77auto worklist_repository::parse_timestamp(
const std::string& str)
const
78 -> std::chrono::system_clock::time_point {
84 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
85 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
91 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
94auto worklist_repository::format_timestamp(
95 std::chrono::system_clock::time_point tp)
const -> std::string {
96 if (tp == std::chrono::system_clock::time_point{}) {
100 auto time = std::chrono::system_clock::to_time_t(tp);
103 localtime_s(&tm, &time);
105 localtime_r(&time, &tm);
109 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
113auto worklist_repository::add_worklist_item(
const worklist_item& item)
115 if (
item.step_id.empty()) {
116 return make_error<int64_t>(-1,
"Step ID is required",
"storage");
118 if (
item.patient_id.empty()) {
119 return make_error<int64_t>(-1,
"Patient ID is required",
"storage");
121 if (
item.modality.empty()) {
122 return make_error<int64_t>(-1,
"Modality is required",
"storage");
124 if (
item.scheduled_datetime.empty()) {
125 return make_error<int64_t>(-1,
"Scheduled datetime is required",
128 if (!db() || !db()->is_connected()) {
129 return make_error<int64_t>(-1,
"Database not connected",
"storage");
132 auto builder = query_builder();
133 builder.insert_into(table_name())
134 .values({{
"step_id",
item.step_id},
135 {
"step_status", std::string(
"SCHEDULED")},
136 {
"patient_id",
item.patient_id},
137 {
"patient_name",
item.patient_name},
138 {
"birth_date",
item.birth_date},
140 {
"accession_no",
item.accession_no},
141 {
"requested_proc_id",
item.requested_proc_id},
142 {
"study_uid",
item.study_uid},
143 {
"scheduled_datetime",
item.scheduled_datetime},
144 {
"station_ae",
item.station_ae},
145 {
"station_name",
item.station_name},
146 {
"modality",
item.modality},
147 {
"procedure_desc",
item.procedure_desc},
148 {
"protocol_code",
item.protocol_code},
149 {
"referring_phys",
item.referring_phys},
150 {
"referring_phys_id",
item.referring_phys_id}});
152 auto insert_result = db()->insert(builder.build());
153 if (insert_result.is_err()) {
154 return make_error<int64_t>(
156 kcenon::pacs::compat::format(
"Failed to add worklist item: {}",
157 insert_result.error().message),
161 auto inserted = find_worklist_item(
item.step_id,
item.accession_no);
162 if (!inserted.has_value()) {
163 return make_error<int64_t>(
164 -1,
"Worklist item inserted but could not retrieve record",
171auto worklist_repository::update_worklist_status(std::string_view step_id,
172 std::string_view accession_no,
173 std::string_view new_status)
175 if (new_status !=
"SCHEDULED" && new_status !=
"STARTED" &&
176 new_status !=
"COMPLETED") {
177 return make_error<std::monostate>(
179 "Invalid status. Must be 'SCHEDULED', 'STARTED', or 'COMPLETED'",
182 if (!db() || !db()->is_connected()) {
183 return make_error<std::monostate>(-1,
"Database not connected",
187 std::map<std::string, database::core::database_value> update_data{
188 {
"step_status", std::string(new_status)},
189 {
"updated_at", std::string(
"datetime('now')")}};
191 auto builder = query_builder();
192 builder.update(table_name())
194 .where(
"step_id",
"=", std::string(step_id))
195 .where(
"accession_no",
"=", std::string(accession_no));
197 auto result = db()->update(builder.build());
198 if (result.is_err()) {
199 return make_error<std::monostate>(
201 kcenon::pacs::compat::format(
"Failed to update worklist status: {}",
202 result.error().message),
209auto worklist_repository::query_worklist(
const worklist_query& query)
210 -> Result<std::vector<worklist_item>> {
211 if (!db() || !db()->is_connected()) {
212 return make_error<std::vector<worklist_item>>(-1,
213 "Database not connected",
217 auto builder = query_builder();
218 builder.select(select_columns()).from(table_name());
220 if (!
query.include_all_status) {
221 builder.where(
"step_status",
"=", std::string(
"SCHEDULED"));
223 if (
query.station_ae.has_value()) {
224 builder.where(
"station_ae",
"=", *
query.station_ae);
226 if (
query.modality.has_value()) {
227 builder.where(
"modality",
"=", *
query.modality);
229 if (
query.scheduled_date_from.has_value()) {
230 builder.where(database::query_condition(
231 "substr(scheduled_datetime, 1, 8)",
">=",
232 *
query.scheduled_date_from));
234 if (
query.scheduled_date_to.has_value()) {
235 builder.where(database::query_condition(
236 "substr(scheduled_datetime, 1, 8)",
"<=",
237 *
query.scheduled_date_to));
239 if (
query.patient_id.has_value()) {
240 builder.where(
"patient_id",
"LIKE",
241 to_like_pattern(*
query.patient_id));
243 if (
query.patient_name.has_value()) {
244 builder.where(
"patient_name",
"LIKE",
245 to_like_pattern(*
query.patient_name));
247 if (
query.accession_no.has_value()) {
248 builder.where(
"accession_no",
"=", *
query.accession_no);
250 if (
query.step_id.has_value()) {
251 builder.where(
"step_id",
"=", *
query.step_id);
254 builder.order_by(
"scheduled_datetime", database::sort_order::asc);
256 if (
query.limit > 0) {
257 builder.limit(
query.limit);
259 if (
query.offset > 0) {
260 builder.offset(
query.offset);
263 auto result = db()->select(builder.build());
264 if (result.is_err()) {
265 return Result<std::vector<worklist_item>>::err(result.error());
268 std::vector<worklist_item> items;
269 items.reserve(result.value().size());
270 for (
const auto& row : result.value()) {
271 items.push_back(map_row_to_entity(row));
274 return ok(std::move(items));
277auto worklist_repository::find_worklist_item(std::string_view step_id,
278 std::string_view accession_no)
279 -> std::optional<worklist_item> {
280 if (!db() || !db()->is_connected()) {
284 auto builder = query_builder();
285 auto query = builder.select(select_columns())
287 .where(
"step_id",
"=", std::string(step_id))
288 .where(
"accession_no",
"=", std::string(accession_no))
291 auto result = db()->select(query);
292 if (result.is_err() || result.value().empty()) {
296 return map_row_to_entity(result.value()[0]);
299auto worklist_repository::find_worklist_by_pk(int64_t pk)
300 -> std::optional<worklist_item> {
301 auto result = find_by_id(pk);
302 if (result.is_err()) {
305 return result.value();
308auto worklist_repository::delete_worklist_item(std::string_view step_id,
309 std::string_view accession_no)
311 if (!db() || !db()->is_connected()) {
312 return make_error<std::monostate>(-1,
"Database not connected",
316 auto builder = query_builder();
317 auto query = builder.delete_from(table_name())
318 .where(
"step_id",
"=", std::string(step_id))
319 .where(
"accession_no",
"=", std::string(accession_no))
322 auto result = db()->remove(query);
323 if (result.is_err()) {
324 return make_error<std::monostate>(
326 kcenon::pacs::compat::format(
"Failed to delete worklist item: {}",
327 result.error().message),
334auto worklist_repository::cleanup_old_worklist_items(std::chrono::hours age)
336 auto cutoff = std::chrono::system_clock::now() - age;
337 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
341 std::ostringstream oss;
342 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
343 auto cutoff_str = oss.str();
345 if (!db() || !db()->is_connected()) {
346 return make_error<size_t>(-1,
"Database not connected",
"storage");
349 auto builder = query_builder();
350 builder.delete_from(table_name())
351 .where(
"step_status",
"!=", std::string(
"SCHEDULED"))
352 .where(database::query_condition(
"created_at",
"<", cutoff_str));
354 auto result = db()->remove(builder.build());
355 if (result.is_err()) {
356 return make_error<size_t>(
358 kcenon::pacs::compat::format(
"Failed to cleanup old worklist items: {}",
359 result.error().message),
363 return ok(
static_cast<size_t>(result.value()));
366auto worklist_repository::cleanup_worklist_items_before(
367 std::chrono::system_clock::time_point before) -> Result<size_t> {
368 auto before_time = std::chrono::system_clock::to_time_t(before);
372 std::ostringstream oss;
373 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
374 auto before_str = oss.str();
376 if (!db() || !db()->is_connected()) {
377 return make_error<size_t>(-1,
"Database not connected",
"storage");
380 auto builder = query_builder();
381 builder.delete_from(table_name())
382 .where(
"step_status",
"!=", std::string(
"SCHEDULED"))
383 .where(database::query_condition(
"scheduled_datetime",
"<", before_str));
385 auto result = db()->remove(builder.build());
386 if (result.is_err()) {
387 return make_error<size_t>(
389 kcenon::pacs::compat::format(
390 "Failed to cleanup worklist items before {}: {}",
391 before_str, result.error().message),
395 return ok(
static_cast<size_t>(result.value()));
398auto worklist_repository::worklist_count() -> Result<size_t> {
402auto worklist_repository::worklist_count(std::string_view status)
404 if (!db() || !db()->is_connected()) {
405 return make_error<size_t>(-1,
"Database not connected",
"storage");
408 auto query = kcenon::pacs::compat::format(
409 "SELECT COUNT(*) as count FROM {} WHERE step_status = '{}'",
410 table_name(), std::string(status));
411 auto result = db()->select(query);
412 if (result.is_err()) {
413 return Result<size_t>::err(result.error());
416 if (result.value().empty()) {
417 return ok(
size_t{0});
421 return ok(
static_cast<size_t>(
422 std::stoull(result.value()[0].at(
"count"))));
423 }
catch (
const std::exception& e) {
424 return make_error<size_t>(
426 kcenon::pacs::compat::format(
"Failed to parse worklist count: {}",
432auto worklist_repository::map_row_to_entity(
const database_row& row)
const
435 item.pk = get_int64(row,
"worklist_pk");
436 item.step_id = get_string(row,
"step_id");
437 item.step_status = get_string(row,
"step_status");
438 item.patient_id = get_string(row,
"patient_id");
439 item.patient_name = get_string(row,
"patient_name");
440 item.birth_date = get_string(row,
"birth_date");
441 item.sex = get_string(row,
"sex");
442 item.accession_no = get_string(row,
"accession_no");
443 item.requested_proc_id = get_string(row,
"requested_proc_id");
444 item.study_uid = get_string(row,
"study_uid");
445 item.scheduled_datetime = get_string(row,
"scheduled_datetime");
446 item.station_ae = get_string(row,
"station_ae");
447 item.station_name = get_string(row,
"station_name");
448 item.modality = get_string(row,
"modality");
449 item.procedure_desc = get_string(row,
"procedure_desc");
450 item.protocol_code = get_string(row,
"protocol_code");
451 item.referring_phys = get_string(row,
"referring_phys");
452 item.referring_phys_id = get_string(row,
"referring_phys_id");
454 auto created_at = get_string(row,
"created_at");
455 if (!created_at.empty()) {
456 item.created_at = parse_timestamp(created_at);
459 auto updated_at = get_string(row,
"updated_at");
460 if (!updated_at.empty()) {
461 item.updated_at = parse_timestamp(updated_at);
467auto worklist_repository::entity_to_row(
const worklist_item& entity)
const
468 -> std::map<std::string, database_value> {
470 {
"step_id", entity.step_id},
471 {
"step_status", entity.step_status},
472 {
"patient_id", entity.patient_id},
473 {
"patient_name", entity.patient_name},
474 {
"birth_date", entity.birth_date},
476 {
"accession_no", entity.accession_no},
477 {
"requested_proc_id", entity.requested_proc_id},
478 {
"study_uid", entity.study_uid},
479 {
"scheduled_datetime", entity.scheduled_datetime},
480 {
"station_ae", entity.station_ae},
481 {
"station_name", entity.station_name},
482 {
"modality", entity.modality},
483 {
"procedure_desc", entity.procedure_desc},
484 {
"protocol_code", entity.protocol_code},
485 {
"referring_phys", entity.referring_phys},
486 {
"referring_phys_id", entity.referring_phys_id},
487 {
"created_at", format_timestamp(entity.created_at)},
488 {
"updated_at", format_timestamp(entity.updated_at)},
492auto worklist_repository::get_pk(
const worklist_item& entity)
const -> int64_t {
496auto worklist_repository::has_pk(
const worklist_item& entity)
const ->
bool {
497 return entity.pk > 0;
500auto worklist_repository::select_columns() const -> std::vector<std::
string> {
501 return {
"worklist_pk",
"step_id",
"step_status",
502 "patient_id",
"patient_name",
"birth_date",
503 "sex",
"accession_no",
"requested_proc_id",
504 "study_uid",
"scheduled_datetime",
"station_ae",
505 "station_name",
"modality",
"procedure_desc",
506 "protocol_code",
"referring_phys",
"referring_phys_id",
507 "created_at",
"updated_at"};
518using kcenon::common::make_error;
519using kcenon::common::ok;
523auto parse_datetime(
const char* str) -> std::chrono::system_clock::time_point {
524 if (!str || *str ==
'\0') {
525 return std::chrono::system_clock::time_point{};
529 std::istringstream ss(str);
530 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
532 return std::chrono::system_clock::time_point{};
535 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
538auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
540 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
541 return text ? std::string(
text) : std::string{};
557 if (
this != &other) {
567 result.reserve(pattern.size());
569 for (
char c : pattern) {
572 }
else if (c ==
'?') {
574 }
else if (c ==
'%' || c ==
'_') {
586 -> std::chrono::system_clock::time_point {
587 return parse_datetime(str.c_str());
592 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
595 item.pk = sqlite3_column_int64(stmt, 0);
596 item.step_id = get_text(stmt, 1);
597 item.step_status = get_text(stmt, 2);
598 item.patient_id = get_text(stmt, 3);
599 item.patient_name = get_text(stmt, 4);
600 item.birth_date = get_text(stmt, 5);
601 item.sex = get_text(stmt, 6);
602 item.accession_no = get_text(stmt, 7);
603 item.requested_proc_id = get_text(stmt, 8);
604 item.study_uid = get_text(stmt, 9);
605 item.scheduled_datetime = get_text(stmt, 10);
606 item.station_ae = get_text(stmt, 11);
607 item.station_name = get_text(stmt, 12);
608 item.modality = get_text(stmt, 13);
609 item.procedure_desc = get_text(stmt, 14);
610 item.protocol_code = get_text(stmt, 15);
611 item.referring_phys = get_text(stmt, 16);
612 item.referring_phys_id = get_text(stmt, 17);
613 item.created_at = parse_datetime(get_text(stmt, 18).c_str());
614 item.updated_at = parse_datetime(get_text(stmt, 19).c_str());
621 if (item.step_id.empty()) {
622 return make_error<int64_t>(-1,
"Step ID is required",
"storage");
624 if (item.patient_id.empty()) {
625 return make_error<int64_t>(-1,
"Patient ID is required",
"storage");
627 if (item.modality.empty()) {
628 return make_error<int64_t>(-1,
"Modality is required",
"storage");
630 if (item.scheduled_datetime.empty()) {
631 return make_error<int64_t>(-1,
"Scheduled datetime is required",
635 const char* sql = R
"(
636 INSERT INTO worklist (
637 step_id, step_status, patient_id, patient_name, birth_date, sex,
638 accession_no, requested_proc_id, study_uid, scheduled_datetime,
639 station_ae, station_name, modality, procedure_desc, protocol_code,
640 referring_phys, referring_phys_id, updated_at
641 ) VALUES (?, 'SCHEDULED', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
643 RETURNING worklist_pk;
646 sqlite3_stmt* stmt = nullptr;
647 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
648 if (rc != SQLITE_OK) {
649 return make_error<int64_t>(
651 kcenon::pacs::compat::format(
"Failed to prepare statement: {}",
652 sqlite3_errmsg(db_)),
656 sqlite3_bind_text(stmt, 1, item.step_id.c_str(), -1, SQLITE_TRANSIENT);
657 sqlite3_bind_text(stmt, 2, item.patient_id.c_str(), -1, SQLITE_TRANSIENT);
658 sqlite3_bind_text(stmt, 3, item.patient_name.c_str(), -1, SQLITE_TRANSIENT);
659 sqlite3_bind_text(stmt, 4, item.birth_date.c_str(), -1, SQLITE_TRANSIENT);
660 sqlite3_bind_text(stmt, 5, item.sex.c_str(), -1, SQLITE_TRANSIENT);
661 sqlite3_bind_text(stmt, 6, item.accession_no.c_str(), -1, SQLITE_TRANSIENT);
662 sqlite3_bind_text(stmt, 7, item.requested_proc_id.c_str(), -1,
664 sqlite3_bind_text(stmt, 8, item.study_uid.c_str(), -1, SQLITE_TRANSIENT);
665 sqlite3_bind_text(stmt, 9, item.scheduled_datetime.c_str(), -1,
667 sqlite3_bind_text(stmt, 10, item.station_ae.c_str(), -1, SQLITE_TRANSIENT);
668 sqlite3_bind_text(stmt, 11, item.station_name.c_str(), -1, SQLITE_TRANSIENT);
669 sqlite3_bind_text(stmt, 12, item.modality.c_str(), -1, SQLITE_TRANSIENT);
670 sqlite3_bind_text(stmt, 13, item.procedure_desc.c_str(), -1,
672 sqlite3_bind_text(stmt, 14, item.protocol_code.c_str(), -1,
674 sqlite3_bind_text(stmt, 15, item.referring_phys.c_str(), -1,
676 sqlite3_bind_text(stmt, 16, item.referring_phys_id.c_str(), -1,
679 rc = sqlite3_step(stmt);
680 if (rc != SQLITE_ROW) {
681 auto error_msg = sqlite3_errmsg(db_);
682 sqlite3_finalize(stmt);
683 return make_error<int64_t>(
684 rc, kcenon::pacs::compat::format(
"Failed to add worklist item: {}", error_msg),
688 auto pk = sqlite3_column_int64(stmt, 0);
689 sqlite3_finalize(stmt);
694 std::string_view accession_no,
695 std::string_view new_status)
697 if (new_status !=
"SCHEDULED" && new_status !=
"STARTED" &&
698 new_status !=
"COMPLETED") {
699 return make_error<std::monostate>(
701 "Invalid status. Must be 'SCHEDULED', 'STARTED', or 'COMPLETED'",
705 const char* sql = R
"(
708 updated_at = datetime('now')
709 WHERE step_id = ? AND accession_no = ?;
712 sqlite3_stmt* stmt = nullptr;
713 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
714 if (rc != SQLITE_OK) {
715 return make_error<std::monostate>(
717 kcenon::pacs::compat::format(
"Failed to prepare statement: {}",
718 sqlite3_errmsg(db_)),
722 sqlite3_bind_text(stmt, 1, new_status.data(),
723 static_cast<int>(new_status.size()), SQLITE_TRANSIENT);
724 sqlite3_bind_text(stmt, 2, step_id.data(),
static_cast<int>(step_id.size()),
726 sqlite3_bind_text(stmt, 3, accession_no.data(),
727 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
729 rc = sqlite3_step(stmt);
730 sqlite3_finalize(stmt);
732 if (rc != SQLITE_DONE) {
733 return make_error<std::monostate>(
735 kcenon::pacs::compat::format(
"Failed to update worklist status: {}",
736 sqlite3_errmsg(db_)),
745 std::vector<worklist_item> results;
746 std::string sql = R
"(
747 SELECT worklist_pk, step_id, step_status, patient_id, patient_name,
748 birth_date, sex, accession_no, requested_proc_id, study_uid,
749 scheduled_datetime, station_ae, station_name, modality,
750 procedure_desc, protocol_code, referring_phys, referring_phys_id,
751 created_at, updated_at
755 std::vector<std::string> params;
757 if (!query.include_all_status) {
758 sql +=
" AND step_status = 'SCHEDULED'";
760 if (query.station_ae.has_value()) {
761 sql +=
" AND station_ae = ?";
762 params.push_back(*query.station_ae);
764 if (query.modality.has_value()) {
765 sql +=
" AND modality = ?";
766 params.push_back(*query.modality);
768 if (query.scheduled_date_from.has_value()) {
769 sql +=
" AND substr(scheduled_datetime, 1, 8) >= ?";
770 params.push_back(*query.scheduled_date_from);
772 if (query.scheduled_date_to.has_value()) {
773 sql +=
" AND substr(scheduled_datetime, 1, 8) <= ?";
774 params.push_back(*query.scheduled_date_to);
776 if (query.patient_id.has_value()) {
777 sql +=
" AND patient_id LIKE ?";
778 params.push_back(to_like_pattern(*query.patient_id));
780 if (query.patient_name.has_value()) {
781 sql +=
" AND patient_name LIKE ?";
782 params.push_back(to_like_pattern(*query.patient_name));
784 if (query.accession_no.has_value()) {
785 sql +=
" AND accession_no = ?";
786 params.push_back(*query.accession_no);
788 if (query.step_id.has_value()) {
789 sql +=
" AND step_id = ?";
790 params.push_back(*query.step_id);
793 sql +=
" ORDER BY scheduled_datetime ASC";
795 if (query.limit > 0) {
796 sql += kcenon::pacs::compat::format(
" LIMIT {}", query.limit);
798 if (query.offset > 0) {
799 sql += kcenon::pacs::compat::format(
" OFFSET {}", query.offset);
802 sqlite3_stmt* stmt =
nullptr;
803 auto rc = sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt,
nullptr);
804 if (rc != SQLITE_OK) {
805 return make_error<std::vector<worklist_item>>(
807 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
808 sqlite3_errmsg(db_)),
812 for (
size_t i = 0; i < params.size(); ++i) {
813 sqlite3_bind_text(stmt,
static_cast<int>(i + 1), params[i].c_str(), -1,
817 while (sqlite3_step(stmt) == SQLITE_ROW) {
818 results.push_back(parse_worklist_row(stmt));
821 sqlite3_finalize(stmt);
822 return ok(std::move(results));
826 std::string_view accession_no)
const
827 -> std::optional<worklist_item> {
828 const char* sql = R
"(
829 SELECT worklist_pk, step_id, step_status, patient_id, patient_name,
830 birth_date, sex, accession_no, requested_proc_id, study_uid,
831 scheduled_datetime, station_ae, station_name, modality,
832 procedure_desc, protocol_code, referring_phys, referring_phys_id,
833 created_at, updated_at
835 WHERE step_id = ? AND accession_no = ?;
838 sqlite3_stmt* stmt = nullptr;
839 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
840 if (rc != SQLITE_OK) {
844 sqlite3_bind_text(stmt, 1, step_id.data(),
static_cast<int>(step_id.size()),
846 sqlite3_bind_text(stmt, 2, accession_no.data(),
847 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
849 rc = sqlite3_step(stmt);
850 if (rc != SQLITE_ROW) {
851 sqlite3_finalize(stmt);
855 auto record = parse_worklist_row(stmt);
856 sqlite3_finalize(stmt);
861 -> std::optional<worklist_item> {
862 const char* sql = R
"(
863 SELECT worklist_pk, step_id, step_status, patient_id, patient_name,
864 birth_date, sex, accession_no, requested_proc_id, study_uid,
865 scheduled_datetime, station_ae, station_name, modality,
866 procedure_desc, protocol_code, referring_phys, referring_phys_id,
867 created_at, updated_at
869 WHERE worklist_pk = ?;
872 sqlite3_stmt* stmt = nullptr;
873 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
874 if (rc != SQLITE_OK) {
878 sqlite3_bind_int64(stmt, 1, pk);
879 rc = sqlite3_step(stmt);
880 if (rc != SQLITE_ROW) {
881 sqlite3_finalize(stmt);
885 auto record = parse_worklist_row(stmt);
886 sqlite3_finalize(stmt);
891 std::string_view accession_no)
894 "DELETE FROM worklist WHERE step_id = ? AND accession_no = ?;";
896 sqlite3_stmt* stmt =
nullptr;
897 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
898 if (rc != SQLITE_OK) {
899 return make_error<std::monostate>(
901 kcenon::pacs::compat::format(
"Failed to prepare delete: {}",
902 sqlite3_errmsg(db_)),
906 sqlite3_bind_text(stmt, 1, step_id.data(),
static_cast<int>(step_id.size()),
908 sqlite3_bind_text(stmt, 2, accession_no.data(),
909 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
911 rc = sqlite3_step(stmt);
912 sqlite3_finalize(stmt);
914 if (rc != SQLITE_DONE) {
915 return make_error<std::monostate>(
917 kcenon::pacs::compat::format(
"Failed to delete worklist item: {}",
918 sqlite3_errmsg(db_)),
927 auto cutoff = std::chrono::system_clock::now() - age;
928 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
932 std::ostringstream oss;
933 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
934 auto cutoff_str = oss.str();
936 const char* sql = R
"(
938 WHERE step_status != 'SCHEDULED'
942 sqlite3_stmt* stmt = nullptr;
943 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
944 if (rc != SQLITE_OK) {
945 return make_error<size_t>(
947 kcenon::pacs::compat::format(
"Failed to prepare cleanup: {}",
948 sqlite3_errmsg(db_)),
952 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
953 rc = sqlite3_step(stmt);
954 sqlite3_finalize(stmt);
956 if (rc != SQLITE_DONE) {
957 return make_error<size_t>(
959 kcenon::pacs::compat::format(
"Failed to cleanup old worklist items: {}",
960 sqlite3_errmsg(db_)),
964 return static_cast<size_t>(sqlite3_changes(db_));
969 auto before_time = std::chrono::system_clock::to_time_t(before);
973 std::ostringstream oss;
974 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
975 auto before_str = oss.str();
977 const char* sql = R
"(
979 WHERE step_status != 'SCHEDULED'
980 AND scheduled_datetime < ?;
983 sqlite3_stmt* stmt = nullptr;
984 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
985 if (rc != SQLITE_OK) {
986 return make_error<size_t>(
988 kcenon::pacs::compat::format(
"Failed to prepare cleanup: {}",
989 sqlite3_errmsg(db_)),
993 sqlite3_bind_text(stmt, 1, before_str.c_str(), -1, SQLITE_TRANSIENT);
994 rc = sqlite3_step(stmt);
995 sqlite3_finalize(stmt);
997 if (rc != SQLITE_DONE) {
998 return make_error<size_t>(
1000 kcenon::pacs::compat::format(
1001 "Failed to cleanup worklist items before {}: {}",
1002 before_str, sqlite3_errmsg(db_)),
1006 return static_cast<size_t>(sqlite3_changes(db_));
1010 const char* sql =
"SELECT COUNT(*) FROM worklist;";
1011 sqlite3_stmt* stmt =
nullptr;
1012 auto rc = sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr);
1013 if (rc != SQLITE_OK) {
1014 return make_error<size_t>(
1016 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1017 sqlite3_errmsg(
db_)),
1022 if (sqlite3_step(stmt) == SQLITE_ROW) {
1023 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1025 sqlite3_finalize(stmt);
1031 const char* sql =
"SELECT COUNT(*) FROM worklist WHERE step_status = ?;";
1032 sqlite3_stmt* stmt =
nullptr;
1033 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
1034 if (rc != SQLITE_OK) {
1035 return make_error<size_t>(
1037 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
1038 sqlite3_errmsg(db_)),
1042 sqlite3_bind_text(stmt, 1, status.data(),
static_cast<int>(status.size()),
1046 if (sqlite3_step(stmt) == SQLITE_ROW) {
1047 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1049 sqlite3_finalize(stmt);
worklist_repository(sqlite3 *db)
auto cleanup_old_worklist_items(std::chrono::hours age) -> Result< size_t >
auto operator=(const worklist_repository &) -> worklist_repository &=delete
auto update_worklist_status(std::string_view step_id, std::string_view accession_no, std::string_view new_status) -> VoidResult
auto cleanup_worklist_items_before(std::chrono::system_clock::time_point before) -> Result< size_t >
auto worklist_count() const -> Result< size_t >
auto find_worklist_by_pk(int64_t pk) const -> std::optional< worklist_item >
auto query_worklist(const worklist_query &query) const -> Result< std::vector< worklist_item > >
static auto parse_timestamp(const std::string &str) -> std::chrono::system_clock::time_point
auto find_worklist_item(std::string_view step_id, std::string_view accession_no) const -> std::optional< worklist_item >
auto parse_worklist_row(void *stmt) const -> worklist_item
auto delete_worklist_item(std::string_view step_id, std::string_view accession_no) -> VoidResult
auto add_worklist_item(const worklist_item &item) -> Result< int64_t >
static auto to_like_pattern(std::string_view pattern) -> std::string
std::tm * localtime_safe(const std::time_t *time, std::tm *result)
Cross-platform thread-safe local time conversion.
std::tm * gmtime_safe(const std::time_t *time, std::tm *result)
Cross-platform thread-safe UTC time conversion.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
Worklist item record from the database.
Compatibility header for cross-platform time functions.
Repository for modality worklist persistence using base_repository pattern.