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

175 lines
8.9 KiB
C#

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;
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 not CSV. Using cache."; }
else { result.Error = "Google returned HTML not CSV. Publish as CSV."; return result; }
}
else File.WriteAllText(_cachePath, csvData);
}
catch (WebException ex)
{
if (File.Exists(_cachePath)) { csvData = File.ReadAllText(_cachePath); result.Warning = "Using cache. " + ex.Message; }
else { result.Error = "Fetch failed: " + ex.Message; 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 found.";
}
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);
var usedIds = new HashSet<int>();
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 rawTimestamp = RawGet(fields, _config.ColTimestamp);
var rawEmail = RawGet(fields, _config.ColEmail);
var rawTask = RawGet(fields, _config.ColTask);
var rawIssue = RawGet(fields, _config.ColIssue);
var ticket = new ExternalTicket
{
Source = "ntt",
Timestamp = SafeGetDate(fields, _config.ColTimestamp),
RequesterEmail = SafeGet(fields, _config.ColEmail),
RequesterName = SafeGet(fields, _config.ColRequestedBy),
TaskTitle = SafeGet(fields, _config.ColTask),
DeviceName = SafeGet(fields, _config.ColTask),
Location = SafeGet(fields, _config.ColLocation),
IssueDescription = SafeGet(fields, _config.ColIssue) ?? SafeGet(fields, _config.ColTask),
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)
};
var stableHash = GenerateStableHash(rawTimestamp, rawEmail, rawTask, rawIssue);
ticket.InternalId = StableIdFromHash(stableHash, usedIds);
ticket.ExternalId = "NTT" + ticket.InternalId.ToString();
usedIds.Add(ticket.InternalId);
// Use Contains for flexible status matching
var status = (ticket.RawStatus ?? "").ToLower().Trim();
ticket.IsOpen = !status.Contains("complete") && !status.Contains("closed")
&& !status.Contains("resolved") && !status.Contains("done")
&& !status.Contains("cancel") && !status.Contains("finished");
if (ticket.IsOpen && (!string.IsNullOrWhiteSpace(ticket.TaskTitle) || !string.IsNullOrWhiteSpace(ticket.IssueDescription)))
tickets.Add(ticket);
}
return tickets;
}
private int GenerateStableHash(string rawTimestamp, string rawEmail, string rawTask, string rawIssue)
{
var key = (rawTimestamp ?? "").Trim().ToLower() + "|" + (rawEmail ?? "").Trim().ToLower() + "|" + (rawTask ?? "").Trim().ToLower() + "|" + (rawIssue ?? "").Trim().ToLower();
uint hash = 5381;
for (int i = 0; i < key.Length; i++) hash = ((hash << 5) + hash) + (uint)key[i];
return (int)(hash % 800000) + 100000;
}
private int StableIdFromHash(int baseId, HashSet<int> used) { int id = baseId; while (used.Contains(id)) id++; return id; }
private string RawGet(List<string> fields, int index) { if (index < 0 || index >= fields.Count) return ""; return fields[index] ?? ""; }
public static string MapPriority(string raw)
{
if (string.IsNullOrEmpty(raw)) return "medium";
var lower = raw.ToLower().Trim();
if (lower.Contains("urgent") || lower.Contains("same day") || lower.Contains("critical")) return "critical";
if (lower.Contains("high") || lower.Contains("1-2 day")) return "high";
if (lower.Contains("medium") || lower.Contains("3-5 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";
return "medium";
}
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.MinValue; DateTime dt; if (DateTime.TryParse(val, out dt)) return dt; return DateTime.MinValue; }
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>(); }
}
}