Skip to main content

max / goingson

8.0 KB · 289 lines History Blame Raw
1 // CSV Import Plugin for GoingsOn
2 //
3 // Imports tasks, projects, or events from CSV files.
4 // Supports common field mappings and flexible date parsing.
5
6 // Plugin metadata
7 fn describe() {
8 #{
9 name: "CSV Import",
10 file_extensions: ["csv", "tsv"],
11 entity_types: ["task", "project", "event"]
12 }
13 }
14
15 // Parse a CSV file and return items for import
16 fn parse(file_path, options) {
17 // Read and parse CSV
18 let content = goingson::read_file(file_path);
19 let rows = goingson::parse_csv(content, options);
20
21 if rows.len() == 0 {
22 goingson::log_warn("CSV file is empty");
23 return goingson::task_result([]);
24 }
25
26 // Auto-detect entity type from columns
27 let first_row = rows[0];
28 let columns = [];
29 for key in first_row.keys() {
30 columns.push(key.to_lower());
31 }
32
33 let entity_type = detect_entity_type(columns);
34 goingson::log_info(`Detected entity type: ${entity_type}`);
35
36 // Parse based on entity type
37 switch entity_type {
38 "task" => parse_tasks(rows),
39 "project" => parse_projects(rows),
40 "event" => parse_events(rows),
41 _ => {
42 goingson::log_error("Could not determine entity type from columns");
43 goingson::task_result([])
44 }
45 }
46 }
47
48 // Detect entity type from column names
49 fn detect_entity_type(columns) {
50 // Event indicators: start/end times
51 if columns.contains("start") || columns.contains("start_time") || columns.contains("start_date") {
52 return "event";
53 }
54
55 // Project indicators
56 if columns.contains("project_type") || (columns.contains("name") && !columns.contains("description")) {
57 return "project";
58 }
59
60 // Default to task
61 "task"
62 }
63
64 // Parse rows as tasks
65 fn parse_tasks(rows) {
66 let items = [];
67 let row_num = 0;
68
69 for row in rows {
70 row_num += 1;
71
72 // Get description (required)
73 let description = get_field(row, ["description", "task", "title", "name", "subject"]);
74 if description == "" {
75 goingson::log_warn(`Row ${row_num}: Missing description, skipping`);
76 continue;
77 }
78
79 // Get optional fields
80 let due = get_field(row, ["due", "due_date", "deadline", "date"]);
81 let priority = normalize_priority(get_field(row, ["priority", "pri", "importance"]));
82 let status = normalize_status(get_field(row, ["status", "state"]));
83 let project_name = get_field(row, ["project", "project_name", "category"]);
84 let tags = parse_tags(get_field(row, ["tags", "labels", "categories"]));
85 let notes = get_field(row, ["notes", "note", "comments", "body"]);
86
87 // Parse due date if present
88 if due != "" {
89 due = goingson::parse_date(due);
90 }
91
92 items.push(#{
93 description: description,
94 due: due,
95 priority: priority,
96 status: status,
97 project_name: project_name,
98 tags: tags,
99 notes: notes
100 });
101 }
102
103 goingson::log_info(`Parsed ${items.len()} tasks`);
104 goingson::task_result(items)
105 }
106
107 // Parse rows as projects
108 fn parse_projects(rows) {
109 let items = [];
110 let row_num = 0;
111
112 for row in rows {
113 row_num += 1;
114
115 // Get name (required)
116 let name = get_field(row, ["name", "project", "title"]);
117 if name == "" {
118 goingson::log_warn(`Row ${row_num}: Missing name, skipping`);
119 continue;
120 }
121
122 // Get optional fields
123 let description = get_field(row, ["description", "desc", "notes"]);
124 let project_type = normalize_project_type(get_field(row, ["type", "project_type", "category"]));
125 let status = normalize_project_status(get_field(row, ["status", "state"]));
126
127 items.push(#{
128 name: name,
129 description: description,
130 project_type: project_type,
131 status: status
132 });
133 }
134
135 goingson::log_info(`Parsed ${items.len()} projects`);
136 goingson::project_result(items)
137 }
138
139 // Parse rows as events
140 fn parse_events(rows) {
141 let items = [];
142 let row_num = 0;
143
144 for row in rows {
145 row_num += 1;
146
147 // Get title (required)
148 let title = get_field(row, ["title", "name", "event", "subject", "summary"]);
149 if title == "" {
150 goingson::log_warn(`Row ${row_num}: Missing title, skipping`);
151 continue;
152 }
153
154 // Get start time (required)
155 let start = get_field(row, ["start", "start_time", "start_date", "date", "when"]);
156 if start == "" {
157 goingson::log_warn(`Row ${row_num}: Missing start time, skipping`);
158 continue;
159 }
160 start = goingson::parse_date(start);
161
162 // Get optional fields
163 let end = get_field(row, ["end", "end_time", "end_date"]);
164 if end != "" {
165 end = goingson::parse_date(end);
166 }
167
168 let location = get_field(row, ["location", "place", "venue", "where"]);
169 let description = get_field(row, ["description", "notes", "body", "details"]);
170 let project_name = get_field(row, ["project", "project_name", "category"]);
171
172 items.push(#{
173 title: title,
174 start: start,
175 end: end,
176 location: location,
177 description: description,
178 project_name: project_name
179 });
180 }
181
182 goingson::log_info(`Parsed ${items.len()} events`);
183 goingson::event_result(items)
184 }
185
186 // Get first matching field from row
187 fn get_field(row, field_names) {
188 for name in field_names {
189 if row.contains(name) {
190 let value = row[name];
191 if value != () && value != "" {
192 return value.to_string().trim();
193 }
194 }
195 // Also check lowercase version
196 let lower_name = name.to_lower();
197 if row.contains(lower_name) {
198 let value = row[lower_name];
199 if value != () && value != "" {
200 return value.to_string().trim();
201 }
202 }
203 }
204 ""
205 }
206
207 // Normalize priority value
208 fn normalize_priority(value) {
209 if value == "" { return (); }
210
211 let lower = value.to_lower();
212 switch lower {
213 "high" | "1" | "h" | "urgent" | "critical" => "High",
214 "low" | "3" | "l" | "minor" => "Low",
215 _ => "Medium"
216 }
217 }
218
219 // Normalize task status
220 fn normalize_status(value) {
221 if value == "" { return (); }
222
223 let lower = value.to_lower();
224 switch lower {
225 "done" | "complete" | "completed" | "finished" | "closed" => "Done",
226 "in progress" | "inprogress" | "started" | "working" | "active" => "InProgress",
227 _ => "Pending"
228 }
229 }
230
231 // Normalize project type
232 fn normalize_project_type(value) {
233 if value == "" { return (); }
234
235 let lower = value.to_lower();
236 switch lower {
237 "job" | "work" | "employment" => "Job",
238 "side project" | "sideproject" | "personal" | "hobby" => "SideProject",
239 "company" | "business" | "startup" => "Company",
240 "essay" | "writing" => "Essay",
241 "article" | "blog" | "post" => "Article",
242 "painting" | "art" | "visual" => "Painting",
243 _ => "Other"
244 }
245 }
246
247 // Normalize project status
248 fn normalize_project_status(value) {
249 if value == "" { return (); }
250
251 let lower = value.to_lower();
252 switch lower {
253 "active" | "current" | "ongoing" => "Active",
254 "on hold" | "onhold" | "paused" | "waiting" => "OnHold",
255 "completed" | "done" | "finished" => "Completed",
256 "archived" | "inactive" | "closed" => "Archived",
257 _ => "Active"
258 }
259 }
260
261 // Parse tags from string
262 fn parse_tags(value) {
263 if value == "" { return []; }
264
265 // Split by comma, semicolon, or pipe
266 let tags = [];
267 let current = "";
268
269 for char in value.chars() {
270 if char == ',' || char == ';' || char == '|' {
271 let tag = current.trim();
272 if tag != "" {
273 tags.push(tag);
274 }
275 current = "";
276 } else {
277 current += char;
278 }
279 }
280
281 // Don't forget the last tag
282 let tag = current.trim();
283 if tag != "" {
284 tags.push(tag);
285 }
286
287 tags
288 }
289