diff --git a/app/Import/Providers/BaseImport.php b/app/Import/Providers/BaseImport.php index f934e59fde13..0ad36b20474a 100644 --- a/app/Import/Providers/BaseImport.php +++ b/app/Import/Providers/BaseImport.php @@ -163,7 +163,7 @@ class BaseImport private function groupTasks($csvData, $key) { - if (! $key) { + if (! $key || count(array_column($csvData, $key)) == 0) { return $csvData; } @@ -448,10 +448,9 @@ class BaseImport $task_transformer = $this->transformer; $task_repository = new TaskRepository(); - // $task_repository->import_mode = true; $tasks = $this->groupTasks($tasks, $task_number_key); - + foreach ($tasks as $raw_task) { $task_data = []; try { @@ -496,7 +495,7 @@ class BaseImport ]; } } - +nlog($count); return $count; } diff --git a/app/Import/Providers/Csv.php b/app/Import/Providers/Csv.php index 97444ddb6f92..63b19aa2f044 100644 --- a/app/Import/Providers/Csv.php +++ b/app/Import/Providers/Csv.php @@ -73,7 +73,7 @@ class Csv extends BaseImport implements ImportInterface 'quote', 'bank_transaction', 'recurring_invoice', - 'tasks', + 'task', ]) ) { $this->{$entity}(); @@ -362,7 +362,7 @@ class Csv extends BaseImport implements ImportInterface } if (empty($data)) { - $this->entity_count['invoices'] = 0; + $this->entity_count['tasks'] = 0; return; } @@ -378,6 +378,8 @@ class Csv extends BaseImport implements ImportInterface $task_count = $this->ingestTasks($data, 'task.number'); $this->entity_count['tasks'] = $task_count; + + } public function transform(array $data) diff --git a/app/Import/Transformer/BaseTransformer.php b/app/Import/Transformer/BaseTransformer.php index 31c05ceb16a5..c1f1d43cbdf5 100644 --- a/app/Import/Transformer/BaseTransformer.php +++ b/app/Import/Transformer/BaseTransformer.php @@ -676,6 +676,9 @@ class BaseTransformer */ public function getProjectId($name, $clientId = null) { + if(strlen($name) == 0) + return null; + $project = Project::query()->where('company_id', $this->company->id) ->where('is_deleted', false) ->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [ diff --git a/app/Import/Transformer/Csv/TaskTransformer.php b/app/Import/Transformer/Csv/TaskTransformer.php index 060e5b04b0ce..9f817ab04ad4 100644 --- a/app/Import/Transformer/Csv/TaskTransformer.php +++ b/app/Import/Transformer/Csv/TaskTransformer.php @@ -11,6 +11,7 @@ namespace App\Import\Transformer\Csv; +use App\Models\TaskStatus; use App\Import\Transformer\BaseTransformer; /** @@ -35,12 +36,14 @@ class TaskTransformer extends BaseTransformer $this->getString($task_data, 'client.email') ); + $projectId = $task_data['project.name'] ?? ''; + $transformed = [ 'company_id' => $this->company->id, 'number' => $this->getString($task_data, 'task.number'), 'user_id' => $this->getString($task_data, 'task.user_id'), 'client_id' => $clientId, - 'project_id' => $this->getProjectId($task_data['project.name'], $clientId), + 'project_id' => $this->getProjectId($projectId, $clientId), 'description' => $this->getString($task_data, 'task.description'), 'status' => $this->getTaskStatusId($task_data), 'custom_value1' => $this->getString($task_data, 'task.custom_value1'), @@ -49,6 +52,11 @@ class TaskTransformer extends BaseTransformer 'custom_value4' => $this->getString($task_data, 'task.custom_value4'), ]; + if(count($task_items_data) == count($task_items_data, COUNT_RECURSIVE)) { + $transformed['time_log'] = json_encode([$this->parseLog($task_items_data)]); + return $transformed; + } + $time_log = collect($task_items_data) ->map(function ($item) { @@ -56,32 +64,36 @@ class TaskTransformer extends BaseTransformer })->toJson(); - nlog($time_log); - $transformed['time_log'] = $time_log; return $transformed; } - private function parseLog($item): array + private function parseLog($item) { $start_date = false; $end_date = false; $notes = $item['task.notes'] ?? ''; - $is_billable = $item['task.is_billable'] ?? false; + + if(isset($item['task.is_billable']) && is_string($item['task.is_billable']) && in_array($item['task.is_billable'], ['yes', 'true', '1'])) + $is_billable = true; + elseif(isset($item['task.is_billable']) && is_bool($item['task.is_billable'])) + $is_billable = $item['task.is_billable']; + else + $is_billable = false; - if(isset($item['start_date']) && - isset($item['end_date'])) { + if(isset($item['task.start_date']) && + isset($item['task.end_date'])) { $start_date = $this->resolveStartDate($item); $end_date = $this->resolveEndDate($item); - } elseif(isset($item['duration'])) { - $duration = strtotime($item['duration']) - strtotime('TODAY'); + } elseif(isset($item['task.duration'])) { + $duration = strtotime($item['task.duration']) - strtotime('TODAY'); $start_date = $this->stubbed_timestamp; $end_date = $this->stubbed_timestamp + $duration; - $this->stubbed_timestamp++; + $this->stubbed_timestamp; } else { - return []; + return ''; } return [$start_date, $end_date, $notes, $is_billable]; @@ -90,13 +102,17 @@ class TaskTransformer extends BaseTransformer private function resolveStartDate($item) { - $stub_start_date = $item['start_date'] . ' ' . isset($item['start_time']) ?? ''; + $stub_start_date = $item['task.start_date']; + $stub_start_date .= isset($item['task.start_time']) ? " ".$item['task.start_time'] : ''; try { + $stub_start_date = \Carbon\Carbon::parse($stub_start_date); $this->stubbed_timestamp = $stub_start_date->timestamp; + return $stub_start_date->timestamp; } catch (\Exception $e) { + nlog($e->getMessage()); return $this->stubbed_timestamp; } @@ -105,22 +121,50 @@ class TaskTransformer extends BaseTransformer private function resolveEndDate($item) { - $stub_start_date = $item['end_date'] . ' ' . isset($item['end_time']) ?? ''; + $stub_end_date = $item['task.end_date']; + $stub_end_date .= isset($item['task.end_time']) ? " ".$item['task.end_time'] : ''; try { - $stub_start_date = \Carbon\Carbon::parse($stub_start_date); - if($stub_start_date->timestamp == $this->stubbed_timestamp) { - $this->stubbed_timestamp++; + $stub_end_date = \Carbon\Carbon::parse($stub_end_date); + + if($stub_end_date->timestamp == $this->stubbed_timestamp) { + $this->stubbed_timestamp; return $this->stubbed_timestamp; } - $this->stubbed_timestamp = $stub_start_date->timestamp++; - return $stub_start_date->timestamp; + $this->stubbed_timestamp = $stub_end_date->timestamp; + return $stub_end_date->timestamp; } catch (\Exception $e) { - return $this->stubbed_timestamp++; + nlog($e->getMessage()); + + return $this->stubbed_timestamp; } } + private function getTaskStatusId($item): ?int + { + if(isset($item['task.status'])) + { + $name = strtolower(trim($item['task.status'])); + + $ts = TaskStatus::query()->where('company_id', $this->company->id) + ->where('is_deleted', false) + ->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [ + strtolower(str_replace(' ', '', $name)), + ]) + ->first(); + + if($ts) + return $ts->id; + } + + return TaskStatus::where('company_id', $this->company->id) + ->where('is_deleted', false) + ->orderBy('status_order', 'asc') + ->first()->id ?? null; + + } + } diff --git a/tests/Feature/Import/CSV/TaskImportTest.php b/tests/Feature/Import/CSV/TaskImportTest.php new file mode 100644 index 000000000000..4ebfaaa6cc61 --- /dev/null +++ b/tests/Feature/Import/CSV/TaskImportTest.php @@ -0,0 +1,168 @@ +withoutMiddleware(ThrottleRequests::class); + + config(['database.default' => config('ninja.db.default')]); + + $this->makeTestData(); + + $this->withoutExceptionHandling(); + + auth()->login($this->user); + } + + public function testTaskImportWithGroupedTaskNumbers() + { + Task::query() + ->where('company_id', $this->company->id) + ->forceDelete(); + + $this->assertEquals(0, Task::withTrashed()->where('company_id', $this->company->id)->count()); + + /*Need to import clients first*/ + $csv = file_get_contents( + base_path().'/tests/Feature/Import/tasks2.csv' + ); + $hash = Str::random(32); + $column_map = [ + 0 => 'task.user_id', + 3 => 'project.name', + 2 => 'client.name', + 4 => 'task.number', + 5 => 'task.description', + 6 => 'task.is_billable', + 7 => 'task.start_date', + 9 => 'task.end_date', + 8 => 'task.start_time', + 10 => 'task.end_time', + 11 => 'task.duration', + ]; + + $data = [ + 'hash' => $hash, + 'column_map' => ['task' => ['mapping' => $column_map]], + 'skip_header' => true, + 'import_type' => 'csv', + ]; + + Cache::put($hash.'-task', base64_encode($csv), 360); + + $csv_importer = new Csv($data, $this->company); + + $this->assertInstanceOf(Csv::class, $csv_importer); + + $csv_importer->import('task'); + + $base_transformer = new BaseTransformer($this->company); + + $task = Task::where('company_id', $this->company->id)->where('number', 'x1234')->first(); + $this->assertNotNull($task); + $this->assertEquals(1998, $task->calcDuration()); + + $time_log = json_decode($task->time_log); + + foreach($time_log as $log) { + $this->assertFalse($log[3]); + } + + $task = Task::where('company_id', $this->company->id)->where('number', 'x1233')->first(); + $this->assertNotNull($task); + $this->assertEquals(9833, $task->calcDuration()); + + $time_log = json_decode($task->time_log); + + foreach($time_log as $log) + { + $this->assertFalse($log[3]); + } + + + } + + + + public function testTaskImport() + { + Task::query() + ->where('company_id', $this->company->id) + ->forceDelete(); + + $this->assertEquals(0, Task::withTrashed()->where('company_id', $this->company->id)->count()); + + /*Need to import clients first*/ + $csv = file_get_contents( + base_path().'/tests/Feature/Import/tasks.csv' + ); + $hash = Str::random(32); + $column_map = [ + 0 => 'task.user_id', + 3 => 'project.name', + 2 => 'client.name', + 5 => 'task.description', + 6 => 'task.is_billable', + 7 => 'task.start_date', + 9 => 'task.end_date', + 8 => 'task.start_time', + 10 => 'task.end_time', + 11 => 'task.duration', + ]; + + $data = [ + 'hash' => $hash, + 'column_map' => ['task' => ['mapping' => $column_map]], + 'skip_header' => true, + 'import_type' => 'csv', + ]; + + Cache::put($hash.'-task', base64_encode($csv), 360); + + $csv_importer = new Csv($data, $this->company); + + $this->assertInstanceOf(Csv::class, $csv_importer); + + $csv_importer->import('task'); + + $base_transformer = new BaseTransformer($this->company); + + } + + +} \ No newline at end of file diff --git a/tests/Feature/Import/tasks.csv b/tests/Feature/Import/tasks.csv new file mode 100644 index 000000000000..c0c6f3c2b393 --- /dev/null +++ b/tests/Feature/Import/tasks.csv @@ -0,0 +1,48 @@ +User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Duration,Tags,Amount () +Jimmy,user@example.com,,Fixup My Code,,Short and sweet,No,2023-10-23,10:01:07,2023-10-23,12:15:02,02:13:55,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-23,13:07:17,2023-10-23,14:11:17,01:04:00,, +Jimmy,user@example.com,,,,Side Hustle,No,2023-10-23,14:11:19,2023-10-23,17:08:38,02:57:19,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-23,14:49:00,2023-10-23,18:03:43,03:14:43,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-23,17:31:40,2023-10-23,18:18:56,00:47:16,, +Bob,bob@example.com,,,,,No,2023-10-24,10:00:00,2023-10-24,17:05:00,07:05:00,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,10:01:00,2023-10-24,10:35:55,00:34:55,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,10:35:55,2023-10-24,10:39:49,00:03:54,, +Jimmy,user@example.com,,Gov Proj,,Fix all the servers,No,2023-10-24,10:39:50,2023-10-24,11:05:25,00:25:35,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:05:27,2023-10-24,11:10:56,00:05:29,, +Jimmy,user@example.com,,Gov Proj,,clean the house,No,2023-10-24,11:11:08,2023-10-24,11:17:11,00:06:03,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,11:17:13,2023-10-24,11:21:36,00:04:23,, +Jimmy,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:21:42,2023-10-24,11:22:58,00:01:16,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,11:22:59,2023-10-24,11:32:55,00:09:56,, +Jimmy,user@example.com,,Gov Proj,,wax the floor,No,2023-10-24,11:32:55,2023-10-24,12:19:17,00:46:22,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,12:19:19,2023-10-24,12:40:57,00:21:38,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,13:18:54,2023-10-24,17:05:55,03:47:01,, +Jimmy,user@example.com,,Gov Proj,,Sand the car,No,2023-10-24,17:05:59,2023-10-24,17:08:27,00:02:28,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-24,17:08:29,2023-10-24,18:00:33,00:52:04,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-25,09:58:45,2023-10-25,10:32:09,00:33:24,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,10:09:06,2023-10-25,13:01:49,02:52:43,, +Bob,bob@example.com,,Gov Proj,,Attack the windows,No,2023-10-25,10:32:10,2023-10-25,15:54:19,05:22:09,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,13:52:30,2023-10-25,15:30:48,01:38:18,, +Jimmy,user@example.com,,Gov Proj,,assist old lady to cross the street,No,2023-10-25,15:30:48,2023-10-25,15:49:10,00:18:22,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-25,15:49:11,2023-10-25,18:09:43,02:20:32,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-25,16:08:29,2023-10-25,18:06:12,01:57:43,, +Jlecrenay,jlecrenay@solyme.com,,Gestion interne,,admin work,No,2023-10-26,09:24:00,2023-10-26,12:22:00,02:58:00,, +Bob,bob@example.com,,,,admin work,No,2023-10-26,09:25:49,2023-10-26,11:58:34,02:32:45,, +Jimmy,user@example.com,,,,,No,2023-10-26,09:26:22,2023-10-26,19:04:06,09:37:44,, +Bob,bob@example.com,,Fixup My Code,,administration,No,2023-10-26,13:25:23,2023-10-26,17:08:00,03:42:37,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,training,No,2023-10-26,13:37:31,2023-10-26,18:14:00,04:36:29,, +Jlecrenay,jlecrenay@solyme.com,,,,reading,No,2023-10-27,08:37:17,2023-10-27,09:24:45,00:47:28,, +Jlecrenay,jlecrenay@solyme.com,,LOCATION,,travel,No,2023-10-27,09:25:29,2023-10-27,09:41:46,00:16:17,, +Jlecrenay,jlecrenay@solyme.com,,,,travel,No,2023-10-27,09:42:04,2023-10-27,10:46:35,01:04:31,, +Jimmy,user@example.com,,Fixup My Code,,travel,No,2023-10-27,09:55:02,2023-10-27,10:25:52,00:30:50,, +Bob,bob@example.com,,Fixup My Code,,travel,No,2023-10-27,10:10:00,2023-10-27,10:44:00,00:34:00,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:25:53,2023-10-27,10:41:22,00:15:29,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:41:23,2023-10-27,10:59:56,00:18:33,, +Bob,bob@example.com,,Fixup My Code,,,No,2023-10-27,10:44:00,2023-10-27,10:59:00,00:15:00,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,travel,No,2023-10-27,10:46:38,2023-10-27,12:20:13,01:33:35,, +Jimmy,user@example.com,,Gov Proj,,travel,No,2023-10-27,11:01:42,2023-10-27,11:06:01,00:04:19,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-27,11:09:38,2023-10-27,12:20:07,01:10:29,, +Jimmy,user@example.com,,Fixup My Code,,,No,2023-10-27,13:02:18,2023-10-27,13:57:17,00:54:59,, +Jlecrenay,jlecrenay@solyme.com,,Covert Ops,,Holiday,No,2023-10-27,13:20:27,2023-10-27,18:10:58,04:50:31,, +Jimmy,user@example.com,,Gov Proj,,fix the wifi,No,2023-10-27,13:57:17,2023-10-27,14:39:11,00:41:54,, +Bob,bob@example.com,,Gov Proj,,fix the spark plugs,No,2023-10-27,14:29:25,2023-10-27,16:31:24,02:01:59,, +Jimmy,user@example.com,,BLOOM,,travel,No,2023-10-27,14:39:12,2023-10-27,15:12:30,00:33:18,, diff --git a/tests/Feature/Import/tasks2.csv b/tests/Feature/Import/tasks2.csv new file mode 100644 index 000000000000..15d3911eca3d --- /dev/null +++ b/tests/Feature/Import/tasks2.csv @@ -0,0 +1,48 @@ +User,Email,Client,Project,Task,Description,Billable,Start date,Start time,End date,End time,Duration,Tags,Amount () +Bob,user@example.com,,Fixup My Code,x111,Short and sweet,No,2023-10-23,10:01:07,2023-10-23,12:15:02,02:13:55,, +Bob,user@example.com,,Fixup My Code,x111,,No,2023-10-23,13:07:17,2023-10-23,14:11:17,01:04:00,, +Bob,user@example.com,,,,Side Hustle,No,2023-10-23,14:11:19,2023-10-23,17:08:38,02:57:19,, +James,james@example.com,,Fixup My Code,x111,,No,2023-10-23,14:49:00,2023-10-23,18:03:43,03:14:43,, +Bob,user@example.com,,Fixup My Code,x111,,No,2023-10-23,17:31:40,2023-10-23,18:18:56,00:47:16,, +James,james@example.com,,,,,No,2023-10-24,10:00:00,2023-10-24,17:05:00,07:05:00,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,10:01:00,2023-10-24,10:35:55,00:34:55,, +Bob,user@example.com,,Gov Proj,x112,euro handball training,No,2023-10-24,10:35:55,2023-10-24,10:39:49,00:03:54,, +Bob,user@example.com,,Gov Proj,x112,Fix all the servers,No,2023-10-24,10:39:50,2023-10-24,11:05:25,00:25:35,, +Bob,user@example.com,,Gov Proj,x112,euro handball training,No,2023-10-24,11:05:27,2023-10-24,11:10:56,00:05:29,, +Bob,user@example.com,,Gov Proj,x112,clean the house,No,2023-10-24,11:11:08,2023-10-24,11:17:11,00:06:03,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,11:17:13,2023-10-24,11:21:36,00:04:23,, +Bob,user@example.com,,Gov Proj,,euro handball training,No,2023-10-24,11:21:42,2023-10-24,11:22:58,00:01:16,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,11:22:59,2023-10-24,11:32:55,00:09:56,, +Bob,user@example.com,,Gov Proj,,wax the floor,No,2023-10-24,11:32:55,2023-10-24,12:19:17,00:46:22,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,12:19:19,2023-10-24,12:40:57,00:21:38,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,13:18:54,2023-10-24,17:05:55,03:47:01,, +Bob,user@example.com,,Gov Proj,,Sand the car,No,2023-10-24,17:05:59,2023-10-24,17:08:27,00:02:28,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-24,17:08:29,2023-10-24,18:00:33,00:52:04,, +James,james@example.com,,Fixup My Code,,,No,2023-10-25,09:58:45,2023-10-25,10:32:09,00:33:24,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,10:09:06,2023-10-25,13:01:49,02:52:43,, +James,james@example.com,,Gov Proj,,Attack the windows,No,2023-10-25,10:32:10,2023-10-25,15:54:19,05:22:09,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,13:52:30,2023-10-25,15:30:48,01:38:18,, +Bob,user@example.com,,Gov Proj,,assist old lady to cross the street,No,2023-10-25,15:30:48,2023-10-25,15:49:10,00:18:22,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-25,15:49:11,2023-10-25,18:09:43,02:20:32,, +James,james@example.com,,Fixup My Code,,,No,2023-10-25,16:08:29,2023-10-25,18:06:12,01:57:43,, +Jlecrenay,jlecrenay@solyme.com,,Gestion interne,,admin work,No,2023-10-26,09:24:00,2023-10-26,12:22:00,02:58:00,, +James,james@example.com,,,,admin work,No,2023-10-26,09:25:49,2023-10-26,11:58:34,02:32:45,, +Bob,user@example.com,,,,,No,2023-10-26,09:26:22,2023-10-26,19:04:06,09:37:44,, +James,james@example.com,,Fixup My Code,,administration,No,2023-10-26,13:25:23,2023-10-26,17:08:00,03:42:37,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,training,No,2023-10-26,13:37:31,2023-10-26,18:14:00,04:36:29,, +Jlecrenay,jlecrenay@solyme.com,,,,reading,No,2023-10-27,08:37:17,2023-10-27,09:24:45,00:47:28,, +Jlecrenay,jlecrenay@solyme.com,,LOCATION,,travel,No,2023-10-27,09:25:29,2023-10-27,09:41:46,00:16:17,, +Jlecrenay,jlecrenay@solyme.com,,,,travel,No,2023-10-27,09:42:04,2023-10-27,10:46:35,01:04:31,, +Bob,user@example.com,,Fixup My Code,,travel,No,2023-10-27,09:55:02,2023-10-27,10:25:52,00:30:50,, +James,james@example.com,,Fixup My Code,,travel,No,2023-10-27,10:10:00,2023-10-27,10:44:00,00:34:00,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:25:53,2023-10-27,10:41:22,00:15:29,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,10:41:23,2023-10-27,10:59:56,00:18:33,, +James,james@example.com,,Fixup My Code,,,No,2023-10-27,10:44:00,2023-10-27,10:59:00,00:15:00,, +Jlecrenay,jlecrenay@solyme.com,,Holiday Project,,travel,No,2023-10-27,10:46:38,2023-10-27,12:20:13,01:33:35,, +Bob,user@example.com,,Gov Proj,,travel,No,2023-10-27,11:01:42,2023-10-27,11:06:01,00:04:19,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-27,11:09:38,2023-10-27,12:20:07,01:10:29,, +Bob,user@example.com,,Fixup My Code,,,No,2023-10-27,13:02:18,2023-10-27,13:57:17,00:54:59,, +Jlecrenay,jlecrenay@solyme.com,,Covert Ops,,Holiday,No,2023-10-27,13:20:27,2023-10-27,18:10:58,04:50:31,, +Bob,user@example.com,,Gov Proj,x1233,fix the wifi,No,2023-10-27,13:57:17,2023-10-27,14:39:11,00:41:54,, +James,james@example.com,,Gov Proj,x1233,fix the spark plugs,No,2023-10-27,14:29:25,2023-10-27,16:31:24,02:01:59,, +Bob,user@example.com,,BLOOM,x1234,travel,No,2023-10-27,14:39:12,2023-10-27,15:12:30,00:33:18,,