1use chrono::{DateTime, Utc};
6use serde::Serialize;
7use sqlx::PgPool;
8use uuid::Uuid;
9
10use crate::{
11 auth::Actor,
12 domain,
13 error::{AppError, AppResult},
14 services::access,
15};
16
17pub const LIMIT: i64 = 500;
19
20#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
22pub struct Entry {
23 pub item_id: Uuid,
24 pub number: String,
25 pub item_type: String,
26 pub project_id: Option<Uuid>,
27 pub project_code: Option<String>,
28 pub revision_id: Uuid,
29 pub code: String,
30 pub name: String,
31 pub state: String,
32 pub checked_out_by: Option<Uuid>,
33 pub checked_out_by_name: Option<String>,
34 pub checked_out_at: Option<DateTime<Utc>>,
35 pub updated_at: DateTime<Utc>,
36 pub file_count: i64,
37}
38
39#[derive(Clone, Debug, Serialize, sqlx::FromRow)]
41pub struct Folder {
42 pub id: Uuid,
43 pub code: String,
44 pub name: String,
45 pub items: i64,
46 pub locked: i64,
47}
48
49pub async fn folders(pool: &PgPool, actor: &Actor) -> AppResult<Vec<Folder>> {
53 Ok(sqlx::query_as::<_, Folder>(&format!(
54 "WITH locked AS (
55 SELECT i.project_id, COUNT(*) AS locked
56 FROM revisions r JOIN items i ON i.id = r.item_id
57 WHERE r.checked_out_by IS NOT NULL
58 GROUP BY i.project_id
59 )
60 SELECT p.id, p.code, p.name,
61 (SELECT COUNT(*) FROM items i WHERE i.project_id = p.id) AS items,
62 COALESCE(l.locked, 0) AS locked
63 FROM projects p LEFT JOIN locked l ON l.project_id = p.id
64 WHERE p.active AND {} ORDER BY p.code",
65 access::project_visible("p.id", 1)
66 ))
67 .bind(&actor.hidden_projects)
68 .fetch_all(pool)
69 .await?)
70}
71
72pub enum Listing<'a> {
74 Project(Uuid),
76 CheckedOutBy(Uuid),
78 AllCheckedOut,
80 Unassigned,
82 Search(&'a str),
84 Favorites(Uuid),
86}
87
88const SELECT: &str =
89 "SELECT i.id AS item_id, i.number, i.item_type, i.project_id, p.code AS project_code,
90 r.id AS revision_id, r.code, r.name, r.state, r.checked_out_by,
91 u.display_name AS checked_out_by_name, r.checked_out_at, r.updated_at,
92 (SELECT COUNT(*) FROM files f WHERE f.revision_id = r.id) AS file_count
93 FROM items i
94 LEFT JOIN projects p ON p.id = i.project_id";
95
96pub async fn list(
100 pool: &PgPool,
101 actor: &Actor,
102 listing: Listing<'_>,
103 query: &str,
104) -> AppResult<Vec<Entry>> {
105 if let Listing::Project(id) = listing {
106 let exists: bool =
108 sqlx::query_scalar("SELECT EXISTS (SELECT 1 FROM projects WHERE id = $1)")
109 .bind(id)
110 .fetch_one(pool)
111 .await?;
112 if !exists {
113 return Err(AppError::not_found("Folder not found."));
114 }
115 access::ensure_visible(actor, Some(id), AppError::not_found("Folder not found."))?;
116 }
117 let pattern = if query.trim().is_empty() {
118 String::new()
119 } else {
120 domain::like_pattern(query)
121 };
122 let latest = "JOIN LATERAL (SELECT * FROM revisions r WHERE r.item_id = i.id
123 ORDER BY r.seq DESC LIMIT 1) r ON true";
124 let (sql, id) = match listing {
125 Listing::Project(id) => (
126 format!("{SELECT} {latest} LEFT JOIN users u ON u.id = r.checked_out_by WHERE i.project_id = $1"),
127 Some(id),
128 ),
129 Listing::Unassigned => (
130 format!("{SELECT} {latest} LEFT JOIN users u ON u.id = r.checked_out_by WHERE i.project_id IS NULL AND $1::uuid IS NULL"),
131 None,
132 ),
133 Listing::Search(_) => (
134 format!("{SELECT} {latest} LEFT JOIN users u ON u.id = r.checked_out_by WHERE $1::uuid IS NULL"),
135 None,
136 ),
137 Listing::CheckedOutBy(user) => (
138 format!("{SELECT} JOIN revisions r ON r.item_id = i.id LEFT JOIN users u ON u.id = r.checked_out_by
139 WHERE r.checked_out_by = $1"),
140 Some(user),
141 ),
142 Listing::Favorites(user) => (
143 format!("{SELECT} {latest} LEFT JOIN users u ON u.id = r.checked_out_by
144 WHERE i.id IN (SELECT item_id FROM favorites WHERE user_id = $1)"),
145 Some(user),
146 ),
147 Listing::AllCheckedOut => (
148 format!("{SELECT} JOIN revisions r ON r.item_id = i.id LEFT JOIN users u ON u.id = r.checked_out_by
149 WHERE r.checked_out_by IS NOT NULL AND $1::uuid IS NULL"),
150 None,
151 ),
152 };
153 let sql = format!(
154 "{sql} AND ($2 = '' OR i.number ILIKE $2 OR r.name ILIKE $2) AND {}
155 ORDER BY i.number LIMIT {LIMIT}",
156 access::item_visible("i", 3)
157 );
158 Ok(sqlx::query_as::<_, Entry>(&sql)
159 .bind(id)
160 .bind(&pattern)
161 .bind(&actor.hidden_projects)
162 .fetch_all(pool)
163 .await?)
164}