feat: add GoogleSheetService for CSV fetch and parse
This commit is contained in:
@@ -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<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;
|
||||
|
||||
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<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 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user