@@ -1,3 +1,5 @@
1+using System.Globalization;
2+using System.Text;
3 using FieldOps.Api.Auditing;
4 using FieldOps.Api.Data;
5 using FieldOps.Api.Data.Entities;
@@ -58,4 +60,38 @@ public async Task<IActionResult> Start(int id, CancellationToken ct)
60 await _db.SaveChangesAsync(ct);
61 return NoContent();
62 }
63+
64+ [HttpGet("export")]
65+ [Authorize(Policy = "WorkOrders.Read")]
66+ public async Task<IActionResult> Export([FromQuery] string customer, CancellationToken ct)
67+ {
68+ // The DbContext's query filter limits this to the current tenant.
69+ var rows = await _db.WorkOrders
70+ .Where(w => w.CustomerName.Contains(customer))
The customer is matched with 'Contains', which is a partial text match. Exporting 'Smith' also pulls in 'Smithson Ltd' and 'Blacksmith Co'. An empty or very short value matches almost every customer. This file is meant to be sent to one customer at month end, so that customer could receive another customer's work orders, prices, and descriptions. The remembered rule: when picking records for one specific owner, match on an exact ID, not on a fragment of a name.
Suggested fix: Take a customer ID (for example [FromQuery] int customerId) and filter with w.CustomerId == customerId. If only the name is available, use an exact match (w.CustomerName == customer), and return 400 when it is empty.
71+ .OrderBy(w => w.ScheduledFor)
72+ .Take(5000)
73+ .ToListAsync(ct);
74+
75+ var csv = new StringBuilder("Id,Customer,Description,Scheduled,Total\n");
76+ foreach (var row in rows)
77+ {
78+ csv.Append(row.Id).Append(',')
79+ .Append(Csv(row.CustomerName)).Append(',')
80+ .Append(Csv(row.Description)).Append(',')
81+ .Append(row.ScheduledFor.ToString("O")).Append(',')
82+ .Append(row.QuotedTotal.ToString(CultureInfo.InvariantCulture)).Append('\n');
83+ }
84+
85+ return File(Encoding.UTF8.GetBytes(csv.ToString()), "text/csv", "work-orders.csv");
86+ }
87+
88+ /// <summary>
89+ /// Quotes a value for CSV. A leading =, +, - or @ is prefixed with a quote
90+ /// mark so a spreadsheet does not run it as a formula.
91+ /// </summary>
92+ private static string Csv(string value)
93+ {
94+ var safe = value.Length > 0 && "=+-@".Contains(value[0]) ? "'" + value : value;
95+ return "\"" + safe.Replace("\"", "\"\"") + "\"";
96+ }
97 }