From 94f02a2911a02fbb8be9a78d705ca69e83a3b2a9 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Wed, 6 May 2026 14:40:29 +1000 Subject: [PATCH] feat: add GoogleSheetService for CSV fetch and parse --- Services/GoogleSheetService.cs | 206 +++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 Services/GoogleSheetService.cs diff --git a/Services/GoogleSheetService.cs b/Services/GoogleSheetService.cs new file mode 100644 index 0000000..e4a3771 --- /dev/null +++ b/Services/GoogleSheetService.cs @@ -0,0 +1,206 @@ +using Disco.Plugins.ServiceTracker.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; + +namespace Disco.Plugins.ServiceTracker.Services +{ + public class GoogleSheetService + { + private readonly GoogleSheetConfig _config; + private readonly string _cachePath; + + public GoogleSheetService(GoogleSheetConfig config, string dataDirectory) + { + _config = config; + _cachePath = Path.Combine(dataDirectory, "sheet_cache.csv"); + } + + public GoogleSheetResult FetchTickets() + { + var result = new GoogleSheetResult(); + if (!_config.Enabled || string.IsNullOrEmpty(_config.SpreadsheetId)) + { + result.Error = "Google Sheet integration is disabled."; + return result; + } + + string csvData = null; + + // Check cache + if (File.Exists(_cachePath)) + { + var cacheAge = DateTime.Now - File.GetLastWriteTime(_cachePath); + if (cacheAge.TotalMinutes < _config.RefreshMinutes) + { + csvData = File.ReadAllText(_cachePath); + } + } + + // Fetch fresh if no cache + if (csvData == null) + { + try + { + var url = string.Format( + "https://docs.google.com/spreadsheets/d/{0}/export?format=csv&gid={1}", + _config.SpreadsheetId, _config.GId); + + using (var client = new WebClient()) + { + client.Encoding = Encoding.UTF8; + csvData = client.DownloadString(url); + } + + // Cache it + File.WriteAllText(_cachePath, csvData); + } + catch (WebException ex) + { + // Try cached data as fallback + if (File.Exists(_cachePath)) + { + csvData = File.ReadAllText(_cachePath); + result.Warning = "Using cached data. Fetch error: " + ex.Message; + } + else + { + result.Error = "Could not fetch Google Sheet. Ensure it is published to web (File > Share > Publish to web > CSV). Error: " + ex.Message; + return result; + } + } + catch (Exception ex) + { + result.Error = "Sheet fetch error: " + ex.Message; + return result; + } + } + + // Parse CSV + try + { + result.Tickets = ParseCsv(csvData); + } + catch (Exception ex) + { + result.Error = "CSV parse error: " + ex.Message; + } + + return result; + } + + private List ParseCsv(string csvData) + { + var tickets = new List(); + var lines = csvData.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + int nextId = 100001; + + for (int i = _config.HeaderRows; i < lines.Length; i++) + { + var fields = ParseCsvLine(lines[i]); + if (fields.Count == 0) continue; + + var ticket = new ExternalTicket + { + InternalId = nextId++, + Source = "ntt", + Timestamp = SafeGetDate(fields, _config.ColTimestamp), + RequesterEmail = SafeGet(fields, _config.ColEmail), + DeviceName = SafeGet(fields, _config.ColDeviceName), + Location = SafeGet(fields, _config.ColLocation), + IssueDescription = SafeGet(fields, _config.ColIssue), + RawPriority = SafeGet(fields, _config.ColPriority), + RawStatus = SafeGet(fields, _config.ColStatus), + AssignedTo = SafeGet(fields, _config.ColAssignedTo) + }; + + // Generate stable ID from key fields + ticket.ExternalId = GenerateStableId(ticket); + + // Determine if open + var status = (ticket.RawStatus ?? "").ToLower().Trim(); + ticket.IsOpen = status != "closed" && status != "resolved" && status != "completed" && status != "done"; + + if (ticket.IsOpen && !string.IsNullOrWhiteSpace(ticket.IssueDescription)) + tickets.Add(ticket); + } + + return tickets; + } + + private string GenerateStableId(ExternalTicket t) + { + var key = (t.Timestamp.ToString("yyyyMMddHHmm") + "|" + (t.RequesterEmail ?? "") + "|" + (t.DeviceName ?? "")).ToLower(); + int hash = 0; + for (int i = 0; i < key.Length; i++) + hash = ((hash << 5) - hash) + key[i]; + return "NTT" + Math.Abs(hash).ToString().PadLeft(6, '0').Substring(0, 6); + } + + private string SafeGet(List fields, int index) + { + if (index < 0 || index >= fields.Count) return null; + var val = fields[index].Trim(); + return string.IsNullOrEmpty(val) ? null : val; + } + + private DateTime SafeGetDate(List fields, int index) + { + var val = SafeGet(fields, index); + if (val == null) return DateTime.Now; + DateTime dt; + if (DateTime.TryParse(val, out dt)) return dt; + return DateTime.Now; + } + + private List ParseCsvLine(string line) + { + var fields = new List(); + bool inQuotes = false; + var current = new StringBuilder(); + + for (int i = 0; i < line.Length; i++) + { + char c = line[i]; + if (c == '"') + { + if (inQuotes && i + 1 < line.Length && line[i + 1] == '"') + { + current.Append('"'); + i++; + } + else + { + inQuotes = !inQuotes; + } + } + else if (c == ',' && !inQuotes) + { + fields.Add(current.ToString()); + current.Clear(); + } + else if (c != '\r') + { + current.Append(c); + } + } + fields.Add(current.ToString()); + return fields; + } + } + + public class GoogleSheetResult + { + public List Tickets { get; set; } + public string Error { get; set; } + public string Warning { get; set; } + + public GoogleSheetResult() + { + Tickets = new List(); + } + } +}