diff --git a/app/Filament/Pages/JiraImport.php b/app/Filament/Pages/JiraImport.php
index fe56abe56..c7367d0b4 100644
--- a/app/Filament/Pages/JiraImport.php
+++ b/app/Filament/Pages/JiraImport.php
@@ -19,6 +19,7 @@
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
+use App\Settings\JiraSettings;
class JiraImport extends Page implements HasForms
{
@@ -48,10 +49,17 @@ class JiraImport extends Page implements HasForms
public $selected_tickets;
public $data = [];
public $ticketsDataApi;
+ public $epicKeys = [];
+ public $taskKeys = [];
public function mount(): void
{
- $this->form->fill();
+ $settings = app(JiraSettings::class);
+ $this->form->fill([
+ 'host' => $settings->host,
+ 'username' => $settings->username,
+ 'token' => $settings->token,
+ ]);
}
public static function shouldRegisterNavigation(): bool
@@ -87,7 +95,7 @@ public function getFormSchema(): array
'class' => 'bg-primary-500 rounded-lg border border-primary-600 text-white font-medium text-sm py-3 px-4'
])
->hiddenLabel()
- ->content(__('Important: Your jira credentials are only used to communicate with jira REST API, and will not be stored in this application')),
+ ->content(__('Your Jira credentials are used to communicate with the Jira REST API. They will be saved (with the API token encrypted) so you do not need to re-enter them each time.')),
Grid::make()
->schema([
@@ -109,8 +117,14 @@ public function getFormSchema(): array
]),
])
->afterValidation(function () {
+ $settings = app(JiraSettings::class);
+ $settings->host = $this->host;
+ $settings->username = $this->username;
+ $settings->token = $this->token;
+ $settings->save();
+
$this->loadingProjects = true;
- $this->emit('updateJiraProjects');
+ $this->dispatch('updateJiraProjects');
}),
Wizard\Step::make(__('Jira projects'))
@@ -164,7 +178,7 @@ public function getFormSchema(): array
])
->afterValidation(function () {
$this->loadingTickets = true;
- $this->emit('updateJiraTickets');
+ $this->dispatch('updateJiraTickets');
}),
Wizard\Step::make(__('Jira tickets'))
@@ -187,6 +201,18 @@ public function getFormSchema(): array
->visible(fn() => $this->loadingTickets)
->content(__('Loading tickets, please wait...'));
+ if (!$this->loadingTickets && $this->tickets) {
+ $fields[] = Placeholder::make('selection_buttons')
+ ->hiddenLabel()
+ ->content(new HtmlString(
+ "
"
+ . "" . __('Select All Epics') . " "
+ . "" . __('Select All Tasks') . " "
+ . "" . __('Deselect All') . " "
+ . "
"
+ ));
+ }
+
if (!$this->loadingTickets) {
if ($this->tickets) {
foreach ($this->tickets as $projectKey => $ticket) {
@@ -201,9 +227,13 @@ public function getFormSchema(): array
foreach ($ticket['issues'] as $issue) {
$fields[] = Checkbox::make('data.' . Str::slug($projectKey) . '_' . Str::slug($issue['code']))
->label(function () use ($issue) {
+ $epicBadge = !empty($issue['isEpic'])
+ ? "EPIC "
+ : '';
return new HtmlString(
""
. "
"
+ . $epicBadge
. "
" . $issue['code'] . " " . $issue['name'] . "
"
. "
"
. "
"
@@ -239,8 +269,13 @@ public function import(): void
if ($this->data && sizeof($this->data)) {
$tickets = [];
foreach (array_keys($this->data) as $item) {
- $url = $this->ticketsDataApi[$item];
- $tickets[] = $this->getJiraTicketDetails($this->host, $this->username, $this->token, $url);
+ $url = $this->ticketsDataApi[$item] ?? null;
+ if ($url) {
+ $ticket = $this->getJiraTicketDetails($this->host, $this->username, $this->token, $url);
+ if ($ticket) {
+ $tickets[] = $ticket;
+ }
+ }
}
dispatch(new ImportJiraTicketsJob($tickets, auth()->user()));
Notification::make()
@@ -266,13 +301,42 @@ public function updateJiraProjects(): void
public function updateJiraTickets(): void
{
$this->ticketsDataApi = [];
+ $this->epicKeys = [];
+ $this->taskKeys = [];
$client = $this->connectToJira($this->host, $this->username, $this->token);
$this->tickets = $this->getJiraTicketsByProject($client, $this->selected_projects);
- foreach ($this->tickets as $projectKey => $ticket) {
- foreach ($ticket['issues'] as $issue) {
- $this->ticketsDataApi[Str::slug($projectKey) . '_' . Str::slug($issue['code'])] = $issue['data']->self;
+ if ($this->tickets) {
+ foreach ($this->tickets as $projectKey => $ticket) {
+ foreach ($ticket['issues'] as $issue) {
+ $key = Str::slug($projectKey) . '_' . Str::slug($issue['code']);
+ $this->ticketsDataApi[$key] = $issue['data']->self;
+ if (!empty($issue['isEpic'])) {
+ $this->epicKeys[] = $key;
+ } else {
+ $this->taskKeys[] = $key;
+ }
+ }
}
}
$this->loadingTickets = false;
}
+
+ public function selectAllEpics(): void
+ {
+ foreach ($this->epicKeys as $key) {
+ $this->data[$key] = true;
+ }
+ }
+
+ public function selectAllTasks(): void
+ {
+ foreach ($this->taskKeys as $key) {
+ $this->data[$key] = true;
+ }
+ }
+
+ public function deselectAll(): void
+ {
+ $this->data = [];
+ }
}
diff --git a/app/Filament/Pages/ManageJiraSettings.php b/app/Filament/Pages/ManageJiraSettings.php
new file mode 100644
index 000000000..71f5cfb3c
--- /dev/null
+++ b/app/Filament/Pages/ManageJiraSettings.php
@@ -0,0 +1,77 @@
+user()->can('Import from Jira');
+ }
+
+ public function getHeading(): string|Htmlable
+ {
+ return __('Manage Jira settings');
+ }
+
+ public static function getNavigationLabel(): string
+ {
+ return __('Jira');
+ }
+
+ public static function getNavigationGroup(): ?string
+ {
+ return __('Settings');
+ }
+
+ public function form(Schema $schema): Schema
+ {
+ return $schema->columns(1)->components([
+ Section::make(__('Jira credentials'))
+ ->description(__('Configure your Jira connection credentials. The API token is stored encrypted.'))
+ ->columnSpanFull()
+ ->schema([
+ Grid::make(2)
+ ->schema([
+ TextInput::make('host')
+ ->label(__('Host'))
+ ->helperText(__('The URL used to access your Jira account (e.g. https://yourcompany.atlassian.net)'))
+ ->required(),
+
+ TextInput::make('username')
+ ->label(__('Username'))
+ ->helperText(__('Your Jira account username (email)'))
+ ->required(),
+
+ TextInput::make('token')
+ ->label(__('API Token'))
+ ->helperText(__('Your Jira account API token'))
+ ->password()
+ ->required(),
+ ]),
+ ]),
+ ]);
+ }
+
+ public function getSaveFormAction(): Action
+ {
+ return parent::getSaveFormAction()->label(__('Save'));
+ }
+}
diff --git a/app/Filament/Pages/TimesheetDashboard.php b/app/Filament/Pages/TimesheetDashboard.php
index 2c21f8d8b..e526c9b88 100644
--- a/app/Filament/Pages/TimesheetDashboard.php
+++ b/app/Filament/Pages/TimesheetDashboard.php
@@ -5,16 +5,14 @@
use App\Filament\Widgets\Timesheet\ActivitiesReport;
use App\Filament\Widgets\Timesheet\MonthlyReport;
use App\Filament\Widgets\Timesheet\WeeklyReport;
-use Filament\Pages\Page;
+use Filament\Pages\Dashboard;
-class TimesheetDashboard extends Page
+class TimesheetDashboard extends Dashboard
{
protected static ?string $slug = 'timesheet-dashboard';
protected static ?int $navigationSort = 2;
- protected string $view = 'filament::pages.dashboard';
-
public function getColumns(): int | array
{
return 6;
diff --git a/app/Filament/Resources/TicketResource/Pages/ViewTicket.php b/app/Filament/Resources/TicketResource/Pages/ViewTicket.php
index f7e0c89e6..fcb19a163 100644
--- a/app/Filament/Resources/TicketResource/Pages/ViewTicket.php
+++ b/app/Filament/Resources/TicketResource/Pages/ViewTicket.php
@@ -108,14 +108,14 @@ public function getHeaderActions(): array
->label(__('Activity'))
->searchable()
->live()
- ->options(function (\Filament\Forms\Get $get, \Filament\Forms\Set $set) {
+ ->options(function (\Filament\Schemas\Components\Utilities\Get $get, \Filament\Schemas\Components\Utilities\Set $set) {
return Activity::all()->pluck('name', 'id')->toArray();
}),
Textarea::make('comment')
->label(__('Comment'))
->rows(3),
])
- ->action(function (Collection $records, array $data): void {
+ ->action(function (array $data): void {
$value = $data['time'];
$comment = $data['comment'];
TicketHour::create([
diff --git a/app/Filament/Widgets/Timesheet/ActivitiesReport.php b/app/Filament/Widgets/Timesheet/ActivitiesReport.php
index d45319496..269082940 100644
--- a/app/Filament/Widgets/Timesheet/ActivitiesReport.php
+++ b/app/Filament/Widgets/Timesheet/ActivitiesReport.php
@@ -19,7 +19,13 @@ class ActivitiesReport extends ChartWidget
'lg' => 3
];
- public ?string $filter = '2023';
+ public ?string $filter = null;
+
+ public function mount(): void
+ {
+ $this->filter = (string) Carbon::now()->year;
+ parent::mount();
+ }
public function getHeading(): string
{
@@ -33,16 +39,23 @@ public function getType(): string
public function getFilters(): ?array
{
- return [
- 2022 => 2022,
- 2023 => 2023
- ];
+ $currentYear = (int) Carbon::now()->year;
+ $firstYear = (int) (TicketHour::min('created_at')
+ ? Carbon::parse(TicketHour::min('created_at'))->year
+ : $currentYear);
+
+ $years = [];
+ for ($year = $firstYear; $year <= $currentYear; $year++) {
+ $years[(string) $year] = (string) $year;
+ }
+
+ return $years;
}
public function getData(): array
{
$collection = $this->filter(auth()->user(), [
- 'year' => $this->filter
+ 'year' => $this->filter ?: Carbon::now()->year
]);
$datasets = $this->getDatasets($collection);
diff --git a/app/Filament/Widgets/Timesheet/MonthlyReport.php b/app/Filament/Widgets/Timesheet/MonthlyReport.php
index f9e13eb8a..c2831dc44 100644
--- a/app/Filament/Widgets/Timesheet/MonthlyReport.php
+++ b/app/Filament/Widgets/Timesheet/MonthlyReport.php
@@ -18,7 +18,13 @@ public function getHeading(): string
return __('Logged time monthly');
}
- public ?string $filter = '2023';
+ public ?string $filter = null;
+
+ public function mount(): void
+ {
+ $this->filter = (string) Carbon::now()->year;
+ parent::mount();
+ }
public function getType(): string
{
@@ -52,10 +58,17 @@ public function getData(): array
public function getFilters(): ?array
{
- return [
- 2022 => 2022,
- 2023 => 2023
- ];
+ $currentYear = (int) Carbon::now()->year;
+ $firstYear = (int) (TicketHour::min('created_at')
+ ? Carbon::parse(TicketHour::min('created_at'))->year
+ : $currentYear);
+
+ $years = [];
+ for ($year = $firstYear; $year <= $currentYear; $year++) {
+ $years[(string) $year] = (string) $year;
+ }
+
+ return $years;
}
protected ?array $options = [
diff --git a/app/Filament/Widgets/Timesheet/WeeklyReport.php b/app/Filament/Widgets/Timesheet/WeeklyReport.php
index 1785982c0..5509d6180 100644
--- a/app/Filament/Widgets/Timesheet/WeeklyReport.php
+++ b/app/Filament/Widgets/Timesheet/WeeklyReport.php
@@ -20,13 +20,12 @@ class WeeklyReport extends ChartWidget
'lg' => 3
];
- public function __construct($id = null)
+ public function mount(): void
{
- $weekDaysData = $this->getWeekStartAndFinishDays();
+ parent::mount();
+ $weekDaysData = $this->getWeekStartAndFinishDays();
$this->filter = $weekDaysData['weekStartDate'] . ' - ' . $weekDaysData['weekEndDate'];
-
- parent::__construct($id);
}
public function getHeading(): string
@@ -41,6 +40,11 @@ public function getType(): string
public function getData(): array
{
+ if (empty($this->filter) || !str_contains($this->filter, ' - ')) {
+ $weekDefaults = $this->getWeekStartAndFinishDays();
+ $this->filter = $weekDefaults['weekStartDate'] . ' - ' . $weekDefaults['weekEndDate'];
+ }
+
$weekDaysData = explode(' - ', $this->filter);
$collection = $this->filter(auth()->user(), [
diff --git a/app/Helpers/JiraHelper.php b/app/Helpers/JiraHelper.php
index 804f10ede..4f45f932b 100644
--- a/app/Helpers/JiraHelper.php
+++ b/app/Helpers/JiraHelper.php
@@ -24,7 +24,7 @@ public function connectToJira($host, $username, $token): Client|null
public function getJiraProjects(Client $client): array|null
{
try {
- $response = $client->get('/rest/api/2/project');
+ $response = $client->get('/rest/api/3/project');
return json_decode($response->getBody()->getContents());
} catch (GuzzleException $e) {
Log::error($e->getTraceAsString());
@@ -41,6 +41,7 @@ public function getJiraTicketsByProject(Client $client, $projectKeys): array|nul
$results[] = [
'code' => $issue->key,
'name' => $issue->fields->summary,
+ 'isEpic' => isset($issue->fields->issuetype) && strtolower($issue->fields->issuetype->name) === 'epic',
'data' => $issue
];
}
@@ -48,16 +49,21 @@ public function getJiraTicketsByProject(Client $client, $projectKeys): array|nul
};
$results = [];
foreach ($projectKeys as $projectKey) {
- $response = $client->get('/rest/api/2/search?jql=project=' . $projectKey);
+ $response = $client->get('/rest/api/3/search/jql?jql=project=' . $projectKey . '&fields=*navigable');
$data = json_decode($response->getBody()->getContents());
+ $issues = $data->issues ?? [];
$results[$projectKey] = [
- 'total' => $data->total,
- 'issues' => $formatIssues($data->issues)
+ 'total' => count($issues),
+ 'issues' => $formatIssues($issues)
];
}
return $results;
+ } catch (\GuzzleHttp\Exception\RequestException $e) {
+ $response = $e->hasResponse() ? $e->getResponse()->getBody()->getContents() : 'No response';
+ Log::error('Jira API error: ' . $e->getMessage() . ' | Response: ' . $response);
+ return null;
} catch (GuzzleException $e) {
- Log::error($e->getTraceAsString());
+ Log::error('Jira connection error: ' . $e->getMessage());
return null;
}
}
@@ -67,7 +73,7 @@ public function getJiraTicketDetails($host, $username, $token, $url)
try {
$client = $this->connectToJira($host, $username, $token);
$url = explode('/', $url);
- $response = $client->get('/rest/api/2/issue/' . $url[sizeof($url) - 1]);
+ $response = $client->get('/rest/api/3/issue/' . $url[sizeof($url) - 1]);
return json_decode($response->getBody()->getContents());
} catch (GuzzleException $e) {
Log::error($e->getTraceAsString());
diff --git a/app/Jobs/ImportJiraTicketsJob.php b/app/Jobs/ImportJiraTicketsJob.php
index f603dd1e2..4a8abc2e4 100644
--- a/app/Jobs/ImportJiraTicketsJob.php
+++ b/app/Jobs/ImportJiraTicketsJob.php
@@ -44,6 +44,9 @@ public function handle()
{
if ($this->tickets && sizeof($this->tickets)) {
foreach ($this->tickets as $ticket) {
+ if (!$ticket || !isset($ticket->fields)) {
+ continue;
+ }
$projectDetails = $ticket->fields->project;
$ticketData = $ticket->fields;
@@ -52,7 +55,7 @@ public function handle()
$project = Project::create([
'name' => $projectDetails->name,
'description' => __('Project imported from Jira, project key:') . $projectDetails->key,
- 'status_id' => ProjectStatus::where('is_default', true)->first()->id,
+ 'status_id' => ProjectStatus::where('is_default', true)->first()?->id ?? ProjectStatus::first()->id,
'owner_id' => $this->user->id,
'ticket_prefix' => $projectDetails->key
]);
@@ -66,12 +69,14 @@ public function handle()
Ticket::create([
'name' => $ticketData->summary,
- 'content' => $ticketData->description ?? __('No content found in jira ticket'),
+ 'content' => is_string($ticketData->description ?? null)
+ ? $ticketData->description
+ : (isset($ticketData->description) ? json_encode($ticketData->description) : __('No content found in jira ticket')),
'owner_id' => $this->user->id,
- 'status_id' => TicketStatus::where('is_default', true)->first()->id,
+ 'status_id' => TicketStatus::where('is_default', true)->first()?->id ?? TicketStatus::first()->id,
'project_id' => $project->id,
- 'type_id' => TicketType::where('is_default', true)->first()->id,
- 'priority_id' => TicketPriority::where('is_default', true)->first()->id,
+ 'type_id' => TicketType::where('is_default', true)->first()?->id ?? TicketType::first()->id,
+ 'priority_id' => TicketPriority::where('is_default', true)->first()?->id ?? TicketPriority::first()->id,
]);
}
FilamentNotification::make()
diff --git a/app/Settings/JiraSettings.php b/app/Settings/JiraSettings.php
new file mode 100644
index 000000000..ccbd8b961
--- /dev/null
+++ b/app/Settings/JiraSettings.php
@@ -0,0 +1,22 @@
+migrator->add('jira.host', '');
+ $this->migrator->add('jira.username', '');
+ $this->migrator->addEncrypted('jira.token', '');
+ }
+}