import fs from 'fs' import yargs from 'yargs/yargs' import { hideBin } from 'yargs/helpers' import { parse } from 'csv-parse/sync' const argv = yargs(hideBin(process.argv)).command('Converter', 'Checks CSV for missig stuff, then converts CSV to Jiracula import file.', {}) .options({ 'file': { description: 'CSV file', alias: 'f', type: 'string', demandOption: true, }, 'month': { description: 'Month number to extract', alias: 'm', type: 'number', demandOption: true, } }).help().version('1.0').argv const colors = { red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", bold: "\x1b[1m" } // Regex patterns const durationPattern = /(\d+(?:[.,]\d+)?)\s*(h|H)/ const codePattern = /(SMEIMKT[- ]?\d+)/ // Read CSV if(!fs.existsSync(argv.file)) { console.error(`${colors.red}Cannot find this file !?`) process.exit(1) } const csv = fs.readFileSync(argv.file, 'utf8') const records = parse(csv, { columns: false, skip_empty_lines: true }) // Skip header const [header, ...rows] = records // Transform let results = [] for(let row of rows) { if (row.length<3) continue const date = row[0] const tasksStr = row[2] if (!tasksStr) continue if(date.substring(3,5)!=String(argv.month).padStart(2, '0')) continue const tasks = tasksStr.split('+').map(t => t.trim()) for (let t of tasks) { const durationMatch = durationPattern.exec(t) let duration = durationMatch ? durationMatch[1] : null const codeMatch = codePattern.exec(t) const code = codeMatch ? codeMatch[1].replace(' ', '-') : null results.push({ Date: date.trim(), TaskCode: code, Duration: parseFloat(duration) || 0, Description: t }) } } let errs=0, tot=0, timePerDate = {} results.forEach(r => { if(!r.TaskCode){ errs++ } else if(!r.Duration){ errs++ } else { if(r.Date in timePerDate) timePerDate[r.Date] += r.Duration else timePerDate[r.Date] = r.Duration } console.log( `${colors.cyan}${r.Date} ` + `${(r.TaskCode) ? colors.yellow+r.TaskCode : colors.red+'Missing Code'} ` + `${(r.Duration) ? colors.yellow+r.Duration : colors.red+'Missing Duration'} ` + `${colors.green}${r.Description}` ) tot++ }) if(errs>0){ console.log(`\n${colors.red}${errs} parsing errors on ${tot} tasks.${colors.green}\n`) } else { console.log(`\n${colors.green}${tot} task entries, no parsing errors.\n`) } const badDailyHours = {} let billable = 0 for(let dat in timePerDate){ if((timePerDate[dat] != 8) && (timePerDate[dat] != 4)) badDailyHours[dat] = timePerDate[dat] else billable += (timePerDate[dat]/8) } if(Object.keys(badDailyHours).length>0){ console.log(`${colors.red}Some dates don't have 8H...\n`) for(let dat in badDailyHours){ console.log(`${colors.red} ${dat}: ${badDailyHours[dat]}H`) } } else { console.log(`\n${colors.green}Ready to export to Jiracula !`) console.log(`Total billable days : ${billable}\n`) const outfile = argv.file.substring(0, argv.file.lastIndexOf('.'))+'.jrcl.json' fs.writeFileSync(outfile, JSON.stringify(results, null, 2), 'utf8') }