Files
disco-service-tracker-plugin/Services/GoogleSheetService.cs
T

251 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Disco.Plugins.ServiceTracker.Models;
using System;
using System.Collections.Generic;
using System.IO;
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);
}
if (csvData == null)
{
try
{
string url;
var id = _config.SpreadsheetId.Trim();
if (id.StartsWith("2PACX", StringComparison.OrdinalIgnoreCase))
url = "https://docs.google.com/spreadsheets/d/e/" + id + "/pub?output=csv&gid=" + _config.GId;
else
url = "https://docs.google.com/spreadsheets/d/" + id + "/export?format=csv&gid=" + _config.GId;
using (var client = new WebClient())
{
client.Encoding = Encoding.UTF8;
client.Headers.Add("User-Agent", "Mozilla/5.0 DiscoServiceTracker/1.0");
csvData = client.DownloadString(url);
}
if (csvData != null && csvData.TrimStart().StartsWith("<!DOCTYPE", StringComparison.OrdinalIgnoreCase))
{
if (File.Exists(_cachePath))
{
csvData = File.ReadAllText(_cachePath);
result.Warning = "Got HTML instead of CSV. Using cache. Publish sheet as CSV format.";
}
else
{
result.Error = "Google returned HTML not CSV. Publish the sheet tab as CSV: File > Share > Publish to web > select tab > CSV > Publish.";
return result;
}
}
else
{
File.WriteAllText(_cachePath, csvData);
}
}
catch (WebException ex)
{
if (File.Exists(_cachePath))
{
csvData = File.ReadAllText(_cachePath);
result.Warning = "Using cache. Error: " + ex.Message;
}
else
{
result.Error = "Fetch failed: " + ex.Message + ". Publish the sheet tab as CSV.";
return result;
}
}
catch (Exception ex)
{
result.Error = "Error: " + ex.Message;
return result;
}
}
try
{
result.Tickets = ParseCsv(csvData);
if (result.Tickets.Count == 0 && string.IsNullOrEmpty(result.Warning))
result.Warning = "Sheet OK but no open tickets. All may be Completed/Closed.";
}
catch (Exception ex)
{
result.Error = "Parse error: " + ex.Message;
}
return result;
}
private List<ExternalTicket> ParseCsv(string csvData)
{
var tickets = new List<ExternalTicket>();
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;
bool allEmpty = true;
for (int j = 0; j < fields.Count; j++)
if (!string.IsNullOrWhiteSpace(fields[j])) { allEmpty = false; break; }
if (allEmpty) continue;
var taskTitle = SafeGet(fields, _config.ColTask);
var issueDesc = SafeGet(fields, _config.ColIssue);
var requestedBy = SafeGet(fields, _config.ColRequestedBy);
var ticket = new ExternalTicket
{
InternalId = nextId++,
Source = "ntt",
Timestamp = SafeGetDate(fields, _config.ColTimestamp),
RequesterEmail = SafeGet(fields, _config.ColEmail),
RequesterName = requestedBy,
TaskTitle = taskTitle,
DeviceName = taskTitle,
Location = SafeGet(fields, _config.ColLocation),
IssueDescription = issueDesc ?? taskTitle,
RawPriority = SafeGet(fields, _config.ColPriority),
RawStatus = SafeGet(fields, _config.ColStatus),
AssignedTo = SafeGet(fields, _config.ColAssignedTo),
PreferredDate = SafeGetDateNullable(fields, _config.ColPreferredDate),
SheetNotes = SafeGet(fields, _config.ColNotes)
};
ticket.ExternalId = GenerateStableId(ticket);
// Determine if open
var status = (ticket.RawStatus ?? "").ToLower().Trim();
ticket.IsOpen = status != "completed" && status != "closed"
&& status != "resolved" && status != "done" && status != "cancelled";
if (ticket.IsOpen && (!string.IsNullOrWhiteSpace(taskTitle) || !string.IsNullOrWhiteSpace(issueDesc)))
tickets.Add(ticket);
}
return tickets;
}
public static string MapPriority(string raw)
{
if (string.IsNullOrEmpty(raw)) return "medium";
var lower = raw.ToLower().Trim();
// Handle emoji priority format: "🔴 Urgent (Same Day)", "🟠 High (12 days)" etc.
if (lower.Contains("urgent") || lower.Contains("same day") || lower.Contains("critical"))
return "critical";
if (lower.Contains("high") || lower.Contains("1-2 day") || lower.Contains("1\u20132 day"))
return "high";
if (lower.Contains("medium") || lower.Contains("3-5 day") || lower.Contains("3\u20135 day"))
return "medium";
if (lower.Contains("low") || lower.Contains("1 week") || lower.Contains("week+"))
return "low";
if (lower.Contains("scheduled") || lower.Contains("planned"))
return "scheduled";
// Try matching by emoji colour
if (raw.Contains("\U0001F534") || raw.Contains("\uD83D\uDD34")) return "critical"; // red circle
if (raw.Contains("\U0001F7E0") || raw.Contains("\uD83D\uDFE0")) return "high"; // orange circle
if (raw.Contains("\U0001F7E1") || raw.Contains("\uD83D\uDFE1")) return "medium"; // yellow circle
if (raw.Contains("\U0001F7E2") || raw.Contains("\uD83D\uDFE2")) return "low"; // green circle
return "medium";
}
private string GenerateStableId(ExternalTicket t)
{
var key = (t.Timestamp.ToString("yyyyMMddHHmm") + "|" + (t.RequesterEmail ?? "") + "|" + (t.TaskTitle ?? "")).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<string> 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<string> 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 DateTime? SafeGetDateNullable(List<string> fields, int index)
{
var val = SafeGet(fields, index);
if (val == null) return null;
DateTime dt;
if (DateTime.TryParse(val, out dt)) return dt;
return null;
}
private List<string> ParseCsvLine(string line)
{
var fields = new List<string>();
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<ExternalTicket> Tickets { get; set; }
public string Error { get; set; }
public string Warning { get; set; }
public GoogleSheetResult() { Tickets = new List<ExternalTicket>(); }
}
}