Skip to content
This repository has been archived by the owner on Feb 20, 2024. It is now read-only.

Commit

Permalink
Added feature to save reports.
Browse files Browse the repository at this point in the history
  • Loading branch information
straube committed Jul 10, 2020
1 parent 011d1c0 commit 2c88652
Show file tree
Hide file tree
Showing 11 changed files with 73,956 additions and 16 deletions.
34 changes: 34 additions & 0 deletions app/Http/Controllers/Api/ReportsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\ReportRequest;
use App\Report;

/**
* Reports API controller.
*
* @version 2.0.0
* @author Gustavo Straube <[email protected]>
*/
class ReportsController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

public function store(ReportRequest $request): Report
{
$report = Report::create($request->validated());
$report->url = route('reports.index', [ 'report_id' => $report->id ]);

return $report;
}
}
1 change: 1 addition & 0 deletions app/Http/Controllers/ReportsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private function getReportFromRequest(Request $request): Report

$filter = $request->filter;
if (!empty($filter)) {
// TODO: Validate filters
return new Report([ 'name' => __('Custom filter'), 'filter' => $filter ]);
}

Expand Down
85 changes: 85 additions & 0 deletions app/Http/Requests/ReportRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Str;

/**
* Report request.
*
* @version 2.0.0
* @author Gustavo Straube <[email protected]>
*/
class ReportRequest 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 [
'name' => [
'required',
'string',
'max:100',
],
'filter' => [
'required',
'array',
'min:1',
],
'filter.started' => [
'nullable',
'string',
'date_format:Y-m-d',
],
'filter.end' => [
'nullable',
'string',
'date_format:Y-m-d',
'after:filter.started',
],
'filter.user_id' => [
'nullable',
'integer',
'exists:users,id',
],
'filter.project_id' => [
'nullable',
'integer',
'exists:projects,id',
],
'filter.activity_id' => [
'nullable',
'integer',
'exists:activities,id',
],
];
}

/**
* Get the validated data from the request.
*
* @return array
*/
public function validated()
{
$data = parent::validated();
$data['code'] = Str::random(20);

return $data;
}
}
4 changes: 2 additions & 2 deletions app/Http/Requests/TimeRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public function rules()
return [
'project_id' => [
'required',
'numeric',
'integer',
'exists:projects,id',
],
'activity_id' => [
'required',
'numeric',
'integer',
'exists:activities,id',
],
'description' => [
Expand Down
11,022 changes: 11,021 additions & 1 deletion public/css/app.css

Large diffs are not rendered by default.

62,652 changes: 62,650 additions & 2 deletions public/js/app.js

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions resources/js/components/CreateReport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<div class="modal fade" ref="modal" id="create-project" tabindex="-1" role="dialog" aria-labelledby="create-project-label" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" @submit.prevent="onSubmit">
<div class="modal-header">
<h5 class="modal-title" id="create-project-label">New Report</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-row">
<div class="form-group col">
<label>Name</label>
<input :class="{ 'form-control': true, 'is-invalid': error('name') }" type="text" v-model="name" />
<span v-if="error('name')" class="invalid-feedback" role="alert">
{{ error('name') }}
</span>
</div>
</div>
<span v-if="error('filter')" class="invalid-feedback d-block" role="alert">
{{ error('filter') }}
</span>
<span v-for="error in filterErrors()" class="invalid-feedback d-block" role="alert">
{{ error }}
</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-link" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="isSubmitting">Save</button>
</div>
</form>
</div>
</div>
</template>

<script>
export default {
data() {
return {
isSubmitting: false,
errors: {},
// Form
name: '',
filters: {},
}
},
methods: {
reset() {
this.isSubmitting = false;
this.errors = {};
this.name = '';
this.filters = {};
},
error(name) {
if (!this.errors || !this.errors[name]) {
return null;
}
return this.errors[name].join(' ');
},
filterErrors() {
const errors = [];
Object.keys(this.errors).filter((name) => !!name.match(/^filter\./)).forEach((name) => {
errors.push(this.error(name));
});
return errors;
},
fieldsToObject(form) {
return Array.from(form.elements).reduce((data, element) => {
if (!element.value) {
return data;
}
const name = element.name.replace(/^.*\[(.+)\]$/, '$1');
return Object.assign(data, { [name] : element.value });
}, {});
},
onCreate(form) {
this.reset();
this.filters = this.fieldsToObject(form);
jQuery(this.$refs.modal).modal('show');
},
onModalHide(event) {
if (event.target !== this.$refs.modal) {
return;
}
this.reset();
},
onSubmit() {
this.isSubmitting = true;
this.errors = {};
(async () => {
try {
const data = {
name: this.name,
filter: this.filters,
};
const report = await axios.post(this.$root.route('api.reports.store'), data);
this.isSubmitting = false;
jQuery(this.$refs.modal).modal('hide');
location = report.data.url;
} catch (e) {
console.error(e);
this.isSubmitting = false;
if (e.response && e.response.data && e.response.data.errors) {
this.errors = e.response.data.errors;
return;
}
this.$root.alert('Something went wrong while saving the report. Please check the info you provided and try again.');
}
})();
},
},
created() {
jQuery(document).on('hidden.bs.modal', this.reset);
this.$root.$on('create-report', this.onCreate);
},
beforeDestroyed() {
jQuery(document).off('hidden.bs.modal', this.reset);
},
}
</script>
18 changes: 16 additions & 2 deletions resources/js/components/DateInput.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
<template>
<input type="text" ref="input" :value="date" @keypress="onKey" @input="onInput" placeholder="YYYY-MM-DD" />
<input type="text" ref="input" :value="date_ !== null ? date_ : date" @keypress="onKey" @input="onInput" placeholder="YYYY-MM-DD" />
</template>

<script>
export default {
props: [
'value'
],
data() {
return {
date_: null,
};
},
computed: {
date() {
return this.format(this.value);
},
},
methods: {
format(value) {
if (!value) {
return '';
}
value = value.replace(/\D+/g, '').substring(0, 8);
const parts = value.match(/\d{1,2}/g);
if (!parts) {
Expand All @@ -32,7 +40,13 @@
}
},
onInput() {
this.$emit('input', this.format(this.$refs.input.value));
const value = this.format(this.$refs.input.value);
// It's likely using v-model
if (this.$listeners.input) {
this.$emit('input', value);
return;
}
this.date_ = value;
},
},
}
Expand Down
8 changes: 4 additions & 4 deletions resources/js/ziggy.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
var Ziggy = {
namedRoutes: {"api.projects.index":{"uri":"api\/projects","methods":["GET","HEAD"],"domain":null},"api.projects.store":{"uri":"api\/projects","methods":["POST"],"domain":null},"api.projects.update":{"uri":"api\/projects\/{project}","methods":["PUT"],"domain":null},"api.activities.index":{"uri":"api\/activities","methods":["GET","HEAD"],"domain":null},"api.activities.store":{"uri":"api\/activities","methods":["POST"],"domain":null},"api.activities.update":{"uri":"api\/activities\/{activity}","methods":["PUT"],"domain":null},"api.times.store":{"uri":"api\/times","methods":["POST"],"domain":null},"api.times.update":{"uri":"api\/times\/{time}","methods":["PUT"],"domain":null},"api.times.destroy":{"uri":"api\/times\/{time}","methods":["DELETE"],"domain":null},"api.users.store":{"uri":"api\/users","methods":["POST"],"domain":null},"api.users.update":{"uri":"api\/users\/{user}","methods":["PUT"],"domain":null},"api.timezones.index":{"uri":"api\/timezones","methods":["GET","HEAD"],"domain":null},"api.timezones.search":{"uri":"api\/timezones\/search","methods":["GET","HEAD"],"domain":null}},
baseUrl: 'https://tracker.straube.co/',
baseProtocol: 'https',
baseDomain: 'tracker.straube.co',
namedRoutes: {"api.projects.index":{"uri":"api\/projects","methods":["GET","HEAD"],"domain":null},"api.projects.store":{"uri":"api\/projects","methods":["POST"],"domain":null},"api.projects.update":{"uri":"api\/projects\/{project}","methods":["PUT"],"domain":null},"api.activities.index":{"uri":"api\/activities","methods":["GET","HEAD"],"domain":null},"api.activities.store":{"uri":"api\/activities","methods":["POST"],"domain":null},"api.activities.update":{"uri":"api\/activities\/{activity}","methods":["PUT"],"domain":null},"api.times.store":{"uri":"api\/times","methods":["POST"],"domain":null},"api.times.update":{"uri":"api\/times\/{time}","methods":["PUT"],"domain":null},"api.times.destroy":{"uri":"api\/times\/{time}","methods":["DELETE"],"domain":null},"api.users.store":{"uri":"api\/users","methods":["POST"],"domain":null},"api.users.update":{"uri":"api\/users\/{user}","methods":["PUT"],"domain":null},"api.timezones.index":{"uri":"api\/timezones","methods":["GET","HEAD"],"domain":null},"api.timezones.search":{"uri":"api\/timezones\/search","methods":["GET","HEAD"],"domain":null},"api.reports.store":{"uri":"api\/reports","methods":["POST"],"domain":null}},
baseUrl: 'http://tracker.test/',
baseProtocol: 'http',
baseDomain: 'tracker.test',
basePort: false,
defaultParameters: []
};
Expand Down
21 changes: 16 additions & 5 deletions resources/views/reports/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
<div class="row">
<div class="col-md-8" action="{{ route('reports.index') }}" method="get">
<div class="card" id="report-options">
<form class="card-body" action="{{ route('reports.index') }}" method="get">
<!--
TODO: Move this form to a Vue component
The Save button must be hidden when the filter changes
to force users to Apply the filters before saving.
-->
<form class="card-body" action="{{ route('reports.index') }}" method="get" ref="filter">
<h5>{{ __('Custom filter') }}</h5>
<div class="form-row">
<div class="form-group col">
Expand Down Expand Up @@ -42,15 +47,20 @@
</div>
<div class="form-group col">
<label for="report-started">{{ __('From') }}</label>
<input class="form-control" type="text" name="filter[started]" id="report-started" value="{{ request('filter.started') }}" placeholder="YYYY-MM-DD" />
<date-input class="form-control" value="{{ request('filter.started') }}" name="filter[started]" id="report-started" />
</div>
<div class="form-group col">
<label for="form-finished">{{ __('To') }}</label>
<input class="form-control" type="text" name="filter[finished]" id="report-finished" value="{{ request('filter.finished') }}" placeholder="YYYY-MM-DD" />
<date-input class="form-control" value="{{ request('filter.finished') }}" name="filter[finished]" id="report-finished" />
</div>
</div>
<button type="submit" class="btn btn-primary">{{ __('Apply') }}</button>
<button type="button" class="btn btn-primary">{{ __('Apply & Save') }}</button>
<div class="text-right">
@if (request('filter'))
<a href="{{ route('reports.index') }}" class="btn btn-link">{{ __('Reset') }}</a>
<button type="button" class="btn btn-primary" @click.prevent="$emit('create-report',$refs.filter)">{{ __('Save') }}</button>
@endif
<button type="submit" class="btn btn-primary ml-auto">{{ __('Apply') }}</button>
</div>
</form>
</div>
</div>
Expand Down Expand Up @@ -155,5 +165,6 @@ class="btn btn-link">Edit</a>
@endsection

@push('modals')
<create-report></create-report>
<edit-time></edit-time>
@endpush
2 changes: 2 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@

Route::get('/timezones', 'TimezonesController@index')->name('timezones.index');
Route::get('/timezones/search', 'TimezonesController@search')->name('timezones.search');

Route::post('/reports', 'ReportsController@store')->name('reports.store');

0 comments on commit 2c88652

Please sign in to comment.