csv-export.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import type {
  2. RevenueReport,
  3. ServiceReport,
  4. CustomerReport,
  5. InventoryReport,
  6. TechnicianReport,
  7. PartsUsageReport,
  8. JobAnalyticsReport,
  9. CustomerRetentionReport,
  10. TaxReport,
  11. PastDueInvoicesReport,
  12. VehicleReportData,
  13. } from '../Schema/reportTypes'
  14. import { formatCurrency } from '@/lib/format'
  15. function downloadCsv(
  16. filename: string,
  17. headers: string[],
  18. rows: Record<string, unknown>[],
  19. keys: string[]
  20. ) {
  21. const escapeCsv = (v: unknown) => {
  22. const s = String(v ?? '')
  23. return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s
  24. }
  25. const lines = [
  26. headers.map(escape).join(','),
  27. ...rows.map((r) => keys.map((k) => escapeCsv(r[k])).join(',')),
  28. ]
  29. const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' })
  30. const url = URL.createObjectURL(blob)
  31. const a = document.createElement('a')
  32. a.href = url
  33. a.download = filename
  34. a.click()
  35. URL.revokeObjectURL(url)
  36. }
  37. export function exportRevenueCsv(
  38. data: RevenueReport,
  39. currencyCode: string,
  40. headers: [string, string, string, string, string, string, string, string] = [
  41. 'Month',
  42. 'Revenue',
  43. 'Collected',
  44. 'Count',
  45. 'Parts Cost',
  46. 'Parts Net Profit',
  47. 'Labor',
  48. 'Net Profit',
  49. ]
  50. ) {
  51. const fmt = (n: number) => formatCurrency(n, currencyCode)
  52. const rows = data.monthly.map((m) => ({
  53. month: m.month,
  54. revenue: fmt(m.revenue),
  55. collected: fmt(m.collected),
  56. count: m.count,
  57. partsCost: fmt(m.partsCost),
  58. partsNetProfit: fmt(m.partsNetProfit),
  59. laborRevenue: fmt(m.laborRevenue),
  60. netProfit: fmt(m.netProfit),
  61. }))
  62. downloadCsv('revenue-report.csv', headers, rows, [
  63. 'month',
  64. 'revenue',
  65. 'collected',
  66. 'count',
  67. 'partsCost',
  68. 'partsNetProfit',
  69. 'laborRevenue',
  70. 'netProfit',
  71. ])
  72. }
  73. export function exportServicesCsv(
  74. data: ServiceReport,
  75. headers: [string, string, string] = ['Category', 'Label', 'Count']
  76. ) {
  77. const rows = [
  78. ...data.byStatus.map((s) => ({ category: 'Status', label: s.status, count: s.count })),
  79. ...data.byType.map((t) => ({ category: 'Type', label: t.type, count: t.count })),
  80. ]
  81. downloadCsv('services-report.csv', headers, rows, ['category', 'label', 'count'])
  82. }
  83. export function exportCustomersCsv(
  84. data: CustomerReport,
  85. currencyCode: string,
  86. headers: [string, string, string, string] = ['Name', 'Company', 'Services', 'Total Spent']
  87. ) {
  88. const fmt = (n: number) => formatCurrency(n, currencyCode)
  89. const rows = data.topCustomers.map((c) => ({
  90. name: c.name,
  91. company: c.company ?? '',
  92. serviceCount: c.serviceCount,
  93. totalSpent: fmt(c.totalSpent),
  94. }))
  95. downloadCsv('customers-report.csv', headers, rows, [
  96. 'name',
  97. 'company',
  98. 'serviceCount',
  99. 'totalSpent',
  100. ])
  101. }
  102. export function exportInventoryCsv(
  103. data: InventoryReport,
  104. currencyCode: string,
  105. headers: [string, string, string, string, string] = [
  106. 'Name',
  107. 'Part #',
  108. 'Quantity',
  109. 'Min Quantity',
  110. 'Unit Cost',
  111. ]
  112. ) {
  113. const fmt = (n: number) => formatCurrency(n, currencyCode)
  114. const rows = data.lowStock.map((p) => ({
  115. name: p.name,
  116. partNumber: p.partNumber ?? '',
  117. quantity: p.unit ? `${p.quantity} ${p.unit}` : p.quantity,
  118. minQuantity:
  119. p.minQuantity != null ? (p.unit ? `${p.minQuantity} ${p.unit}` : p.minQuantity) : '',
  120. unitCost: p.unitCost != null ? fmt(p.unitCost) : '',
  121. }))
  122. downloadCsv('inventory-report.csv', headers, rows, [
  123. 'name',
  124. 'partNumber',
  125. 'quantity',
  126. 'minQuantity',
  127. 'unitCost',
  128. ])
  129. }
  130. export function exportTechniciansCsv(
  131. data: TechnicianReport,
  132. currencyCode: string,
  133. headers: [string, string, string, string, string, string] = [
  134. 'Technician',
  135. 'Jobs',
  136. 'Total Revenue',
  137. 'Avg Revenue',
  138. 'Total Hours',
  139. 'Avg Hours',
  140. ]
  141. ) {
  142. const fmt = (n: number) => formatCurrency(n, currencyCode)
  143. const rows = data.technicians.map((t) => ({
  144. techName: t.techName,
  145. jobCount: t.jobCount,
  146. totalRevenue: fmt(t.totalRevenue),
  147. avgRevenue: fmt(t.avgRevenue),
  148. totalHours: t.totalLaborHours.toFixed(1),
  149. avgHours: t.avgHours.toFixed(1),
  150. }))
  151. downloadCsv('technicians-report.csv', headers, rows, [
  152. 'techName',
  153. 'jobCount',
  154. 'totalRevenue',
  155. 'avgRevenue',
  156. 'totalHours',
  157. 'avgHours',
  158. ])
  159. }
  160. export function exportPartsCsv(
  161. data: PartsUsageReport,
  162. currencyCode: string,
  163. headers: [string, string, string, string, string, string, string] = [
  164. 'Part Name',
  165. 'Part #',
  166. 'Usage Count',
  167. 'Total Qty',
  168. 'Revenue',
  169. 'Cost',
  170. 'Net Profit',
  171. ]
  172. ) {
  173. const fmt = (n: number) => formatCurrency(n, currencyCode)
  174. const rows = data.parts.map((p) => ({
  175. name: p.name,
  176. partNumber: p.partNumber ?? '',
  177. usageCount: p.usageCount,
  178. totalQuantity: p.totalQuantity,
  179. totalRevenue: fmt(p.totalRevenue),
  180. totalCost: fmt(p.totalCost),
  181. netProfit: fmt(p.netProfit),
  182. }))
  183. downloadCsv('parts-usage-report.csv', headers, rows, [
  184. 'name',
  185. 'partNumber',
  186. 'usageCount',
  187. 'totalQuantity',
  188. 'totalRevenue',
  189. 'totalCost',
  190. 'netProfit',
  191. ])
  192. }
  193. export function exportJobAnalyticsCsv(
  194. data: JobAnalyticsReport,
  195. currencyCode: string,
  196. headers: [string, string, string, string] = ['Service Type', 'Count', 'Avg Value', 'Avg Hours']
  197. ) {
  198. const fmt = (n: number) => formatCurrency(n, currencyCode)
  199. const rows = data.topServiceTypes.map((t) => ({
  200. type: t.type,
  201. count: t.count,
  202. avgValue: fmt(t.avgValue),
  203. avgHours: t.avgHours.toFixed(1),
  204. }))
  205. downloadCsv('job-analytics-report.csv', headers, rows, ['type', 'count', 'avgValue', 'avgHours'])
  206. }
  207. export function exportRetentionCsv(
  208. data: CustomerRetentionReport,
  209. currencyCode: string,
  210. headers: [string, string, string, string, string] = [
  211. 'Customer',
  212. 'Company',
  213. 'Visits',
  214. 'Total Spent',
  215. 'Avg Days Between Visits',
  216. ]
  217. ) {
  218. const fmt = (n: number) => formatCurrency(n, currencyCode)
  219. const rows = data.topReturning.map((c) => ({
  220. name: c.name,
  221. company: c.company ?? '',
  222. visitCount: c.visitCount,
  223. totalSpent: fmt(c.totalSpent),
  224. avgDaysBetweenVisits: c.avgTimeBetweenVisits ?? '',
  225. }))
  226. downloadCsv('retention-report.csv', headers, rows, [
  227. 'name',
  228. 'company',
  229. 'visitCount',
  230. 'totalSpent',
  231. 'avgDaysBetweenVisits',
  232. ])
  233. }
  234. export function exportPastDueInvoicesCsv(
  235. data: PastDueInvoicesReport,
  236. currencyCode: string,
  237. headers: [string, string, string, string, string, string, string, string] = [
  238. 'Customer',
  239. 'Company',
  240. 'Invoice #',
  241. 'Total Amount',
  242. 'Amount Paid',
  243. 'Amount Due',
  244. 'Due Date',
  245. 'Days Past Due',
  246. ]
  247. ) {
  248. const fmt = (n: number) => formatCurrency(n, currencyCode)
  249. const rows = data.invoices.map((inv) => ({
  250. customer: inv.customerName,
  251. company: inv.customerCompany ?? '',
  252. invoiceNumber: inv.invoiceNumber ?? '',
  253. totalAmount: fmt(inv.totalAmount),
  254. amountPaid: fmt(inv.amountPaid),
  255. amountDue: fmt(inv.amountDue),
  256. dueDate: inv.dueDate,
  257. daysPastDue: inv.daysPastDue,
  258. }))
  259. downloadCsv('past-due-invoices-report.csv', headers, rows, [
  260. 'customer',
  261. 'company',
  262. 'invoiceNumber',
  263. 'totalAmount',
  264. 'amountPaid',
  265. 'amountDue',
  266. 'dueDate',
  267. 'daysPastDue',
  268. ])
  269. }
  270. export function exportVehicleReportCsv(
  271. data: VehicleReportData,
  272. currencyCode: string,
  273. headers: [string, string, string, string, string, string, string] = [
  274. 'Date',
  275. 'Title',
  276. 'Type',
  277. 'Status',
  278. 'Total',
  279. 'Parts',
  280. 'Labor Hours',
  281. ]
  282. ) {
  283. const fmt = (n: number) => formatCurrency(n, currencyCode)
  284. const rows = data.serviceHistory.map((s) => ({
  285. date: s.date,
  286. title: s.title,
  287. type: s.type,
  288. status: s.status,
  289. totalAmount: fmt(s.totalAmount),
  290. partsCount: s.partsCount,
  291. laborHours: s.laborHours.toFixed(1),
  292. }))
  293. const vehicleLabel = `${data.vehicleInfo.year} ${data.vehicleInfo.make} ${data.vehicleInfo.model}`
  294. downloadCsv(`vehicle-report-${vehicleLabel.replace(/\s+/g, '-')}.csv`, headers, rows, [
  295. 'date',
  296. 'title',
  297. 'type',
  298. 'status',
  299. 'totalAmount',
  300. 'partsCount',
  301. 'laborHours',
  302. ])
  303. }
  304. export function exportTaxCsv(
  305. data: TaxReport,
  306. currencyCode: string,
  307. headers: [string, string, string, string] = [
  308. 'Month',
  309. 'Tax Collected',
  310. 'Taxable Amount',
  311. 'Invoice Count',
  312. ]
  313. ) {
  314. const fmt = (n: number) => formatCurrency(n, currencyCode)
  315. const rows = data.monthly.map((m) => ({
  316. month: m.month,
  317. taxCollected: fmt(m.taxCollected),
  318. taxableAmount: fmt(m.taxableAmount),
  319. invoiceCount: m.invoiceCount,
  320. }))
  321. downloadCsv('tax-report.csv', headers, rows, [
  322. 'month',
  323. 'taxCollected',
  324. 'taxableAmount',
  325. 'invoiceCount',
  326. ])
  327. }