Compare commits

..

15 Commits

Author SHA1 Message Date
Miguel Nogueira 82123b1c26 RSM-38 Added Discord Routing for relevant notifications 2020-10-10 00:39:53 +01:00
Miguel Nogueira 2a43e213f9 RSM-81 Fix issues with Options provider 2020-10-09 23:55:46 +01:00
Miguel Nogueira b2adcee51e Merged in RSM-19_feature (pull request #1)
RSM-19 feature

Approved-by: Miguel Nogueira <miguel456@spacejewel-hosting.com>
2020-10-09 21:37:48 +00:00
Miguel Nogueira 077ead9612 RSM-5 Made Vacancies easily linkable to Teams 2020-10-09 22:27:36 +01:00
Miguel Nogueira 6cc99d2ebe Prevent empty form creation 2020-10-09 00:56:11 +01:00
Miguel Nogueira 0930c29b9a RSM-5 Update to Laravel 8 2020-10-08 23:47:23 +01:00
Miguel Nogueira 596a469e15 Add user invitation facilities RSM-5
Adds user invitation to teams, and framework for assigning taems
Also adds user acc. deletion.
2020-10-08 19:19:10 +01:00
Miguel Nogueira 75f4404259 RSM-6 Team features 2020-10-03 21:36:35 +01:00
Miguel Nogueira 7c0c244e21 Team page skeleton 2020-09-10 23:09:23 +01:00
Miguel Nogueira 982854d5c6 RSM-25 add relevant permisisons to teams 2020-09-10 22:47:51 +01:00
Miguel Nogueira 551741c687 RSM-3 Update team assoc relationship 2020-09-10 19:41:19 +01:00
Miguel Nogueira dbeddd6fdb RSM-3 Update vacancy schema with team assoc 2020-09-10 19:24:55 +01:00
Miguel Nogueira b68449d3bf RSM-3 Add team functionality to users 2020-09-10 19:05:26 +01:00
Miguel Nogueira 67d1df7571 RSM-3 Add teams migration and config 2020-09-10 18:43:58 +01:00
Miguel Nogueira d4f1b433dc RSM-2 Update lock file 2020-09-10 18:39:24 +01:00
73 changed files with 27515 additions and 24107 deletions

View File

@ -2,11 +2,14 @@
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/database/factories" isTestSource="false" packagePrefix="Database\Factories\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" /> <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" />
<sourceFolder url="file://$MODULE_DIR$/database/seeders" isTestSource="false" packagePrefix="Database\Seeders\" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="App\" /> <sourceFolder url="file://$MODULE_DIR$/app" isTestSource="false" packagePrefix="App\" />
<excludeFolder url="file://$MODULE_DIR$/vendor/almasaeed2010/adminlte" /> <excludeFolder url="file://$MODULE_DIR$/vendor/almasaeed2010/adminlte" />
<excludeFolder url="file://$MODULE_DIR$/vendor/asm89/stack-cors" /> <excludeFolder url="file://$MODULE_DIR$/vendor/asm89/stack-cors" />
<excludeFolder url="file://$MODULE_DIR$/vendor/awssat/discord-notification-channel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-debugbar" /> <excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/brick/math" /> <excludeFolder url="file://$MODULE_DIR$/vendor/brick/math" />
<excludeFolder url="file://$MODULE_DIR$/vendor/clue/stream-filter" /> <excludeFolder url="file://$MODULE_DIR$/vendor/clue/stream-filter" />
@ -27,6 +30,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/filp/whoops" /> <excludeFolder url="file://$MODULE_DIR$/vendor/filp/whoops" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fruitcake/laravel-cors" /> <excludeFolder url="file://$MODULE_DIR$/vendor/fruitcake/laravel-cors" />
<excludeFolder url="file://$MODULE_DIR$/vendor/fzaninotto/faker" /> <excludeFolder url="file://$MODULE_DIR$/vendor/fzaninotto/faker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/guzzle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/promises" />
<excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" /> <excludeFolder url="file://$MODULE_DIR$/vendor/guzzlehttp/psr7" />
@ -44,6 +48,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/mcamara/laravel-localization" /> <excludeFolder url="file://$MODULE_DIR$/vendor/mcamara/laravel-localization" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mockery/mockery" /> <excludeFolder url="file://$MODULE_DIR$/vendor/mockery/mockery" />
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" /> <excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
<excludeFolder url="file://$MODULE_DIR$/vendor/mpociot/teamwork" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" /> <excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" /> <excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" /> <excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
@ -66,6 +71,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/phpspec/prophecy" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpspec/prophecy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-token-stream" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-token-stream" />
@ -82,12 +88,16 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/collection" /> <excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/collection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/uuid" /> <excludeFolder url="file://$MODULE_DIR$/vendor/ramsey/uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/scrivo/highlight.php" /> <excludeFolder url="file://$MODULE_DIR$/vendor/scrivo/highlight.php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/code-unit-reverse-lookup" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" /> <excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
@ -106,6 +116,8 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" /> <excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />

View File

@ -141,9 +141,19 @@
<path value="$PROJECT_DIR$/vendor/graham-campbell/markdown" /> <path value="$PROJECT_DIR$/vendor/graham-campbell/markdown" />
<path value="$PROJECT_DIR$/vendor/league/mime-type-detection" /> <path value="$PROJECT_DIR$/vendor/league/mime-type-detection" />
<path value="$PROJECT_DIR$/vendor/mcamara/laravel-localization" /> <path value="$PROJECT_DIR$/vendor/mcamara/laravel-localization" />
<path value="$PROJECT_DIR$/vendor/mpociot/teamwork" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/graham-campbell/result-type" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/awssat/discord-notification-channel" />
</include_path> </include_path>
</component> </component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.2" /> <component name="PhpProjectSharedConfiguration" php_language_level="7.3" />
<component name="PhpUnit"> <component name="PhpUnit">
<phpunit_settings> <phpunit_settings>
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" /> <PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />

6
.vscode/launch.json vendored
View File

@ -4,11 +4,15 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Listen for XDebug", "name": "Listen for XDebug",
"type": "php", "type": "php",
"request": "launch", "request": "launch",
"port": 9000 "port": 9000,
"ignore": [
"**/vendor/**/*.php"
]
}, },
{ {
"name": "Launch currently open script", "name": "Launch currently open script",

View File

@ -1,6 +1,6 @@
# RB Recruiter v 0.6.2 [![Crowdin](https://badges.crowdin.net/raspberry-staff-manager/localized.svg)](https://crowdin.com/project/raspberry-staff-manager) # Raspberry Teams - The Simple Staff Application Manager v 0.6.2 [![Crowdin](https://badges.crowdin.net/raspberry-staff-manager/localized.svg)](https://crowdin.com/project/raspberry-staff-manager)
## The quick and pain-free form management solution for communities ## The quick and pain-free staff application manager
Have you ever gotten tired of managing your Minecraft server/network's applications through Discord (or anything else) and having to scroll through hundreds of new messages just to find that one applicant's username? Have you ever gotten tired of managing your Minecraft server/network's applications through Discord (or anything else) and having to scroll through hundreds of new messages just to find that one applicant's username?

View File

@ -99,7 +99,7 @@ class Install extends Command
$settings['MAIL_PASSWORD'] = $this->secret('SMTP Password (Input won\'t be seen)'); $settings['MAIL_PASSWORD'] = $this->secret('SMTP Password (Input won\'t be seen)');
$settings['MAIL_PORT'] = $this->ask('SMTP Server Port'); $settings['MAIL_PORT'] = $this->ask('SMTP Server Port');
$settings['MAIL_HOST'] = $this->ask('SMTP Server Hostname'); $settings['MAIL_HOST'] = $this->ask('SMTP Server Hostname');
$settings['MAIL_FROM_ADDRESS'] = $this->ask('E-mail address to send from'); $settings['MAIL_FROM'] = $this->ask('E-mail address to send from: ');
$this->info('== Notification Settings (5/6) (Slack) =='); $this->info('== Notification Settings (5/6) (Slack) ==');
$settings['SLACK_INTEGRATION_WEBHOOK'] = $this->ask('Integration webhook URL'); $settings['SLACK_INTEGRATION_WEBHOOK'] = $this->ask('Integration webhook URL');

View File

@ -3,7 +3,17 @@
namespace App\Facades; namespace App\Facades;
use \Illuminate\Support\Facades\Facade; use \Illuminate\Support\Facades\Facade;
use phpDocumentor\Reflection\Types\Boolean;
/**
* Class Options
* @package App\Facades
*
* @method static void setOption(string $option, string $value, string $description)
* @method static string getOption(string $option)
* @method static void changeOption(string $option, string $newValue)
* @method static Boolean optionExists(string $option)
*/
class Options extends Facade class Options extends Facade
{ {
public static function getFacadeAccessor() public static function getFacadeAccessor()

View File

@ -13,9 +13,12 @@ class Options
public function getOption(string $option): string public function getOption(string $option): string
{ {
$value = Cache::get($option); $value = Cache::get($option);
$fromCache = true;
if (is_null($value)) if (is_null($value))
{ {
$fromCache = false;
Log::debug('Option ' . $option . 'not found in cache, refreshing from database'); Log::debug('Option ' . $option . 'not found in cache, refreshing from database');
$value = Option::where('option_name', $option)->first(); $value = Option::where('option_name', $option)->first();
if (is_null($value)) if (is_null($value))
@ -25,7 +28,9 @@ class Options
Cache::put($option . '_desc', 'Undefined description'); Cache::put($option . '_desc', 'Undefined description');
} }
return $value->option_value; return (!$fromCache)
? $value->option_value
: $value;
} }
public function setOption(string $option, string $value, string $description) public function setOption(string $option, string $value, string $description)

View File

@ -33,6 +33,14 @@ class FormController extends Controller
$this->authorize('create', Form::class); $this->authorize('create', Form::class);
$fields = $request->all(); $fields = $request->all();
if (count($fields) == 2)
{
// form is probably empty, since forms with fields will alawys have more than 2 items
$request->session()->flash('error', 'Sorry, but you may not create empty forms.');
return redirect()->to(route('showForms'));
}
$contextValidation = ContextAwareValidator::getValidator($fields, true, true); $contextValidation = ContextAwareValidator::getValidator($fields, true, true);
if (!$contextValidation->get('validator')->fails()) if (!$contextValidation->get('validator')->fails())

View File

@ -0,0 +1,262 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\EditTeamRequest;
use App\Http\Requests\NewTeamRequest;
use App\Http\Requests\SendInviteRequest;
use App\Mail\InviteToTeam;
use App\Team;
use App\User;
use App\Vacancy;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Mpociot\Teamwork\Exceptions\UserNotInTeamException;
use Mpociot\Teamwork\Facades\Teamwork;
use Mpociot\Teamwork\TeamInvite;
class TeamController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$teams = Team::with('users.roles')->get();
return view('dashboard.teams.teams')
->with('teams', $teams);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(NewTeamRequest $request)
{
$team = Team::create([
'name' => $request->teamName,
'owner_id' => Auth::user()->id
]);
Auth::user()->teams()->attach($team->id);
$request->session()->flash('success', 'Team successfully created.');
return redirect()->back();
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit(Team $team)
{
return view('dashboard.teams.edit-team')
->with('team', $team)
->with('users', User::all())
->with('vacancies', Vacancy::with('teams')->get()->all());
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(EditTeamRequest $request, Team $team)
{
$team->description = $request->teamDescription;
$team->openJoin = $request->joinType;
$team->save();
$request->session()->flash('success', 'Team edited successfully.');
return redirect()->to(route('teams.index'));
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
public function invite(SendInviteRequest $request, Team $team)
{
$user = User::findOrFail($request->user);
if (!$team->openJoin)
{
if (!Teamwork::hasPendingInvite($user->email, $team))
{
Teamwork::inviteToTeam($user, $team, function(TeamInvite $invite) use ($user) {
Mail::to($user)->send(new InviteToTeam($invite));
});
$request->session()->flash('success', 'Invite sent! They can now accept or deny it.');
}
else
{
$request->session()->flash('error', 'This user has already been invited.');
}
}
else
{
$request->session()->flash('error', 'You can\'t invite users to public teams.');
}
return redirect()->back();
}
public function processInviteAction(Request $request, $action, $token)
{
switch($action)
{
case 'accept':
$invite = Teamwork::getInviteFromAcceptToken($token);
if ($invite && $invite->user->is(Auth::user()))
{
Teamwork::acceptInvite($invite);
$request->session()->flash('success', 'Invite accepted! You have now joined ' . $invite->team->name . '.');
}
else
{
$request->session()->flash('error', 'Invalid or expired invite URL.');
}
break;
case 'deny':
$invite = Teamwork::getInviteFromDenyToken($token);
if ($invite && $invite->user->is(Auth::user()))
{
Teamwork::denyInvite($invite);
$request->session()->flash('success', 'Invite denied! Ask for another invite if this isn\'t what you meant.');
}
else
{
$request->session()->flash('error', 'Invalid or expired invite URL.');
}
break;
default:
$request->session()->flash('error', 'Sorry, but the invite URL you followed was malformed. Try asking for another invite, or submit a bug report.');
}
// This page will show the user's current teams
return redirect()->to(route('teams.index'));
}
public function switchTeam(Request $request, Team $team)
{
try
{
Auth::user()->switchTeam($team);
$request->session()->flash('success', 'Switched teams! Your team dashboard will now use this context.');
}
catch(UserNotInTeamException $ex)
{
$request->session()->flash('error', 'You can\'t switch to a team you don\'t belong to.');
}
return redirect()->back();
}
// Since it's a separate form, we shouldn't use the same update method
public function assignVacancies(Request $request, Team $team)
{
// P.S. To future developers
// This method gave me a lot of trouble lol. It's hard to write code when you're half asleep.
// There may be an n+1 query in the view and I don't think there's a way to avoid that without writing a lot of extra code.
$requestVacancies = $request->assocVacancies;
$currentVacancies = $team->vacancies->pluck('id')->all();
if (is_null($requestVacancies))
{
foreach ($team->vacancies as $vacancy)
{
$team->vacancies()->detach($vacancy->id);
}
$request->session()->flash('success', 'Removed all vacancy associations.');
return redirect()->back();
}
$vacancyDiff = array_diff($requestVacancies, $currentVacancies);
$deselectedDiff = array_diff($currentVacancies, $requestVacancies);
if (!empty($vacancyDiff) || !empty($deselectedDiff))
{
foreach ($vacancyDiff as $selectedVacancy)
{
$team->vacancies()->attach($selectedVacancy);
}
foreach ($deselectedDiff as $deselectedVacancy)
{
$team->vacancies()->detach($deselectedVacancy);
}
}
else
{
$team->vacancies()->attach($requestVacancies);
}
$request->session()->flash('success', 'Assignments changed successfully.');
return redirect()->back();
}
}

View File

@ -23,11 +23,13 @@ use App\Notifications\EmailChanged;
use App\Notifications\ChangedPassword; use App\Notifications\ChangedPassword;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use App\Traits\ReceivesAccountTokens;
use Google2FA; use Google2FA;
class UserController extends Controller class UserController extends Controller
{ {
use ReceivesAccountTokens;
public function showStaffMembers() public function showStaffMembers()
{ {
@ -220,7 +222,7 @@ class UserController extends Controller
if ($request->confirmPrompt == 'DELETE ACCOUNT') if ($request->confirmPrompt == 'DELETE ACCOUNT')
{ {
$user->delete(); $user->forceDelete();
$request->session()->flash('success','User deleted successfully. PII has been erased.'); $request->session()->flash('success','User deleted successfully. PII has been erased.');
} }
else else
@ -232,6 +234,7 @@ class UserController extends Controller
return redirect()->route('registeredPlayerList'); return redirect()->route('registeredPlayerList');
} }
public function update(UpdateUserRequest $request, User $user) public function update(UpdateUserRequest $request, User $user)
{ {
@ -356,4 +359,6 @@ class UserController extends Controller
//TODO: Dispatch event //TODO: Dispatch event
return redirect()->back(); return redirect()->back();
} }
} }

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class EditTeamRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'teamDescription' => 'required|string|max:200',
'joinType' => 'required|boolean'
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class NewTeamRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'teamName' => 'required|max:200|string'
];
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SendInviteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'user' => 'required|integer'
];
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class UserDeleteRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
if (Auth::user()->has2FA())
{
return [
'currentPassword' => 'required|password:web',
'otp' => 'required|integer|max:6'
];
}
return [
'currentPassword' => 'required|password:web'
];
}
}

55
app/Mail/InviteToTeam.php Normal file
View File

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Mpociot\Teamwork\TeamInvite;
class InviteToTeam extends Mailable
{
use Queueable, SerializesModels;
public $teamName;
public $name;
public $inviterName;
public $denyToken;
public $acceptToken;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(TeamInvite $invite)
{
$this->teamName = $invite->team->name;
$this->name = $invite->user->name;
$this->inviterName = $invite->inviter->name;
$this->acceptToken = $invite->accept_token;
$this->denyToken = $invite->deny_token;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->subject('You have just been invited to ' . $this->teamName)
->view('mail.invited-to-team');
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\User;
class UserAccountDeleteConfirmation extends Mailable
{
use Queueable, SerializesModels;
public $deleteToken;
public $cancelToken;
public $originalIP;
public $name;
public $userID;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, array $tokens, string $originalIP)
{
$this->deleteToken = $tokens['delete'];
$this->cancelToken = $tokens['cancel'];
$this->originalIP = $originalIP;
$this->name = $user->name;
$this->userID = $user->id;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('mail.deleted-account');
}
}

View File

@ -4,6 +4,7 @@ namespace App\Notifications;
use App\Facades\Options; use App\Facades\Options;
use App\Traits\Cancellable; use App\Traits\Cancellable;
use App\Traits\DiscordRoutable;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@ -13,7 +14,7 @@ use App\Application;
class ApplicationApproved extends Notification implements ShouldQueue class ApplicationApproved extends Notification implements ShouldQueue
{ {
use Queueable, Cancellable; use Queueable, Cancellable, DiscordRoutable;
public $application; public $application;
@ -80,6 +81,11 @@ class ApplicationApproved extends Notification implements ShouldQueue
}); });
} }
public function toDiscord($notifiable)
{
return $this->toSlack($notifiable);
}
/** /**
* Get the array representation of the notification. * Get the array representation of the notification.
* *

View File

@ -2,6 +2,9 @@
namespace App\Notifications; namespace App\Notifications;
use App\Facades\Options;
use App\Traits\DiscordRoutable;
use Awssat\Notifications\Messages\DiscordMessage;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@ -11,7 +14,7 @@ use App\Application;
class ApplicationDenied extends Notification implements ShouldQueue class ApplicationDenied extends Notification implements ShouldQueue
{ {
use Queueable; use Queueable, DiscordRoutable;
public $application; public $application;
@ -34,14 +37,22 @@ class ApplicationDenied extends Notification implements ShouldQueue
*/ */
public function via($notifiable) public function via($notifiable)
{ {
return ['mail', 'slack']; $options = ['mail'];
if (Options::getOption('enable_discord_notifications'))
array_push($options, 'discord');
if (Options::getOption('enable_slack_notifications'))
array_push($options, 'slack');
return $options;
} }
/** /**
* Get the mail representation of the notification. * Get the mail representation of the notification.
* *
* @param mixed $notifiable * @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage * @return MailMessage
*/ */
public function toMail($notifiable) public function toMail($notifiable)
{ {
@ -70,6 +81,13 @@ class ApplicationDenied extends Notification implements ShouldQueue
}); });
} }
public function toDiscord($notifiable)
{
// SlackMessage is similar to DiscordMessage, so they're compatible
return $this->toSlack($notifiable);
}
/** /**
* Get the array representation of the notification. * Get the array representation of the notification.
* *

View File

@ -2,6 +2,7 @@
namespace App\Notifications; namespace App\Notifications;
use App\Traits\DiscordRoutable;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@ -15,7 +16,7 @@ use App\Facades\Options;
class NewApplicant extends Notification implements ShouldQueue class NewApplicant extends Notification implements ShouldQueue
{ {
use Queueable, Cancellable; use Queueable, Cancellable, DiscordRoutable;
protected $application; protected $application;
@ -26,7 +27,8 @@ class NewApplicant extends Notification implements ShouldQueue
/** /**
* Create a new notification instance. * Create a new notification instance.
* *
* @return void * @param Application $application
* @param Vacancy $vacancy
*/ */
public function __construct(Application $application, Vacancy $vacancy) public function __construct(Application $application, Vacancy $vacancy)
{ {
@ -36,12 +38,7 @@ class NewApplicant extends Notification implements ShouldQueue
public function channels() public function channels()
{ {
if (Options::getOption('enable_slack_notifications') == 1) $this->chooseChannelsViaOptions();
{
return ['slack'];
}
return [];
} }
public function optOut($notifiable) public function optOut($notifiable)
@ -90,6 +87,12 @@ class NewApplicant extends Notification implements ShouldQueue
->action('Review application', $url); ->action('Review application', $url);
}); });
} }
public function toDiscord($notifiable)
{
return $this->toSlack($notifiable);
}
/** /**
* Get the array representation of the notification. * Get the array representation of the notification.
* *

View File

@ -21,7 +21,8 @@ class NewComment extends Notification implements ShouldQueue
/** /**
* Create a new notification instance. * Create a new notification instance.
* *
* @return void * @param Comment $comment
* @param Application $application
*/ */
public function __construct(Comment $comment, Application $application) public function __construct(Comment $comment, Application $application)
{ {

View File

@ -2,6 +2,7 @@
namespace App\Notifications; namespace App\Notifications;
use App\Traits\DiscordRoutable;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@ -15,7 +16,7 @@ use App\Facades\Options;
class NewUser extends Notification implements ShouldQueue class NewUser extends Notification implements ShouldQueue
{ {
use Queueable, Cancellable; use Queueable, Cancellable, DiscordRoutable;
public $user; public $user;
@ -83,6 +84,11 @@ class NewUser extends Notification implements ShouldQueue
}); });
} }
public function toDiscord($notifiable)
{
return $this->toSlack($notifiable);
}
/** /**
* Get the array representation of the notification. * Get the array representation of the notification.
* *

View File

@ -8,6 +8,12 @@ use Illuminate\Support\Facades\Log;
class UserObserver class UserObserver
{ {
public function __construct()
{
Log::debug('User observer has been initialised and ready for use!');
}
/** /**
* Handle the user "created" event. * Handle the user "created" event.
* *
@ -39,7 +45,9 @@ class UserObserver
public function deleting(User $user) public function deleting(User $user)
{ {
$user->profile()->delete(); if ($user->isForceDeleting())
{
$user->profile->delete();
Log::debug('Referential integrity cleanup: Deleted profile!'); Log::debug('Referential integrity cleanup: Deleted profile!');
$applications = $user->applications; $applications = $user->applications;
@ -55,6 +63,12 @@ class UserObserver
} }
} }
}
else
{
Log::debug('RIC: Not cleaning up soft deleted models!');
}
Log::debug('RIC: Cleanup done!'); Log::debug('RIC: Cleanup done!');
} }
@ -66,7 +80,6 @@ class UserObserver
*/ */
public function deleted(User $user) public function deleted(User $user)
{ {
//
} }
/** /**
@ -88,6 +101,8 @@ class UserObserver
*/ */
public function forceDeleted(User $user) public function forceDeleted(User $user)
{ {
// Log::info('Model has been force deleted', [
'modelID' => $user->id
]);
} }
} }

View File

@ -8,6 +8,7 @@ class Options extends Model
{ {
public $fillable = [ public $fillable = [
'option_name', 'option_name',
'option_value' 'option_value',
'friendly_name'
]; ];
} }

View File

@ -6,8 +6,11 @@ use App\Application;
use App\Observers\ApplicationObserver; use App\Observers\ApplicationObserver;
use App\Observers\UserObserver; use App\Observers\UserObserver;
use App\User; use App\User;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Sentry; use Sentry;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -35,6 +38,9 @@ class AppServiceProvider extends ServiceProvider
Schema::defaultStringLength(191); Schema::defaultStringLength(191);
// Keep using Bootstrap; Laravel 8 has the paginator use Tailwind. Quite opinionated tbh
Paginator::useBootstrap();
User::observe(UserObserver::class); User::observe(UserObserver::class);
Application::observe(ApplicationObserver::class); Application::observe(ApplicationObserver::class);

View File

@ -7,14 +7,6 @@ use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
/**
* This namespace is applied to your controller routes.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
protected $namespace = 'App\Http\Controllers';
/** /**
* The path to the "home" route for your application. * The path to the "home" route for your application.
@ -59,7 +51,6 @@ class RouteServiceProvider extends ServiceProvider
protected function mapWebRoutes() protected function mapWebRoutes()
{ {
Route::middleware('web') Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php')); ->group(base_path('routes/web.php'));
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Services;
use App\Vacancy;
use App\Application;
class VacancyApplicationService
{
/**
* Finds all applications associated with $model.
*
* @param Vacancy $model The model you want to search through.
* @return Illuminate\Support\Collection A collection of applications
*/
public function findApplications(Vacancy $model)
{
$applications = collect([]);
foreach(Application::all() as $application)
{
if ($application->response->vacancy->id == $model->id)
{
$applications->push($application);
}
}
return $applications;
}
}

22
app/Team.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Mpociot\Teamwork\TeamworkTeam;
class Team extends TeamworkTeam
{
public $fillable = [
'owner_id',
'name',
'description',
'openJoin'
];
public function vacancies()
{
return $this->belongsToMany('App\Vacancy', 'team_has_vacancy');
}
}

View File

@ -9,6 +9,8 @@ use App\Facades\Options;
trait Cancellable trait Cancellable
{ {
// This method is only used if you want this default set of channels;
// Other channels can always be configured by overloading the channels method here.
public function chooseChannelsViaOptions() public function chooseChannelsViaOptions()
{ {
$channels = []; $channels = [];
@ -17,11 +19,17 @@ trait Cancellable
{ {
array_push($channels, 'slack'); array_push($channels, 'slack');
} }
elseif(Options::getOption('enable_email_notifications') == 1)
if (Options::getOption('enable_email_notifications') == 1)
{ {
array_push($channels, 'email'); array_push($channels, 'email');
} }
if (Options::getOption('enable_discord_notifications'))
{
array_push($channels, 'discord');
}
return $channels; return $channels;
} }

View File

@ -0,0 +1,15 @@
<?php
namespace App\Traits;
trait DiscordRoutable
{
public function routeNotificationForDiscord()
{
return config('channels.notifications.discord.webhook_url');
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Hash;
trait HandlesAccountTokens
{
public function generateAccountTokens()
{
$deleteToken = bin2hex(openssl_random_pseudo_bytes(32));
$cancelToken = bin2hex(openssl_random_pseudo_bytes(32));
$tokens = [
'delete' => Hash::make($deleteToken),
'cancel' => Hash::make($cancelToken)
];
$this->account_tokens = json_encode($tokens);
$this->save();
return [
'delete' => $deleteToken,
'cancel' => $cancelToken
];
}
public function verifyAccountToken(string $token, string $type): bool
{
$tokens = json_decode($this->account_tokens);
if ($type == 'deleteToken')
{
return Hash::check($token, $tokens->delete);
}
elseif ($type == 'cancelToken')
{
return Hash::check($token, $tokens->cancel);
}
return false;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Http\Requests\UserDeleteRequest;
use App\Mail\UserAccountDeleteConfirmation;
use App\User;
trait ReceivesAccountTokens
{
public function userDelete(UserDeleteRequest $request)
{
// a little verbose
$user = User::find(Auth::user()->id);
$tokens = $user->generateAccountTokens();
Mail::to($user)->send(new UserAccountDeleteConfirmation($user, $tokens, $request->ip()));
$user->delete();
Auth::logout();
$request->session()->flash('success', 'Please check your email to finish deleting your account.');
return redirect()->to('/');
}
public function processDeleteConfirmation(Request $request, $ID, $action, $token)
{
// We can't rely on Laravel's route model injection, because it'll ignore soft-deleted models,
// so we have to use a special scope to find them ourselves.
$user = User::withTrashed()->findOrFail($ID);
$email = $user->email;
switch($action)
{
case 'confirm':
if ($user->verifyAccountToken($token, 'deleteToken'))
{
Log::info('SECURITY: User deleted account!', [
'confirmDeleteToken' => $token,
'ipAddress' => $request->ip(),
'email' => $user->email
]);
$user->forceDelete();
$request->session()->flash('success', 'Account permanently deleted. Thank you for using our service.');
return redirect()->to('/');
}
break;
case 'cancel':
if ($user->verifyAccountToken($token, 'cancelToken'))
{
$user->restore();
$request->session()->flash('success', 'Account deletion cancelled! You may now login.');
return redirect()->to(route('login'));
}
break;
default:
abort(404, 'The page you were trying to access may not exist or may be expired.');
}
}
}

View File

@ -2,15 +2,18 @@
namespace App; namespace App;
use App\Traits\HandlesAccountTokens;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Mpociot\Teamwork\Traits\UserHasTeams;
use Spatie\Permission\Traits\HasRoles; use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable implements MustVerifyEmail class User extends Authenticatable implements MustVerifyEmail
{ {
use Notifiable; use UserHasTeams, Notifiable, HasRoles, SoftDeletes, HandlesAccountTokens;
use HasRoles;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -74,7 +77,6 @@ class User extends Authenticatable implements MustVerifyEmail
public function isStaffMember() public function isStaffMember()
{ {
return $this->hasAnyRole('reviewer', 'admin', 'hiringManager'); return $this->hasAnyRole('reviewer', 'admin', 'hiringManager');
@ -86,7 +88,6 @@ class User extends Authenticatable implements MustVerifyEmail
} }
public function routeNotificationForSlack($notification) public function routeNotificationForSlack($notification)
{ {
return config('slack.webhook.integrationURL'); return config('slack.webhook.integrationURL');

View File

@ -5,12 +5,16 @@ namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Mpociot\Teamwork\Traits\UsedByTeams;
use GrahamCampbell\Markdown\Facades\Markdown; use GrahamCampbell\Markdown\Facades\Markdown;
class Vacancy extends Model class Vacancy extends Model
{ {
//use UsedByTeams;
public $fillable = [ public $fillable = [
'permissionGroupName', 'permissionGroupName',
@ -21,7 +25,8 @@ class Vacancy extends Model
'vacancyFormID', 'vacancyFormID',
'vacancyCount', 'vacancyCount',
'vacancyStatus', 'vacancyStatus',
'vacancySlug' 'vacancySlug',
'team_id'
]; ];
@ -45,6 +50,12 @@ class Vacancy extends Model
} }
public function teams()
{
return $this->belongsToMany('App\Team', 'team_has_vacancy');
}
public function forms() public function forms()
{ {
return $this->belongsTo('App\Form', 'vacancyFormID', 'id'); return $this->belongsTo('App\Form', 'vacancyFormID', 'id');
@ -69,4 +80,32 @@ class Vacancy extends Model
} }
/**
* Check if the Modal is attached to the $checkingTeam Model
*
* @param Team $checkingTeam The mdoel you want to check against
* @return boolean Whether the models are attached
*/
public function hasTeam(Team $checkingTeam): bool
{
$myTeams = $this->teams;
if (empty($myTeams))
{
// no associated teams
return false;
}
foreach($myTeams as $team)
{
if ($team->id === $checkingTeam->id)
{
return true;
}
}
return false;
}
} }

View File

@ -8,33 +8,35 @@
], ],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^7.2.5", "php": "^7.3.4",
"ext-imagick": "*", "ext-imagick": "*",
"ext-json": "*", "ext-json": "*",
"arcanedev/log-viewer": "^7.0", "arcanedev/log-viewer": "^8.0",
"awssat/discord-notification-channel": "^1.4",
"doctrine/dbal": "^2.10", "doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.2", "fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^1.0", "fruitcake/laravel-cors": "^1.0",
"geo-sot/laravel-env-editor": "^0.9.9", "geo-sot/laravel-env-editor": "^0.9.9",
"graham-campbell/markdown": "^12.0", "graham-campbell/markdown": "^13.1",
"guzzlehttp/guzzle": "^6.5", "guzzlehttp/guzzle": "^7.0.1",
"jeroennoten/laravel-adminlte": "^3.2", "jeroennoten/laravel-adminlte": "^3.2",
"laravel/framework": "^7.0", "laravel/framework": "^8.0",
"laravel/slack-notification-channel": "^2.0", "laravel/slack-notification-channel": "^2.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"laravel/ui": "^2.0", "laravel/ui": "^3.0",
"mcamara/laravel-localization": "^1.5", "mcamara/laravel-localization": "^1.5",
"mpociot/teamwork": "^6.0",
"pragmarx/google2fa-laravel": "^1.3", "pragmarx/google2fa-laravel": "^1.3",
"sentry/sentry-laravel": "1.7.1", "sentry/sentry-laravel": "2.1.1",
"spatie/laravel-permission": "^3.13" "spatie/laravel-permission": "^3.13"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.3", "barryvdh/laravel-debugbar": "^3.3",
"facade/ignition": "^2.0", "facade/ignition": "^2.3.6",
"fzaninotto/faker": "^1.9.1", "fzaninotto/faker": "^1.9.1",
"mockery/mockery": "^1.3.1", "mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^4.1", "nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^8.5" "phpunit/phpunit": "^9.0"
}, },
"config": { "config": {
"optimize-autoloader": true, "optimize-autoloader": true,
@ -48,12 +50,10 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"App\\": "app/" "App\\": "app/",
}, "Database\\Factories\\": "database/factories/",
"classmap": [ "Database\\Seeders\\": "database/seeders/"
"database/seeds", }
"database/factories"
]
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {

2422
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -299,6 +299,12 @@ return [
'url' => '/hr/players', 'url' => '/hr/players',
'can' => 'admin.userlist' 'can' => 'admin.userlist'
], ],
[
'text' => 'm_teams',
'icon' => 'fas fa-user-friends',
'url' => 'teams',
'can' => 'teams.view'
],
[ [
'text' => 'sm_hiring_man', 'text' => 'sm_hiring_man',
'icon' => 'far fa-calendar-plus', 'icon' => 'far fa-calendar-plus',
@ -529,6 +535,17 @@ return [
] ]
] ]
], ],
[
'name' => 'CheckboxValues',
'active' => true,
'files' => [
[
'type' => 'js',
'asset' => false,
'location' => '/js/switches.js'
]
]
],
[ [
'name' => 'AuthCustomisations', 'name' => 'AuthCustomisations',
'active' => true, 'active' => true,
@ -539,6 +556,38 @@ return [
'location' => '/css/authpages.css' 'location' => '/css/authpages.css'
] ]
] ]
],
[
'name' => 'BootstrapToggleButton',
'active' => true,
'files' => [
[
'type' => 'css',
'asset' => false,
'location' => 'https://gitcdn.github.io/bootstrap-toggle/2.2.2/css/bootstrap-toggle.min.css'
],
[
'type' => 'js',
'asset' => false,
'location' => 'https://gitcdn.github.io/bootstrap-toggle/2.2.2/js/bootstrap-toggle.min.js'
]
]
],
[
'name' => 'BootstrapMultiselectDropdown',
'active' => true,
'files' => [
[
'type' => 'js',
'asset' => 'false',
'location' => 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/js/bootstrap-multiselect.min.js'
],
[
'type' => 'css',
'asset' => false,
'location' => 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.15/css/bootstrap-multiselect.css'
]
]
] ]
], ],
]; ];

17
config/channels.php Normal file
View File

@ -0,0 +1,17 @@
<?php
return [
'notifications' => [
'discord' => [
'webhook_url' => env('DISCORD_INTEGRATION_WEBHOOK')
],
'slack' => [
'webhook_url' => env('SLACK_INTEGRATION_WEBHOOK')
]
]
];

View File

@ -81,7 +81,7 @@ return [
*/ */
'failed' => [ 'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'mysql'), 'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs', 'table' => 'failed_jobs',
], ],

84
config/teamwork.php Normal file
View File

@ -0,0 +1,84 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Auth Model
|--------------------------------------------------------------------------
|
| This is the Auth model used by Teamwork.
|
*/
'user_model' => config('auth.providers.users.model', App\User::class),
/*
|--------------------------------------------------------------------------
| Teamwork users Table
|--------------------------------------------------------------------------
|
| This is the users table name used by Teamwork.
|
*/
'users_table' => 'users',
/*
|--------------------------------------------------------------------------
| Teamwork Team Model
|--------------------------------------------------------------------------
|
| This is the Team model used by Teamwork to create correct relations. Update
| the team if it is in a different namespace.
|
*/
'team_model' => Mpociot\Teamwork\TeamworkTeam::class,
/*
|--------------------------------------------------------------------------
| Teamwork teams Table
|--------------------------------------------------------------------------
|
| This is the teams table name used by Teamwork to save teams to the database.
|
*/
'teams_table' => 'teams',
/*
|--------------------------------------------------------------------------
| Teamwork team_user Table
|--------------------------------------------------------------------------
|
| This is the team_user table used by Teamwork to save assigned teams to the
| database.
|
*/
'team_user_table' => 'team_user',
/*
|--------------------------------------------------------------------------
| User Foreign key on Teamwork's team_user Table (Pivot)
|--------------------------------------------------------------------------
*/
'user_foreign_key' => 'id',
/*
|--------------------------------------------------------------------------
| Teamwork Team Invite Model
|--------------------------------------------------------------------------
|
| This is the Team Invite model used by Teamwork to create correct relations.
| Update the team if it is in a different namespace.
|
*/
'invite_model' => Mpociot\Teamwork\TeamInvite::class,
/*
|--------------------------------------------------------------------------
| Teamwork team invites Table
|--------------------------------------------------------------------------
|
| This is the team invites table name used by Teamwork to save sent/pending
| invitation into teams to the database.
|
*/
'team_invites_table' => 'team_invites',
];

View File

@ -0,0 +1,83 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class TeamworkSetupTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table(\Config::get('teamwork.users_table'), function (Blueprint $table) {
$table->integer('current_team_id')->unsigned()->nullable();
});
Schema::create(\Config::get('teamwork.teams_table'), function (Blueprint $table) {
$table->increments('id')->unsigned();
$table->integer('owner_id')->unsigned()->nullable();
$table->string('name');
$table->timestamps();
});
Schema::create(\Config::get('teamwork.team_user_table'), function (Blueprint $table) {
$table->bigInteger('user_id')->unsigned();
$table->integer('team_id')->unsigned();
$table->timestamps();
$table->foreign('user_id')
->references(\Config::get('teamwork.user_foreign_key'))
->on(\Config::get('teamwork.users_table'))
->onUpdate('cascade')
->onDelete('cascade');
$table->foreign('team_id')
->references('id')
->on(\Config::get('teamwork.teams_table'))
->onDelete('cascade');
});
Schema::create(\Config::get('teamwork.team_invites_table'), function (Blueprint $table) {
$table->increments('id');
$table->bigInteger('user_id')->unsigned();
$table->integer('team_id')->unsigned();
$table->enum('type', ['invite', 'request']);
$table->string('email');
$table->string('accept_token');
$table->string('deny_token');
$table->timestamps();
$table->foreign('team_id')
->references('id')
->on(\Config::get('teamwork.teams_table'))
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table(\Config::get('teamwork.users_table'), function (Blueprint $table) {
$table->dropColumn('current_team_id');
});
Schema::table(\Config::get('teamwork.team_user_table'), function (Blueprint $table) {
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign(\Config::get('teamwork.team_user_table').'_user_id_foreign');
}
if (DB::getDriverName() !== 'sqlite') {
$table->dropForeign(\Config::get('teamwork.team_user_table').'_team_id_foreign');
}
});
Schema::drop(\Config::get('teamwork.team_user_table'));
Schema::drop(\Config::get('teamwork.team_invites_table'));
Schema::drop(\Config::get('teamwork.teams_table'));
}
}

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTeamDetails extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table(config('teamwork.teams_table'), function(Blueprint $table){
$table->text('description')->after('name')->nullable();
$table->enum('status', ['ACTIVE','SUSPENDED'])->after('description');
$table->boolean('openJoin')->default(false)->after('status');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table(config('teamwork.teams_table'), function(Blueprint $table){
$table->dropColumn('description');
$table->dropColumn('status');
$table->dropColumn('openJoin');
});
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class VacancyNullableTeamId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('vacancies', function(Blueprint $table){
$table->dropForeign('vacancies_ownerteamid_foreign');
$table->dropColumn('ownerTeamID');
$table->bigInteger('team_id')->nullable()->unsigned();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('vacancies', function(Blueprint $table){
$table->dropColumn('team_id');
});
}
}

View File

@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class TeamHasVacancy extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('team_has_vacancy', function(Blueprint $table){
$table->id();
$table->integer('team_id')->unsigned();
$table->bigInteger('vacancy_id')->unsigned();
$table->timestamps();
$table->foreign('team_id')
->references('id')
->on(config('teamwork.teams_table'));
$table->foreign('vacancy_id')
->references('id')
->on('vacancies');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddAccountTokensToUser extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('account_tokens')->after('password')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('account_tokens');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSoftDeletesToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->softDeletes()->after('account_tokens');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUuidToFailedJobs extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('failed_jobs', function (Blueprint $table) {
$table->string('uuid')->after('id')->nullable()->unique();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('failed_jobs', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
}

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Database\Seeders;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Database\Seeders;
use App\Facades\Options; use App\Facades\Options;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@ -22,6 +23,7 @@ class DefaultOptionsSeeder extends Seeder
Options::setOption('enable_slack_notifications', true, 'Enable slack notifications'); Options::setOption('enable_slack_notifications', true, 'Enable slack notifications');
Options::setOption('enable_email_notifications', true, 'Enable e-mail notifications'); Options::setOption('enable_email_notifications', true, 'Enable e-mail notifications');
Options::setOption('enable_discord_notifications', true, 'Enable discord notifications');
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Database\Seeders;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;

View File

@ -1,5 +1,7 @@
<?php <?php
namespace Database\Seeders;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission; use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;

View File

@ -0,0 +1,65 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
class TeamSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Permission::create([
'name' => 'teams.user.view.own'
]);
Permission::create([
'name' => 'teams.admin.view.all'
]);
// Has access to the teams feature
Permission::create([
'name' => 'teams.view'
]);
Permission::create([
'name' => 'teams.admin.create',
]);
Permission::create([
'name' => 'teams.admin.delete',
]);
Permission::create([
'name' => 'teams.user.join',
]);
Permission::create([
'name' => 'teams.user.leave',
]);
Permission::create([
'name' => 'teams.admin.vacancies.assign',
]);
Permission::create([
'name' => 'teams.admin.vacancies.unassign',
]);
Permission::create([
'name' => 'teams.admin.applications.changeteam',
]);
Permission::create([
'name' => 'teams.members.appointment.schedule',
]);
Permission::create([
'name' => 'teams.members.appointment.deleteappointment',
]);
Permission::create([
'name' => 'teams.members.groupchat',
]);
Permission::create([
'name' => 'chat.use',
]);
}
}

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Database\Seeders;
use App\Profile; use App\Profile;
use App\User; use App\User;

3503
package-lock.json generated

File diff suppressed because it is too large Load Diff

202
public/css/app.css vendored
View File

@ -1,9 +1,9 @@
@import url(https://fonts.googleapis.com/css?family=Nunito);@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito);@charset "UTF-8";
/*! /*!
* Bootstrap v4.4.1 (https://getbootstrap.com/) * Bootstrap v4.5.0 (https://getbootstrap.com/)
* Copyright 2011-2019 The Bootstrap Authors * Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2019 Twitter, Inc. * Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/ */
@ -202,6 +202,7 @@ pre {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow: auto; overflow: auto;
-ms-overflow-style: scrollbar;
} }
figure { figure {
@ -269,6 +270,10 @@ select {
text-transform: none; text-transform: none;
} }
[role=button] {
cursor: pointer;
}
select { select {
word-wrap: normal; word-wrap: normal;
} }
@ -301,13 +306,6 @@ input[type=checkbox] {
padding: 0; padding: 0;
} }
input[type=date],
input[type=time],
input[type=datetime-local],
input[type=month] {
-webkit-appearance: listbox;
}
textarea { textarea {
overflow: auto; overflow: auto;
resize: vertical; resize: vertical;
@ -749,6 +747,7 @@ pre code {
.col { .col {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
min-width: 0;
max-width: 100%; max-width: 100%;
} }
@ -956,6 +955,7 @@ pre code {
.col-sm { .col-sm {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
min-width: 0;
max-width: 100%; max-width: 100%;
} }
@ -1168,6 +1168,7 @@ pre code {
.col-md { .col-md {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
min-width: 0;
max-width: 100%; max-width: 100%;
} }
@ -1380,6 +1381,7 @@ pre code {
.col-lg { .col-lg {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
min-width: 0;
max-width: 100%; max-width: 100%;
} }
@ -1592,6 +1594,7 @@ pre code {
.col-xl { .col-xl {
flex-basis: 0; flex-basis: 0;
flex-grow: 1; flex-grow: 1;
min-width: 0;
max-width: 100%; max-width: 100%;
} }
@ -2187,11 +2190,6 @@ pre code {
box-shadow: 0 0 0 0.2rem rgba(52, 144, 220, 0.25); box-shadow: 0 0 0 0.2rem rgba(52, 144, 220, 0.25);
} }
.form-control::-webkit-input-placeholder {
color: #6c757d;
opacity: 1;
}
.form-control::-moz-placeholder { .form-control::-moz-placeholder {
color: #6c757d; color: #6c757d;
opacity: 1; opacity: 1;
@ -2218,6 +2216,15 @@ pre code {
opacity: 1; opacity: 1;
} }
input[type=date].form-control,
input[type=time].form-control,
input[type=datetime-local].form-control,
input[type=month].form-control {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
select.form-control:focus::-ms-value { select.form-control:focus::-ms-value {
color: #495057; color: #495057;
background-color: #fff; background-color: #fff;
@ -2653,7 +2660,6 @@ textarea.form-control.is-invalid {
color: #212529; color: #212529;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
cursor: pointer;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
@ -2689,6 +2695,10 @@ textarea.form-control.is-invalid {
opacity: 0.65; opacity: 0.65;
} }
.btn:not(:disabled):not(.disabled) {
cursor: pointer;
}
a.btn.disabled, a.btn.disabled,
fieldset:disabled a.btn { fieldset:disabled a.btn {
pointer-events: none; pointer-events: none;
@ -3324,7 +3334,6 @@ fieldset:disabled a.btn {
.btn-link:focus, .btn-link:focus,
.btn-link.focus { .btn-link.focus {
text-decoration: underline; text-decoration: underline;
box-shadow: none;
} }
.btn-link:disabled, .btn-link:disabled,
@ -3789,7 +3798,8 @@ input[type=button].btn-block {
.input-group > .custom-select, .input-group > .custom-select,
.input-group > .custom-file { .input-group > .custom-file {
position: relative; position: relative;
flex: 1 1 0%; flex: 1 1 auto;
width: 1%;
min-width: 0; min-width: 0;
margin-bottom: 0; margin-bottom: 0;
} }
@ -4889,7 +4899,7 @@ input[type=button].btn-block {
} }
.navbar-light .navbar-toggler-icon { .navbar-light .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
} }
.navbar-light .navbar-text { .navbar-light .navbar-text {
@ -4940,7 +4950,7 @@ input[type=button].btn-block {
} }
.navbar-dark .navbar-toggler-icon { .navbar-dark .navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
} }
.navbar-dark .navbar-text { .navbar-dark .navbar-text {
@ -4973,14 +4983,21 @@ input[type=button].btn-block {
margin-left: 0; margin-left: 0;
} }
.card > .list-group:first-child .list-group-item:first-child { .card > .list-group {
border-top-left-radius: 0.25rem; border-top: inherit;
border-top-right-radius: 0.25rem; border-bottom: inherit;
} }
.card > .list-group:last-child .list-group-item:last-child { .card > .list-group:first-child {
border-bottom-right-radius: 0.25rem; border-top-width: 0;
border-bottom-left-radius: 0.25rem; border-top-left-radius: calc(0.25rem - 1px);
border-top-right-radius: calc(0.25rem - 1px);
}
.card > .list-group:last-child {
border-bottom-width: 0;
border-bottom-right-radius: calc(0.25rem - 1px);
border-bottom-left-radius: calc(0.25rem - 1px);
} }
.card-body { .card-body {
@ -5196,6 +5213,10 @@ input[type=button].btn-block {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.breadcrumb-item {
display: flex;
}
.breadcrumb-item + .breadcrumb-item { .breadcrumb-item + .breadcrumb-item {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
@ -5667,6 +5688,7 @@ a.badge-dark.focus {
display: flex; display: flex;
height: 1rem; height: 1rem;
overflow: hidden; overflow: hidden;
line-height: 0;
font-size: 0.675rem; font-size: 0.675rem;
background-color: #e9ecef; background-color: #e9ecef;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -5721,6 +5743,7 @@ a.badge-dark.focus {
flex-direction: column; flex-direction: column;
padding-left: 0; padding-left: 0;
margin-bottom: 0; margin-bottom: 0;
border-radius: 0.25rem;
} }
.list-group-item-action { .list-group-item-action {
@ -5751,13 +5774,13 @@ a.badge-dark.focus {
} }
.list-group-item:first-child { .list-group-item:first-child {
border-top-left-radius: 0.25rem; border-top-left-radius: inherit;
border-top-right-radius: 0.25rem; border-top-right-radius: inherit;
} }
.list-group-item:last-child { .list-group-item:last-child {
border-bottom-right-radius: 0.25rem; border-bottom-right-radius: inherit;
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: inherit;
} }
.list-group-item.disabled, .list-group-item.disabled,
@ -5787,26 +5810,26 @@ a.badge-dark.focus {
flex-direction: row; flex-direction: row;
} }
.list-group-horizontal .list-group-item:first-child { .list-group-horizontal > .list-group-item:first-child {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.list-group-horizontal .list-group-item:last-child { .list-group-horizontal > .list-group-item:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-group-horizontal .list-group-item.active { .list-group-horizontal > .list-group-item.active {
margin-top: 0; margin-top: 0;
} }
.list-group-horizontal .list-group-item + .list-group-item { .list-group-horizontal > .list-group-item + .list-group-item {
border-top-width: 1px; border-top-width: 1px;
border-left-width: 0; border-left-width: 0;
} }
.list-group-horizontal .list-group-item + .list-group-item.active { .list-group-horizontal > .list-group-item + .list-group-item.active {
margin-left: -1px; margin-left: -1px;
border-left-width: 1px; border-left-width: 1px;
} }
@ -5816,26 +5839,26 @@ a.badge-dark.focus {
flex-direction: row; flex-direction: row;
} }
.list-group-horizontal-sm .list-group-item:first-child { .list-group-horizontal-sm > .list-group-item:first-child {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.list-group-horizontal-sm .list-group-item:last-child { .list-group-horizontal-sm > .list-group-item:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-group-horizontal-sm .list-group-item.active { .list-group-horizontal-sm > .list-group-item.active {
margin-top: 0; margin-top: 0;
} }
.list-group-horizontal-sm .list-group-item + .list-group-item { .list-group-horizontal-sm > .list-group-item + .list-group-item {
border-top-width: 1px; border-top-width: 1px;
border-left-width: 0; border-left-width: 0;
} }
.list-group-horizontal-sm .list-group-item + .list-group-item.active { .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
margin-left: -1px; margin-left: -1px;
border-left-width: 1px; border-left-width: 1px;
} }
@ -5846,26 +5869,26 @@ a.badge-dark.focus {
flex-direction: row; flex-direction: row;
} }
.list-group-horizontal-md .list-group-item:first-child { .list-group-horizontal-md > .list-group-item:first-child {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.list-group-horizontal-md .list-group-item:last-child { .list-group-horizontal-md > .list-group-item:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-group-horizontal-md .list-group-item.active { .list-group-horizontal-md > .list-group-item.active {
margin-top: 0; margin-top: 0;
} }
.list-group-horizontal-md .list-group-item + .list-group-item { .list-group-horizontal-md > .list-group-item + .list-group-item {
border-top-width: 1px; border-top-width: 1px;
border-left-width: 0; border-left-width: 0;
} }
.list-group-horizontal-md .list-group-item + .list-group-item.active { .list-group-horizontal-md > .list-group-item + .list-group-item.active {
margin-left: -1px; margin-left: -1px;
border-left-width: 1px; border-left-width: 1px;
} }
@ -5876,26 +5899,26 @@ a.badge-dark.focus {
flex-direction: row; flex-direction: row;
} }
.list-group-horizontal-lg .list-group-item:first-child { .list-group-horizontal-lg > .list-group-item:first-child {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.list-group-horizontal-lg .list-group-item:last-child { .list-group-horizontal-lg > .list-group-item:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-group-horizontal-lg .list-group-item.active { .list-group-horizontal-lg > .list-group-item.active {
margin-top: 0; margin-top: 0;
} }
.list-group-horizontal-lg .list-group-item + .list-group-item { .list-group-horizontal-lg > .list-group-item + .list-group-item {
border-top-width: 1px; border-top-width: 1px;
border-left-width: 0; border-left-width: 0;
} }
.list-group-horizontal-lg .list-group-item + .list-group-item.active { .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
margin-left: -1px; margin-left: -1px;
border-left-width: 1px; border-left-width: 1px;
} }
@ -5906,42 +5929,40 @@ a.badge-dark.focus {
flex-direction: row; flex-direction: row;
} }
.list-group-horizontal-xl .list-group-item:first-child { .list-group-horizontal-xl > .list-group-item:first-child {
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 0.25rem;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
.list-group-horizontal-xl .list-group-item:last-child { .list-group-horizontal-xl > .list-group-item:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-group-horizontal-xl .list-group-item.active { .list-group-horizontal-xl > .list-group-item.active {
margin-top: 0; margin-top: 0;
} }
.list-group-horizontal-xl .list-group-item + .list-group-item { .list-group-horizontal-xl > .list-group-item + .list-group-item {
border-top-width: 1px; border-top-width: 1px;
border-left-width: 0; border-left-width: 0;
} }
.list-group-horizontal-xl .list-group-item + .list-group-item.active { .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
margin-left: -1px; margin-left: -1px;
border-left-width: 1px; border-left-width: 1px;
} }
} }
.list-group-flush .list-group-item { .list-group-flush {
border-right-width: 0;
border-left-width: 0;
border-radius: 0; border-radius: 0;
} }
.list-group-flush .list-group-item:first-child { .list-group-flush > .list-group-item {
border-top-width: 0; border-width: 0 0 1px;
} }
.list-group-flush:last-child .list-group-item:last-child { .list-group-flush > .list-group-item:last-child {
border-bottom-width: 0; border-bottom-width: 0;
} }
@ -6105,9 +6126,6 @@ button.close {
padding: 0; padding: 0;
background-color: transparent; background-color: transparent;
border: 0; border: 0;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
} }
a.close.disabled { a.close.disabled {
@ -6234,6 +6252,9 @@ a.close.disabled {
.modal-dialog-centered::before { .modal-dialog-centered::before {
display: block; display: block;
height: calc(100vh - 1rem); height: calc(100vh - 1rem);
height: -webkit-min-content;
height: -moz-min-content;
height: min-content;
content: ""; content: "";
} }
@ -6351,6 +6372,9 @@ a.close.disabled {
.modal-dialog-centered::before { .modal-dialog-centered::before {
height: calc(100vh - 3.5rem); height: calc(100vh - 3.5rem);
height: -webkit-min-content;
height: -moz-min-content;
height: min-content;
} }
.modal-sm { .modal-sm {
@ -6900,6 +6924,7 @@ a.close.disabled {
50% { 50% {
opacity: 1; opacity: 1;
transform: none;
} }
} }
@ -6910,6 +6935,7 @@ a.close.disabled {
50% { 50% {
opacity: 1; opacity: 1;
transform: none;
} }
} }
@ -8201,6 +8227,27 @@ button.bg-dark:focus {
} }
} }
.user-select-all {
-webkit-user-select: all !important;
-moz-user-select: all !important;
-ms-user-select: all !important;
user-select: all !important;
}
.user-select-auto {
-webkit-user-select: auto !important;
-moz-user-select: auto !important;
-ms-user-select: auto !important;
user-select: auto !important;
}
.user-select-none {
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
user-select: none !important;
}
.overflow-auto { .overflow-auto {
overflow: auto !important; overflow: auto !important;
} }
@ -8357,18 +8404,6 @@ button.bg-dark:focus {
height: 100vh !important; height: 100vh !important;
} }
.stretched-link::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
pointer-events: auto;
content: "";
background-color: rgba(0, 0, 0, 0);
}
.m-0 { .m-0 {
margin: 0 !important; margin: 0 !important;
} }
@ -10537,6 +10572,18 @@ button.bg-dark:focus {
} }
} }
.stretched-link::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
pointer-events: auto;
content: "";
background-color: rgba(0, 0, 0, 0);
}
.text-monospace { .text-monospace {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
} }
@ -10768,8 +10815,7 @@ a.text-dark:focus {
} }
.text-break { .text-break {
word-break: break-word !important; word-wrap: break-word !important;
overflow-wrap: break-word !important;
} }
.text-reset { .text-reset {

View File

@ -1050,6 +1050,7 @@ Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css
/* don't display any button-related controls */ /* don't display any button-related controls */
} }
} }
/* DayGridView /* DayGridView
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
/* day row structure */ /* day row structure */
@ -1128,6 +1129,7 @@ Lots taken from Flatly (MIT): https://bootswatch.com/4/flatly/bootstrap.css
display: inline-block; display: inline-block;
min-width: 1.25em; min-width: 1.25em;
} }
/* Scroller /* Scroller
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
.fc-scroller-clip { .fc-scroller-clip {
@ -1479,6 +1481,7 @@ TODO: figure out better styling
.fc-rtl .fc-timeline-event.fc-not-start:before { .fc-rtl .fc-timeline-event.fc-not-start:before {
border-right: 0; border-right: 0;
} }
@charset "UTF-8"; @charset "UTF-8";
/* TimeGridView all-day area /* TimeGridView all-day area
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
@ -1788,6 +1791,7 @@ be a descendant of the grid when it is being dragged.
border-top-color: transparent; border-top-color: transparent;
border-bottom-color: transparent; border-bottom-color: transparent;
} }
/* List View /* List View
--------------------------------------------------------------------------------------------------*/ --------------------------------------------------------------------------------------------------*/
/* possibly reusable */ /* possibly reusable */
@ -1906,6 +1910,7 @@ be a descendant of the grid when it is being dragged.
/* theme will provide own background */ /* theme will provide own background */
background-color: #eee; background-color: #eee;
} }
.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px);}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.rightMost:after{left:auto;right:22px}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/* .flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px);}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.showTimeInput.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.rightMost:after{left:auto;right:22px}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/*
/*rtl:begin:ignore*/left:0;/* /*rtl:begin:ignore*/left:0;/*
/*rtl:end:ignore*/}/* /*rtl:end:ignore*/}/*

1
public/img/editable.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

41
public/img/new_team.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

1
public/img/team.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,14 +1,23 @@
<?php <?php
/**
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true)); define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Check If Application Is Under Maintenance
|--------------------------------------------------------------------------
|
| If the application is maintenance / demo mode via the "down" command we
| will require this file so that any prerendered template can be shown
| instead of starting the framework, which could cause an exception.
|
*/
if (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {
require __DIR__.'/../storage/framework/maintenance.php';
}
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Register The Auto Loader | Register The Auto Loader

40870
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,94 @@
/* flatpickr v4.6.3, @license MIT */
/*!
* Bootstrap v4.5.0 (https://getbootstrap.com/)
* Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*!
* Chart.js v2.9.3
* https://www.chartjs.org
* (c) 2019 Chart.js Contributors
* Released under the MIT License
*/
/*!
* Sizzle CSS Selector Engine v2.3.5
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2020-03-14
*/
/*!
* jQuery JavaScript Library v3.5.1
* https://jquery.com/
*
* Includes Sizzle.js
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2020-05-04T22:49Z
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**!
* @fileOverview Kickass library to create and place poppers near their reference elements.
* @version 1.16.1
* @license
* Copyright (c) 2016 Federico Zivolo and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
//! moment.js
//! moment.js language configuration
//! moment.js locale configuration

View File

@ -1,3 +1,5 @@
$(document).ready(function() { $(document).ready(function() {
$('input[rel="txtTooltip"]').tooltip(); $('input[rel="txtTooltip"]').tooltip();
$('span[rel="spanTxtTooltip"]').tooltip(); // Also allow span tooltips
$('button[rel="buttonTxtTooltip"]').tooltip(); // button tooltip
}); });

7
public/js/switches.js vendored Normal file
View File

@ -0,0 +1,7 @@
$("#jointype").on('change', function() {
if ($(this).is(':checked')) {
$(this).attr('value', '1');
} else {
$(this).attr('value', '0');
}
});

6
public/js/team-editor.js vendored Normal file
View File

@ -0,0 +1,6 @@
$(function() {
$('#assocVacancies').multiselect({
disableIfEmpty: true,
nonSelectedText: 'Choose vacancies...'
});
});

3
resources/js/app.js vendored
View File

@ -43,3 +43,6 @@ $("#comment").keyup(function(){
$("#submitComment").on('click', function(){ $("#submitComment").on('click', function(){
$("#newComment").submit(); $("#newComment").submit();
}); });
$("#jointype").bootstrapToggle();

View File

@ -605,6 +605,13 @@ return [
'max_chars' => 'max characters', // Context: A number is added before max characters 'max_chars' => 'max characters', // Context: A number is added before max characters
'post' => 'Post', // Context: Post as in post comment 'post' => 'Post', // Context: Post as in post comment
],
'teams' => [
'm_teams_page' => 'Teams'
] ]
// ==================== END OF MAIN I18N FILE ====================== // ==================== END OF MAIN I18N FILE ======================

View File

@ -0,0 +1,213 @@
@extends('adminlte::page')
@section('title', config('app.name') . ' | ' . __('messages.teams.m_teams_page'))
@section('content_header')
<h1>{{config('app.name')}} / {{__('messages.teams.m_teams_page')}}</h1>
@stop
@section('js')
<x-global-errors></x-global-errors>
<script src="/js/team-editor.js"></script>
@stop
@section('content')
@if($team->openJoin == false)
<x-modal id="addUserModal" modal-label="addUserModalLabel" modal-title="Invite User" include-close-button="true">
<form id="inviteToTeam" method="POST" action="{{ route('sendInvite', ['team' => $team->id]) }}">
@csrf
<div class="form-group">
<select class="custom-select" name="user">
<option disabled selected>Choose a user to invite</option>
@foreach ($users as $user)
<option value="{{ $user->id }}" {{ ($user->id == Auth::user()->id) ? 'disabled' : '' }}>{{ $user->name }}</option>
@endforeach
</select>
<span class="text-sm text-muted"><i class="fas fa-info-circle"></i> This user will receive an email notification asking them to join your team.</span>
</div>
</form>
<x-slot name="modalFooter">
<button type="button" class="btn btn-success" onclick="$('#inviteToTeam').submit()"><i class="fas fa-paper-plane"></i> Send invite</button>
</x-slot>
</x-modal>
@endif
<x-modal id="userlist_modal" modal-label="UserListModalLabel" modal-title="Team Members and Invites" include-close-button="true">
<p><i class="fas fa-info-circle"></i> Team members and pending invites will appear here.</p>
@if (!$team->users->isEmpty())
<table class="table table-borderless">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Roles</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($team->users as $teammate)
<tr>
<td>{{ $teammate->id }}</td>
<td><a target="_blank" href="{{ route('showSingleProfile', ['user' => $teammate->id]) }}">{{ $teammate->name }}</a></td>
<td>
@foreach ($teammate->roles as $teammate_role)
<span class="badge badge-secondary">{{ $teammate_role->name }}</span>
@endforeach
</td>
<td>
@if ($teammate->isOwnerOfTeam($team))
<span class="badge badge-success">Team Owner</span>
@else
<span class="badge badge-primary">Team Member</span>
@endif
</td>
<td>
<button rel="buttonTxtTooltip" data-toggle="tooltip" data-placement="top" title="Kick User" type="button" class="btn btn-danger btn-sm"><i class="fas fa-bolt"></i></button>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="alert alert-warning">
<span><i class="fas fa-exclamation-triangle"></i> <b>There don't seem to be any teammates here!</b></span>
<p>Start inviting some people and grow your team.</p>
</div>
@endif
<x-slot name="modalFooter"></x-slot>
</x-modal>
<div class="row">
<div class="col text-center">
<img src="/img/editable.svg" alt="Edit illustration" height="220px" width="220px">
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h4 class="card-title">Edit Team</h4>
</div>
<div class="card-body">
<form id="editTeam" method="POST" action="{{ route('teams.update', ['team' => $team->id]) }}">
@csrf
@method('PATCH')
<div class="form-group">
<label for="teamName">Team name</label>
<input type="text" class="form-control" id="teamName" value="{{ $team->name }}" disabled>
<label for="teamDescription">Team description</label>
<textarea class="form-control" rows="4" name="teamDescription" id="teamDescription">{{ $team->description }}</textarea>
<span class="text-left text-muted text-sm"><a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet"><i class="fab fa-markdown"></i></a> Markdown supported</span>
<div class="controlbuttons mt-3">
<span rel="spanTxtTooltip" data-toggle="tooltip" title="This setting controls whether people can join the team freely." data-placement="top">
<input type="hidden" name="joinType" value="0"> <!-- unchecked checkbox hack (no js) -->
<input value="0" type="checkbox" {{ ($team->openJoin == 1) ? 'checked' : '' }} name="joinType" id="jointype" data-toggle="toggle" data-on="Public Team" data-off="Private" data-onstyle="success" data-offstyle="danger" data-width="100">
</span>
<button onclick="$('#addUserModal').modal('show')" type="button" id="inviteUser" name="inviteUser" class="btn btn-success" {{ ($team->openJoin) ? 'disabled' : '' }}><i class="fas fa-user-plus"></i> Invite user</button>
</div>
</div>
</form>
</div>
<div class="card-footer">
<button type="button" class="btn btn-success ml-2" onclick="$('#editTeam').submit()"><i class="fas fa-save"></i> Save and Close</button>
<button type="button" class="btn btn-warning ml-2" onclick="$('#userlist_modal').modal('show')"><i class="fas fa-users"></i> Team Members</button>
<button type="button" class="btn btn-danger ml-2" onclick="location.href='{{ route('teams.index') }}'"><i class="far fa-trash-alt"></i> Cancel</button>
</div>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header">
<div class="card-title"><h4>Team Vacancies</h4></div>
</div>
<div class="card-body">
<span class="text-muted"><i class="fas fa-info-circle"></i> The vacancies you select determine what applications your team members see. All applications under the vacancies you choose will be displayed.</span>
<form method="POST" id="vacancyChangeForm" action="{{ route('assignVacancies', ['team' => $team->id]) }}">
@csrf
@method('PATCH')
<select id="assocVacancies" name="assocVacancies[]" multiple="multiple">
@foreach($vacancies as $vacancy)
<!-- fixme: n+1 query here -->
<option {{ ($vacancy->hasTeam($team)) ? 'selected' : '' }} value="{{ $vacancy->id }}">{{ $vacancy->vacancyName }}</option>
@endforeach
</select>
</form>
</div>
<div class="card-footer">
<button onclick="$('#vacancyChangeForm').submit()" type="button" class="btn btn-success"><i class="far fa-save"></i> Update Assignments</button>
</div>
</div>
</div>
</div>
@stop

View File

@ -0,0 +1,147 @@
@extends('adminlte::page')
@section('title', config('app.name') . ' | ' . __('messages.teams.m_teams_page'))
@section('content_header')
<h1>{{config('app.name')}} / {{__('messages.teams.m_teams_page')}}</h1>
@stop
@section('js')
<x-global-errors></x-global-errors>
@endsection
@section('content')
<x-modal id="newTeamModal" modal-label="newTeamModalLabel" modal-title="New team" include-close-button="true">
<div class="row">
<div class="col offset-3">
<img src="/img/new_team.svg" height="220px" width="220px" alt="New Team illustration">
</div>
</div>
<form action="{{ route('teams.store') }}" method="POST" id="newTeamForm">
@csrf
<div class="text-center">
<input type="text" id="teamName" class="form-control" required name="teamName">
</div>
<p class="text-muted text-sm">This is the name team members will see.</p>
</form>
<x-slot name="modalFooter">
<button type="button" class="btn btn-success" onclick="$('#newTeamForm').submit()"><i class="fas fa-check"></i> Create</button>
</x-slot>
</x-modal>
<div class="row">
<div class="col-md-4 offset-4 text-center">
<img src="/img/team.svg" height="230px" width="230px" alt="Team illustration">
</div>
</div>
<div class="row">
<div class="col">
<div class="card bg-gray-dark">
<div class="card-header bg-indigo">
<div class="row">
<div class="col">
<div class="card-title"><h4>{{ __('messages.teams.m_teams_page') }} <span class="badge badge-warning"><i class="fas fa-check-circle"></i> {{ (Auth::user()->currentTeam) ? Auth::user()->currentTeam->name : 'Select a team' }}</span></h4></div>
</div>
</div>
</div>
<div class="card-body">
@if (!$teams->isEmpty())
<table class="table-borderless table-active table">
<thead>
<tr>
<th>#</th>
<th>Team Owner</th>
<th>Team Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach ($teams as $team)
<tr>
<td>{{ $team->id }}</td>
<td>{{ $team->owner_id }}</td>
<td>{{ $team->name }}</td>
<td>
<button type="button" class="btn btn-success btn-sm ml-2" onclick="location.href='{{ route('teams.edit', ['team' => $team->id]) }}'"><i class="fas fa-cogs"></i> Settings</button>
<button onclick="location.href='{{ route('switchTeam', ['team' => $team]) }}'" rel="buttonTxtTooltip" data-placement="top" data-toggle="tooltip" title="Select your active team (for dasboard context, etc)" type="button" class="btn btn-warning btn-sm ml-2"><i class="fas fa-random"></i> Switch To</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i> <b> There don't seem to be any teams here</b>
<p>Have you tried creating or joining a team? You may also click an invite link if you've been invited.</p>
</div>
@endif
</div>
<div class="card-footer">
<button type="button" class="btn btn-success btn-sm ml-3" onclick="$('#newTeamModal').modal('show')"><i class="fas fa-plus-circle"></i> New team</button>
<button type="button" class="btn btn-warning btn-sm ml-3"><i class="fas fas fa-long-arrow-alt-right"></i> Team Dashboard</button>
</div>
</div>
</div>
</div>
@stop

View File

@ -20,9 +20,62 @@
@section('content') @section('content')
<x-modal id="deleteAccountModal" modal-label="deleteAccountModalLabel" modal-title="Close account" include-close-button="true">
<p>Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):</p>
<ul>
<li>Last IP address</li>
<li>Name, Email and MC Username</li>
<li>Your previous applications</li>
<li>Your profile data and preferences</li>
<li>If you were a staff member:</li>
<ul>
<li>Your comments</li>
<li>Any votes</li>
<li>Your roles</li>
</ul>
</ul>
<p>What is not deleted:</p>
<ul>
<li>Server logs of your visits, including IP addresses</li>
</ul>
<form id="deleteAccountForm" method="POST" action="{{ route('userDelete') }}">
@csrf
@method('PATCH')
<div class="form-group">
<label for="currentPassword">Re-enter your password</label>
<input class="form-control" autocomplete="current-password" type="password" name="currentPassword" id="currentPassword" required>
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> For your security, your password is always required for sensitive operations. <a href="{{ route('password.request') }}">Forgot your password?</a></p>
</div>
@if (Auth::user()->has2FA())
<div class="form-group mt-5">
<label for="otp">Two-factor authentication code</label>
<input type="text" id="otp" name="otp" class="form-control">
<p class="text-muted text-sm"><i class="fas fa-info-circle"></i> You cannot recover lost 2FA secrets.</p>
</div>
@endif
</form>
<x-slot name="modalFooter">
<button onclick="$('#deleteAccountForm').submit()" type="button" class="btn btn-warning"><i class="fas fa-exclamation-triangle"></i> Continue</button>
</x-slot>
</x-modal>
@if (!Auth::user()->has2FA()) @if (!Auth::user()->has2FA())
<x-modal id="twoFactorAuthModal" modal-label="2faLabel" modal-title="{{__('messages.2fa_txt')}}" include-close-button="true"> <x-modal id="twoFactorAuthModal" modal-label="2faLabel" modal-title="{{__('messages.2fa_txt')}}" include-close-button="true">
<h3><i class="fas fa-user-shield"></i> {{__('messages.profile.2fa_welcome')}}</h3> <h3><i class="fas fa-user-shield"></i> {{__('messages.profile.2fa_welcome')}}</h3>
@ -170,6 +223,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" id="contactSettingsTab" data-toggle="tab" href="#contactSettings" role="tab" aria-controls="ContactSettings" aria-selected="false">{{__('messages.profile.contact_settings')}}</a> <a class="nav-link" id="contactSettingsTab" data-toggle="tab" href="#contactSettings" role="tab" aria-controls="ContactSettings" aria-selected="false">{{__('messages.profile.contact_settings')}}</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" id="dangerZoneTab" data-toggle="tab" href="#dangerZone" role="tab" aria-controls="DangerZone" aria-selected="false">Danger Zone</a>
</li>
</ul> </ul>
</div> </div>
@ -250,6 +306,15 @@
<button class="btn btn-success" type="button" onclick="document.getElementById('changeEmail').submit()">{{__('messages.profile.change_email')}}</button> <button class="btn btn-success" type="button" onclick="document.getElementById('changeEmail').submit()">{{__('messages.profile.change_email')}}</button>
</div> </div>
<div class="tab-pane fade p-3" id="dangerZone" role="tabpanel" aria-labelledby="dangerZoneTab">
<h5 class="card-title">Danger Zone</h5>
<p class="card-text text-bold"><i class="fas fa-radiation"></i> Careful! Actions in these tab might result in irreversible loss of data.</p>
<button onclick="$('#deleteAccountModal').modal('show')" rel="buttonTxtTooltip" data-toggle="tooltip" data-placement="top" title="This action will delete your account permanently." class="btn btn-danger" type="button"><i class="fas fa-user-slash"></i> Close Account</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,167 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Your account has been deleted</title>
<style>
/* -------------------------------------
INLINED WITH htmlemail.io/inline
------------------------------------- */
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Your account has just been deleted!</span>
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi {{ $name }},</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Someone (hopefully you) has requested that your account at {{ config('app.name') }} be deleted.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">As a security measure, an email is always sent out to make sure you really want to delete your account.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">This is the IP address the request was made from: {{ $originalIP }}.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you don't do anything, your account will automatically be permanently deleted in 30 days, and will remain unaccessible for that time period.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click one of the buttons below to make a decision.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processDeleteConfirmation', ['ID' => $userID, 'action' => 'confirm', 'token' => $deleteToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Delete Account</a> </td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processDeleteConfirmation', ['ID' => $userID, 'action' => 'cancel', 'token' => $cancelToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Cancel Deletion</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Thank you!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">Staff Manager</span>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,167 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>You have just been invited to a team </title>
<style>
/* -------------------------------------
INLINED WITH htmlemail.io/inline
------------------------------------- */
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: 16px !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
.btn-primary table td:hover {
background-color: #34495e !important;
}
.btn-primary a:hover {
background-color: #34495e !important;
border-color: #34495e !important;
}
}
</style>
</head>
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">You have just been invited to {{ $teamName }}</span>
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Hi {{ $name }},</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">{{ $inviterName }} has just invited you to join {{ $teamName }} (team).</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Joining a team confers many benefits, such as access to the team calendar, shared files, assigned applications and much more.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">If you accept this invite, you'll be added to the team immediately. Conversely, if you refuse it, {{ $inviterName }} won't be notified and you'll need to be invited again to join it.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Click one of the buttons below to make a decision.</p>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processInvite', ['action' => 'accept', 'token' => $acceptToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Accept Invite</a> </td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; background-color: #3498db; border-radius: 5px; text-align: center; margin-left: 5px;"> <a href="{{ route('processInvite', ['action' => 'deny', 'token' => $denyToken]) }}" target="_blank" style="display: inline-block; color: #ffffff; background-color: #3498db; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: #3498db;">Deny Invite</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">The accept and deny buttons do not expire (unless your invite is cancelled), so you can take your time to make a decision.</p>
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">Thank you!</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">Staff Manager</span>
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -1,6 +1,23 @@
<?php <?php
use App\Http\Controllers\ApplicationController;
use App\Http\Controllers\AppointmentController;
use App\Http\Controllers\Auth\TwofaController;
use App\Http\Controllers\BanController;
use App\Http\Controllers\CommentController;
use App\Http\Controllers\ContactController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\DevToolsController;
use App\Http\Controllers\FormController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TeamController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\VacancyController;
use App\Http\Controllers\VoteController;
use App\Http\Controllers\OptionsController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization; use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
/* /*
@ -19,72 +36,95 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => [ 'l
Auth::routes(['verify' => true]); Auth::routes(['verify' => true]);
Route::post('/twofa/authenticate', 'Auth\TwofaController@verify2FA') Route::post('/twofa/authenticate', [TwofaController::class, 'verify2FA'])
->name('verify2FA'); ->name('verify2FA');
}); });
Route::get('/','HomeController@index') Route::get('/', [HomeController::class, 'index'])
->middleware('eligibility'); ->middleware('eligibility');
Route::post('/form/contact', 'ContactController@create') Route::post('/form/contact', [ContactController::class, 'create'])
->name('sendSubmission'); ->name('sendSubmission');
Route::get('/accounts/danger-zone/{ID}/{action}/{token}', [UserController::class, 'processDeleteConfirmation'])
->name('processDeleteConfirmation');
Route::group(['middleware' => ['auth', 'forcelogout', '2fa', 'verified']], function(){ Route::group(['middleware' => ['auth', 'forcelogout', '2fa', 'verified']], function(){
Route::get('/dashboard', 'DashboardController@index') Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard') ->name('dashboard')
->middleware('eligibility'); ->middleware('eligibility');
Route::get('users/directory', 'ProfileController@index') Route::get('users/directory', [ProfileController::class, 'index'])
->name('directory'); ->name('directory');
Route::resource('teams', TeamController::class);
Route::post('teams/{team}/invites/send', [TeamController::class, 'invite'])
->name('sendInvite');
Route::get('teams/{team}/switch', [TeamController::class, 'switchTeam'])
->name('switchTeam');
Route::patch('teams/{team}/vacancies/update', [TeamController::class, 'assignVacancies'])
->name('assignVacancies');
Route::get('teams/invites/{action}/{token}', [TeamController::class, 'processInviteAction'])
->name('processInvite');
Route::group(['prefix' => '/applications'], function (){ Route::group(['prefix' => '/applications'], function (){
Route::get('/my-applications', 'ApplicationController@showUserApps') Route::get('/my-applications', [ApplicationController::class, 'showUserApps'])
->name('showUserApps') ->name('showUserApps')
->middleware('eligibility'); ->middleware('eligibility');
Route::get('/view/{application}', 'ApplicationController@showUserApp') Route::get('/view/{application}', [ApplicationController::class, 'showUserApp'])
->name('showUserApp'); ->name('showUserApp');
Route::post('/{application}/comments', 'CommentController@insert') Route::post('/{application}/comments', [CommentController::class, 'insert'])
->name('addApplicationComment'); ->name('addApplicationComment');
Route::delete('/comments/{comment}/delete', 'CommentController@delete') Route::delete('/comments/{comment}/delete', [CommentController::class, 'delete'])
->name('deleteApplicationComment'); ->name('deleteApplicationComment');
Route::patch('/notes/save/{application}', 'AppointmentController@saveNotes') Route::patch('/notes/save/{application}', [AppointmentController::class, 'saveNotes'])
->name('saveNotes'); ->name('saveNotes');
Route::patch('/update/{application}/{newStatus}', 'ApplicationController@updateApplicationStatus') Route::patch('/update/{application}/{newStatus}', [ApplicationController::class, 'updateApplicationStatus'])
->name('updateApplicationStatus'); ->name('updateApplicationStatus');
Route::delete('{application}/delete', 'ApplicationController@delete') Route::delete('{application}/delete', [ApplicationController::class, 'delete'])
->name('deleteApplication'); ->name('deleteApplication');
Route::get('/staff/all', 'ApplicationController@showAllApps') Route::get('/staff/all', [ApplicationController::class, 'showAllApps'])
->name('allApplications'); ->name('allApplications');
Route::get('/staff/outstanding', 'ApplicationController@showAllPendingApps') Route::get('/staff/outstanding', [ApplicationController::class, 'showAllPendingApps'])
->name('staffPendingApps'); ->name('staffPendingApps');
Route::get('/staff/peer-review', 'ApplicationController@showPeerReview') Route::get('/staff/peer-review', [ApplicationController::class, 'showPeerReview'])
->name('peerReview'); ->name('peerReview');
Route::get('/staff/pending-interview', 'ApplicationController@showPendingInterview') Route::get('/staff/pending-interview', [ApplicationController::class, 'showPendingInterview'])
->name('pendingInterview'); ->name('pendingInterview');
Route::post('{application}/staff/vote', 'VoteController@vote') Route::post('{application}/staff/vote', [VoteController::class, 'vote'])
->name('voteApplication'); ->name('voteApplication');
@ -92,143 +132,146 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => [ 'l
Route::group(['prefix' => 'appointments'], function (){ Route::group(['prefix' => 'appointments'], function (){
Route::post('schedule/appointments/{application}', 'AppointmentController@saveAppointment') Route::post('schedule/appointments/{application}', [AppointmentController::class, 'saveAppointment'])
->name('scheduleAppointment'); ->name('scheduleAppointment');
Route::patch('update/appointments/{application}/{status}', 'AppointmentController@updateAppointment') Route::patch('update/appointments/{application}/{status}', [AppointmentController::class, 'updateAppointment'])
->name('updateAppointment'); ->name('updateAppointment');
}); });
Route::group(['prefix' => 'apply', 'middleware' => ['eligibility']], function (){ Route::group(['prefix' => 'apply', 'middleware' => ['eligibility']], function (){
Route::get('positions/{vacancySlug}', 'ApplicationController@renderApplicationForm') Route::get('positions/{vacancySlug}', [ApplicationController::class, 'renderApplicationForm'])
->name('renderApplicationForm'); ->name('renderApplicationForm');
Route::post('positions/{vacancySlug}/submit', 'ApplicationController@saveApplicationAnswers') Route::post('positions/{vacancySlug}/submit', [ApplicationController::class, 'saveApplicationAnswers'])
->name('saveApplicationForm'); ->name('saveApplicationForm');
}); });
Route::group(['prefix' => '/profile'], function (){ Route::group(['prefix' => '/profile'], function (){
Route::get('/settings', 'ProfileController@showProfile') Route::get('/settings', [ProfileController::class, 'showProfile'])
->name('showProfileSettings'); ->name('showProfileSettings');
Route::patch('/settings/save', 'ProfileController@saveProfile') Route::patch('/settings/save', [ProfileController::class, 'saveProfile'])
->name('saveProfileSettings'); ->name('saveProfileSettings');
Route::get('user/{user}', 'ProfileController@showSingleProfile') Route::get('user/{user}', [ProfileController::class, 'showSingleProfile'])
->name('showSingleProfile'); ->name('showSingleProfile');
Route::get('/settings/account', 'UserController@showAccount') Route::get('/settings/account', [UserController::class, 'showAccount'])
->name('showAccountSettings'); ->name('showAccountSettings');
Route::patch('/settings/account/change-password', 'UserController@changePassword') Route::patch('/settings/account/change-password', [UserController::class, 'changePassword'])
->name('changePassword'); ->name('changePassword');
Route::patch('/settings/account/change-email', 'UserController@changeEmail') Route::patch('/settings/account/change-email', [UserController::class, 'changeEmail'])
->name('changeEmail'); ->name('changeEmail');
Route::post('/settings/account/flush-sessions', 'UserController@flushSessions') Route::post('/settings/account/flush-sessions', [UserController::class, 'flushSessions'])
->name('flushSessions'); ->name('flushSessions');
Route::patch('/settings/account/twofa/enable', 'UserController@add2FASecret') Route::patch('/settings/account/twofa/enable', [UserController::class, 'add2FASecret'])
->name('enable2FA'); ->name('enable2FA');
Route::patch('/settings/account/twofa/disable', 'UserController@remove2FASecret') Route::patch('/settings/account/twofa/disable', [UserController::class, 'remove2FASecret'])
->name('disable2FA'); ->name('disable2FA');
Route::patch('/settings/account/dg/delete', [UserController::class, 'userDelete'])
->name('userDelete');
}); });
Route::group(['prefix' => '/hr'], function (){ Route::group(['prefix' => '/hr'], function (){
Route::get('staff-members', 'UserController@showStaffMembers') Route::get('staff-members', [UserController::class, 'showStaffMembers'])
->name('staffMemberList'); ->name('staffMemberList');
Route::get('players', 'UserController@showPlayers') Route::get('players', [UserController::class, 'showPlayers'])
->name('registeredPlayerList'); ->name('registeredPlayerList');
Route::post('players/search', 'UserController@showPlayersLike') Route::post('players/search', [UserController::class, 'showPlayersLike'])
->name('searchRegisteredPLayerList'); ->name('searchRegisteredPLayerList');
Route::patch('staff-members/terminate/{user}', 'UserController@terminate') Route::patch('staff-members/terminate/{user}', [UserController::class, 'terminate'])
->name('terminateStaffMember'); ->name('terminateStaffMember');
}); });
Route::group(['prefix' => 'admin'], function (){ Route::group(['prefix' => 'admin'], function (){
Route::get('settings', 'OptionsController@index') Route::get('settings', [OptionsController::class, 'index'])
->name('showSettings'); ->name('showSettings');
Route::post('settings/save', 'OptionsController@saveSettings') Route::post('settings/save', [OptionsController::class, 'saveSettings'])
->name('saveSettings'); ->name('saveSettings');
Route::post('players/ban/{user}', 'BanController@insert') Route::post('players/ban/{user}', [BanController::class, 'insert'])
->name('banUser'); ->name('banUser');
Route::delete('players/unban/{user}', 'BanController@delete') Route::delete('players/unban/{user}', [BanController::class, 'delete'])
->name('unbanUser'); ->name('unbanUser');
Route::delete('players/delete/{user}', 'UserController@delete') Route::delete('players/delete/{user}', [UserController::class, 'delete'])
->name('deleteUser'); ->name('deleteUser');
Route::patch('players/update/{user}', 'UserController@update') Route::patch('players/update/{user}', [UserController::class, 'update'])
->name('updateUser'); ->name('updateUser');
Route::get('positions', 'VacancyController@index') Route::get('positions', [VacancyController::class, 'index'])
->name('showPositions'); ->name('showPositions');
Route::post('positions/save', 'VacancyController@store') Route::post('positions/save', [VacancyController::class, 'store'])
->name('savePosition'); ->name('savePosition');
Route::get('positions/edit/{vacancy}', 'VacancyController@edit') Route::get('positions/edit/{vacancy}', [VacancyController::class, 'edit'])
->name('editPosition'); ->name('editPosition');
Route::patch('positions/update/{vacancy}', 'VacancyController@update') Route::patch('positions/update/{vacancy}', [VacancyController::class, 'update'])
->name('updatePosition'); ->name('updatePosition');
Route::patch('positions/availability/{status}/{vacancy}', 'VacancyController@updatePositionAvailability') Route::patch('positions/availability/{status}/{vacancy}', [VacancyController::class, 'updatePositionAvailability'])
->name('updatePositionAvailability'); ->name('updatePositionAvailability');
Route::get('forms/builder', 'FormController@showFormBuilder') Route::get('forms/builder', [FormController::class, 'showFormBuilder'])
->name('showFormBuilder'); ->name('showFormBuilder');
Route::post('forms/save', 'FormController@saveForm') Route::post('forms/save', [FormController::class, 'saveForm'])
->name('saveForm'); ->name('saveForm');
Route::delete('forms/destroy/{form}', 'FormController@destroy') Route::delete('forms/destroy/{form}', [FormController::class, 'destroy'])
->name('destroyForm'); ->name('destroyForm');
Route::get('forms', 'FormController@index') Route::get('forms', [FormController::class, 'index'])
->name('showForms'); ->name('showForms');
Route::get('forms/preview/{form}', 'FormController@preview') Route::get('forms/preview/{form}', [FormController::class, 'preview'])
->name('previewForm'); ->name('previewForm');
Route::get('forms/edit/{form}', 'FormController@edit') Route::get('forms/edit/{form}', [FormController::class, 'edit'])
->name('editForm'); ->name('editForm');
Route::patch('forms/update/{form}', 'FormController@update') Route::patch('forms/update/{form}', [FormController::class, 'update'])
->name('updateForm'); ->name('updateForm');
Route::get('devtools', 'DevToolsController@index') Route::get('devtools', [DevToolsController::class, 'index'])
->name('devTools'); ->name('devTools');
// we could use route model binding // we could use route model binding
Route::post('devtools/vote-evaluation/force', 'DevToolsController@forceVoteCount') Route::post('devtools/vote-evaluation/force', [DevToolsController::class, 'forceVoteCount'])
->name('devToolsForceVoteCount'); ->name('devToolsForceVoteCount');
}); });