1use chrono::{DateTime, NaiveDate, Utc};
13use rust_decimal::Decimal;
14use sqlx::{PgPool, Row};
15use uuid::Uuid;
16
17use backbone_orm::{company_scope, org_scope};
18
19use crate::domain::entity::MaintenanceRequest;
20
21pub const TABLE_NAME: &str = "maintenance.maintenance_requests";
23
24pub struct MaintenanceRequestRepository(
29 backbone_orm::GenericCrudRepository<MaintenanceRequest, backbone_orm::SoftDelete>,
30);
31
32impl std::ops::Deref for MaintenanceRequestRepository {
33 type Target = backbone_orm::GenericCrudRepository<MaintenanceRequest, backbone_orm::SoftDelete>;
34 fn deref(&self) -> &Self::Target { &self.0 }
35}
36
37impl MaintenanceRequestRepository {
38 pub fn new(pool: PgPool) -> Self {
40 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
41 }
42}
43
44pub struct NewRequestRow<'a> {
50 pub id: Uuid,
51 pub name: &'a str,
52 pub description: Option<&'a str>,
53 pub schedule_date: Option<DateTime<Utc>>,
54 pub schedule_end: Option<DateTime<Utc>>,
55 pub duration: Decimal,
56 pub owner_user_id: Option<Uuid>,
57 pub user_id: Option<Uuid>,
58 pub asset_id: Option<Uuid>,
59 pub stage_id: Uuid,
60 pub kanban_state: &'a str,
61 pub priority: &'a str,
62 pub maintenance_type: &'a str,
63 pub recurring: bool,
64 pub repeat_interval: i32,
65 pub repeat_unit: &'a str,
66 pub repeat_type: &'a str,
67 pub repeat_until: Option<NaiveDate>,
68 pub successor_of_request_id: Option<Uuid>,
70}
71
72pub struct RequestTransitionRow {
75 pub name: String,
76 pub description: Option<String>,
77 pub schedule_date: Option<DateTime<Utc>>,
78 pub schedule_end: Option<DateTime<Utc>>,
79 pub close_date: Option<NaiveDate>,
80 pub duration: Decimal,
81 pub owner_user_id: Option<Uuid>,
82 pub user_id: Option<Uuid>,
83 pub asset_id: Option<Uuid>,
84 pub stage_id: Uuid,
85 pub kanban_state: String,
86 pub priority: String,
87 pub maintenance_type: String,
88 pub recurring: bool,
89 pub repeat_interval: i32,
90 pub repeat_unit: String,
91 pub repeat_type: String,
92 pub repeat_until: Option<NaiveDate>,
93 pub successor_request_id: Option<Uuid>,
94 pub stage_done: bool,
96}
97
98pub struct StageRefRow {
100 pub id: Uuid,
101 pub name: String,
102 pub sequence: i32,
103 pub done: bool,
104}
105
106pub struct RequestFieldUpdates<'a> {
110 pub name: Option<&'a str>,
111 pub description: Option<&'a str>,
112 pub schedule_date: Option<DateTime<Utc>>,
113 pub schedule_end: Option<DateTime<Utc>>,
114 pub duration: Option<Decimal>,
115 pub owner_user_id: Option<Uuid>,
116 pub user_id: Option<Uuid>,
117 pub asset_id: Option<Uuid>,
118 pub priority: Option<&'a str>,
119 pub maintenance_type: Option<&'a str>,
120 pub recurring: Option<bool>,
121 pub repeat_interval: Option<i32>,
122 pub repeat_unit: Option<&'a str>,
123 pub repeat_type: Option<&'a str>,
124 pub repeat_until: Option<NaiveDate>,
125}
126
127impl<'a> RequestFieldUpdates<'a> {
128 pub fn is_empty(&self) -> bool {
130 self.name.is_none()
131 && self.description.is_none()
132 && self.schedule_date.is_none()
133 && self.schedule_end.is_none()
134 && self.duration.is_none()
135 && self.owner_user_id.is_none()
136 && self.user_id.is_none()
137 && self.asset_id.is_none()
138 && self.priority.is_none()
139 && self.maintenance_type.is_none()
140 && self.recurring.is_none()
141 && self.repeat_interval.is_none()
142 && self.repeat_unit.is_none()
143 && self.repeat_type.is_none()
144 && self.repeat_until.is_none()
145 }
146}
147
148impl MaintenanceRequestRepository {
151 pub async fn insert_request_on(
155 &self,
156 conn: &mut sqlx::PgConnection,
157 r: &NewRequestRow<'_>,
158 ) -> Result<(), sqlx::Error> {
159 sqlx::query(
160 r#"INSERT INTO maintenance.maintenance_requests
161 (id, name, description, schedule_date, schedule_end, duration,
162 owner_user_id, user_id, asset_id, stage_id, kanban_state, priority,
163 maintenance_type, recurring, repeat_interval, repeat_unit, repeat_type,
164 repeat_until, successor_of_request_id)
165 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::request_kanban_state,
166 $12::request_priority,$13::maintenance_type,$14,$15,$16::repeat_unit,
167 $17::repeat_type,$18,$19)"#,
168 )
169 .bind(r.id).bind(r.name).bind(r.description)
170 .bind(r.schedule_date).bind(r.schedule_end).bind(r.duration)
171 .bind(r.owner_user_id).bind(r.user_id).bind(r.asset_id).bind(r.stage_id)
172 .bind(r.kanban_state).bind(r.priority).bind(r.maintenance_type).bind(r.recurring)
173 .bind(r.repeat_interval).bind(r.repeat_unit).bind(r.repeat_type).bind(r.repeat_until)
174 .bind(r.successor_of_request_id)
175 .execute(conn)
176 .await?;
177 Ok(())
178 }
179
180 pub async fn insert_request(
184 &self,
185 pool: &PgPool,
186 r: &NewRequestRow<'_>,
187 ) -> Result<(), sqlx::Error> {
188 org_scope::execute_scoped(
189 pool,
190 sqlx::query(
191 r#"INSERT INTO maintenance.maintenance_requests
192 (id, name, description, schedule_date, schedule_end, duration,
193 owner_user_id, user_id, asset_id, stage_id, kanban_state, priority,
194 maintenance_type, recurring, repeat_interval, repeat_unit, repeat_type,
195 repeat_until, successor_of_request_id)
196 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::request_kanban_state,
197 $12::request_priority,$13::maintenance_type,$14,$15,$16::repeat_unit,
198 $17::repeat_type,$18,$19)"#,
199 )
200 .bind(r.id).bind(r.name).bind(r.description)
201 .bind(r.schedule_date).bind(r.schedule_end).bind(r.duration)
202 .bind(r.owner_user_id).bind(r.user_id).bind(r.asset_id).bind(r.stage_id)
203 .bind(r.kanban_state).bind(r.priority).bind(r.maintenance_type).bind(r.recurring)
204 .bind(r.repeat_interval).bind(r.repeat_unit).bind(r.repeat_type).bind(r.repeat_until)
205 .bind(r.successor_of_request_id),
206 )
207 .await?;
208 Ok(())
209 }
210
211 pub async fn fetch_for_transition(
215 &self,
216 pool: &PgPool,
217 request_id: Uuid,
218 ) -> Result<Option<RequestTransitionRow>, sqlx::Error> {
219 let row = company_scope::fetch_optional_row_scoped(
220 pool,
221 sqlx::query(
222 r#"SELECT r.name, r.description, r.schedule_date, r.schedule_end,
223 r.close_date, r.duration, r.owner_user_id, r.user_id, r.asset_id,
224 r.stage_id, r.kanban_state::text AS kanban_state,
225 r.priority::text AS priority, r.maintenance_type::text AS maintenance_type,
226 r.recurring, r.repeat_interval, r.repeat_unit::text AS repeat_unit,
227 r.repeat_type::text AS repeat_type, r.repeat_until,
228 r.successor_request_id, COALESCE(s.done, FALSE) AS stage_done
229 FROM maintenance.maintenance_requests r
230 LEFT JOIN maintenance.maintenance_stages s ON s.id = r.stage_id
231 WHERE r.id = $1 AND (r.metadata->>'deleted_at') IS NULL"#,
232 )
233 .bind(request_id),
234 )
235 .await?;
236 Ok(row.map(|r| RequestTransitionRow {
237 name: r.get("name"),
238 description: r.get("description"),
239 schedule_date: r.get("schedule_date"),
240 schedule_end: r.get("schedule_end"),
241 close_date: r.get("close_date"),
242 duration: r.get("duration"),
243 owner_user_id: r.get("owner_user_id"),
244 user_id: r.get("user_id"),
245 asset_id: r.get("asset_id"),
246 stage_id: r.get("stage_id"),
247 kanban_state: r.get("kanban_state"),
248 priority: r.get("priority"),
249 maintenance_type: r.get("maintenance_type"),
250 recurring: r.get("recurring"),
251 repeat_interval: r.get("repeat_interval"),
252 repeat_unit: r.get("repeat_unit"),
253 repeat_type: r.get("repeat_type"),
254 repeat_until: r.get("repeat_until"),
255 successor_request_id: r.get("successor_request_id"),
256 stage_done: r.get("stage_done"),
257 }))
258 }
259
260 pub async fn fetch_stage(
263 &self,
264 pool: &PgPool,
265 stage_id: Uuid,
266 ) -> Result<Option<StageRefRow>, sqlx::Error> {
267 let row = company_scope::fetch_optional_row_scoped(
268 pool,
269 sqlx::query(
270 r#"SELECT id, name, sequence, done
271 FROM maintenance.maintenance_stages
272 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
273 )
274 .bind(stage_id),
275 )
276 .await?;
277 Ok(row.map(|r| StageRefRow {
278 id: r.get("id"),
279 name: r.get("name"),
280 sequence: r.get("sequence"),
281 done: r.get("done"),
282 }))
283 }
284
285 pub async fn fetch_first_stage_id(&self, pool: &PgPool) -> Result<Option<Uuid>, sqlx::Error> {
288 company_scope::fetch_optional_scalar_scoped(
289 pool,
290 sqlx::query_scalar(
291 r#"SELECT id FROM maintenance.maintenance_stages
292 WHERE (metadata->>'deleted_at') IS NULL
293 ORDER BY sequence, id LIMIT 1"#,
294 ),
295 )
296 .await
297 }
298
299 pub async fn fetch_first_stage_id_on(
303 &self,
304 conn: &mut sqlx::PgConnection,
305 ) -> Result<Option<Uuid>, sqlx::Error> {
306 let row = sqlx::query_scalar::<_, Uuid>(
307 r#"SELECT id FROM maintenance.maintenance_stages
308 WHERE (metadata->>'deleted_at') IS NULL
309 ORDER BY sequence, id LIMIT 1"#,
310 )
311 .fetch_optional(conn)
312 .await?;
313 Ok(row)
314 }
315
316 pub async fn transition_cas(
324 &self,
325 conn: &mut sqlx::PgConnection,
326 request_id: Uuid,
327 target_stage_id: Uuid,
328 kanban_override: Option<&str>,
329 ) -> Result<u64, sqlx::Error> {
330 let r = match kanban_override {
331 Some(k) => sqlx::query(
332 r#"UPDATE maintenance.maintenance_requests
333 SET stage_id = $2, kanban_state = $3::request_kanban_state
334 WHERE id = $1 AND stage_id IS DISTINCT FROM $2
335 AND (metadata->>'deleted_at') IS NULL"#,
336 )
337 .bind(request_id)
338 .bind(target_stage_id)
339 .bind(k)
340 .execute(conn)
341 .await?,
342 None => sqlx::query(
343 r#"UPDATE maintenance.maintenance_requests
344 SET stage_id = $2
345 WHERE id = $1 AND stage_id IS DISTINCT FROM $2
346 AND (metadata->>'deleted_at') IS NULL"#,
347 )
348 .bind(request_id)
349 .bind(target_stage_id)
350 .execute(conn)
351 .await?,
352 };
353 Ok(r.rows_affected())
354 }
355
356 pub async fn claim_successor_slot(
361 &self,
362 conn: &mut sqlx::PgConnection,
363 request_id: Uuid,
364 successor_id: Uuid,
365 ) -> Result<u64, sqlx::Error> {
366 let r = sqlx::query(
367 r#"UPDATE maintenance.maintenance_requests SET successor_request_id = $2
368 WHERE id = $1 AND successor_request_id IS NULL
369 AND (metadata->>'deleted_at') IS NULL"#,
370 )
371 .bind(request_id)
372 .bind(successor_id)
373 .execute(conn)
374 .await?;
375 Ok(r.rows_affected())
376 }
377
378 pub async fn update_request_fields(
381 &self,
382 pool: &PgPool,
383 request_id: Uuid,
384 u: &RequestFieldUpdates<'_>,
385 ) -> Result<u64, sqlx::Error> {
386 let r = org_scope::execute_scoped(
387 pool,
388 sqlx::query(
389 r#"UPDATE maintenance.maintenance_requests SET
390 name = COALESCE($2, name),
391 description = COALESCE($3, description),
392 schedule_date = COALESCE($4::timestamptz, schedule_date),
393 schedule_end = COALESCE($5::timestamptz, schedule_end),
394 duration = COALESCE($6, duration),
395 owner_user_id = COALESCE($7, owner_user_id),
396 user_id = COALESCE($8, user_id),
397 asset_id = COALESCE($9, asset_id),
398 priority = COALESCE($10::request_priority, priority),
399 maintenance_type = COALESCE($11::maintenance_type, maintenance_type),
400 recurring = COALESCE($12, recurring),
401 repeat_interval = COALESCE($13, repeat_interval),
402 repeat_unit = COALESCE($14::repeat_unit, repeat_unit),
403 repeat_type = COALESCE($15::repeat_type, repeat_type),
404 repeat_until = COALESCE($16, repeat_until)
405 WHERE id = $1 AND (metadata->>'deleted_at') IS NULL"#,
406 )
407 .bind(request_id)
408 .bind(u.name)
409 .bind(u.description)
410 .bind(u.schedule_date)
411 .bind(u.schedule_end)
412 .bind(u.duration)
413 .bind(u.owner_user_id)
414 .bind(u.user_id)
415 .bind(u.asset_id)
416 .bind(u.priority)
417 .bind(u.maintenance_type)
418 .bind(u.recurring)
419 .bind(u.repeat_interval)
420 .bind(u.repeat_unit)
421 .bind(u.repeat_type)
422 .bind(u.repeat_until),
423 )
424 .await?;
425 Ok(r.rows_affected())
426 }
427}
428
429backbone_core::impl_crud_repository!(MaintenanceRequestRepository, MaintenanceRequest, soft_delete);