32 changed files with 9063 additions and 7448 deletions
-
5app/Entities/Projects/Feature.php
-
26app/Entities/Users/Event.php
-
2app/Entities/Users/User.php
-
7app/Exceptions/Handler.php
-
171app/Http/Controllers/Api/EventsController.php
-
2app/Http/Controllers/Projects/ProjectsController.php
-
3app/Http/routes.php
-
29app/Http/routes/calendar.php
-
22app/Policies/EventPolicy.php
-
2app/Providers/AuthServiceProvider.php
-
3composer.json
-
591composer.lock
-
5config/app.php
-
17database/factories/ModelFactory.php
-
7316public/assets/css/app.css
-
1public/assets/css/app.css.map
-
7376public/assets/css/app.s.css
-
1public/assets/css/app.s.css.map
-
5public/assets/css/bootstrap-theme.min.css
-
5public/assets/css/bootstrap.min.css
-
4public/assets/css/font-awesome.min.css
-
353public/assets/css/sb-admin-2.css
-
10public/assets/js/bootstrap.min.js
-
2resources/assets/sass/_bootstrap.scss
-
1resources/views/features/partials/feature-show.blade.php
-
5resources/views/layouts/app.blade.php
-
1resources/views/layouts/partials/sidebar.blade.php
-
14resources/views/projects/features-export-excel.blade.php
-
71resources/views/projects/features-export-progress-excel.blade.php
-
1resources/views/projects/index.blade.php
-
313resources/views/users/calendar.blade.php
-
147tests/api/ApiEventsTest.php
@ -0,0 +1,26 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Entities\Users; |
||||
|
|
||||
|
use App\Entities\Findings\Finding; |
||||
|
use App\Entities\Projects\Project; |
||||
|
use App\Entities\Users\User; |
||||
|
use Illuminate\Database\Eloquent\Model; |
||||
|
use Laracasts\Presenter\PresentableTrait; |
||||
|
|
||||
|
class Event extends Model |
||||
|
{ |
||||
|
protected $table = 'user_events'; |
||||
|
protected $guarded = ['id','created_at','updated_at']; |
||||
|
// protected $casts = ['is_allday' => 'boolean'];
|
||||
|
|
||||
|
public function user() |
||||
|
{ |
||||
|
return $this->belongsTo(User::class); |
||||
|
} |
||||
|
|
||||
|
public function project() |
||||
|
{ |
||||
|
return $this->belongsTo(Project::class); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Http\Controllers\Api; |
||||
|
|
||||
|
use App\Entities\Users\Event; |
||||
|
use App\Http\Controllers\Controller; |
||||
|
use Illuminate\Http\Request; |
||||
|
|
||||
|
class EventsController extends Controller |
||||
|
{ |
||||
|
public function index(Request $request) |
||||
|
{ |
||||
|
$start = $request->get('start'); |
||||
|
$end = $request->get('end'); |
||||
|
$events = Event::where(function($query) use ($start, $end) { |
||||
|
if ($start && $end) |
||||
|
$query->whereBetween('start', [$start, $end]); |
||||
|
})->with('user')->get(); |
||||
|
|
||||
|
$response = fractal() |
||||
|
->collection($events) |
||||
|
->transformWith(function($event) { |
||||
|
$isOwnEvent = $event->user_id == auth()->id(); |
||||
|
return [ |
||||
|
'id' => $event->id, |
||||
|
'user' => $event->user->name, |
||||
|
'user_id' => $event->user_id, |
||||
|
'title' => $event->title, |
||||
|
'body' => $event->body, |
||||
|
'start' => $event->start, |
||||
|
'end' => $event->end, |
||||
|
'allDay' => $event->is_allday, |
||||
|
'editable' => true, |
||||
|
'color' => $isOwnEvent ? '' : '#B7B7B7', |
||||
|
]; |
||||
|
}) |
||||
|
->toArray(); |
||||
|
|
||||
|
return response()->json($response['data'], 200); |
||||
|
} |
||||
|
|
||||
|
public function store(Request $request) |
||||
|
{ |
||||
|
$this->validate($request, [ |
||||
|
'title' => 'required|string|max:60', |
||||
|
'body' => 'string|max:255', |
||||
|
'start' => 'required|date|date_format:Y-m-d H:i:s', |
||||
|
'end' => 'date|date_format:Y-m-d H:i:s', |
||||
|
'is_allday' => '', |
||||
|
]); |
||||
|
|
||||
|
$event = new Event; |
||||
|
$event->user_id = auth()->id(); |
||||
|
$event->title = $request->get('title'); |
||||
|
$event->body = $request->get('body'); |
||||
|
$event->start = $request->get('start'); |
||||
|
$event->end = $request->get('is_allday') == "true" ? null : $request->get('end'); |
||||
|
$event->is_allday = $request->get('is_allday') == "true" ? 1 : 0; |
||||
|
|
||||
|
$event->save(); |
||||
|
|
||||
|
$response = [ |
||||
|
'message' => trans('event.created') |
||||
|
] + fractal()->item($event) |
||||
|
->transformWith(function($event) { |
||||
|
return [ |
||||
|
'id' => $event->id, |
||||
|
'user' => $event->user->name, |
||||
|
'title' => $event->title, |
||||
|
'body' => $event->body, |
||||
|
'start' => $event->start, |
||||
|
'end' => $event->end, |
||||
|
'allDay' => $event->is_allday, |
||||
|
]; |
||||
|
}) |
||||
|
->toArray(); |
||||
|
|
||||
|
return response()->json($response, 201); |
||||
|
} |
||||
|
|
||||
|
public function show($id) |
||||
|
{ |
||||
|
//
|
||||
|
} |
||||
|
|
||||
|
public function update(Request $request) |
||||
|
{ |
||||
|
$this->validate($request, [ |
||||
|
'id' => 'required|numeric|exists:user_events,id', |
||||
|
'title' => 'required|string|max:60', |
||||
|
'body' => 'string|max:255', |
||||
|
]); |
||||
|
|
||||
|
$event = Event::findOrFail($request->get('id')); |
||||
|
$this->authorize('update', $event); |
||||
|
|
||||
|
$event->title = $request->get('title'); |
||||
|
$event->body = $request->get('body'); |
||||
|
|
||||
|
$event->save(); |
||||
|
|
||||
|
$response = [ |
||||
|
'message' => trans('event.updated') |
||||
|
] + fractal()->item($event) |
||||
|
->transformWith(function($event) { |
||||
|
return [ |
||||
|
'id' => $event->id, |
||||
|
'user' => $event->user->name, |
||||
|
'title' => $event->title, |
||||
|
'body' => $event->body, |
||||
|
'start' => $event->start, |
||||
|
'end' => $event->end, |
||||
|
'allDay' => $event->is_allday, |
||||
|
]; |
||||
|
}) |
||||
|
->toArray(); |
||||
|
|
||||
|
return response()->json($response, 200); |
||||
|
} |
||||
|
|
||||
|
public function destroy(Request $request) |
||||
|
{ |
||||
|
$this->validate($request, [ |
||||
|
'id' => 'required|numeric|exists:user_events,id' |
||||
|
]); |
||||
|
|
||||
|
$event = Event::findOrFail($request->get('id')); |
||||
|
$this->authorize('delete', $event); |
||||
|
$event->delete(); |
||||
|
|
||||
|
return response()->json(['message' => trans('event.deleted')], 200); |
||||
|
} |
||||
|
|
||||
|
public function reschedule(Request $request) |
||||
|
{ |
||||
|
$this->validate($request, [ |
||||
|
'id' => 'required|numeric|exists:user_events,id', |
||||
|
'start' => 'required|date|date_format:Y-m-d H:i:s', |
||||
|
'end' => 'date|date_format:Y-m-d H:i:s', |
||||
|
]); |
||||
|
|
||||
|
$event = Event::findOrFail($request->get('id')); |
||||
|
$this->authorize('update', $event); |
||||
|
$event->start = $request->get('start'); |
||||
|
|
||||
|
if ($request->has('end')) { |
||||
|
$event->end = $request->get('end'); |
||||
|
$event->is_allday = false; |
||||
|
} |
||||
|
|
||||
|
$event->save(); |
||||
|
|
||||
|
$response = [ |
||||
|
'message' => trans('event.rescheduled') |
||||
|
] + fractal()->item($event) |
||||
|
->transformWith(function($event) { |
||||
|
return [ |
||||
|
'id' => $event->id, |
||||
|
'user' => $event->user->name, |
||||
|
'title' => $event->title, |
||||
|
'body' => $event->body, |
||||
|
'start' => $event->start, |
||||
|
'end' => $event->end, |
||||
|
'allDay' => $event->is_allday, |
||||
|
]; |
||||
|
}) |
||||
|
->toArray(); |
||||
|
|
||||
|
return response()->json($response, 200); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
<?php |
||||
|
|
||||
|
Route::group(['middleware' => ['web','auth'], 'namespace' => 'Api'], function() { |
||||
|
/** |
||||
|
* Savety Calendar |
||||
|
*/ |
||||
|
Route::get('my-calendar', ['as' => 'users.calendar', 'uses' => function() { |
||||
|
return view('users.calendar'); |
||||
|
}]); |
||||
|
/** |
||||
|
* Savety Calendar |
||||
|
*/ |
||||
|
Route::get('get-events', ['as' => 'api.events.index', 'uses' => 'EventsController@index']); |
||||
|
Route::post('events', ['as' => 'api.events.store', 'uses' => 'EventsController@store']); |
||||
|
Route::patch('events/update', ['as' => 'api.events.update', 'uses' => 'EventsController@update']); |
||||
|
Route::patch('events/reschedule', ['as' => 'api.events.reschedule', 'uses' => 'EventsController@reschedule']); |
||||
|
Route::delete('events/delete', ['as' => 'api.events.destroy', 'uses' => 'EventsController@destroy']); |
||||
|
}); |
||||
|
|
||||
|
// Route::group(['middleware' => ['api','auth:api'], 'namespace' => 'Api'], function() {
|
||||
|
// /**
|
||||
|
// * Savety Calendar
|
||||
|
// */
|
||||
|
// Route::get('get-events', ['as' => 'api.events.index', 'uses' => 'EventsController@index']);
|
||||
|
// Route::post('events', ['as' => 'api.events.store', 'uses' => 'EventsController@store']);
|
||||
|
// Route::patch('events/update', ['as' => 'api.events.update', 'uses' => 'EventsController@update']);
|
||||
|
// Route::patch('events/reschedule', ['as' => 'api.events.reschedule', 'uses' => 'EventsController@reschedule']);
|
||||
|
// Route::delete('events/delete', ['as' => 'api.events.destroy', 'uses' => 'EventsController@destroy']);
|
||||
|
// });
|
||||
@ -0,0 +1,22 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Policies; |
||||
|
|
||||
|
use App\Entities\Users\Event; |
||||
|
use App\Entities\Users\User; |
||||
|
use Illuminate\Auth\Access\HandlesAuthorization; |
||||
|
|
||||
|
class EventPolicy |
||||
|
{ |
||||
|
use HandlesAuthorization; |
||||
|
|
||||
|
public function update(User $user, Event $event) |
||||
|
{ |
||||
|
return $user->id == $event->user_id; |
||||
|
} |
||||
|
|
||||
|
public function delete(User $user, Event $event) |
||||
|
{ |
||||
|
return $user->id == $event->user_id; |
||||
|
} |
||||
|
} |
||||
591
composer.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
7316
public/assets/css/app.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1
public/assets/css/app.css.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
7376
public/assets/css/app.s.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1
public/assets/css/app.s.css.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
5
public/assets/css/bootstrap-theme.min.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
5
public/assets/css/bootstrap.min.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
4
public/assets/css/font-awesome.min.css
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,353 @@ |
|||||
|
/*! |
||||
|
* Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com) |
||||
|
* Code licensed under the Apache License v2.0. |
||||
|
* For details, see http://www.apache.org/licenses/LICENSE-2.0. |
||||
|
*/ |
||||
|
|
||||
|
body { |
||||
|
background-color: #f8f8f8; |
||||
|
} |
||||
|
|
||||
|
#wrapper { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
#page-wrapper { |
||||
|
padding: 0 15px; |
||||
|
min-height: 568px; |
||||
|
background-color: #fff; |
||||
|
} |
||||
|
|
||||
|
@media(min-width:768px) { |
||||
|
#page-wrapper { |
||||
|
position: inherit; |
||||
|
margin: 0 0 0 250px; |
||||
|
padding: 0 30px; |
||||
|
border-left: 1px solid #e7e7e7; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links { |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links li { |
||||
|
display: inline-block; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links li:last-child { |
||||
|
margin-right: 15px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links li a { |
||||
|
padding: 15px; |
||||
|
min-height: 50px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-menu li { |
||||
|
display: block; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-menu li:last-child { |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-menu li a { |
||||
|
padding: 3px 20px; |
||||
|
min-height: 0; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-menu li a div { |
||||
|
white-space: normal; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-messages, |
||||
|
.navbar-top-links .dropdown-tasks, |
||||
|
.navbar-top-links .dropdown-alerts { |
||||
|
width: 310px; |
||||
|
min-width: 0; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-messages { |
||||
|
margin-left: 5px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-tasks { |
||||
|
margin-left: -59px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-alerts { |
||||
|
margin-left: -123px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-user { |
||||
|
right: 0; |
||||
|
left: auto; |
||||
|
} |
||||
|
|
||||
|
.sidebar .sidebar-nav.navbar-collapse { |
||||
|
padding-right: 0; |
||||
|
padding-left: 0; |
||||
|
} |
||||
|
|
||||
|
.sidebar .sidebar-search { |
||||
|
padding: 15px; |
||||
|
} |
||||
|
|
||||
|
.sidebar ul li { |
||||
|
border-bottom: 1px solid #e7e7e7; |
||||
|
} |
||||
|
|
||||
|
.sidebar ul li a.active { |
||||
|
background-color: #eee; |
||||
|
} |
||||
|
|
||||
|
.sidebar .arrow { |
||||
|
float: right; |
||||
|
} |
||||
|
|
||||
|
.sidebar .fa.arrow:before { |
||||
|
content: "\f104"; |
||||
|
} |
||||
|
|
||||
|
.sidebar .active>a>.fa.arrow:before { |
||||
|
content: "\f107"; |
||||
|
} |
||||
|
|
||||
|
.sidebar .nav-second-level li, |
||||
|
.sidebar .nav-third-level li { |
||||
|
border-bottom: 0!important; |
||||
|
} |
||||
|
|
||||
|
.sidebar .nav-second-level li a { |
||||
|
padding-left: 37px; |
||||
|
} |
||||
|
|
||||
|
.sidebar .nav-third-level li a { |
||||
|
padding-left: 52px; |
||||
|
} |
||||
|
|
||||
|
@media(min-width:768px) { |
||||
|
.sidebar { |
||||
|
z-index: 1; |
||||
|
position: absolute; |
||||
|
width: 250px; |
||||
|
margin-top: 101px; |
||||
|
} |
||||
|
|
||||
|
.navbar-top-links .dropdown-messages, |
||||
|
.navbar-top-links .dropdown-tasks, |
||||
|
.navbar-top-links .dropdown-alerts { |
||||
|
margin-left: auto; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.btn-outline { |
||||
|
color: inherit; |
||||
|
background-color: transparent; |
||||
|
transition: all .5s; |
||||
|
} |
||||
|
|
||||
|
.btn-primary.btn-outline { |
||||
|
color: #428bca; |
||||
|
} |
||||
|
|
||||
|
.btn-success.btn-outline { |
||||
|
color: #5cb85c; |
||||
|
} |
||||
|
|
||||
|
.btn-info.btn-outline { |
||||
|
color: #5bc0de; |
||||
|
} |
||||
|
|
||||
|
.btn-warning.btn-outline { |
||||
|
color: #f0ad4e; |
||||
|
} |
||||
|
|
||||
|
.btn-danger.btn-outline { |
||||
|
color: #d9534f; |
||||
|
} |
||||
|
|
||||
|
.btn-primary.btn-outline:hover, |
||||
|
.btn-success.btn-outline:hover, |
||||
|
.btn-info.btn-outline:hover, |
||||
|
.btn-warning.btn-outline:hover, |
||||
|
.btn-danger.btn-outline:hover { |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
.chat { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
list-style: none; |
||||
|
} |
||||
|
|
||||
|
.chat li { |
||||
|
margin-bottom: 10px; |
||||
|
padding-bottom: 5px; |
||||
|
border-bottom: 1px dotted #999; |
||||
|
} |
||||
|
|
||||
|
.chat li.left .chat-body { |
||||
|
margin-left: 60px; |
||||
|
} |
||||
|
|
||||
|
.chat li.right .chat-body { |
||||
|
margin-right: 60px; |
||||
|
} |
||||
|
|
||||
|
.chat li .chat-body p { |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.panel .slidedown .glyphicon, |
||||
|
.chat .glyphicon { |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
|
||||
|
.chat-panel .panel-body { |
||||
|
height: 350px; |
||||
|
overflow-y: scroll; |
||||
|
} |
||||
|
|
||||
|
.flot-chart { |
||||
|
display: block; |
||||
|
height: 400px; |
||||
|
} |
||||
|
|
||||
|
.flot-chart-content { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
table.dataTable thead .sorting, |
||||
|
table.dataTable thead .sorting_asc, |
||||
|
table.dataTable thead .sorting_desc, |
||||
|
table.dataTable thead .sorting_asc_disabled, |
||||
|
table.dataTable thead .sorting_desc_disabled { |
||||
|
background: 0 0; |
||||
|
} |
||||
|
|
||||
|
table.dataTable thead .sorting_asc:after { |
||||
|
content: "\f0de"; |
||||
|
float: right; |
||||
|
font-family: fontawesome; |
||||
|
} |
||||
|
|
||||
|
table.dataTable thead .sorting_desc:after { |
||||
|
content: "\f0dd"; |
||||
|
float: right; |
||||
|
font-family: fontawesome; |
||||
|
} |
||||
|
|
||||
|
table.dataTable thead .sorting:after { |
||||
|
content: "\f0dc"; |
||||
|
float: right; |
||||
|
font-family: fontawesome; |
||||
|
color: rgba(50,50,50,.5); |
||||
|
} |
||||
|
|
||||
|
.btn-circle { |
||||
|
width: 30px; |
||||
|
height: 30px; |
||||
|
padding: 6px 0; |
||||
|
border-radius: 15px; |
||||
|
text-align: center; |
||||
|
font-size: 12px; |
||||
|
line-height: 1.428571429; |
||||
|
} |
||||
|
|
||||
|
.btn-circle.btn-lg { |
||||
|
width: 50px; |
||||
|
height: 50px; |
||||
|
padding: 10px 16px; |
||||
|
border-radius: 25px; |
||||
|
font-size: 18px; |
||||
|
line-height: 1.33; |
||||
|
} |
||||
|
|
||||
|
.btn-circle.btn-xl { |
||||
|
width: 70px; |
||||
|
height: 70px; |
||||
|
padding: 10px 16px; |
||||
|
border-radius: 35px; |
||||
|
font-size: 24px; |
||||
|
line-height: 1.33; |
||||
|
} |
||||
|
|
||||
|
.show-grid [class^=col-] { |
||||
|
padding-top: 10px; |
||||
|
padding-bottom: 10px; |
||||
|
border: 1px solid #ddd; |
||||
|
background-color: #eee!important; |
||||
|
} |
||||
|
|
||||
|
.show-grid { |
||||
|
margin: 15px 0; |
||||
|
} |
||||
|
|
||||
|
.huge { |
||||
|
font-size: 40px; |
||||
|
} |
||||
|
|
||||
|
.panel-green { |
||||
|
border-color: #5cb85c; |
||||
|
} |
||||
|
|
||||
|
.panel-green .panel-heading { |
||||
|
border-color: #5cb85c; |
||||
|
color: #fff; |
||||
|
background-color: #5cb85c; |
||||
|
} |
||||
|
|
||||
|
.panel-green a { |
||||
|
color: #5cb85c; |
||||
|
} |
||||
|
|
||||
|
.panel-green a:hover { |
||||
|
color: #3d8b3d; |
||||
|
} |
||||
|
|
||||
|
.panel-red { |
||||
|
border-color: #d9534f; |
||||
|
} |
||||
|
|
||||
|
.panel-red .panel-heading { |
||||
|
border-color: #d9534f; |
||||
|
color: #fff; |
||||
|
background-color: #d9534f; |
||||
|
} |
||||
|
|
||||
|
.panel-red a { |
||||
|
color: #d9534f; |
||||
|
} |
||||
|
|
||||
|
.panel-red a:hover { |
||||
|
color: #b52b27; |
||||
|
} |
||||
|
|
||||
|
.panel-yellow { |
||||
|
border-color: #f0ad4e; |
||||
|
} |
||||
|
|
||||
|
.panel-yellow .panel-heading { |
||||
|
border-color: #f0ad4e; |
||||
|
color: #fff; |
||||
|
background-color: #f0ad4e; |
||||
|
} |
||||
|
|
||||
|
.panel-yellow a { |
||||
|
color: #f0ad4e; |
||||
|
} |
||||
|
|
||||
|
.panel-yellow a:hover { |
||||
|
color: #df8a13; |
||||
|
} |
||||
|
|
||||
|
.chosen-container-multi .chosen-choices li.search-field input[type=text] { |
||||
|
height: 25px; |
||||
|
} |
||||
|
|
||||
|
.button-form-inline { |
||||
|
display: inline; |
||||
|
} |
||||
10
public/assets/js/bootstrap.min.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,71 @@ |
|||||
|
<?php |
||||
|
// $filename = str_slug(trans('project.features') . '-' . $project->name) . '.xls';
|
||||
|
// header("Content-Disposition: attachment; filename=\"$filename\"");
|
||||
|
// header("Content-Type: application/vnd.ms-excel");
|
||||
|
?>
|
||||
|
<!DOCTYPE html> |
||||
|
<html> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
{{-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> --}} |
||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> |
||||
|
<title>{{ $project->name }}</title> |
||||
|
<style> |
||||
|
th, td { |
||||
|
padding: 5px; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body> |
||||
|
<table border="1" class="table table-condensed table-striped"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<td colspan="4" style="text-align:center"> |
||||
|
<h1>{{ trans('project.features') }} {{ $project->name }}</h1> |
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<th>{{ trans('app.table_no') }}</th> |
||||
|
<th>{{ trans('feature.name') }}</th> |
||||
|
<th class="text-center">{{ trans('feature.progress') }}</th> |
||||
|
{{-- <th class="text-right">{{ trans('feature.price') }}</th> --}} |
||||
|
<th>{{ trans('app.description') }}</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="sort-features"> |
||||
|
@forelse($features as $key => $feature) |
||||
|
<tr> |
||||
|
<td>{{ 1 + $key }}</td> |
||||
|
<td> |
||||
|
{{ $feature->name }} |
||||
|
</td> |
||||
|
<td class="text-center">{{ $feature->progress = $feature->tasks->avg('progress')/100 }}</td> |
||||
|
{{-- <td class="text-right">{{ $feature->price }}</td> --}} |
||||
|
<td style="wrap-text: true;">{!! nl2br($feature->description) !!}</td> |
||||
|
</tr> |
||||
|
|
||||
|
@if ($feature->tasks->count()) |
||||
|
@foreach($feature->tasks as $task) |
||||
|
<tr> |
||||
|
<td></td> |
||||
|
<td>{{ $task->name }}</td> |
||||
|
<td></td> |
||||
|
<td style="wrap-text: true;">{!! nl2br($task->description) !!}</td> |
||||
|
</tr> |
||||
|
@endforeach |
||||
|
@endif |
||||
|
@empty |
||||
|
<tr><td colspan="7">{{ trans('feature.empty') }}</td></tr> |
||||
|
@endforelse |
||||
|
</tbody> |
||||
|
<tfoot> |
||||
|
<tr> |
||||
|
<th class="text-right" colspan="2">Total</th> |
||||
|
<th class="text-center">{{ $project->getFeatureOveralProgress() }} %</th> |
||||
|
{{-- <th class="text-right">{{ $features->sum('price') }}</th> --}} |
||||
|
<th></th> |
||||
|
</tr> |
||||
|
</tfoot> |
||||
|
</table> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,313 @@ |
|||||
|
@extends('layouts.app') |
||||
|
|
||||
|
@section('title', 'User Calendar') |
||||
|
|
||||
|
@section('content') |
||||
|
|
||||
|
<div class=""> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12"> |
||||
|
<div class="x_panel"> |
||||
|
<div class="x_title"><h3>User Calendar <small>Click to add/edit events</small></h3></div> |
||||
|
<div class="x_content"> |
||||
|
<div id='calendar'></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<!-- calendar modal --> |
||||
|
<div id="CalenderModalNew" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog"> |
||||
|
<div class="modal-content"> |
||||
|
|
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
||||
|
<h4 class="modal-title" id="myModalLabel">New Calendar Entry</h4> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<div id="testmodal" style="padding: 5px 20px;"> |
||||
|
<form id="antoform" class="form-horizontal calender" role="form"> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">User</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<span class="form-control" disabled>{{ auth()->user()->name }}</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">Title</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<input type="text" class="form-control" id="title" name="title"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">Description</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<textarea class="form-control" style="height:55px;" id="descr" name="descr"></textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label"></label> |
||||
|
<div class="col-sm-9"> |
||||
|
<label for="is_allday"><input id="is_allday" type="checkbox" name="is_allday"> All Day Event?</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-default antoclose" data-dismiss="modal">Close</button> |
||||
|
<button type="button" class="btn btn-primary antosubmit">Save</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="CalenderModalEdit" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> |
||||
|
<div class="modal-dialog"> |
||||
|
<div class="modal-content"> |
||||
|
|
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
||||
|
<h4 class="modal-title" id="myModalLabel2">Edit Calendar Entry</h4> |
||||
|
</div> |
||||
|
<form id="antoform2" class="form-horizontal calender" role="form"> |
||||
|
<div class="modal-body"> |
||||
|
<div id="testmodal2" style="padding: 5px 20px;"> |
||||
|
<input type="hidden" name="id2" id="id2"> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">User</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<span class="form-control" disabled id="user2"></span> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">Title</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<input type="text" class="form-control" id="title2" name="title2"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="form-group"> |
||||
|
<label class="col-sm-3 control-label">Description</label> |
||||
|
<div class="col-sm-9"> |
||||
|
<textarea class="form-control" style="height:55px;" id="descr2" name="descr2"></textarea> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-danger pull-left antodelete2" title="Delete Event"><i class="fa fa-times"></i></button> |
||||
|
<button type="button" class="btn btn-default antoclose2" data-dismiss="modal">Close</button> |
||||
|
<button type="submit" class="btn btn-primary antosubmit2">Save changes</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div id="fc_create" data-toggle="modal" data-target="#CalenderModalNew"></div> |
||||
|
<div id="fc_edit" data-toggle="modal" data-target="#CalenderModalEdit"></div> |
||||
|
<!-- /calendar modal --> |
||||
|
|
||||
|
@endsection |
||||
|
|
||||
|
@section('ext_css') |
||||
|
{!! Html::style(url('assets/css/plugins/fullcalendar.min.css')) !!} |
||||
|
@endsection |
||||
|
|
||||
|
@section('ext_js') |
||||
|
{!! Html::script(url('assets/js/plugins/moment.min.js')) !!} |
||||
|
{!! Html::script(url('assets/js/plugins/fullcalendar.min.js')) !!} |
||||
|
@endsection |
||||
|
|
||||
|
@section('script') |
||||
|
<script> |
||||
|
(function() { |
||||
|
var date = new Date(), |
||||
|
d = date.getDate(), |
||||
|
m = date.getMonth(), |
||||
|
y = date.getFullYear(), |
||||
|
started, |
||||
|
categoryClass; |
||||
|
|
||||
|
var calendar = $('#calendar').fullCalendar({ |
||||
|
header: { |
||||
|
left: 'prev,next today', |
||||
|
center: 'title', |
||||
|
right: 'month,agendaWeek,agendaDay' |
||||
|
}, |
||||
|
height: 550, |
||||
|
selectable: true, |
||||
|
selectHelper: true, |
||||
|
droppable: false, |
||||
|
editable: false, |
||||
|
// eventLimit: true,
|
||||
|
slotLabelFormat: 'HH:mm', |
||||
|
slotDuration: '01:00:00', |
||||
|
events: { |
||||
|
url: "{{ route('api.events.index') }}", |
||||
|
type: "GET", |
||||
|
error: function() { |
||||
|
alert('there was an error while fetching events!'); |
||||
|
} |
||||
|
}, |
||||
|
eventRender: function(calEvent, element) { |
||||
|
// element.find('.fc-content').prepend('<strong style="display:block">' + calEvent.user + '</strong>');
|
||||
|
}, |
||||
|
select: function(start, end, allDay) { |
||||
|
$('#fc_create').click(); |
||||
|
|
||||
|
started = start; |
||||
|
ended = end; |
||||
|
|
||||
|
$(".antosubmit").on("click", function() { |
||||
|
var title = $("#title").val(); |
||||
|
var body = $("#descr").val(); |
||||
|
var is_allday = $("#is_allday").is(':checked'); |
||||
|
|
||||
|
if (title) { |
||||
|
$.ajax({ |
||||
|
url: "{{ route('api.events.store') }}", |
||||
|
method: "POST", |
||||
|
data: { title: title, body: body, start: started.format("YYYY-MM-DD HH:mm:ss"), end: ended.format("YYYY-MM-DD HH:mm:ss"), is_allday: is_allday }, |
||||
|
success: function(response){ |
||||
|
if(response.message == 'event.created') { |
||||
|
calendar.fullCalendar('renderEvent', { |
||||
|
id: response.data.id, |
||||
|
title: title, |
||||
|
body: body, |
||||
|
start: started.format("YYYY-MM-DD HH:mm"), |
||||
|
end: ended.format("YYYY-MM-DD HH:mm"), |
||||
|
user: "{{ auth()->user()->name }}", |
||||
|
user_id: "{{ auth()->id() }}", |
||||
|
allDay: is_allday, |
||||
|
editable: true |
||||
|
}, true); |
||||
|
} |
||||
|
}, |
||||
|
error: function(e){ |
||||
|
alert('Error processing your request: '+e.responseText); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
$('#title').val(''); |
||||
|
$('#descr').val(''); |
||||
|
|
||||
|
calendar.fullCalendar('unselect'); |
||||
|
|
||||
|
$('.antoclose').click(); |
||||
|
|
||||
|
return false; |
||||
|
}); |
||||
|
}, |
||||
|
eventClick: function(calEvent, jsEvent, view) { |
||||
|
|
||||
|
if (calEvent.editable) { |
||||
|
$('#user2').text(calEvent.user); |
||||
|
$('#title2').val(calEvent.title); |
||||
|
$('#descr2').val(calEvent.body); |
||||
|
$('#CalenderModalEdit').modal(); |
||||
|
} |
||||
|
else { |
||||
|
$('#user3').text(calEvent.user); |
||||
|
$('#title3').html(calEvent.title); |
||||
|
$('#descr3').html(calEvent.body); |
||||
|
$('#CalenderModalView').modal(); |
||||
|
} |
||||
|
|
||||
|
$(".antodelete2").off("click").on("click", function() { |
||||
|
var confirmBox = confirm('Delete this event?'); |
||||
|
|
||||
|
if (confirmBox) { |
||||
|
$.ajax({ |
||||
|
url: "{{ route('api.events.destroy') }}", |
||||
|
method: "DELETE", |
||||
|
beforeSend: function(xhr){ |
||||
|
xhr.setRequestHeader('Authorization', 'Bearer ' + "{{ auth()->user()->api_token }}"); |
||||
|
}, |
||||
|
data: { id: calEvent.id }, |
||||
|
success: function(response){ |
||||
|
if(response.message == 'event.deleted') |
||||
|
calendar.fullCalendar('removeEvents', calEvent.id); |
||||
|
console.log(calEvent); |
||||
|
}, |
||||
|
error: function(e){ |
||||
|
alert('Error processing your request: '+e.responseText); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
$('#CalenderModalEdit').modal('hide'); |
||||
|
$('#CalenderModalView').modal('hide'); |
||||
|
}); |
||||
|
|
||||
|
$("#antoform2").off("submit").on("submit", function() { |
||||
|
calEvent.title = $("#title2").val(); |
||||
|
calEvent.body = $("#descr2").val(); |
||||
|
|
||||
|
$.ajax({ |
||||
|
url: "{{ route('api.events.update') }}", |
||||
|
method: "PATCH", |
||||
|
data: { id: calEvent.id, title: calEvent.title, body: calEvent.body }, |
||||
|
success: function(response){ |
||||
|
if(response.message == 'event.updated') |
||||
|
$('#calendar').fullCalendar('updateEvent',calEvent); |
||||
|
console.log(calEvent); |
||||
|
}, |
||||
|
error: function(e){ |
||||
|
alert('Error processing your request: '+e.responseText); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
$('#CalenderModalEdit').modal('hide'); |
||||
|
$('#CalenderModalView').modal('hide'); |
||||
|
|
||||
|
return false; |
||||
|
}); |
||||
|
|
||||
|
calendar.fullCalendar('unselect'); |
||||
|
}, |
||||
|
eventDrop: function(calEvent, delta, revertFunc) { |
||||
|
var start = calEvent.start.format('YYYY-MM-DD HH:mm:ss'); |
||||
|
var end = calEvent.end ? calEvent.end.format('YYYY-MM-DD HH:mm:ss') : null; |
||||
|
$.ajax({ |
||||
|
url: "{{ route('api.events.reschedule') }}", |
||||
|
method: "PATCH", |
||||
|
data: { id: calEvent.id, start: start, end: end }, |
||||
|
success: function(response){ |
||||
|
if(response.message != 'event.rescheduled') |
||||
|
revertFunc(); |
||||
|
}, |
||||
|
error: function(e){ |
||||
|
revertFunc(); |
||||
|
alert('Error processing your request: '+e.responseText); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
eventResize: function(calEvent, delta, revertFunc) { |
||||
|
var start = calEvent.start.format('YYYY-MM-DD HH:mm:ss'); |
||||
|
var end = calEvent.end.format('YYYY-MM-DD HH:mm:ss'); |
||||
|
$.ajax({ |
||||
|
url: "{{ route('api.events.reschedule') }}", |
||||
|
method: "PATCH", |
||||
|
data: { id: calEvent.id, start: start, end: end }, |
||||
|
success: function(response){ |
||||
|
if(response.message != 'event.rescheduled') |
||||
|
revertFunc(); |
||||
|
}, |
||||
|
error: function(e){ |
||||
|
revertFunc(); |
||||
|
alert('Error processing your request: '+e.responseText); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
} |
||||
|
}); |
||||
|
})(); |
||||
|
</script> |
||||
|
@endsection |
||||
@ -0,0 +1,147 @@ |
|||||
|
<?php |
||||
|
|
||||
|
use App\Entities\Users\Event; |
||||
|
use App\Entities\Users\User; |
||||
|
use Illuminate\Foundation\Testing\DatabaseMigrations; |
||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions; |
||||
|
use Illuminate\Foundation\Testing\WithoutMiddleware; |
||||
|
|
||||
|
class ApiEventsTest extends TestCase |
||||
|
{ |
||||
|
use DatabaseTransactions; |
||||
|
|
||||
|
/** @test */ |
||||
|
public function it_can_get_all_existing_events() |
||||
|
{ |
||||
|
$user = factory(User::class)->create(); |
||||
|
$events = factory(Event::class, 5)->create(['user_id' => $user->id]); |
||||
|
|
||||
|
$this->json('get', route('api.events.index'), [ |
||||
|
'Authorization' => 'Bearer ' . $user->api_token |
||||
|
]); |
||||
|
|
||||
|
$this->seeStatusCode(200); |
||||
|
|
||||
|
$this->seeJsonStructure([ |
||||
|
'*' => [ |
||||
|
'user', |
||||
|
'title', |
||||
|
'body', |
||||
|
'start', |
||||
|
] |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** @test */ |
||||
|
public function user_can_create_new_event() |
||||
|
{ |
||||
|
$user = factory(User::class)->create(); |
||||
|
|
||||
|
$this->json('post', route('api.events.store'), [ |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
'start' => '2016-07-21 12:20:00', |
||||
|
], [ |
||||
|
'Authorization' => 'Bearer ' . $user->api_token |
||||
|
]); |
||||
|
|
||||
|
$this->seeStatusCode(201); |
||||
|
|
||||
|
$this->seeJson([ |
||||
|
'message' => trans('event.created'), |
||||
|
'user' => $user->name, |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
'start' => '2016-07-21 12:20:00', |
||||
|
'end' => null, |
||||
|
'allDay' => false, |
||||
|
]); |
||||
|
|
||||
|
$this->seeInDatabase('user_events', [ |
||||
|
'user_id' => $user->id, |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
'start' => '2016-07-21 12:20:00', |
||||
|
'end' => null, |
||||
|
'is_allday' => 0, |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** @test */ |
||||
|
public function user_can_update_their_event() |
||||
|
{ |
||||
|
$user = factory(User::class)->create(); |
||||
|
$event = factory(Event::class)->create(['user_id' => $user->id]); |
||||
|
// dump($event->toArray());
|
||||
|
$this->json('patch', route('api.events.update'), [ |
||||
|
'id' => $event->id, |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
], [ |
||||
|
'Authorization' => 'Bearer ' . $user->api_token |
||||
|
]); |
||||
|
|
||||
|
$this->dump(); |
||||
|
|
||||
|
$this->seeStatusCode(200); |
||||
|
|
||||
|
$this->seeJson([ |
||||
|
'message' => trans('event.updated'), |
||||
|
'user' => $user->name, |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
]); |
||||
|
|
||||
|
$this->seeInDatabase('user_events', [ |
||||
|
'user_id' => $user->id, |
||||
|
'title' => 'New Event Title', |
||||
|
'body' => 'New Event Body', |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** @test */ |
||||
|
public function user_can_delete_their_event() |
||||
|
{ |
||||
|
$user = factory(User::class)->create(); |
||||
|
$event = factory(Event::class)->create(['user_id' => $user->id]); |
||||
|
|
||||
|
$this->json('delete',route('api.events.destroy'), ['id' => $event->id], [ |
||||
|
'Authorization' => 'Bearer ' . $user->api_token |
||||
|
]); |
||||
|
|
||||
|
$this->seeStatusCode(200); |
||||
|
|
||||
|
$this->seeJson([ |
||||
|
'message' => trans('event.deleted'), |
||||
|
]); |
||||
|
} |
||||
|
|
||||
|
/** @test */ |
||||
|
public function user_can_reschedule_their_event() |
||||
|
{ |
||||
|
$user = factory(User::class)->create(); |
||||
|
$event = factory(Event::class)->create(['user_id' => $user->id, 'start' => '2016-11-17 12:00:00']); |
||||
|
|
||||
|
$this->json('patch', route('api.events.reschedule'), [ |
||||
|
'id' => $event->id, |
||||
|
'start' => '2016-11-07 13:00:00', |
||||
|
'start' => '2016-11-07 15:00:00', |
||||
|
], [ |
||||
|
'Authorization' => 'Bearer ' . $user->api_token |
||||
|
]); |
||||
|
|
||||
|
$this->dump(); |
||||
|
$this->seeStatusCode(200); |
||||
|
|
||||
|
$this->seeJson([ |
||||
|
'message' => trans('event.rescheduled'), |
||||
|
]); |
||||
|
|
||||
|
$this->seeInDatabase('user_events', [ |
||||
|
'id' => $event->id, |
||||
|
'user_id' => $user->id, |
||||
|
'start' => '2016-11-07 13:00:00', |
||||
|
'start' => '2016-11-07 15:00:00', |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue