Skip to main content

raps_cli/commands/rfi/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! RFI (Request for Information) Commands
5//!
6//! Commands for managing RFIs in ACC projects.
7
8mod crud;
9#[cfg(test)]
10mod tests;
11
12use std::path::PathBuf;
13
14use anyhow::Result;
15use clap::Subcommand;
16use serde::Serialize;
17
18use crate::commands::interactive;
19use crate::output::OutputFormat;
20use raps_acc::RfiClient;
21use raps_dm::DataManagementClient;
22
23#[derive(Debug, Subcommand)]
24pub enum RfiCommands {
25    /// List RFIs in a project
26    List {
27        /// Project ID (without "b." prefix)
28        project_id: Option<String>,
29
30        /// Filter by status (open, answered, closed, void)
31        #[arg(long)]
32        status: Option<String>,
33
34        /// Only show RFIs created after this date (YYYY-MM-DD)
35        #[arg(long)]
36        since: Option<String>,
37
38        /// Hub ID (for interactive mode)
39        #[arg(long, hide = true)]
40        hub_id: Option<String>,
41    },
42
43    /// Get details of a specific RFI
44    Get {
45        /// Project ID (without "b." prefix)
46        project_id: Option<String>,
47
48        /// RFI ID
49        rfi_id: Option<String>,
50
51        /// Hub ID (for interactive mode)
52        #[arg(long, hide = true)]
53        hub_id: Option<String>,
54    },
55
56    /// Create a new RFI
57    Create {
58        /// Project ID (without "b." prefix)
59        project_id: Option<String>,
60
61        /// RFI title
62        #[arg(long)]
63        title: Option<String>,
64
65        /// RFI question/description
66        #[arg(long)]
67        question: Option<String>,
68
69        /// Priority (low, normal, high, critical)
70        #[arg(long, default_value = "normal")]
71        priority: String,
72
73        /// Due date (ISO 8601 format: YYYY-MM-DD)
74        #[arg(long)]
75        due_date: Option<String>,
76
77        /// User ID to assign to
78        #[arg(long)]
79        assigned_to: Option<String>,
80
81        /// Location reference
82        #[arg(long)]
83        location: Option<String>,
84
85        /// Discipline
86        #[arg(long)]
87        discipline: Option<String>,
88
89        /// Create RFIs from CSV file (columns: title, description, assigned_to)
90        #[arg(long, value_name = "FILE")]
91        from_csv: Option<PathBuf>,
92
93        /// Hub ID (for interactive mode)
94        #[arg(long, hide = true)]
95        hub_id: Option<String>,
96    },
97
98    /// Update an existing RFI
99    Update {
100        /// Project ID (without "b." prefix)
101        project_id: Option<String>,
102
103        /// RFI ID
104        rfi_id: Option<String>,
105
106        /// New title
107        #[arg(long)]
108        title: Option<String>,
109
110        /// Update question
111        #[arg(long)]
112        question: Option<String>,
113
114        /// Set answer (typically transitions to 'answered' status)
115        #[arg(long)]
116        answer: Option<String>,
117
118        /// New status (open, answered, closed, void)
119        #[arg(long)]
120        status: Option<String>,
121
122        /// New priority
123        #[arg(long)]
124        priority: Option<String>,
125
126        /// New due date
127        #[arg(long)]
128        due_date: Option<String>,
129
130        /// Reassign to user
131        #[arg(long)]
132        assigned_to: Option<String>,
133
134        /// Update location
135        #[arg(long)]
136        location: Option<String>,
137
138        /// Hub ID (for interactive mode)
139        #[arg(long, hide = true)]
140        hub_id: Option<String>,
141    },
142
143    /// Delete an RFI
144    Delete {
145        /// Project ID (without "b." prefix)
146        project_id: Option<String>,
147
148        /// RFI ID
149        rfi_id: Option<String>,
150
151        /// Hub ID (for interactive mode)
152        #[arg(long, hide = true)]
153        hub_id: Option<String>,
154    },
155}
156
157impl RfiCommands {
158    pub async fn execute(
159        self,
160        client: &RfiClient,
161        dm_client: &DataManagementClient,
162        output_format: OutputFormat,
163    ) -> Result<()> {
164        match self {
165            RfiCommands::List {
166                project_id,
167                status,
168                since,
169                hub_id,
170            } => {
171                let (p_id, _) = resolve_rfi_args(
172                    dm_client,
173                    client,
174                    hub_id,
175                    project_id,
176                    Some("ignore".to_string()),
177                )
178                .await?;
179                crud::list_rfis(client, &p_id, status.as_deref(), since, output_format).await
180            }
181            RfiCommands::Get {
182                project_id,
183                rfi_id,
184                hub_id,
185            } => {
186                let (p_id, r_id) =
187                    resolve_rfi_args(dm_client, client, hub_id, project_id, rfi_id).await?;
188                crud::get_rfi(client, &p_id, &r_id, output_format).await
189            }
190            RfiCommands::Create {
191                project_id,
192                title,
193                question,
194                priority,
195                due_date,
196                assigned_to,
197                location,
198                discipline,
199                from_csv,
200                hub_id,
201            } => {
202                let (p_id, _) = resolve_rfi_args(
203                    dm_client,
204                    client,
205                    hub_id,
206                    project_id,
207                    Some("ignore".to_string()),
208                )
209                .await?;
210                crud::create_rfi(
211                    client,
212                    &p_id,
213                    title,
214                    question,
215                    &priority,
216                    due_date,
217                    assigned_to,
218                    location,
219                    discipline,
220                    from_csv,
221                    output_format,
222                )
223                .await
224            }
225            RfiCommands::Update {
226                project_id,
227                rfi_id,
228                title,
229                question,
230                answer,
231                status,
232                priority,
233                due_date,
234                assigned_to,
235                location,
236                hub_id,
237            } => {
238                let (p_id, r_id) =
239                    resolve_rfi_args(dm_client, client, hub_id, project_id, rfi_id).await?;
240                crud::update_rfi(
241                    client,
242                    &p_id,
243                    &r_id,
244                    title,
245                    question,
246                    answer,
247                    status,
248                    priority,
249                    due_date,
250                    assigned_to,
251                    location,
252                    output_format,
253                )
254                .await
255            }
256            RfiCommands::Delete {
257                project_id,
258                rfi_id,
259                hub_id,
260            } => {
261                let (p_id, r_id) =
262                    resolve_rfi_args(dm_client, client, hub_id, project_id, rfi_id).await?;
263                crud::delete_rfi(client, &p_id, &r_id, output_format).await
264            }
265        }
266    }
267}
268
269// ---------------------------------------------------------------------------
270// Shared helpers
271// ---------------------------------------------------------------------------
272
273async fn resolve_rfi_args(
274    dm_client: &DataManagementClient,
275    rfi_client: &RfiClient,
276    opt_hub_id: Option<String>,
277    opt_project_id: Option<String>,
278    opt_rfi_id: Option<String>,
279) -> Result<(String, String)> {
280    let hub_id = match (&opt_hub_id, &opt_project_id, &opt_rfi_id) {
281        (Some(h), _, _) => h.clone(),
282        (None, Some(_), Some(_)) => String::new(), // Not needed if both P and R are provided
283        (None, _, _) => interactive::prompt_for_hub(dm_client).await?,
284    };
285
286    let project_id = match opt_project_id {
287        Some(p) => p,
288        None => interactive::prompt_for_project(dm_client, &hub_id).await?,
289    };
290
291    let rfi_id = match opt_rfi_id {
292        Some(r) if r == "ignore" => String::new(),
293        Some(r) => r,
294        None => interactive::prompt_for_rfi(rfi_client, &project_id).await?,
295    };
296
297    Ok((project_id, rfi_id))
298}
299
300#[derive(Serialize)]
301pub(super) struct RfiOutput {
302    pub(super) id: String,
303    pub(super) number: Option<String>,
304    pub(super) title: String,
305    pub(super) status: String,
306    pub(super) priority: Option<String>,
307    pub(super) question: Option<String>,
308    pub(super) answer: Option<String>,
309    pub(super) due_date: Option<String>,
310    pub(super) assigned_to_name: Option<String>,
311    pub(super) created_at: Option<String>,
312}
313
314pub(super) fn truncate_str(s: &str, max_len: usize) -> String {
315    if s.len() <= max_len {
316        s.to_string()
317    } else {
318        format!("{}...", &s[..max_len - 3])
319    }
320}