251 lines
10 KiB
C#
251 lines
10 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;
|
||
|
||
// 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 (1–2 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>(); }
|
||
}
|
||
}
|