diff --git a/src/Acl/EsiRolesMap.php b/src/Acl/EsiRolesMap.php index a1e51a618..ece97b4f8 100644 --- a/src/Acl/EsiRolesMap.php +++ b/src/Acl/EsiRolesMap.php @@ -142,6 +142,9 @@ class EsiRolesMap 'corporation.summary', 'corporation.market', ], + 'Project Manager' => [ + 'corporation.projects', + ], ]; /** diff --git a/src/Config/Permissions/corporation.php b/src/Config/Permissions/corporation.php index 19d761460..94e4017aa 100644 --- a/src/Config/Permissions/corporation.php +++ b/src/Config/Permissions/corporation.php @@ -183,4 +183,8 @@ 'label' => 'web::permissions.corporation_tracking_label', 'description' => 'web::permissions.corporation_tracking_description', ], + 'projects' => [ + 'label' => 'web::permissions.corporation_projects_label', + 'description' => 'web::permissions.corporation_projects_description', + ], ]; diff --git a/src/Config/package.corporation.menu.php b/src/Config/package.corporation.menu.php index fc3327dab..205da95af 100644 --- a/src/Config/package.corporation.menu.php +++ b/src/Config/package.corporation.menu.php @@ -64,6 +64,14 @@ 'highlight_view' => 'industry', 'route' => 'seatcore::corporation.view.industry', ], + [ + 'name' => 'project', + 'label' => 'web::seat.project', + 'plural' => true, + 'permission' => 'corporation.projects', + 'highlight_view' => 'projects', + 'route' => 'seatcore::corporation.view.projects', + ], [ 'name' => 'killmails', 'label' => 'web::seat.killmails', diff --git a/src/Config/web.jobnames.php b/src/Config/web.jobnames.php index 6b530f6bc..d1fa35dd6 100644 --- a/src/Config/web.jobnames.php +++ b/src/Config/web.jobnames.php @@ -76,6 +76,7 @@ 'transactions' => \Seat\Eveapi\Jobs\Wallet\Corporation\Transactions::class, 'starbases' => \Seat\Eveapi\Jobs\Corporation\Starbases::class, 'structures' => \Seat\Eveapi\Jobs\Corporation\Structures::class, + 'projects' => \Seat\Eveapi\Jobs\CorporationProjects\Projects::class, ], 'alliance' => [ 'contacts' => [ diff --git a/src/Http/Controllers/Corporation/ProjectController.php b/src/Http/Controllers/Corporation/ProjectController.php new file mode 100644 index 000000000..76a360217 --- /dev/null +++ b/src/Http/Controllers/Corporation/ProjectController.php @@ -0,0 +1,169 @@ +addScope(new CorporationScope('corporation.projects', [$corporation->corporation_id])) + ->render('web::corporation.projects.list', compact('corporation')); + } + + /** + * @param \Seat\Eveapi\Models\Corporation\CorporationInfo $corporation + * @param string $project_id + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function getProject(CorporationInfo $corporation, string $project_id) + { + $project = CorporationProject::with('contributors', 'creator') + ->where('id', $project_id) + ->first(); + $config = $this->normalizeProjectConfiguration($project->configuration); + + return view('web::corporation.projects.modals.content', compact('corporation', 'project', 'config')); + } + + /** + * Normalize project configuration into an array of items: + * [ ['key'=>'...', 'label'=>'...', 'value'=>'...', 'description'=>null], ... ] + */ + protected function normalizeProjectConfiguration($configuration): array + { + if (is_string($configuration)) { + // sometimes stored as JSON string + $decoded = json_decode($configuration, true); + if (json_last_error() === JSON_ERROR_NONE) { + $configuration = $decoded; + } + } + + // If null or empty + if (empty($configuration)) { + return []; + } + + $items = []; + + // Case: configuration is an array of option objects + if (is_array($configuration) && array_values($configuration) === $configuration) { + foreach ($configuration as $i => $opt) { + if (is_array($opt)) { + $items[] = [ + 'key' => $opt['name'] ?? "option_{$i}", + 'label' => $opt['display_name'] ?? $opt['name'] ?? "Option {$i}", + 'value' => isset($opt['value']) ? $opt['value'] : (isset($opt['default']) ? $opt['default'] : null), + 'description' => $opt['description'] ?? null, + ]; + } else { + // primitive in array + $items[] = [ + 'key' => "option_{$i}", + 'label' => "Option {$i}", + 'value' => (string) $opt, + 'description' => null, + ]; + } + } + + return $items; + } + + // Case: configuration is an associative object/array of named options + if (is_array($configuration)) { + foreach ($configuration as $k => $v) { + if (is_array($v) || is_object($v)) { + $vArr = (array) $v; + $items[] = [ + 'key' => $k, + 'label' => $vArr['label'] ?? $vArr['name'] ?? $k, + 'value' => $vArr['value'] ?? $vArr['default'] ?? json_encode($vArr), + 'description' => $vArr['description'] ?? null, + ]; + } else { + $items[] = [ + 'key' => $k, + 'label' => $k, + 'value' => $v, + 'description' => null, + ]; + } + } + + return $items; + } + + // Case: object (stdClass) + if (is_object($configuration)) { + $arr = (array) $configuration; + foreach ($arr as $k => $v) { + if (is_object($v) || is_array($v)) { + $vArr = (array) $v; + $items[] = [ + 'key' => $k, + 'label' => $vArr['label'] ?? $vArr['name'] ?? $k, + 'value' => $vArr['value'] ?? $vArr['default'] ?? json_encode($vArr), + 'description' => $vArr['description'] ?? null, + ]; + } else { + $items[] = [ + 'key' => $k, + 'label' => $k, + 'value' => $v, + 'description' => null, + ]; + } + } + + return $items; + } + + // Fallback: stringify whatever it is + return [ + [ + 'key' => 'raw', + 'label' => 'Configuration', + 'value' => is_scalar($configuration) ? (string) $configuration : json_encode($configuration), + 'description' => null, + ], + ]; + } +} diff --git a/src/Http/DataTables/Corporation/Financial/ProjectDataTable.php b/src/Http/DataTables/Corporation/Financial/ProjectDataTable.php new file mode 100644 index 000000000..9c151d017 --- /dev/null +++ b/src/Http/DataTables/Corporation/Financial/ProjectDataTable.php @@ -0,0 +1,117 @@ +eloquent($this->applyScopes($this->query())) + ->editColumn('state', function ($row) { + return ucfirst(str_replace('_', ' ', $row->state)); + }) + ->addColumn('progress', function ($raw) { + $row = (object) [ + 'min' => 0, + 'value' => $raw->progress_current, + 'max' => $raw->progress_desired, + 'showval' => true, + ]; + + return view('web::partials.progress', compact('row'))->render(); + }) + ->addColumn('reward', function ($raw) use (&$rawColumns) { + if (is_null($raw->reward_initial) || $raw->reward_initial == 0) { + return trans('web::seat.no_reward'); + } else { + $row = (object) [ + 'min' => 0, + 'value' => $raw->reward_initial - $raw->reward_remaining, + 'max' => $raw->reward_initial, + 'showval' => true, + ]; + $rawColumns[] = 'reward'; + + return view('web::partials.progress', compact('row'))->render(); + } + }) + ->editColumn('action', function ($row) use ($rawColumns) { + return view('web::corporation.projects.buttons.action', compact('row'))->render(); + }) + ->rawColumns(['action', 'progress', 'reward']) + ->toJson(); + } + + /** + * @return \Yajra\DataTables\Html\Builder + */ + public function html() + { + return $this->builder() + ->postAjax() + ->columns($this->getColumns()) + ->addAction() + ->addTableClass('table-striped table-hover') + ->parameters([ + 'drawCallback' => 'function() { $("[data-toggle=tooltip]").tooltip(); }', + ]); + } + + /** + * @return \Illuminate\Database\Eloquent\Builder + */ + public function query() + { + return CorporationProject::withCount('contributors'); + } + + /** + * @return array + */ + public function getColumns() + { + return [ + ['data' => 'name', 'title' => trans_choice('web::seat.name', 1)], + ['data' => 'last_modified', 'title' => trans_choice('web::seat.last_modified', 1)], + ['data' => 'state', 'title' => trans('web::seat.state')], + ['data' => 'progress', 'title' => trans('web::seat.progress'), 'orderable' => false], + ['data' => 'reward', 'title' => trans('web::seat.reward'), 'orderable' => false], + ['data' => 'contributors_count', 'title' => trans_choice('web::seat.contributor', 2)], + ]; + } +} diff --git a/src/Http/Routes/Corporation/View.php b/src/Http/Routes/Corporation/View.php index 0ff225479..f087422d2 100644 --- a/src/Http/Routes/Corporation/View.php +++ b/src/Http/Routes/Corporation/View.php @@ -214,3 +214,13 @@ Route::post('/{corporation}/transactions/export') ->uses('WalletController@transactions') ->middleware('can:corporation.transaction,corporation'); + +Route::get('/{corporation}/projects') + ->name('seatcore::corporation.view.projects') + ->uses('ProjectController@getProjects') + ->middleware('can:corporation.projects,corporation'); + +Route::get('/{corporation}/project/{project_id}') + ->name('seatcore::corporation.view.projects.details') + ->uses('ProjectController@getProject') + ->middleware('can:corporation.projects,corporation'); diff --git a/src/WebServiceProvider.php b/src/WebServiceProvider.php index 430e02d81..4e8f0ac1d 100644 --- a/src/WebServiceProvider.php +++ b/src/WebServiceProvider.php @@ -582,6 +582,7 @@ private function register_settings() 'esi-universe.read_structures.v1', 'esi-wallet.read_character_wallet.v1', 'esi-wallet.read_corporation_wallets.v1', + 'esi-corporations.read_projects.v1', ], ], ]); diff --git a/src/resources/lang/en/seat.php b/src/resources/lang/en/seat.php index d23c6f797..429b406ae 100644 --- a/src/resources/lang/en/seat.php +++ b/src/resources/lang/en/seat.php @@ -44,6 +44,7 @@ 'id' => 'ID|IDs', 'type' => 'Type|Types', 'expiry' => 'Expiry', + 'no_expiry' => 'No Expiry', 'never' => 'Never', 'detail' => 'Detail|Details', 'delete' => 'Delete', @@ -58,12 +59,14 @@ 'owner' => 'Owner', 'general' => 'General', 'description' => 'Description', + 'no_description' => 'No Description', 'labels' => 'Labels', 'created' => 'Created', 'issuer' => 'Issuer', 'title' => 'Title|Titles', 'price' => 'Price', 'reward' => 'Reward', + 'no_reward' => 'No Reward', 'collateral' => 'Collateral', 'assignee' => 'Assignee', 'acceptor' => 'Acceptor', @@ -104,6 +107,7 @@ 'week' => 'week', 'day' => 'day', 'save' => 'Save', + 'unlimited' => 'Unlimited', // Requirements 'requirements' => 'Requirements', @@ -201,6 +205,8 @@ 'tracking' => 'Tracking', 'about' => 'About', 'market_browser' => 'Market Browser', + 'project' => 'Project|Projects', + 'last_modified' => 'Last Modified', 'assets' => 'Assets', 'location_flag' => 'Location Flag', @@ -496,6 +502,7 @@ 'update_transactions' => 'Update Transactions', 'update_wallet' => 'Update Wallet', 'update_loyalty_points' => 'Update Loyalty Points', + 'update_projects' => 'Update Projects', // Character 'joined_curr_corp' => 'Joined Current Corporation', @@ -770,4 +777,21 @@ 'sde_version' => 'SDE Version', 'render_in' => 'Rendered In', 'copyright' => 'Copyright', + + // Projects + 'contributor' => 'Contributor|Contributors', + 'contributed' => 'Contributed', + 'no_contributions' => 'No Contributions', + 'no_configuration_project' => 'No configuration available for this project', + 'project_setting' => 'Setting', + 'career' => 'Career', + + 'financial' => 'Financial', + 'reward_initial' => 'Initial Reward', + 'reward_remaining' => 'Remaining Reward', + 'contribution_reward' => 'Reward per Contribution', + 'contribution_rules' => 'Contribution Rules', + 'participation_limit' => 'Participation Limit', + 'submission_limit' => 'Submission Limit', + 'submission_multiplier' => 'Submission Multiplier', ]; diff --git a/src/resources/views/corporation/projects/buttons/action.blade.php b/src/resources/views/corporation/projects/buttons/action.blade.php new file mode 100644 index 000000000..9538c837f --- /dev/null +++ b/src/resources/views/corporation/projects/buttons/action.blade.php @@ -0,0 +1,2 @@ + +@include('web::corporation.projects.buttons.details') \ No newline at end of file diff --git a/src/resources/views/corporation/projects/buttons/details.blade.php b/src/resources/views/corporation/projects/buttons/details.blade.php new file mode 100644 index 000000000..918880b17 --- /dev/null +++ b/src/resources/views/corporation/projects/buttons/details.blade.php @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/src/resources/views/corporation/projects/list.blade.php b/src/resources/views/corporation/projects/list.blade.php new file mode 100644 index 000000000..cf58a2e59 --- /dev/null +++ b/src/resources/views/corporation/projects/list.blade.php @@ -0,0 +1,55 @@ +@extends('web::corporation.layouts.view', ['viewname' => 'projects', 'breadcrumb' => trans_choice('web::seat.project', 2)]) + +@section('page_header', trans_choice('web::seat.corporation', 1) . ' ' . trans_choice('web::seat.project', 2)) + +@inject('request', 'Illuminate\Http\Request') + +@section('corporation_content') + +
+
+ +
+
+

{{ trans_choice('web::seat.project', 2) }}

+
+
+ @include('web::components.jobs.buttons.update', ['type' => 'corporation', 'entity' => $corporation->corporation_id, 'job' => 'corporation.projects', 'label' => trans('web::seat.update_projects')]) +
+
+
+
+ + {{ $dataTable->table() }} + +
+
+ +
+
+ + @include('web::corporation.projects.modals.details') + +@stop + +@push('javascript') + {!! $dataTable->scripts() !!} + + $(function () { + $('[data-toggle="tooltip"]').tooltip() + }) + + + +@endpush diff --git a/src/resources/views/corporation/projects/modals/content.blade.php b/src/resources/views/corporation/projects/modals/content.blade.php new file mode 100644 index 000000000..4eacf3465 --- /dev/null +++ b/src/resources/views/corporation/projects/modals/content.blade.php @@ -0,0 +1,267 @@ +

About

+ +
+ +
+
{{ $project->name }}
+

+ {!! $project->description ?: trans('web::seat.no_description') !!} +

+
+
+ +
+
+
+
{{ trans('web::seat.career') }}
+
{{ $project->career }}
+
+
+
+
+
{{ trans('web::seat.state') }}
+
{{ $project->state }}
+
+
+
+
+
{{ trans('web::seat.created') }}
+
{{ carbon($project->created)->toDayDateTimeString() }}
+
+
+
+ +
+
+
+
{{ trans('web::seat.expiry') }}
+
+ @if(!is_null($project->Expires)) + + @if(carbon()->addDay()->gte($project->expires)) + + @elseif(carbon()->addDays(2)->gte($project->expires)) + + @else + + @endif + {{ human_diff($project->expires) }} + + @else + {{ trans('web::seat.no_expiry') }} + @endif +
+
+
+ +
+
+
{{ trans('web::seat.progress') }}
+
+ {{ number_format($project->progress_current) }} + / + {{ number_format($project->progress_desired) }} +
+
+
+ +
+
+
{{ trans('web::seat.created_by') }}
+
+ @include('web::partials.character', ['character' => $project->creator]) +
+
+
+
+ +

{{ trans('web::seat.financial') }}

+ +
+
+
+
{{ trans('web::seat.reward_initial') }}
+
{{ number_format($project->reward_initial) }} ISK
+
+
+
+
+
{{ trans('web::seat.reward_remaining') }}
+
{{ number_format($project->reward_remaining) }} ISK
+
+
+
+
+
{{ trans('web::seat.contribution_reward') }}
+
{{ number_format($project->contribution_reward) }} ISK
+
+
+
+ +

{{ trans('web::seat.contribution_rules') }}

+ +
+
+
+
{{ trans('web::seat.participation_limit') }}
+
+ {{ $project->contribution_participation_limit ?? trans('web::seat.unlimited') }} +
+
+
+ +
+
+
{{ trans('web::seat.submission_limit') }}
+
+ {{ $project->contribution_submission_limit ?? trans('web::seat.unlimited') }} +
+
+
+ +
+
+
{{ trans('web::seat.submission_multiplier') }}
+
+ {{ $project->contribution_submission_multiplier ?? trans('web::seat.none') }} +
+
+
+
+ +

{{ trans_choice('web::seat.contributor', 2) }}

+ + + + + + + + + + + @forelse($project->contributors as $contributor) + + + + + + @empty + + + + @endforelse + +
{{ trans('web::seat.character') }}{{ trans('web::seat.contributed') }} {{ trans('web::seat.last_updated') }}
+ @include('web::partials.character', ['character' => $contributor->character]) + {{ number_format($contributor->contributed) }}{{ carbon($contributor->updated_at)->toDayDateTimeString() }}
{{ trans('web::seat.no_contributions') }}
+ +

+ {{ trans('web::seat.configuration') }} +

+ + + + + + + +@php +// TODO need a better place for this than here... Dont like PHP in blade + if (!isset($config) || !is_array($config) || empty($config)) { + $config = []; + } + + $entry = $config[0] ?? null; + + if ($entry) { + $topKey = $entry['key'] ?? 'configuration'; + $prettyTopKey = ucwords(str_replace('_', ' ', $topKey)); + + $decoded = $entry['value'] ?? '{}'; + $decoded = is_string($decoded) ? json_decode($decoded, true) : $decoded; + + if (!is_array($decoded)) { + $decoded = []; + } + + // Render values nicely + $renderValue = function ($value) { + // Null + if (is_null($value)) { + return 'null'; + } + + // Boolean + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + // Array of objects or arrays → render each entry on its own line + if (is_array($value)) { + // Array of associative arrays (objects) + if (isset($value[0]) && is_array($value[0])) { + $html = ''; + foreach ($value as $item) { + $html .= '
'
+                          . e(json_encode($item, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))
+                          . '
'; + } + return $html; + } + + // Simple array → each item on its own line + $html = ''; + foreach ($value as $item) { + $html .= '
' . e((string) $item) . '
'; + } + return $html; + } + + // Long strings → scrollable block + if (is_string($value) && strlen($value) > 120) { + return '
'
+                  . e($value)
+                  . '
'; + } + + // Scalar + return e((string) $value); + }; + } +@endphp + +@if(empty($config)) +

{{ trans('web::seat.no_configuration_project') }}

+@else +
{{ $prettyTopKey }}
+ +
+ + + + + + + + + @foreach($decoded as $key => $value) + @php + $prettyKey = ucwords(str_replace('_', ' ', $key)); + @endphp + + + + + + @endforeach + +
{{ trans('web::seat.project_setting') }}Value
+ {{ $prettyKey }} + + {!! $renderValue($value) !!} +
+
+@endif diff --git a/src/resources/views/corporation/projects/modals/details.blade.php b/src/resources/views/corporation/projects/modals/details.blade.php new file mode 100644 index 000000000..4c5232dfb --- /dev/null +++ b/src/resources/views/corporation/projects/modals/details.blade.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/src/resources/views/partials/progress.blade.php b/src/resources/views/partials/progress.blade.php new file mode 100644 index 000000000..cc094e1a2 --- /dev/null +++ b/src/resources/views/partials/progress.blade.php @@ -0,0 +1,26 @@ + +
+ + {{-- Actual fill bar --}} +
+
+ + {{-- Centered overlay text (full width, hoverable, no pointer events) --}} +
+ + @if($row->showval) + {{ round(($row->max > 0) ? ($row->value * 100 / $row->max) : 0) }}% + @endif +
+ +
+