PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
worklist_repository.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <chrono>
15#include <ctime>
16#include <iomanip>
17#include <sstream>
18
21
22#ifdef PACS_WITH_DATABASE_SYSTEM
23
24#include <database/query_builder.h>
25
26namespace kcenon::pacs::storage {
27
28using kcenon::common::make_error;
29using kcenon::common::ok;
30
31namespace {
32
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{};
36}
37
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()) {
41 return 0;
42 }
43
44 try {
45 return std::stoll(it->second);
46 } catch (...) {
47 return 0;
48 }
49}
50
51} // namespace
52
53worklist_repository::worklist_repository(std::shared_ptr<pacs_database_adapter> db)
54 : base_repository(std::move(db), "worklist", "worklist_pk") {}
55
56auto worklist_repository::to_like_pattern(std::string_view pattern)
57 -> std::string {
58 std::string result;
59 result.reserve(pattern.size());
60
61 for (char c : pattern) {
62 if (c == '*') {
63 result += '%';
64 } else if (c == '?') {
65 result += '_';
66 } else if (c == '%' || c == '_') {
67 result += '\\';
68 result += c;
69 } else {
70 result += c;
71 }
72 }
73
74 return result;
75}
76
77auto worklist_repository::parse_timestamp(const std::string& str) const
78 -> std::chrono::system_clock::time_point {
79 if (str.empty()) {
80 return {};
81 }
82
83 std::tm tm{};
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) {
86 return {};
87 }
88
89 tm.tm_year -= 1900;
90 tm.tm_mon -= 1;
91 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
92}
93
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{}) {
97 return "";
98 }
99
100 auto time = std::chrono::system_clock::to_time_t(tp);
101 std::tm tm{};
102#ifdef _WIN32
103 localtime_s(&tm, &time);
104#else
105 localtime_r(&time, &tm);
106#endif
107
108 char buf[32];
109 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
110 return buf;
111}
112
113auto worklist_repository::add_worklist_item(const worklist_item& item)
114 -> Result<int64_t> {
115 if (item.step_id.empty()) {
116 return make_error<int64_t>(-1, "Step ID is required", "storage");
117 }
118 if (item.patient_id.empty()) {
119 return make_error<int64_t>(-1, "Patient ID is required", "storage");
120 }
121 if (item.modality.empty()) {
122 return make_error<int64_t>(-1, "Modality is required", "storage");
123 }
124 if (item.scheduled_datetime.empty()) {
125 return make_error<int64_t>(-1, "Scheduled datetime is required",
126 "storage");
127 }
128 if (!db() || !db()->is_connected()) {
129 return make_error<int64_t>(-1, "Database not connected", "storage");
130 }
131
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},
139 {"sex", item.sex},
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}});
151
152 auto insert_result = db()->insert(builder.build());
153 if (insert_result.is_err()) {
154 return make_error<int64_t>(
155 -1,
156 kcenon::pacs::compat::format("Failed to add worklist item: {}",
157 insert_result.error().message),
158 "storage");
159 }
160
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",
165 "storage");
166 }
167
168 return inserted->pk;
169}
170
171auto worklist_repository::update_worklist_status(std::string_view step_id,
172 std::string_view accession_no,
173 std::string_view new_status)
174 -> VoidResult {
175 if (new_status != "SCHEDULED" && new_status != "STARTED" &&
176 new_status != "COMPLETED") {
177 return make_error<std::monostate>(
178 -1,
179 "Invalid status. Must be 'SCHEDULED', 'STARTED', or 'COMPLETED'",
180 "storage");
181 }
182 if (!db() || !db()->is_connected()) {
183 return make_error<std::monostate>(-1, "Database not connected",
184 "storage");
185 }
186
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')")}};
190
191 auto builder = query_builder();
192 builder.update(table_name())
193 .set(update_data)
194 .where("step_id", "=", std::string(step_id))
195 .where("accession_no", "=", std::string(accession_no));
196
197 auto result = db()->update(builder.build());
198 if (result.is_err()) {
199 return make_error<std::monostate>(
200 -1,
201 kcenon::pacs::compat::format("Failed to update worklist status: {}",
202 result.error().message),
203 "storage");
204 }
205
206 return ok();
207}
208
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",
214 "storage");
215 }
216
217 auto builder = query_builder();
218 builder.select(select_columns()).from(table_name());
219
220 if (!query.include_all_status) {
221 builder.where("step_status", "=", std::string("SCHEDULED"));
222 }
223 if (query.station_ae.has_value()) {
224 builder.where("station_ae", "=", *query.station_ae);
225 }
226 if (query.modality.has_value()) {
227 builder.where("modality", "=", *query.modality);
228 }
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));
233 }
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));
238 }
239 if (query.patient_id.has_value()) {
240 builder.where("patient_id", "LIKE",
241 to_like_pattern(*query.patient_id));
242 }
243 if (query.patient_name.has_value()) {
244 builder.where("patient_name", "LIKE",
245 to_like_pattern(*query.patient_name));
246 }
247 if (query.accession_no.has_value()) {
248 builder.where("accession_no", "=", *query.accession_no);
249 }
250 if (query.step_id.has_value()) {
251 builder.where("step_id", "=", *query.step_id);
252 }
253
254 builder.order_by("scheduled_datetime", database::sort_order::asc);
255
256 if (query.limit > 0) {
257 builder.limit(query.limit);
258 }
259 if (query.offset > 0) {
260 builder.offset(query.offset);
261 }
262
263 auto result = db()->select(builder.build());
264 if (result.is_err()) {
265 return Result<std::vector<worklist_item>>::err(result.error());
266 }
267
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));
272 }
273
274 return ok(std::move(items));
275}
276
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()) {
281 return std::nullopt;
282 }
283
284 auto builder = query_builder();
285 auto query = builder.select(select_columns())
286 .from(table_name())
287 .where("step_id", "=", std::string(step_id))
288 .where("accession_no", "=", std::string(accession_no))
289 .build();
290
291 auto result = db()->select(query);
292 if (result.is_err() || result.value().empty()) {
293 return std::nullopt;
294 }
295
296 return map_row_to_entity(result.value()[0]);
297}
298
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()) {
303 return std::nullopt;
304 }
305 return result.value();
306}
307
308auto worklist_repository::delete_worklist_item(std::string_view step_id,
309 std::string_view accession_no)
310 -> VoidResult {
311 if (!db() || !db()->is_connected()) {
312 return make_error<std::monostate>(-1, "Database not connected",
313 "storage");
314 }
315
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))
320 .build();
321
322 auto result = db()->remove(query);
323 if (result.is_err()) {
324 return make_error<std::monostate>(
325 -1,
326 kcenon::pacs::compat::format("Failed to delete worklist item: {}",
327 result.error().message),
328 "storage");
329 }
330
331 return ok();
332}
333
334auto worklist_repository::cleanup_old_worklist_items(std::chrono::hours age)
335 -> Result<size_t> {
336 auto cutoff = std::chrono::system_clock::now() - age;
337 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
338 std::tm tm{};
339 kcenon::pacs::compat::gmtime_safe(&cutoff_time, &tm);
340
341 std::ostringstream oss;
342 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
343 auto cutoff_str = oss.str();
344
345 if (!db() || !db()->is_connected()) {
346 return make_error<size_t>(-1, "Database not connected", "storage");
347 }
348
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));
353
354 auto result = db()->remove(builder.build());
355 if (result.is_err()) {
356 return make_error<size_t>(
357 -1,
358 kcenon::pacs::compat::format("Failed to cleanup old worklist items: {}",
359 result.error().message),
360 "storage");
361 }
362
363 return ok(static_cast<size_t>(result.value()));
364}
365
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);
369 std::tm tm{};
370 kcenon::pacs::compat::localtime_safe(&before_time, &tm);
371
372 std::ostringstream oss;
373 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
374 auto before_str = oss.str();
375
376 if (!db() || !db()->is_connected()) {
377 return make_error<size_t>(-1, "Database not connected", "storage");
378 }
379
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));
384
385 auto result = db()->remove(builder.build());
386 if (result.is_err()) {
387 return make_error<size_t>(
388 -1,
389 kcenon::pacs::compat::format(
390 "Failed to cleanup worklist items before {}: {}",
391 before_str, result.error().message),
392 "storage");
393 }
394
395 return ok(static_cast<size_t>(result.value()));
396}
397
398auto worklist_repository::worklist_count() -> Result<size_t> {
399 return count();
400}
401
402auto worklist_repository::worklist_count(std::string_view status)
403 -> Result<size_t> {
404 if (!db() || !db()->is_connected()) {
405 return make_error<size_t>(-1, "Database not connected", "storage");
406 }
407
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());
414 }
415
416 if (result.value().empty()) {
417 return ok(size_t{0});
418 }
419
420 try {
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>(
425 -1,
426 kcenon::pacs::compat::format("Failed to parse worklist count: {}",
427 e.what()),
428 "storage");
429 }
430}
431
432auto worklist_repository::map_row_to_entity(const database_row& row) const
433 -> worklist_item {
434 worklist_item item;
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");
453
454 auto created_at = get_string(row, "created_at");
455 if (!created_at.empty()) {
456 item.created_at = parse_timestamp(created_at);
457 }
458
459 auto updated_at = get_string(row, "updated_at");
460 if (!updated_at.empty()) {
461 item.updated_at = parse_timestamp(updated_at);
462 }
463
464 return item;
465}
466
467auto worklist_repository::entity_to_row(const worklist_item& entity) const
468 -> std::map<std::string, database_value> {
469 return {
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},
475 {"sex", entity.sex},
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)},
489 };
490}
491
492auto worklist_repository::get_pk(const worklist_item& entity) const -> int64_t {
493 return entity.pk;
494}
495
496auto worklist_repository::has_pk(const worklist_item& entity) const -> bool {
497 return entity.pk > 0;
498}
499
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"};
508}
509
510} // namespace kcenon::pacs::storage
511
512#else // !PACS_WITH_DATABASE_SYSTEM
513
514#include <sqlite3.h>
515
516namespace kcenon::pacs::storage {
517
518using kcenon::common::make_error;
519using kcenon::common::ok;
520
521namespace {
522
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{};
526 }
527
528 std::tm tm{};
529 std::istringstream ss(str);
530 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
531 if (ss.fail()) {
532 return std::chrono::system_clock::time_point{};
533 }
534
535 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
536}
537
538auto get_text(sqlite3_stmt* stmt, int col) -> std::string {
539 const auto* text =
540 reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
541 return text ? std::string(text) : std::string{};
542}
543
544} // namespace
545
547
549
551 : db_(other.db_) {
552 other.db_ = nullptr;
553}
554
557 if (this != &other) {
558 db_ = other.db_;
559 other.db_ = nullptr;
560 }
561 return *this;
562}
563
564auto worklist_repository::to_like_pattern(std::string_view pattern)
565 -> std::string {
566 std::string result;
567 result.reserve(pattern.size());
568
569 for (char c : pattern) {
570 if (c == '*') {
571 result += '%';
572 } else if (c == '?') {
573 result += '_';
574 } else if (c == '%' || c == '_') {
575 result += '\\';
576 result += c;
577 } else {
578 result += c;
579 }
580 }
581
582 return result;
583}
584
585auto worklist_repository::parse_timestamp(const std::string& str)
586 -> std::chrono::system_clock::time_point {
587 return parse_datetime(str.c_str());
588}
589
591 -> worklist_item {
592 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
593 worklist_item item;
594
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());
615
616 return item;
617}
618
620 -> Result<int64_t> {
621 if (item.step_id.empty()) {
622 return make_error<int64_t>(-1, "Step ID is required", "storage");
623 }
624 if (item.patient_id.empty()) {
625 return make_error<int64_t>(-1, "Patient ID is required", "storage");
626 }
627 if (item.modality.empty()) {
628 return make_error<int64_t>(-1, "Modality is required", "storage");
629 }
630 if (item.scheduled_datetime.empty()) {
631 return make_error<int64_t>(-1, "Scheduled datetime is required",
632 "storage");
633 }
634
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', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
642 datetime('now'))
643 RETURNING worklist_pk;
644 )";
645
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>(
650 rc,
651 kcenon::pacs::compat::format("Failed to prepare statement: {}",
652 sqlite3_errmsg(db_)),
653 "storage");
654 }
655
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,
663 SQLITE_TRANSIENT);
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,
666 SQLITE_TRANSIENT);
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,
671 SQLITE_TRANSIENT);
672 sqlite3_bind_text(stmt, 14, item.protocol_code.c_str(), -1,
673 SQLITE_TRANSIENT);
674 sqlite3_bind_text(stmt, 15, item.referring_phys.c_str(), -1,
675 SQLITE_TRANSIENT);
676 sqlite3_bind_text(stmt, 16, item.referring_phys_id.c_str(), -1,
677 SQLITE_TRANSIENT);
678
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),
685 "storage");
686 }
687
688 auto pk = sqlite3_column_int64(stmt, 0);
689 sqlite3_finalize(stmt);
690 return pk;
691}
692
694 std::string_view accession_no,
695 std::string_view new_status)
696 -> VoidResult {
697 if (new_status != "SCHEDULED" && new_status != "STARTED" &&
698 new_status != "COMPLETED") {
699 return make_error<std::monostate>(
700 -1,
701 "Invalid status. Must be 'SCHEDULED', 'STARTED', or 'COMPLETED'",
702 "storage");
703 }
704
705 const char* sql = R"(
706 UPDATE worklist
707 SET step_status = ?,
708 updated_at = datetime('now')
709 WHERE step_id = ? AND accession_no = ?;
710 )";
711
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>(
716 rc,
717 kcenon::pacs::compat::format("Failed to prepare statement: {}",
718 sqlite3_errmsg(db_)),
719 "storage");
720 }
721
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()),
725 SQLITE_TRANSIENT);
726 sqlite3_bind_text(stmt, 3, accession_no.data(),
727 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
728
729 rc = sqlite3_step(stmt);
730 sqlite3_finalize(stmt);
731
732 if (rc != SQLITE_DONE) {
733 return make_error<std::monostate>(
734 rc,
735 kcenon::pacs::compat::format("Failed to update worklist status: {}",
736 sqlite3_errmsg(db_)),
737 "storage");
738 }
739
740 return ok();
741}
742
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
752 FROM worklist
753 WHERE 1=1
754 )";
755 std::vector<std::string> params;
756
757 if (!query.include_all_status) {
758 sql += " AND step_status = 'SCHEDULED'";
759 }
760 if (query.station_ae.has_value()) {
761 sql += " AND station_ae = ?";
762 params.push_back(*query.station_ae);
763 }
764 if (query.modality.has_value()) {
765 sql += " AND modality = ?";
766 params.push_back(*query.modality);
767 }
768 if (query.scheduled_date_from.has_value()) {
769 sql += " AND substr(scheduled_datetime, 1, 8) >= ?";
770 params.push_back(*query.scheduled_date_from);
771 }
772 if (query.scheduled_date_to.has_value()) {
773 sql += " AND substr(scheduled_datetime, 1, 8) <= ?";
774 params.push_back(*query.scheduled_date_to);
775 }
776 if (query.patient_id.has_value()) {
777 sql += " AND patient_id LIKE ?";
778 params.push_back(to_like_pattern(*query.patient_id));
779 }
780 if (query.patient_name.has_value()) {
781 sql += " AND patient_name LIKE ?";
782 params.push_back(to_like_pattern(*query.patient_name));
783 }
784 if (query.accession_no.has_value()) {
785 sql += " AND accession_no = ?";
786 params.push_back(*query.accession_no);
787 }
788 if (query.step_id.has_value()) {
789 sql += " AND step_id = ?";
790 params.push_back(*query.step_id);
791 }
792
793 sql += " ORDER BY scheduled_datetime ASC";
794
795 if (query.limit > 0) {
796 sql += kcenon::pacs::compat::format(" LIMIT {}", query.limit);
797 }
798 if (query.offset > 0) {
799 sql += kcenon::pacs::compat::format(" OFFSET {}", query.offset);
800 }
801
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>>(
806 rc,
807 kcenon::pacs::compat::format("Failed to prepare query: {}",
808 sqlite3_errmsg(db_)),
809 "storage");
810 }
811
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,
814 SQLITE_TRANSIENT);
815 }
816
817 while (sqlite3_step(stmt) == SQLITE_ROW) {
818 results.push_back(parse_worklist_row(stmt));
819 }
820
821 sqlite3_finalize(stmt);
822 return ok(std::move(results));
823}
824
825auto worklist_repository::find_worklist_item(std::string_view step_id,
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
834 FROM worklist
835 WHERE step_id = ? AND accession_no = ?;
836 )";
837
838 sqlite3_stmt* stmt = nullptr;
839 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
840 if (rc != SQLITE_OK) {
841 return std::nullopt;
842 }
843
844 sqlite3_bind_text(stmt, 1, step_id.data(), static_cast<int>(step_id.size()),
845 SQLITE_TRANSIENT);
846 sqlite3_bind_text(stmt, 2, accession_no.data(),
847 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
848
849 rc = sqlite3_step(stmt);
850 if (rc != SQLITE_ROW) {
851 sqlite3_finalize(stmt);
852 return std::nullopt;
853 }
854
855 auto record = parse_worklist_row(stmt);
856 sqlite3_finalize(stmt);
857 return record;
858}
859
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
868 FROM worklist
869 WHERE worklist_pk = ?;
870 )";
871
872 sqlite3_stmt* stmt = nullptr;
873 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr);
874 if (rc != SQLITE_OK) {
875 return std::nullopt;
876 }
877
878 sqlite3_bind_int64(stmt, 1, pk);
879 rc = sqlite3_step(stmt);
880 if (rc != SQLITE_ROW) {
881 sqlite3_finalize(stmt);
882 return std::nullopt;
883 }
884
885 auto record = parse_worklist_row(stmt);
886 sqlite3_finalize(stmt);
887 return record;
888}
889
890auto worklist_repository::delete_worklist_item(std::string_view step_id,
891 std::string_view accession_no)
892 -> VoidResult {
893 const char* sql =
894 "DELETE FROM worklist WHERE step_id = ? AND accession_no = ?;";
895
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>(
900 rc,
901 kcenon::pacs::compat::format("Failed to prepare delete: {}",
902 sqlite3_errmsg(db_)),
903 "storage");
904 }
905
906 sqlite3_bind_text(stmt, 1, step_id.data(), static_cast<int>(step_id.size()),
907 SQLITE_TRANSIENT);
908 sqlite3_bind_text(stmt, 2, accession_no.data(),
909 static_cast<int>(accession_no.size()), SQLITE_TRANSIENT);
910
911 rc = sqlite3_step(stmt);
912 sqlite3_finalize(stmt);
913
914 if (rc != SQLITE_DONE) {
915 return make_error<std::monostate>(
916 rc,
917 kcenon::pacs::compat::format("Failed to delete worklist item: {}",
918 sqlite3_errmsg(db_)),
919 "storage");
920 }
921
922 return ok();
923}
924
926 -> Result<size_t> {
927 auto cutoff = std::chrono::system_clock::now() - age;
928 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
929 std::tm tm{};
930 kcenon::pacs::compat::gmtime_safe(&cutoff_time, &tm);
931
932 std::ostringstream oss;
933 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
934 auto cutoff_str = oss.str();
935
936 const char* sql = R"(
937 DELETE FROM worklist
938 WHERE step_status != 'SCHEDULED'
939 AND created_at < ?;
940 )";
941
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>(
946 rc,
947 kcenon::pacs::compat::format("Failed to prepare cleanup: {}",
948 sqlite3_errmsg(db_)),
949 "storage");
950 }
951
952 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
953 rc = sqlite3_step(stmt);
954 sqlite3_finalize(stmt);
955
956 if (rc != SQLITE_DONE) {
957 return make_error<size_t>(
958 rc,
959 kcenon::pacs::compat::format("Failed to cleanup old worklist items: {}",
960 sqlite3_errmsg(db_)),
961 "storage");
962 }
963
964 return static_cast<size_t>(sqlite3_changes(db_));
965}
966
968 std::chrono::system_clock::time_point before) -> Result<size_t> {
969 auto before_time = std::chrono::system_clock::to_time_t(before);
970 std::tm tm{};
971 kcenon::pacs::compat::localtime_safe(&before_time, &tm);
972
973 std::ostringstream oss;
974 oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
975 auto before_str = oss.str();
976
977 const char* sql = R"(
978 DELETE FROM worklist
979 WHERE step_status != 'SCHEDULED'
980 AND scheduled_datetime < ?;
981 )";
982
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>(
987 rc,
988 kcenon::pacs::compat::format("Failed to prepare cleanup: {}",
989 sqlite3_errmsg(db_)),
990 "storage");
991 }
992
993 sqlite3_bind_text(stmt, 1, before_str.c_str(), -1, SQLITE_TRANSIENT);
994 rc = sqlite3_step(stmt);
995 sqlite3_finalize(stmt);
996
997 if (rc != SQLITE_DONE) {
998 return make_error<size_t>(
999 rc,
1000 kcenon::pacs::compat::format(
1001 "Failed to cleanup worklist items before {}: {}",
1002 before_str, sqlite3_errmsg(db_)),
1003 "storage");
1004 }
1005
1006 return static_cast<size_t>(sqlite3_changes(db_));
1007}
1008
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>(
1015 rc,
1016 kcenon::pacs::compat::format("Failed to prepare query: {}",
1017 sqlite3_errmsg(db_)),
1018 "storage");
1019 }
1020
1021 size_t count = 0;
1022 if (sqlite3_step(stmt) == SQLITE_ROW) {
1023 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1024 }
1025 sqlite3_finalize(stmt);
1026 return ok(count);
1027}
1028
1029auto worklist_repository::worklist_count(std::string_view status) const
1030 -> Result<size_t> {
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>(
1036 rc,
1037 kcenon::pacs::compat::format("Failed to prepare query: {}",
1038 sqlite3_errmsg(db_)),
1039 "storage");
1040 }
1041
1042 sqlite3_bind_text(stmt, 1, status.data(), static_cast<int>(status.size()),
1043 SQLITE_TRANSIENT);
1044
1045 size_t count = 0;
1046 if (sqlite3_step(stmt) == SQLITE_ROW) {
1047 count = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
1048 }
1049 sqlite3_finalize(stmt);
1050 return ok(count);
1051}
1052
1053} // namespace kcenon::pacs::storage
1054
1055#endif // PACS_WITH_DATABASE_SYSTEM
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
Compatibility header providing kcenon::pacs::compat::format as an alias for std::format.
std::tm * localtime_safe(const std::time_t *time, std::tm *result)
Cross-platform thread-safe local time conversion.
Definition time.h:40
std::tm * gmtime_safe(const std::time_t *time, std::tm *result)
Cross-platform thread-safe UTC time conversion.
Definition time.h:62
constexpr dicom_tag item
Item.
@ 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.