csv-export.ts 9.1 KB

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