Browse Source

Add project collectible earnings

Collectible earnings calculated based on
a project feature price * avg of tasks progress
pull/1/head
Nafies Luthfi 8 years ago
parent
commit
aa8b0430eb
  1. 29
      app/Entities/Projects/Project.php
  2. 8
      app/Entities/Users/User.php
  3. 4
      app/Providers/AppServiceProvider.php
  4. 92
      app/helpers.php
  5. 115
      database/factories/ModelFactory.php
  6. 2
      resources/views/projects/partials/project-show.blade.php
  7. 19
      resources/views/projects/partials/project-stats.blade.php
  8. 2
      resources/views/projects/show.blade.php
  9. 4
      tests/Unit/Models/InvoiceTest.php
  10. 29
      tests/Unit/Models/ProjectTest.php
  11. 2
      tests/Unit/Models/UserTest.php

29
app/Entities/Projects/Project.php

@ -11,13 +11,14 @@ use App\Entities\Users\User;
use Illuminate\Database\Eloquent\Model;
use Laracasts\Presenter\PresentableTrait;
class Project extends Model {
class Project extends Model
{
use PresentableTrait;
protected $presenter = ProjectPresenter::class;
protected $guarded = ['id','created_at','updated_at'];
// protected $dates = ['start_date','end_date'];
protected $guarded = ['id', 'created_at', 'updated_at'];
// protected $dates = ['start_date','end_date'];
public function nameLink()
{
@ -56,24 +57,24 @@ class Project extends Model {
public function payments()
{
return $this->hasMany(Payment::class)->orderBy('date','desc');
return $this->hasMany(Payment::class)->orderBy('date', 'desc');
}
public function customer()
{
return $this->belongsTo(User::class,'customer_id');
return $this->belongsTo(User::class, 'customer_id');
}
public function cashInTotal()
{
return $this->payments->sum(function($payment) {
return $this->payments->sum(function ($payment) {
return $payment->in_out == 1 ? $payment->amount : 0;
});
}
public function cashOutTotal()
{
return $this->payments->sum(function($payment) {
return $this->payments->sum(function ($payment) {
return $payment->in_out == 0 ? $payment->amount : 0;
});
}
@ -98,4 +99,18 @@ class Project extends Model {
return $this->morphMany(File::class, 'fileable');
}
public function getCollectibeEarnings()
{
// Collectible earnings is total of (price * avg task progress of each feature)
$collectibeEarnings = 0;
$this->load('features.tasks');
foreach ($this->features as $feature) {
$progress = $feature->tasks->avg('progress');
$collectibeEarnings += ($progress / 100) * $feature->price;
}
return $collectibeEarnings;
}
}

8
app/Entities/Users/User.php

@ -71,21 +71,21 @@ class User extends Authenticatable
return $this->roles->contains('name', $role);
}
return !! $role->intersect($this->roles)->count();
return !!$role->intersect($this->roles)->count();
}
public function hasRoles(array $roleNameArray)
{
return $this->roles->pluck('name')
->contains(function($role, $key) use ($roleNameArray) {
->contains(function ($role, $key) use ($roleNameArray) {
return in_array($role, $roleNameArray);
});
}
public function scopeHasRoles($query, array $roleNameArray)
{
return $query->whereHas('roles', function($q) use ($roleNameArray) {
$q->whereIn('name',$roleNameArray);
return $query->whereHas('roles', function ($q) use ($roleNameArray) {
$q->whereIn('name', $roleNameArray);
});
}
}

4
app/Providers/AppServiceProvider.php

@ -2,8 +2,6 @@
namespace App\Providers;
use App\Entities\Projects\Project;
use DB;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -15,7 +13,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
require_once app_path() . '/helpers.php';
require_once app_path().'/helpers.php';
}
/**

92
app/helpers.php

@ -7,21 +7,22 @@
*/
function formatNo($number)
{
return number_format($number, 0,',','.');
return number_format($number, 0, ',', '.');
}
function formatRp($number)
{
if ($number == 0) { return '-'; }
if ($number < 0)
if ($number == 0) {return '-';}
if ($number < 0) {
return '- Rp. ' . formatNo(abs($number));
}
return 'Rp. ' . formatNo($number);
}
function formatDecimal($number)
{
return number_format($number, 2,',','.');
return number_format($number, 2, ',', '.');
}
/**
@ -37,17 +38,17 @@ function delete_button($form_params = [], $button_label = 'Delete', $button_opti
$form_params['class'] = isset($form_params['class']) ? $form_params['class'] : 'del-form';
$form_params['style'] = isset($form_params['style']) ? $form_params['style'] : 'display:inline';
if (! isset($button_options['class']))
if (!isset($button_options['class'])) {
$button_options['class'] = 'pull-right';
}
if (! isset($button_options['title']))
if (!isset($button_options['title'])) {
$button_options['title'] = 'Delete this record';
}
$htmlForm = Form::open($form_params);
if (!empty($hiddenFields))
{
foreach ($hiddenFields as $k => $v)
{
if (!empty($hiddenFields)) {
foreach ($hiddenFields as $k => $v) {
$htmlForm .= Form::hidden($k, $v);
}
}
@ -59,8 +60,9 @@ function delete_button($form_params = [], $button_label = 'Delete', $button_opti
function formatDate($date)
{
if (!$date || $date == '0000-00-00')
if (!$date || $date == '0000-00-00') {
return null;
}
$explodedDate = explode('-', $date);
@ -73,9 +75,11 @@ function formatDate($date)
throw new App\Exceptions\InvalidDateException('Kesalahan format tanggal');
}
function dateId($date) {
if (is_null($date) || $date == '0000-00-00')
function dateId($date)
{
if (is_null($date) || $date == '0000-00-00') {
return '-';
}
$explodedDate = explode('-', $date);
@ -87,13 +91,16 @@ function dateId($date) {
throw new App\Exceptions\InvalidDateException('Kesalahan format tanggal');
}
function monthNumber($number) {
function monthNumber($number)
{
return str_pad($number, 2, "0", STR_PAD_LEFT);
}
function monthId($monthNumber) {
if (is_null($monthNumber))
function monthId($monthNumber)
{
if (is_null($monthNumber)) {
return $monthNumber;
}
$months = getMonths();
$monthNumber = monthNumber($monthNumber);
@ -135,13 +142,15 @@ function str_split_ucwords($string)
function getDays()
{
return $days = [1 => 'Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'];
return $days = [1 => 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
}
function getDay($dayIndex = null)
{
$days = getDays();
if (!is_null($dayIndex) && in_array($dayIndex, range(1, 7))) return $days[$dayIndex];
if (!is_null($dayIndex) && in_array($dayIndex, range(1, 7))) {
return $days[$dayIndex];
}
return '-';
}
@ -153,28 +162,17 @@ function sanitizeNumber($number)
function formatSizeUnits($bytes)
{
if ($bytes >= 1073741824)
{
if ($bytes >= 1073741824) {
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
}
elseif ($bytes >= 1048576)
{
} elseif ($bytes >= 1048576) {
$bytes = number_format($bytes / 1048576, 2) . ' MB';
}
elseif ($bytes >= 1024)
{
} elseif ($bytes >= 1024) {
$bytes = number_format($bytes / 1024, 2) . ' KB';
}
elseif ($bytes > 1)
{
} elseif ($bytes > 1) {
$bytes = $bytes . ' bytes';
}
elseif ($bytes == 1)
{
} elseif ($bytes == 1) {
$bytes = $bytes . ' byte';
}
else
{
} else {
$bytes = '0 bytes';
}
@ -188,18 +186,22 @@ function formatSizeUnits($bytes)
* @param array $parameters URL Parameter
* @param array $attributes The anchor tag atributes
*/
function html_link_to_route($name, $title = null, $parameters = [], $attributes = []) {
if (array_key_exists('icon', $attributes))
function html_link_to_route($name, $title = null, $parameters = [], $attributes = [])
{
if (array_key_exists('icon', $attributes)) {
$title = '<i class="fa fa-' . $attributes['icon'] . '"></i> ' . $title;
}
return app('html')->decode(link_to_route($name, $title, $parameters, $attributes));
}
function getProjectStatusesList($statusId = null) {
$statuses = [1 => 'Planned','On Progress','Done','Closed','Canceled','On Hold'];
function getProjectStatusesList($statusId = null)
{
$statuses = [1 => 'Planned', 'On Progress', 'Done', 'Closed', 'Canceled', 'On Hold'];
if (is_null($statusId))
if (is_null($statusId)) {
return $statuses;
}
if (array_key_exists($statusId, $statuses)) {
return $statuses[$statusId];
@ -208,7 +210,7 @@ function getProjectStatusesList($statusId = null) {
return null;
}
function dateDifference($date1 , $date2 , $differenceFormat = '%a' )
function dateDifference($date1, $date2, $differenceFormat = '%a')
{
$datetime1 = date_create($date1);
$datetime2 = date_create($date2);
@ -222,11 +224,13 @@ function paymentTypes($paymentTypeId = null)
{
$paymentTypes = [1 => 'Project', 'Add Feature', 'Maintenance'];
if (is_null($paymentTypeId))
if (is_null($paymentTypeId)) {
return $paymentTypes;
}
if (array_key_exists($paymentTypeId, $paymentTypes))
if (array_key_exists($paymentTypeId, $paymentTypes)) {
return $paymentTypes[$paymentTypeId];
}
return null;
}
}

115
database/factories/ModelFactory.php

@ -7,16 +7,15 @@ use App\Entities\Projects\Project;
use App\Entities\Projects\Task;
use App\Entities\Subscriptions\Subscription;
use App\Entities\Users\Event;
use App\Entities\Users\Role;
use App\Entities\Users\User;
$factory->define(User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->unique()->email,
'password' => 'member',
'name' => $faker->name,
'email' => $faker->unique()->email,
'password' => 'member',
'remember_token' => str_random(10),
'api_token' => str_random(40),
'api_token' => str_random(40),
];
});
@ -24,21 +23,21 @@ $factory->define(Project::class, function (Faker\Generator $faker) {
$proposalDate = $faker->dateTimeBetween('-1 year', '-1 month')->format('Y-m-d');
$startDate = Carbon::parse($proposalDate)->addDays(10);
$endDate = $startDate->addDays(rand(1,13) * 7);
$endDate = $startDate->addDays(rand(1, 13) * 7);
return [
'name' => $faker->sentence(3),
'description' => $faker->paragraph,
'proposal_date' => $proposalDate,
'start_date' => $startDate->format('Y-m-d'),
'end_date' => $endDate->format('Y-m-d'),
'project_value' => $projectValue = rand(1,10) * 500000,
'name' => $faker->sentence(3),
'description' => $faker->paragraph,
'proposal_date' => $proposalDate,
'start_date' => $startDate->format('Y-m-d'),
'end_date' => $endDate->format('Y-m-d'),
'project_value' => $projectValue = rand(1, 10) * 500000,
'proposal_value' => $projectValue,
'status_id' => rand(1,6),
'owner_id' => function () {
'status_id' => rand(1, 6),
'owner_id' => function () {
return factory(User::class)->create()->id;
},
'customer_id' => function () {
'customer_id' => function () {
return factory(User::class)->create()->id;
},
];
@ -47,15 +46,15 @@ $factory->define(Project::class, function (Faker\Generator $faker) {
$factory->define(Payment::class, function (Faker\Generator $faker) {
return [
'project_id' => function () {
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'amount' => rand(1,5) * 500000,
'in_out' => rand(0,1),
'type_id' => rand(1,3),
'date' => $faker->dateTimeBetween('-1 year', '-1 month')->format('Y-m-d'),
'amount' => rand(1, 5) * 500000,
'in_out' => rand(0, 1),
'type_id' => rand(1, 3),
'date' => $faker->dateTimeBetween('-1 year', '-1 month')->format('Y-m-d'),
'description' => $faker->paragraph,
'owner_id' => function () {
'owner_id' => function () {
return factory(User::class)->create()->id;
},
'customer_id' => function () {
@ -69,22 +68,22 @@ $factory->define(Subscription::class, function (Faker\Generator $faker) {
$startDate = Carbon::parse($faker->dateTimeBetween('-1 year', '-1 month')->format('Y-m-d'));
return [
'project_id' => function () {
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'status_id' => 1,
'domain_name' => 'www.' . str_random(10) . '.com',
'domain_price' => 125000,
'epp_code' => str_random(10),
'hosting_capacity' => rand(1,3) . ' GB',
'hosting_price' => rand(1,5) * 100000,
'start_date' => $startDate->format('Y-m-d'),
'due_date' => $startDate->addYears(1)->format('Y-m-d'),
'remark' => $faker->paragraph,
'customer_id' => function () {
'status_id' => 1,
'domain_name' => 'www.' . str_random(10) . '.com',
'domain_price' => 125000,
'epp_code' => str_random(10),
'hosting_capacity' => rand(1, 3) . ' GB',
'hosting_price' => rand(1, 5) * 100000,
'start_date' => $startDate->format('Y-m-d'),
'due_date' => $startDate->addYears(1)->format('Y-m-d'),
'remark' => $faker->paragraph,
'customer_id' => function () {
return factory(User::class)->create()->id;
},
'vendor_id' => function () {
'vendor_id' => function () {
return factory(User::class)->create()->id;
},
];
@ -93,62 +92,62 @@ $factory->define(Subscription::class, function (Faker\Generator $faker) {
$factory->define(Feature::class, function (Faker\Generator $faker) {
return [
'project_id' => function () {
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'name' => $faker->sentence(3),
'price' => rand(1,10) * 100000,
'name' => $faker->sentence(3),
'price' => rand(1, 10) * 100000,
'description' => $faker->paragraph,
'worker_id' => function () {
'worker_id' => function () {
return factory(User::class)->create()->id;
},
'type_id' => rand(1,2),
'position' => rand(1,10),
'type_id' => 1, // Main feature
'position' => rand(1, 10),
];
});
$factory->define(Task::class, function (Faker\Generator $faker) {
return [
'feature_id' => function () {
'feature_id' => function () {
return factory(Feature::class)->create()->id;
},
'name' => $faker->sentence(3),
'name' => $faker->sentence(3),
'description' => $faker->paragraph,
'progress' => rand(40,100),
'route_name' => implode('.', $faker->words(3)),
'position' => rand(1,10),
'progress' => rand(40, 100),
'route_name' => implode('.', $faker->words(3)),
'position' => rand(1, 10),
];
});
$factory->define(Event::class, function (Faker\Generator $faker) {
return [
'user_id' => function () {
'user_id' => function () {
return factory(User::class)->create()->id;
},
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'title' => $faker->words(rand(2,4), true),
'body' => $faker->sentence,
'start' => $faker->dateTimeBetween('-2 months', '-2 months')->format('Y-m-d H:i:s'),
'end' => $faker->dateTimeBetween('-2 months', '-2 months')->format('Y-m-d H:i:s'),
'is_allday' => rand(0,1),
'title' => $faker->words(rand(2, 4), true),
'body' => $faker->sentence,
'start' => $faker->dateTimeBetween('-2 months', '-2 months')->format('Y-m-d H:i:s'),
'end' => $faker->dateTimeBetween('-2 months', '-2 months')->format('Y-m-d H:i:s'),
'is_allday' => rand(0, 1),
];
});
$factory->define(Invoice::class, function (Faker\Generator $faker) {
$invoice = new Invoice;
return [
'project_id' => function () {
'project_id' => function () {
return factory(Project::class)->create()->id;
},
'number' => $invoice->generateNewNumber(),
'items' => [],
'amount' => 100000,
'notes' => $faker->paragraph,
'status_id' => 1,
'user_id' => 1,
'number' => $invoice->generateNewNumber(),
'items' => [],
'amount' => 100000,
'notes' => $faker->paragraph,
'status_id' => 1,
'user_id' => 1,
];
});
});

2
resources/views/projects/partials/project-show.blade.php

@ -23,4 +23,4 @@
</tbody>
</table>
</div>
</div>
</div>

19
resources/views/projects/partials/project-stats.blade.php

@ -15,7 +15,7 @@
</a>
</div>
<div class="col-lg-6 col-md-12">
<a href="{{ route('projects.features',[$project->id]) }}" title="Progress Berdasarkan Index Bobot Biaya Fitur">
<a href="{{ route('projects.features',[$project->id]) }}" title="Total Fitur dan Task">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
@ -29,5 +29,20 @@
</div>
</a>
</div>
<div class="col-lg-6 col-md-12">
<a href="{{ route('projects.features',[$project->id]) }}" title="Collectible Earnings">
<div class="panel panel-success">
<div class="panel-heading">
<div class="row">
<div class="col-xs-12 text-right">
<i class="fa fa-money fa-2x pull-left"></i>
<div class="lead">Collectibe Earnings</div>
<div class="lead" style="font-size: 30px;">{{ formatRp($project->getCollectibeEarnings()) }}</div>
</div>
</div>
</div>
</div>
</a>
</div>
<div class="clearfix"></div>
</div>
</div>

2
resources/views/projects/show.blade.php

@ -27,4 +27,4 @@
{!! Form::close() !!}
</div>
</div>
@endsection
@endsection

4
tests/Unit/Models/InvoiceTest.php

@ -19,9 +19,9 @@ class InvoiceTest extends TestCase
public function it_generates_its_own_number()
{
$invoice1 = factory(Invoice::class)->create();
$this->assertEquals(date('ym').'001', $invoice1->number);
$this->assertEquals(date('ym') . '001', $invoice1->number);
$invoice2 = factory(Invoice::class)->create();
$this->assertEquals(date('ym').'002', $invoice2->number);
$this->assertEquals(date('ym') . '002', $invoice2->number);
}
}

29
tests/Unit/Models/ProjectTest.php

@ -138,4 +138,33 @@ class ProjectTest extends TestCase
$project = factory(Project::class)->make();
$this->assertEquals(link_to_route('projects.show', $project->name, [$project->id]), $project->nameLink());
}
/** @test */
public function a_project_has_collectible_earnings_method()
{
// Collectible earnings is total of (price * avg task progress of each feature)
$project = factory(Project::class)->create();
$collectibeEarnings = 0;
$feature = factory(Feature::class)->create(['project_id' => $project->id, 'type_id' => 1, 'price' => 2000]);
factory(Task::class)->create(['feature_id' => $feature->id, 'progress' => 20]);
$collectibeEarnings += (2000 * (20 / 100)); // feature price * avg task progress
$feature = factory(Feature::class)->create(['project_id' => $project->id, 'type_id' => 1, 'price' => 3000]);
factory(Task::class)->create(['feature_id' => $feature->id, 'progress' => 30]);
$collectibeEarnings += (3000 * (30 / 100));
$feature = factory(Feature::class)->create(['project_id' => $project->id, 'type_id' => 1, 'price' => 1500]);
factory(Task::class)->create(['feature_id' => $feature->id, 'progress' => 100]);
$collectibeEarnings += (1500 * (100 / 100));
$feature = factory(Feature::class)->create(['project_id' => $project->id, 'type_id' => 1, 'price' => 1500]);
factory(Task::class)->create(['feature_id' => $feature->id, 'progress' => 100]);
$collectibeEarnings += (1500 * (100 / 100));
// $collectibeEarnings = 400 + 900 + 1500 + 1500;
$this->assertEquals($collectibeEarnings, $project->getCollectibeEarnings());
}
}

2
tests/Unit/Models/UserTest.php

@ -13,7 +13,7 @@ class UserTest extends TestCase
$user = factory(User::class)->create();
$this->assertEquals(link_to_route('users.show', $user->name, [$user->id], [
'target' => '_blank'
'target' => '_blank',
]), $user->nameLink());
}

Loading…
Cancel
Save