65 Commits

Author SHA1 Message Date
dizox d0d0a0e75c Merge pull request 'mahdi' (#1) from mahdi into master 4 years ago
mahdihty b5ec3615ce add notification helper class 4 years ago
mahdihty 70a7c28f41 add task update notification: ready_to_test, completed_at, assignee, approver 4 years ago
mahdihty c2064f96fc add task create notification 4 years ago
mahdihty 8bc2d8e3d7 some minor change and clean code 4 years ago
mahdihty d5c11b1281 add project user update notification, some clean code, queueing mail notification on redis 4 years ago
mahdihty 6de9a528cc clean code in BusinessUserCreateNotif.php 4 years ago
mahdihty 0ea97f895b complete Fcm push notification 4 years ago
mahdihty 1d0231f6f3 complete Fcm push notification 4 years ago
Mohammad Akbari fc3e13643e
WIP 4 years ago
mahdihty 1d3d111722 some change in cors config 4 years ago
mahdihty 58cd8afb36 merge conflict resolve 4 years ago
Mohammad Akbari 52b2238c5b
Fix namespace with haj mehdi 4 years ago
mahdihty 26e5ac5d1b solve conflict with mohammad 4 years ago
mahdihty 01c5ce3e38 add some event, listener and fsm channel 4 years ago
mahdihty 51d2edd0d5
add event listener, notification and fa validations 4 years ago
Mohammad Akbari 34998e384f
Is there an award for this? Cost worker improvement 4 years ago
mahdihty 9191e7705e add event listener, notification and fa validations 4 years ago
Mohammad Akbari c9bf70b9dc
Fix a few problem after removing lumen 4 years ago
Mohammad Akbari c89ea86838
Remove redunant packages 4 years ago
Mohammad Akbari c2f9518b9a
Bug fix in fingerprints 4 years ago
Mohammad Akbari f909fc3ceb
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
mahdihty 882db1170e minor change in model 4 years ago
Mohammad Akbari e46c83feed
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
Mohammad Akbari 5fb0a4335b
Bug fix and improvments of cost work 4 years ago
mahdihty a02ec57ab0 change .env.example 4 years ago
mahdihty ce7b2e2aa4 fix tags 4 years ago
mahdihty f6afd0a79e fix estimated time 4 years ago
Mohammad Akbari 330910f727
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
mahdihty 42ff482886 add model 4 years ago
mahdihty 3cf22ba267 add activity => model, controller, migration, routes 4 years ago
mahdihty d91bebb9c7
add middleware 4 years ago
mahdihty 215ac23d36 add middleware 4 years ago
Mohammad Akbari af5afbda95
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
Mohammad Akbari fc1bd902c4
Fix max bound 4 years ago
mahdihty 2e8f2d6777 some minor change in user during test 4 years ago
mahdihty fce7fcc652 some minor change in auth and related configs 4 years ago
Mohammad Akbari 2c0e162bf4
Fix task file 4 years ago
Mohammad Akbari 6c868508ee
Fix zarinpal 4 years ago
Mohammad Akbari 3ebb402c18
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
Mohammad Akbari 9c22aa2b12
Fix missing vars in method definition 4 years ago
mahdihty 344e13985d complete getValueOf method 4 years ago
mahdihty 4003c05338 some bug fix 4 years ago
mahdihty ba309d9a1f bug fix work, comment, task cruds during test 4 years ago
Mohammad Akbari 4856f07126
Fix spatie medialibrary 4 years ago
Mohammad Akbari e3330bbd9f
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
mahdihty ab00fc5609 fix amqp and create new seeder 4 years ago
Mohammad Akbari 4eb782a0c6
Fix cache locations 4 years ago
mahdihty 95b899843b fix AuthServiceProvider.php 4 years ago
Mohammad Akbari 4880496d0f
Merge with HAJ MAHDI (A) 4 years ago
mahdihty d08258ebb9 fix namespaces in models, login bug fix 4 years ago
Mohammad Akbari fe4dd7f8dc
I know, I know, this is not how I’m supposed to do it, but I can't think of something better. 4 years ago
mahdihty 176458893d merge task, user routes 4 years ago
Mohammad Akbari c96af7fd78
Nobody had ever created a function like this one before. 4 years ago
Mohammad Akbari 5866521e83
Yet another quality commit. 4 years ago
Mohammad Akbari 11b7d229fa
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
Mohammad Akbari 994c7d9b33
Merge docker services with main docker compose file 4 years ago
mahdihty 3adaad5066 statistic directory, TagTask model, change factory, seeder, api 4 years ago
Mohammad Akbari 272b519ca1
Merge branch 'mahdi' of https://gitea.hooradev.ir/mahdihty/liwo into mohammad 4 years ago
Mohammad Akbari c0eb2bd5bc
Migrating from lumen to laravel, day first! :)) 4 years ago
Mohammad Akbari 9ff6e84f83
WooooHooooo, Hello again my beloved LARAVEL 4 years ago
mahdihty 187340281c copy task, work, comment 4 years ago
mahdihty 5ead18ce01 add phpmyadmin 4 years ago
mahdihty cd5c67afaf add docker compose 4 years ago
mahdihty 7acc7acee5 .idea git ignore 4 years ago
  1. 11
      .env.example
  2. 2
      .gitignore
  3. 5
      .idea/codeStyles/codeStyleConfig.xml
  4. 8
      .idea/liwo.iml
  5. 8
      .idea/modules.xml
  6. 93
      app/Channels/FcmChannel.php
  7. 132
      app/Channels/Messages/FcmMessage.php
  8. 169
      app/Console/Commands/CostCommand.php
  9. 1
      app/Enums/business.php
  10. 1
      app/Enums/comment.php
  11. 11
      app/Enums/cruds.php
  12. 1
      app/Enums/levels.php
  13. 25
      app/Enums/log.php
  14. 1
      app/Enums/post.php
  15. 1
      app/Enums/roles.php
  16. 1
      app/Enums/service.php
  17. 1
      app/Enums/status.php
  18. 83
      app/Enums/tables.php
  19. 1
      app/Enums/ticket.php
  20. 1
      app/Enums/user.php
  21. 20
      app/Events/BusinessUpdate.php
  22. 38
      app/Events/BusinessUserCreate.php
  23. 38
      app/Events/ModelSaved.php
  24. 24
      app/Events/ProjectUserCreate.php
  25. 38
      app/Events/TagCreate.php
  26. 28
      app/Events/TaskCreate.php
  27. 28
      app/Events/TaskUpdate.php
  28. 110
      app/Http/Controllers/ActivityController.php
  29. 268
      app/Http/Controllers/AuthController.php
  30. 195
      app/Http/Controllers/BusinessController.php
  31. 90
      app/Http/Controllers/CommentController.php
  32. 4
      app/Http/Controllers/Controller.php
  33. 69
      app/Http/Controllers/CreditController.php
  34. 207
      app/Http/Controllers/FileController.php
  35. 80
      app/Http/Controllers/InvoiceController.php
  36. 159
      app/Http/Controllers/ProjectController.php
  37. 45
      app/Http/Controllers/SprintController.php
  38. 112
      app/Http/Controllers/StatisticController.php
  39. 35
      app/Http/Controllers/StatusController.php
  40. 40
      app/Http/Controllers/SystemController.php
  41. 34
      app/Http/Controllers/TagController.php
  42. 295
      app/Http/Controllers/TaskController.php
  43. 87
      app/Http/Controllers/TaskFileController.php
  44. 70
      app/Http/Controllers/UserController.php
  45. 242
      app/Http/Controllers/WorkController.php
  46. 67
      app/Http/Controllers/WorkflowController.php
  47. 2
      app/Http/Kernel.php
  48. 34
      app/Http/Resources/BusinessResource.php
  49. 34
      app/Http/Resources/CommentResource.php
  50. 35
      app/Http/Resources/FileResource.php
  51. 34
      app/Http/Resources/FingerprintResource.php
  52. 38
      app/Http/Resources/ProjectResource.php
  53. 23
      app/Http/Resources/TaskCollection.php
  54. 30
      app/Http/Resources/TaskResource.php
  55. 32
      app/Http/Resources/TransactionResource.php
  56. 34
      app/Http/Resources/UserResource.php
  57. 73
      app/Listeners/ActivityRegistration.php
  58. 32
      app/Listeners/BusinessUpdateListener.php
  59. 93
      app/Listeners/BusinessUserCreateNotif.php
  60. 36
      app/Listeners/NotifHandler.php
  61. 93
      app/Listeners/ProjectUserCreateNotif.php
  62. 37
      app/Listeners/TagCreateNotif.php
  63. 104
      app/Listeners/TaskCreateNotif.php
  64. 155
      app/Listeners/TaskUpdateNotif.php
  65. 38
      app/Models/Activity.php
  66. 487
      app/Models/Business.php
  67. 19
      app/Models/Comment.php
  68. 28
      app/Models/Cost.php
  69. 75
      app/Models/File.php
  70. 31
      app/Models/Fingerprint.php
  71. 237
      app/Models/Model.php
  72. 206
      app/Models/Project.php
  73. 140
      app/Models/ReportableRelation.php
  74. 29
      app/Models/SoftDeletes.php
  75. 70
      app/Models/Sprint.php
  76. 53
      app/Models/Status.php
  77. 66
      app/Models/System.php
  78. 56
      app/Models/Tag.php
  79. 12
      app/Models/TagTask.php
  80. 307
      app/Models/Task.php
  81. 164
      app/Models/Transaction.php
  82. 206
      app/Models/User.php
  83. 126
      app/Models/Work.php
  84. 89
      app/Models/Workflow.php
  85. 49
      app/Notifications/DBNotification.php
  86. 51
      app/Notifications/FcmNotification.php
  87. 78
      app/Notifications/MailNotification.php
  88. 10
      app/Providers/AppServiceProvider.php
  89. 22
      app/Providers/AuthServiceProvider.php
  90. 38
      app/Providers/EventServiceProvider.php
  91. 2
      app/Providers/RouteServiceProvider.php
  92. 40
      app/Rules/MaxBound.php
  93. 15
      app/Utilities/Avatar/DefaultConversionFileNamer.php
  94. 41
      app/Utilities/Avatar/DefaultPathGenerator.php
  95. 43
      app/Utilities/BusinessInfoRequestMixin.php
  96. 58
      app/Utilities/Exceptions/Handler.php
  97. 72
      app/Utilities/HelperClass/NotificationHelper.php
  98. 27
      app/Utilities/Helpers/enum.php
  99. 1
      app/Utilities/Helpers/http.php
  100. 1
      app/Utilities/Helpers/index.php

11
.env.example

@ -1,8 +1,11 @@
APP_NAME=Laravel
CONTAINER_NAME=
APP_NAME=Liwo
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_TIMEZONE="Asia/Tehran"
LOG_CHANNEL=stack
LOG_LEVEL=debug
@ -22,8 +25,9 @@ SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_HOST=redis
REDIS_PASSWORD=root
REDIS_USER=root
REDIS_PORT=6379
MAIL_MAILER=smtp
@ -47,3 +51,4 @@ PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

2
.gitignore

@ -11,3 +11,5 @@ Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.idea/

5
.idea/codeStyles/codeStyleConfig.xml

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

8
.idea/liwo.iml

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/liwo.iml" filepath="$PROJECT_DIR$/.idea/liwo.iml" />
</modules>
</component>
</project>

93
app/Channels/FcmChannel.php

@ -0,0 +1,93 @@
<?php
namespace App\Channels;
use App\Channels\Messages\FcmMessage;
use Illuminate\Notifications\Notification;
use GuzzleHttp\Client as HttpClient;
class FcmChannel
{
/**
* The API URL for FCM.
*
* @var string
*/
const API_URI = 'https://fcm.googleapis.com/fcm/send';
/**
* The HTTP client instance.
*
* @var \GuzzleHttp\Client
*/
protected $http;
/**
* The FCM API key.
*
* @var string
*/
protected $apikey;
/**
* Create a new FCM channel instance.
*
* @param \GuzzleHttp\Client $http
* @param string $apiKey
* @return void
*/
public function __construct(HttpClient $http, string $apiKey)
{
$this->http = $http;
$this->apiKey = $apiKey;
}
/**
* Send the given notification.
*
* @param mixed $notifiable
* @param \Illuminate\Notifications\Notification $notification
* @return void
*/
public function send($notifiable, Notification $notification)
{
$message = $notification->toFcm($notifiable);
$message->to($notifiable->routeNotificationFor('fcm', $notification));
if (! $this->apiKey || (! $message->topic && ! $message->to)) {
return;
}
$this->http->post(self::API_URI, [
'headers' => [
'Authorization' => "key={$this->apiKey}",
'Content-Type' => 'application/json',
],
'json' => $this->buildJsonPayload($message),
]);
}
protected function buildJsonPayload(FcmMessage $message)
{
$payload = array_filter([
'priority' => $message->priority,
'data' => $message->data,
'notification' => $message->notification,
'condition' => $message->condition,
]);
if ($message->topic) {
$payload['to'] = "/topics/{$message->topic}";
} else {
if (is_array($message->to)) {
$payload['registration_ids'] = $message->to;
} else {
$payload['to'] = $message->to;
}
}
return $payload;
}
}

132
app/Channels/Messages/FcmMessage.php

@ -0,0 +1,132 @@
<?php
namespace App\Channels\Messages;
class FcmMessage
{
/**
* The devices token to send the message from.
*
* @var array|string
*/
public $to;
/**
* The topic of the FCM message.
*
* @var array
*/
public $topic;
/**
* The data of the FCM message.
*
* @var array
*/
public $data;
/**
* The notification body of the FCM message.
*
* @var array
*/
public $notification;
/**
* The condition for receive the FCM message.
*
* @var array
*/
public $condition;
/**
* The priority of the FCM message.
*
* @var string
*/
public $priority = 'normal';
/**
* Set the devices token to send the message from.
*
* @param array|string $to
* @return $this
*/
public function to($to)
{
if (is_array($to) && count($to) === 1) {
$this->to = $to[0];
} else {
$this->to = $to;
}
return $this;
}
/**
* Set the topic of the FCM message.
*
* @param string $topic
* @return $this
*/
public function topic(string $topic)
{
$this->topic = $topic;
return $this;
}
/**
* Set the data of the FCM message.
*
* @param array $data
* @return $this
*/
public function data(array $data)
{
$this->data = $data;
return $this;
}
/**
* Set the notification of the FCM message.
*
* @param array $notification
* @return $this
*/
public function notification(array $notification)
{
$this->notification = $notification;
return $this;
}
/**
* Set the condition for receive the FCM message.
*
* @param string $condition
* @return $this
*/
public function condition(string $condition)
{
$this->condition = $condition;
return $this;
}
/**
* Set the priority of the FCM message.
*
* @param string $priority
* @return $this
*/
public function priority(string $priority)
{
$this->priority = $priority;
return $this;
}
}

169
app/Console/Commands/CostCommand.php

@ -0,0 +1,169 @@
<?php
namespace App\Console\Commands;
use Throwable;
use Carbon\Carbon;
use App\Models\Cost;
use App\Models\Business;
use Illuminate\Console\Command;
use Morilog\Jalali\CalendarUtils;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
class CostCommand extends Command
{
protected $signature = 'cost:work';
protected $description = 'Run the cost worker';
public function handle()
{
while (true) {
$lock = Cache::get('lock', false);
if ($lock) {
$this->info('There is no business for auditing.');
continue;
}
$business = Business::orderBy('calculated_at')->first();
if ($business === null) {
continue;
}
if ($business->calculated_at->isFuture()) {
continue;
}
$next_month = jdate($business->calculated_at)->addMonths();
[$year, $month, $day] = CalendarUtils::toGregorian(
$next_month->getYear(), $next_month->getMonth(), 1
);
$now = Carbon::createFromDate($year, $month, $day);
$now->setTime(0, 0, 0);
if ($now->isFuture()) {
$now = Carbon::now();
}
// if calculated_at less than an hour stop
if ($now->diffInMinutes($business->calculated_at) <= 59) {
$this->info('Must be one hour after the last audit.');
$this->ensureLockIsWritten();
continue;
}
try {
DB::beginTransaction();
// Fixed amounts of expenses
$business->load('users', 'files');
$costs = 0;
$costs += $this->calculateCostOfBusinessUsers($business, $now);
$costs += $this->calculateCostOfBusinessFiles($business, $now);
// increment and decrement of wallet in php
// deduct costs from your business wallet
// make sure save the calculated_at
$business->update([
'wallet' => $business->wallet - $costs,
'calculated_at' => $now,
]);
DB::commit();
$this->info("The business #{$business->id} was audited.");
} catch (Throwable $throwable) {
throw $throwable;
DB::rollback();
report($throwable);
continue;
}
}
}
public function calculateCostOfBusinessUsers($business, $now)
{
$user_fee = enum('business.fee.user');
$recorded_month = jdate($business->calculated_at)->format("Y-m-01");
if ($business->users->isEmpty()) {
return 0;
}
// get business employee
$users_cost = $business->cost
->where('type', '=', Cost::USER_TYPE)
->where('fee', '=', $user_fee)
->where('month', '=', $recorded_month)
->where('amount', '=', $business->users->count())
->first();
if ($users_cost === null) {
$business->cost()->create([
'type' => Cost::USER_TYPE,
'month' => $recorded_month,
'amount' => $business->users->count(),
'fee' => $user_fee,
'duration' => $duration = $now->diffInMinutes($business->calculated_at), // from the created_at time of the newset fifth user
'additional' => $business->users->pluck('id')->toArray(),
]);
} else {
$users_cost->update([
'duration' => $duration = $now->diffInMinutes($business->calculated_at) + $users_cost->duration, // last calc - (current month - now else last calc - end of the past month),
'additional' => $business->users->pluck('id')->toArray(),
]);
}
return $user_fee * $duration;
}
public function calculateCostOfBusinessFiles($business, $now)
{
$file_fee = enum('business.fee.file');
$calculated_at = $business->calculated_at;
$recorded_month = jdate($business->calculated_at)->format("Y-m-01");
// do the math in php
$packs = intdiv($business->files_volume, 200);
if ($packs === 0) {
return 0;
} else {
$packs--;
}
$files = $business->cost
->where('type', '=', Cost::FILE_TYPE)
->where('fee', '=', $file_fee)
->where('month', '=', $recorded_month)
->where('amount', '=', $packs)
->first();
if ($files === null) {
$business->cost()->create([
'type' => Cost::FILE_TYPE,
'month' => $recorded_month,
'amount' => $packs,
'fee' => $file_fee,
'duration' => $duration = $now->diffInMinutes($calculated_at), // how to determine the file?,
'additional' => ['volume' => $business->files_volume],
]);
} else {
$files->update([
'duration' => $duration = $now->diffInMinutes($calculated_at) + $files->duration, // last calc - (current month - now else last calc - end of the past month),,
'additional' => ['volume' => $business->files_volume],
]);
}
return $file_fee * $duration;
}
public function ensureLockIsWritten(): void
{
Cache::put('lock', true, 3600);
while (Cache::get('lock', false) === false) {
// prevent
}
}
}

1
app/Enums/business.php

@ -0,0 +1 @@
<?php return [ 'fee' => [ 'user' => 100, 'file' => 200, ] ];

1
app/Enums/comment.php

@ -0,0 +1 @@
<?php return [ 'status' => [ 'reject' => [ 'id' => 10, 'label' => 'رد' ], 'approve' => [ 'id' => 20, 'label' => 'تایید' ], ] ];

11
app/Enums/cruds.php

@ -0,0 +1,11 @@
<?php
return [
'inverse' => [
10 => ['name' => 'Create', 'singular_name' => 'create'],
20 => ['name' => 'Update', 'singular_name' => 'update'],
30 => ['name' => 'Delete', 'singular_name' => 'delete'],
],
];

1
app/Enums/levels.php

@ -0,0 +1 @@
<?php return [ 'owner' => [ 'id' => 4, 'name' => 'Owner', 'label' => 'صاحب' ], 'admin' => [ 'id' => 3, 'name' => 'Admin', 'label' => 'مدیر' ], 'colleague' => [ 'id' => 2, 'name' => 'Colleague', 'label' => 'همکار' ], 'guest' => [ 'id' => 1, 'name' => 'Guest', 'label' => 'مهمان' ], 'inactive' => [ 'id' => 0, 'name' => 'Inactive', 'label' => 'غیر فعال' ], ];

25
app/Enums/log.php

@ -0,0 +1,25 @@
<?php
use App\Models\User;
use App\Models\Business;
use App\Project;
use App\Task;
use App\SpentHour;
return [
'types' => [
User::class => 10,
Business::class => 20,
Project::class => 30,
Task::class => 40,
Spenthour::class => 50,
],
'actions' => [
'created' => 10,
'updated' => 20,
'deleted' => 30,
'restored' => 40,
],
];

1
app/Enums/post.php

@ -0,0 +1 @@
<?php return [ 'status' => [ 'draft' => [ 'id' => 10, 'label' => 'پیش نویس' ], 'review' => [ 'id' => 20, 'label' => 'در حال بررسی' ], 'published' => [ 'id' => 30, 'label' => 'منتشر' ], 'trashed' => [ 'id' => 40, 'label' => 'حذف' ], ] ];

1
app/Enums/roles.php

@ -0,0 +1 @@
<?php return [ 'hidden' => [ 'id' => 0, 'label' => 'غیر فعال' ], 'guest' => [ 'id' => 1, 'label' => 'میهمان' ], 'Colleague' => [ 'id' => 2, 'label' => 'همکار' ], 'senior' => [ 'id' => 3, 'label' => 'معاون' ], 'manager' => [ 'id' => 4, 'label' => 'مدیر' ], ];

1
app/Enums/service.php

@ -0,0 +1 @@
<?php return [ 'post' => [ 'file' => [ 'orphanage' => [ 'id' => 'orphanage', 'label' => 'داده‌های موقت', ] ] ] ];

1
app/Enums/status.php

@ -0,0 +1 @@
<?php return [ 'states' => [ 'inactive' => [ 'id' => 0, 'label' => 'غیر فعال', 'name' => 'Inactive' ], 'active' => [ 'id' => 1, 'label' => 'فعال', 'name' => 'Active' ], 'close' => [ 'id' => 2, 'label' => 'بسته', 'name' => 'Close' ], 'done' => [ 'id' => 3, 'label' => 'انجام شده', 'name' => 'Done' ], ], ];

83
app/Enums/tables.php

@ -0,0 +1,83 @@
<?php
return [
'inverse' => [
10 => ['name' => 'Businesses'],
20 => ['name' => 'Projects'],
30 => ['name' => 'Sprints'],
40 => ['name' => 'Statuses'],
50 => ['name' => 'Systems'],
60 => ['name' => 'Workflows'],
70 => ['name' => 'Tags'],
80 => ['name' => 'Tasks'],
90 => ['name' => 'Works'],
210 => ['name' => 'BusinessUser'],
220 => ['name' => 'ProjectUser'],
230 => ['name' => 'TagTask'],
],
//Main Table's
'businesses' => [
'id' => 10,
'name' => 'Businesses',
'singular_name' => 'Business',
],
'projects' => [
'id' => 20,
'name' => 'Projects',
'singular_name' => 'Project',
],
'sprints' => [
'id' => 30,
'name' => 'Sprints',
'singular_name' => 'Sprint',
],
'statuses' => [
'id' => 40,
'name' => 'Statuses',
'singular_name' => 'Status',
],
'systems' => [
'id' => 50,
'name' => 'Systems',
'singular_name' => 'System',
],
'workflows' => [
'id' => 60,
'name' => 'Workflows',
'singular_name' => 'Workflow',
],
'tags' => [
'id' => 70,
'name' => 'Tags',
'singular_name' => 'Tag',
],
'tasks' => [
'id' => 80,
'name' => 'Tasks',
'singular_name' => 'Task',
],
'works' => [
'id' => 90,
'name' => 'Works',
'singular_name' => 'Work',
],
//Relation Table's
'business_user' => [
'id' => 210,
'name' => 'BusinessUser',
'singular_name' => 'BusinessUser',
],
'project_user' => [
'id' => 220,
'name' => 'ProjectUser',
'singular_name' => 'ProjectUser',
],
'tag_task' => [
'id' => 230,
'name' => 'TagTask',
'singular_name' => 'TagTask',
],
];

1
app/Enums/ticket.php

@ -0,0 +1 @@
<?php return [ 'type' => [ 'sale' => [ 'id' => 1, 'label' => 'فروش', ], 'support' => [ 'id' => 2, 'label' => 'پشتیبانی' ] ], 'category' => [ 'webdesign-sale' => [ 'id' => 1, 'label' => 'طراحی سایت', 'type' => 1 ], 'seo-sale' => [ 'id' => 2, 'label' => 'فروش سئو', 'type' => 1 ], 'webdesign-support' => [ 'id' => 3, 'label' => 'پشتیبانی طراحی سایت', 'type' => 2 ] ], 'status' => [ 'active' => [ 'id' => 1, 'label' => 'فعال' ], 'close' => [ 'id' => 2, 'label' => 'بسته' ] ] ];

1
app/Enums/user.php

@ -0,0 +1 @@
<?php return [ 'type' => [ 'guest' => [ 'id' => 1, 'label' => 'مهمان' ], 'user' => [ 'id' => 2, 'label' => 'کاربر' ], 'service' => [ 'id' => 3, 'label' => 'سرویس' ] ], 'status' => [ 'desable' => [ 'id' => 0, 'label' => 'غیر فعال' ], 'active' => [ 'id' => 1, 'label' => 'فعال' ] ], 'permissions' => [ // user 'user-user-create', 'user-user-view-any', 'user-user-update-own', 'user-user-update-any', 'user-user-role-own', 'user-user-role-any', 'user-role-create', 'user-role-view-any', // ticket 'ticket-ticket-create', 'ticket-ticket-view-any', 'ticket-ticket-reply-any', // post 'post-post-create', 'post-post-view-publish', 'post-post-view-any', 'post-post-view-own', 'post-post-update-own', 'post-post-update-any', 'post-post-delete-own', 'post-post-delete-any', // tag 'post-tag-update-any', 'post-tag-create', 'post-tag-delete-any', // taxonomies 'post-taxonomy-view-any', 'post-taxonomy-update-any', 'post-taxonomy-create', 'post-taxonomy-delete-any', // comments 'post-comment-view-any', 'post-comment-view-published', 'post-comment-view-own', 'post-comment-create', 'post-comment-update-any', 'post-comment-delete-any', ] ];

20
app/Events/BusinessUpdate.php

@ -0,0 +1,20 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
class BusinessUpdate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public function __construct($message)
{
$this->message = $message;
}
}

38
app/Events/BusinessUserCreate.php

@ -0,0 +1,38 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class BusinessUserCreate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

38
app/Events/ModelSaved.php

@ -0,0 +1,38 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ModelSaved
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

24
app/Events/ProjectUserCreate.php

@ -0,0 +1,24 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class ProjectUserCreate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
}

38
app/Events/TagCreate.php

@ -0,0 +1,38 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TagCreate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

28
app/Events/TaskCreate.php

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TaskCreate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
}

28
app/Events/TaskUpdate.php

@ -0,0 +1,28 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TaskUpdate
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
}

110
app/Http/Controllers/ActivityController.php

@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use App\Rules\MaxBound;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
class ActivityController extends Controller
{
public function index($business, Request $request)
{
permit('businessAccess');
$this->indexValidation($request);
$per_page = $request->limit > 100 ? 10 : $request->limit;
return $this->indexFiltering($business)->paginate($per_page);
}
public function indexValidation($request)
{
$bound = 10;
$this->validate($request, [
'filter.project_id' => [new MaxBound($bound)] ,
'filter.system_id' => [new MaxBound($bound)] ,
'filter.workflow_id' => [new MaxBound($bound)] ,
'filter.status_id' => [new MaxBound($bound)] ,
'filter.sprint_id' => [new MaxBound($bound)] ,
'filter.actor_id' => [new MaxBound($bound)] ,
'filter.user_id' => [new MaxBound($bound)] ,
'filter.subject_id' => [new MaxBound($bound)] ,
//todo: validation for crud_id and table_id
'filter.creates_before' => 'bail|nullable|date|date_format:Y-m-d' ,
'filter.creates_after' => 'bail|nullable|date|date_format:Y-m-d' ,
'filter.creates_in' => 'bail|nullable|numeric|max:90' ,
]);
}
public function indexFiltering($business)
{
$query = Activity::where('business_id', $business);
$activityQ = QueryBuilder::for($query)
->allowedFilters([
AllowedFilter::exact('project_id'),
AllowedFilter::exact('system_id'),
AllowedFilter::exact('workflow_id'),
AllowedFilter::exact('status_id'),
AllowedFilter::exact('sprint_id'),
AllowedFilter::exact('task_id'),
AllowedFilter::exact('actor_id'),
AllowedFilter::exact('user_id'),
AllowedFilter::exact('crud_id'),
AllowedFilter::exact('table_id'),
AllowedFilter::exact('subject_id'),
AllowedFilter::scope('creates_before'),
AllowedFilter::scope('creates_after'),
AllowedFilter::scope('creates_in'),
])
->defaultSort('-id')
->allowedSorts('id', 'created_at');
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
$requested_projects = isset(\request('filter')['project_id']) ?
array_unique(explode(',',\request('filter')['project_id'] ?? null )) :
null;
$requested_projects = collect($requested_projects)->keyBy(null)->toArray();
$project_ids = $this->myStateProjects($requested_projects);
$activityQ->where(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['non_guest_ids'])
->orWhere(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['guest_ids'])
->where('user_id', auth()->id());
});
});
}
return $activityQ;
}
public function myStateProjects($requested_projects)
{
$non_guest_ids = [];
$guest_ids = [];
$is_empty = empty($requested_projects);
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) {
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level'];
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level > enum('levels.guest.id')) {
array_push($non_guest_ids, $p_id);
}
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level == enum('levels.guest.id')) {
array_push($guest_ids, $p_id);
}
}
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids];
}
public function store($business, Request $request)
{
return Activity::create($request->merge(['business_id' => $business])->all());
}
public function delete()
{
}
}

268
app/Http/Controllers/AuthController.php

@ -0,0 +1,268 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Business;
use App\Models\Fingerprint;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Http\JsonResponse;
use App\Http\Resources\UserResource;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Cache;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Session\TokenMismatchException;
use Symfony\Component\HttpFoundation\Response;
class AuthController extends Controller
{
public function redirectToGoogle()
{
return Socialite::driver('google')->stateless()->redirect();
}
public function handleGoogleCallback(Request $request)
{
try {
$user = Socialite::driver('google')->stateless()->user();
$find_user = User::where('email', $user->email)->first();
if ($find_user) {
$find_user->update([
'active' => true
]);
Auth::setUser($find_user);
} else {
$user = User::create($user->user + [
'password' => Hash::make('google-login-user'),
'username' => $user->email,
'active' => true
]);
Auth::setUser($user);
}
$finger_print = $this->createFingerPrint();
return redirect('http://localhost:3000/login?token='.$finger_print->token);
} catch (Exception $e) {
dd($e->getMessage());
}
}
public function login(Request $request)
{
// todo: Logging in from a new device will result in sending a notification
$this->validate($request, [
'email' => 'required|email|exists:users,email',
'password' => 'required|string|min:6'
]);
$user = User::where('email', $request->email)->first();
if ($user && Hash::check($request->password, $user->password)) {
Auth::setUser($user);
return [
'auth' => $this->createFingerPrint(),
'businesses' => Auth::user()->businesses->keyBy('id')->map(fn($b, $bid) => Business::info($bid))
];
}
return new JsonResponse([
'message' => trans('auth.failed'),
'status' => Response::HTTP_NOT_FOUND,
], Response::HTTP_NOT_FOUND);
}
public function register(Request $request)
{
$this->validate($request, [
'name' => 'required|string|max:225|min:2',
'username' => ['required', Rule::unique('users', 'username')],
'email' => ['required', 'email', Rule::unique('users', 'email')],
'password' => 'required|string|min:8'
]);
$request->merge(['password' => Hash::make($request->password)]);
$code_data = ['verification_code' => $this->sendVerificationCode()];
$method_data = ['method' => 'registerMain'];
Cache::put($request->email, $request->all() + $code_data + $method_data, 3600); // remain one hour
return \response()->json([
'message' => 'Code send for user and user must be verified.'],
Response::HTTP_OK);
}
public function registerMain($user_info)
{
$user = User::create($user_info);
Auth::setUser($user);
return $this->createFingerPrint();
}
public function sendVerificationCode($contact_way = null)
{
$verification_code = 1234; // rand(10001, 99999)
//send code for user with contact way
return $verification_code;
}
public function verification(Request $request)
{
if (!Cache::has($request->email)) {
return \response()->json(['message' => 'Code expired.'], Response::HTTP_BAD_REQUEST);
}
$user_info = Cache::get($request->email);
$this->validate($request, [
'email' => 'required|email',
'verification_code' => 'required|string|min:4|max:4|in:'.$user_info['verification_code']
]);
Cache::forget($request->email);
return isset($user_info['method']) ?
call_user_func('self::'.$user_info['method'], $user_info) :
\response()->json(['message' => 'Code verified successfully.'], Response::HTTP_OK,);
}
public function forgetPassword(Request $request)
{
$this->validate($request, [
'email' => 'required|email|exists:users,email'
]);
$code_data = ['verification_code' => $this->sendVerificationCode()];
Cache::put($request->email, $request->all() + $code_data, 3600); // remain one hour
return \response()->json([
'message' => 'Code send for user and user must be verified.'],
Response::HTTP_OK);
}
public function updatePassword(Request $request)
{
if (!Cache::has($request->email)) {
return \response()->json(['message' => 'Code expired.'], Response::HTTP_BAD_REQUEST);
}
$this->validate($request, [
'email' => 'required|email',
'password' => 'required|string|min:8|confirmed',
'verification_code' => 'required|string|min:4|max:4|in:'.Cache::get($request->email)['verification_code']
]);
$user = User::where('email', $request->email)->first();
$user->update([
'password' => Hash::make($request->password)
]);
Auth::setUser($user);
return $this->createFingerPrint();
}
/**
* @param Request $request
* @return mixed
* @throws TokenMismatchException
*/
public function logout(Request $request)
{
$token = $request->bearerToken();
if (blank($token)) {
return new JsonResponse([
'message' => 'Not authorized request.',
'status' => Response::HTTP_UNAUTHORIZED
]);
}
/** @var Fingerprint $token */
$token = Auth::user()->fingerprints()->firstWhere([
'token' => $token,
]);
if ($token) {
return $token->delete();
}
throw new TokenMismatchException('Invalid token!');
}
/**
* @param string $token
* @throws TokenMismatchException
*/
public function revoke(string $token)
{
/** @var Fingerprint $token */
$token = Fingerprint::firstWhere([
'token' => $token,
]);
if ($token) {
return $token->delete();
}
throw new TokenMismatchException();
}
public function auth()
{
return new UserResource(Auth::user());
}
public function authWithInfo()
{
return [
'auth' => new UserResource(Auth::user()),
'businesses' => Auth::user()->businesses->keyBy('id') ->map(fn($b, $bid) => Business::info($bid))
];
}
public function delete(Request $request)
{
Auth::user()->fingerprints()->delete();
unset(Auth::user()->token);
Auth::user()->delete();
return 'success';
}
public function createFingerPrint()
{
$attributes = [
'agent' => request()->getAgent(),
'ip' => request()->getClientIp(),
'os' => request()->getOS(),
'latitude' => \request()->getLocation()->getAttribute('lat'),
'longitude' => \request()->getLocation()->getAttribute('lon'),
];
$values = [
'token' => Str::random(60)
];
return Auth::user()->fingerprints()->firstOrCreate($attributes, $attributes + $values);
}
}

195
app/Http/Controllers/BusinessController.php

@ -0,0 +1,195 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Business;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class BusinessController extends Controller
{
public function index()
{
return auth()->user()->businesses
->keyBy('id')
->map(fn($b, $bid) => Business::info($bid));
}
public function store(Request $request)
{
// $users = [];
// foreach ($request->users ?? [] as $key => $value) {
// $users[$value] = [];
// }
// $owner = [
// Auth::id() => [
// 'level' => enum('levels.owner.id'),
// ]
// ];
//
// $users = $users + $owner;
//
// $request->merge(['users' => $users]);
$business = Business::create($request->all());
$business->users()->sync([Auth::id() => [
'level' => enum('levels.owner.id'),
]
], false);
return Business::info($business->id);
}
public function show(string $business)
{
permit('businessAccess');
return Business::info($business);
}
public function update(Request $request, string $business)
{
// permit('businessEdit');
$business = Business::findOrFail($business);
$business->fill($request->all())->save();
return Business::info($business->id);
}
public function setAvatar(Request $request, string $business)
{
$business = Business::findOrFail($business);
if ($request->hasFile('avatar')) {
$business->saveAsAvatar($request->file('avatar'));
}
return Business::info($business->id);
}
public function unSetAvatar(Request $request, string $business)
{
$business = Business::findOrFail($business);
$business->deleteAvatar();
return Business::info($business->id);
}
public function info(string $business)
{
return request('_business_info');
}
public function restore(string $business)
{
$business = Business::onlyTrashed()->findOrFail($business);
$business->restore();
return response(['message' => 'business successfully restored.']);
}
public function storeOrUpdateUser($business, Request $request)
{
permit('businessUsers');
$validatedData = $this->validate($request, [
'level' => 'required|numeric|between:0,4',
'user_id' => 'required|numeric|not_in:'.auth()->id(),
]);
DB::transaction(function () use ($validatedData, $request, $business) {
$this->addUser($business, $request->user_id, $validatedData);
if (can('businessAccess', ['user_id'=> $request->user_id])) {
//update
$this->relatedUpdateChanges($request->user_id, $request->level);
}
}, 3);
return Business::info($business, true);
}
public function relatedUpdateChanges($user_id, $level)
{
if ($level == enum('levels.owner.id')) {
// user up level to owner
$this->removeProjectDirectRelation($user_id);
}
if ($level != enum('levels.owner.id') &&
$level > request('_business_info')['info']['users'][$user_id]['level']) {
// user at least up level to $request->level
$this->updateProjectAccessLevel($level, $user_id);
}
}
public function addUser($business, $user, $validatedData)
{
$businessModel = Business::findOrFail($business);
$businessModel->users()->sync([$user => $validatedData], false);
}
public function removeProjectDirectRelation($user)
{
$userModel = User::findOrFail($user);
return $userModel->projects()->sync([], true);
}
public function updateProjectAccessLevel($level, $user)
{
$ids = [];
foreach (request('_business_info')['projects'] as $project_id => $item) {
foreach ($item['members'] as $idx => $member) {
if ($member['id'] == $user && $member['level'] != enum('levels.inactive.id') && $member['level'] < $level) {
$ids[$project_id] = ['level' => $level];
break;
}
}
}
$userModel = User::findOrFail($user);
return $userModel->projects()->sync($ids, false);
}
public function deleteUser($business, $user)
{
permit('businessAccess');
$this->checkDeleteUserPolicy($user);
$businessModel = Business::findOrFail($business);
DB::transaction(function () use ($user, $businessModel) {
$this->detachUser($businessModel, $user);
$this->removeProjectDirectRelation($user);
}, 3);
return Business::info($business, true);
}
public function haveAnotherOwner($user)
{
foreach (request('_business_info')['info']['users'] as $id => $item) {
if ($item['level'] == enum('levels.owner.id') && $id != $user) {
return true;
}
}
return false;
}
public function detachUser($business, $user)
{
return $business->users()->sync(
$business->users->except($user)->pluck('id')->toArray()
);
}
public function checkDeleteUserPolicy($user)
{
if (!can('isBusinessOwner') && auth()->id() != $user ) {
// Non owner user remove another owner
abort(405);
}
if (can('isBusinessOwner') && auth()->id() == $user && !$this->haveAnotherOwner($user)) {
// Owner remove self but business haven't another owner
abort(405);
}
}
}

90
app/Http/Controllers/CommentController.php

@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers;
use App\Models\Task;
use App\Models\Comment;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class CommentController extends Controller
{
public function index($business, $project, $task)
{
permit('projectAccess', ['project_id' => $project]);
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
return $taskModel->assignee_id == \auth()->id() ?
Comment::where([
['business_id', $business],
['project_id', $project],
['task_id', $task],
])->get():
abort(Response::HTTP_FORBIDDEN); // not allowed
} else {
return Comment::where([
['business_id', $business],
['project_id', $project],
['task_id', $task],
])->get();
}
}
public function store($business, $project, $task, Request $request)
{
permit('projectAccess', ['project_id' => $project]);
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
return $taskModel->assignee_id == \auth()->id() ?
Comment::create($request->merge([
'business_id' => $business,
'project_id' => $project,
'task_id' => $task,
'user_id' => \auth()->id(),
])->except('_business_info')) :
abort(Response::HTTP_FORBIDDEN); // not allowed
} else {
return Comment::create($request->merge([
'business_id' => $business,
'project_id' => $project,
'task_id' => $task,
'user_id' => \auth()->id(),
])->except('_business_info'));
}
}
public function show($business, $project, $task, $comment)
{
permit('projectAccess', ['project_id' => $project]);
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
return $taskModel->assignee_id == \auth()->id() ?
Comment::findOrFail($comment) :
abort(Response::HTTP_FORBIDDEN); // not allowed
} else {
return Comment::findOrFail($comment);
}
}
public function update($business, $project, $task, $comment, Request $request)
{
permit('projectAccess', ['project_id' => $project]);
$comment = Comment::findOrFail($comment);
if ($comment->user_id == \auth()->id()) {
$comment->update($request->except('_business_info'));
return $comment;
}
return abort(Response::HTTP_FORBIDDEN); // not allowed
}
public function destroy($business, $project, $task, $comment)
{
permit('projectAccess', ['project_id' => $project]);
$comment = Comment::findOrFail($comment);
if ($comment->user_id == \auth()->id()) {
$comment->delete();
return \response()->json(['message' => 'comment deleted successfully.'], Response::HTTP_OK);
}
return abort(Response::HTTP_FORBIDDEN); // not allowed
}
}

4
app/Http/Controllers/Controller.php

@ -2,10 +2,10 @@
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{

69
app/Http/Controllers/CreditController.php

@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use App\Http\Resources\TransactionResource;
class CreditController extends Controller
{
public function payments(Request $request, int $business)
{
\permit('isBusinessOwner');
$query = Transaction::where('business_id', $business);
$builder = QueryBuilder::for($query)
->allowedSorts([
'amount',
'succeeded',
'created_at',
])
->allowedFilters([
AllowedFilter::exact('user_id'),
AllowedFilter::exact('succeeded'),
]);
return TransactionResource::collection(
$builder->paginate($request->per_page)
);
}
public function pay(Request $request, int $business)
{
\permit('isBusinessOwner');
return Transaction::create([
'user_id'=> Auth::id(),
'business_id'=> $business,
'amount'=> $request->amount,
]);
}
public function redirection($transaction)
{
$transaction = Transaction::findOrFail($transaction);
if ($transaction->isWentToPaymentGateway()) {
throw new \Exception("Siktir baba ye bar ghablan rafti.");
}
return $transaction->prepare()->redirect();
}
public function callback(Request $request)
{
$transaction = Transaction::findByAuthority($request->get('Authority'))->verify();
if (!$transaction->hasBeenAppliedToWallet() && $transaction->succeeded) {
$transaction->business->increment("wallet", $transaction->amount);
$transaction->amountWasAppliedToWallet();
return true;
}
throw new \Exception("تراکنش تایید نشد");
}
}

207
app/Http/Controllers/FileController.php

@ -0,0 +1,207 @@
<?php
namespace App\Http\Controllers;
use App\Models\File;
use App\Models\Project;
use App\Rules\MaxBound;
use App\Models\Business;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use App\Http\Resources\FileResource;
use Illuminate\Support\Facades\Auth;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
use Illuminate\Support\Facades\Storage;
class FileController extends Controller
{
public function index(int $business, Request $request)
{
permit('businessAccess');
$this->indexValidation($request);
$per_page = $request->limit > 100 ? 10 : $request->limit;
return $this->indexFiltering($business)->paginate($per_page);
}
public function indexValidation($request)
{
$bound = 10;
$this->validate($request, [
'filter.project_id' => [new MaxBound($bound)] ,
'filter.user_id' => [new MaxBound($bound)] ,
]);
}
public function indexFiltering($business)
{
$query = File::where('business_id', $business);
$fileQ = QueryBuilder::for($query)
->allowedFilters([
AllowedFilter::exact('user_id'),
AllowedFilter::exact('project_id'),
AllowedFilter::exact('extension'),
]);
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
$requested_projects = isset(\request('filter')['project_id']) ?
array_unique(explode(',',\request('filter')['project_id'] ?? null )) :
null;
$requested_projects = collect($requested_projects)->keyBy(null)->toArray();
$project_ids = $this->myStateProjects($requested_projects);
$fileQ->where(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['non_guest_ids'])
->orWhere(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['guest_ids'])
->where('user_id', auth()->id());
});
});
}
if (request()->filled('group')) {
$fileQ->selectRaw("files.group, count(files.id) as file_count, sum(files.size) as file_size")->groupBy('group');
}
return $fileQ;
}
public function myStateProjects($requested_projects)
{
$non_guest_ids = [];
$guest_ids = [];
$is_empty = empty($requested_projects);
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) {
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level'];
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level > enum('levels.guest.id')) {
array_push($non_guest_ids, $p_id);
}
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level == enum('levels.guest.id')) {
array_push($guest_ids, $p_id);
}
}
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids];
}
public function store(Request $request, int $business, int $project)
{
// different size and different validation
// validate
// validate the wallet is not so much in debt
// create record in the db
// put file in s3
// return file resource
$business = Business::findOrFail($business);
$project = Project::findOrFail($project);
$this->validate($request, ['file' => 'required|file',]);
$file = $request->file('file');
$file_extension = $file->getClientOriginalExtension();
$file_name = Str::random(40).'.'.$file_extension;
$file->storeAs(
$business->id.\DIRECTORY_SEPARATOR.$project->id,
$file_name,
's3'
);
$file_record = File::create([
'user_id' => Auth::id(),
'business_id' => $business->id,
'project_id' => $project->id,
'disk' => 's3', // default disk
'original_name' => $file->getClientOriginalName(),
'extension' => $file_extension,
'name' => $file_name,
'mime' => $file->getClientMimeType(),
'group' => $this->groupDetection($file),
'size' => $file->getSize(),
'description' => $request->description
]);
$business->update([
'files_volume' => $business->files_volume + $file_record->size
]);
return new FileResource($file_record);
}
public function groupDetection(UploadedFile $file)
{
// Media files like mp4, mp3, wma and png or jpeg
[$type, $subtype] = Str::of($file->getMimeType())->explode("/",2)->pad(2, null);
if (in_array($type, ['audio', 'video', 'image'])) {
return $type;
}
// Covert string to \Illuminate\Support\Stringable object
$subtype = Str::of($subtype);
// PDF files
if ($subtype->contains(["pdf"])) {
return "pdf";
}
// Compressed files like zip, cab, rar, etc.
if ($subtype->contains(['compressed']) || in_array($file->getClientOriginalExtension(), ['zip', 'rar','7z','cab'])) {
return "compressed";
}
// Office files like xls, xlsx, doc, docx, etc.
if ($subtype->contains(['vnd.ms', 'vnd.sealed', 'officedocument', 'opendocument'])) {
return "office";
}
// Non of the above files
return "other";
}
public function download(int $business, int $project, int $file)
{
// requested file belongs to this project and this business
// check permisson
// create perma link or temp link
// return the file resource or stream it
return File::findOrFail($file)->getTemporaryLink();
}
public function rename(Request $request, int $business, int $project, int $file)
{
// requested file belongs to this project and this business
// check permisson
// update original name
// return the file resource
// sanitize the name for slashs and back slashes
$this->validate($request, [
'name' => 'required|string'
]);
$file = File::findOrFail($file);
$file->update(['original_name' => $request->name.".".$file->extension]);
return new FileResource($file);
}
public function delete(Request $request, int $business, int $project, int $file)
{
// requested file belongs to this project and this business
// check permisson
// check it's relations
// delete the file form File table
// delete file from s3
$file = File::findOrFail($file);
Storage::disk('s3')->delete($file->getPath());
return $file->delete();
}
}

80
app/Http/Controllers/InvoiceController.php

@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Cost;
use App\Models\Business;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
class InvoiceController extends Controller
{
public function index(Request $request, int $business)
{
$business = Business::findOrFail($business);
$builder = Cost::select('month')
->selectRaw("concat_ws('-',business_id,month) as factor_id")
->selectRaw("MIN(created_at) as begin")
->selectRaw("MAX(updated_at) as end")
->selectRaw("sum(cost) as cost")
->selectRaw("sum(tax) as tax")
->where('business_id','=',$business->id)
->groupBy('month','factor_id');
$costs = QueryBuilder::for($builder)
->allowedSorts([
'factor_id',
'begin',
'end',
'cost',
])
->allowedFilters([
AllowedFilter::exact('month'),
AllowedFilter::exact('type'),
]);
return $costs->paginate($request->per_page);
}
public function indexFiltering($business)
{
$query = File::where('business_id', $business);
$fileQ = QueryBuilder::for($query)
->allowedFilters([
AllowedFilter::exact('user_id'),
AllowedFilter::exact('project_id'),
AllowedFilter::exact('extension'),
]);
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
$requested_projects = isset(\request('filter')['project_id']) ?
array_unique(explode(',', \request('filter')['project_id'] ?? null)) :
null;
$requested_projects = collect($requested_projects)->keyBy(null)->toArray();
$project_ids = $this->myStateProjects($requested_projects);
$fileQ->where(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['non_guest_ids'])
->orWhere(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['guest_ids'])
->where('user_id', auth()->id());
});
});
}
if (request()->filled('group')) {
$fileQ->selectRaw("files.group, count(files.id) as file_count, sum(files.size) as file_size")->groupBy('group');
}
return $fileQ;
}
public function show(Request $request, int $business, string $date)
{
return Cost::where('business_id', '=', $business)
->where("month","=",$date)
->get();
}
}

159
app/Http/Controllers/ProjectController.php

@ -0,0 +1,159 @@
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use App\Models\Business;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ProjectController extends Controller
{
public function index(Request $request, int $business)
{
// permit('businessAccess');
return Project::where('business_id', $business)->get();
}
public function store(Request $request, string $business)
{
permit('businessProjects');
Project::create($request->merge(['business_id' => $business])->all());
return Business::info($request->route('business'), true);
}
public function update(Request $request, int $business, string $project)
{
permit('projectEdit', ['project_id' => $project]);
$project = Project::findOrFail($project);
$project->update($request->except('business_id'));
return Business::info($request->route('business'), true);
}
public function delete(Request $request, int $business, string $project)
{
permit('businessProjects');
$project = Project::findOrFail($project);
$project->delete();
return Business::info($request->route('business'));
}
public function restore(Request $request, int $business, string $project)
{
$project = Project::onlyTrashed()->findOrFail($project);
$project->restore();
return response(['message' => 'project successfully restored.']);
}
public function storeOrUpdateUser($business, $project, Request $request)
{
permit('projectUsers', ['project_id' => $project]);
$validatedData = $this->validate($request, [
'level' => 'required|numeric|between:1,3',
'user_id' => 'required|numeric|not_in:'.auth()->id(),
]);
$this->checkAddUserPolicy($request->user_id, $request->level);
$projectModel = Project::findOrFail($project);
DB::transaction(function () use ($business, $validatedData, $request, $projectModel) {
$projectModel->members()->sync([$request->user_id => $validatedData], false);
if (!can('businessAccess', ['user_id' => $request->user_id])) {
// Register user to business with zero level
//User not exist in the business before
$this->addUserWithZeroLevel($request->user_id, $business);
}
}, 3);
return Business::info($projectModel->business_id, true);
}
public function checkAddUserPolicy($user, $level)
{
if (can('businessAccess', ['user_id' => $user])
&& $level < request('_business_info')['info']['users'][$user]['level']) {// before in business
abort(405);
}
}
public function addUserWithZeroLevel($user_id, $business)
{
$businessModel = Business::findOrFail($business);
return $businessModel->users()->sync([$user_id => [
'level' => 0,
'user_id' => $user_id
]], false);
}
public function deleteUser($business, $project, $user)
{
permit('projectAccess', ['project_id' => $project]);
$this->checkDeleteUserPolicy($user, $project);
$projectModel = Project::findOrFail($project);
DB::transaction(function () use ($project, $business, $user, $projectModel) {
$this->detachMember($projectModel, $user);
if (!can('isActiveUser', ['user_id' => $user]) && !$this->haveOneProject($user, $project)) {
// User level in business is zero
// And haven't another project then remove it form business
$businessModel = Business::findOrFail($business);
$this->detachUser($businessModel, $user);
}
}, 3);
return Business::info($projectModel->business_id, true);
}
public function detachMember($project, $user)
{
return $project->members()->detach($user) ? true : abort(404);
}
public function detachUser($business, $user)
{
return $business->users()->detach($user) ? true : abort(404);
}
public function haveOneProject($user, $project)
{
foreach (request('_business_info')['info']['projects'] as $id => $item) {
if ($item['members'][$user]['level'] > enum('levels.inactive.id') && $id != $project) {
return true;
}
}
return false;
}
public function checkDeleteUserPolicy($user, $project)
{
if (!can('isProjectOwner', ['project_id' => $project]) && (auth()->id() != $user) ) {
abort(405);
}
}
public function setAvatar(Request $request, int $business, string $project)
{
$project = Project::findOrFail($project);
if ($request->hasFile('avatar')) {
$project->saveAsAvatar($request->file('avatar'));
}
return $project;
}
public function unSetAvatar(Request $request,int $business ,string $project)
{
$project = Project::findOrFail($project);
$project->deleteAvatar();
return $project;
}
}

45
app/Http/Controllers/SprintController.php

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use App\Models\Sprint;
use App\Models\Business;
use Illuminate\Http\Request;
class SprintController extends Controller
{
public function store($business, $project, Request $request)
{
permit('projectSprints', ['project_id' => $project]);
Sprint::create($request->merge(
['business_id' => $business, 'project_id' => $project]
)->except('_business_info'));
return Business::info($business, true);
}
public function update($business, $project, $sprint, Request $request)
{
permit('projectSprints', ['project_id' => $project]);
$sprint = Sprint::findOrFail($sprint);
$sprint->update($request->except('_business_info'));
return Business::info($business, true);
}
public function delete($business, $project, $sprint)
{
permit('projectSprints', ['project_id' => $project]);
$sprint = Sprint::findOrFail($sprint);
$sprint->delete();
return Business::info($business, true);
}
}

112
app/Http/Controllers/StatisticController.php

@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Arr;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatisticController extends Controller
{
public $map = [
'test' => 0, // The sum of all tasks that are under review
'total' => 0, // Sum of all tasks
'overdue' => 0, // The sum of all overworked tasks
'spent_time' => 0, // The sum of all the minutes spent on tasks
'estimated_time' => 0, // The sum of all the minutes spent performing tasks is estimated
'over_spent_time' => 0, // The sum of all the minutes spent overworking tasks
'under_spent_time' => 0, // The sum of all the minutes left until estimated time
];
public function index(Request $request, int $business, ?int $project = null)
{
$builder = DB::table('tasks')->where('business_id','=',$business);
if ($project) {
$builder->where('project_id', '=', $project);
}
$tasks = $builder->get();
$tags = DB::table('tag_task')
->whereIn('task_id', $tasks->pluck('id')->toArray())
->get()
->groupBy('task_id')
->toArray();
$result = [];
foreach ($tasks as $task) {
$this->addProjects($result, $task);
$this->addSprints($result, $task);
$this->addSystems($result, $task);
$this->addTags($result, $task, $tags[$task->id] ?? []);
}
return $result;
}
public function addProjects(&$result, $task)
{
$key = "projects.{$task->project_id}";
$this->subset($key, $result, $task);
}
public function addSprints(&$result, $task)
{
$key = "projects.{$task->project_id}.";
$key .= 'sprints.';
$key .= ($task->sprint_id ?? 0) . '.';
$key .= ($task->assignee_id ?? 0) . '.';
$key .= $task->workflow_id . '.';
$key .= $task->status_id;
$this->subset($key, $result, $task);
}
public function addSystems(&$result, $task)
{
$key = "projects.{$task->project_id}.";
$key .= 'systems.';
$key .= ($task->system_id ?? 0) . '.';
$key .= ($task->assignee_id ?? 0) . '.';
$key .= $task->workflow_id . '.';
$key .= $task->status_id;
$this->subset($key, $result, $task);
}
public function addTags(&$result, $task, $tags)
{
foreach ($tags as $tag) {
$key = "projects.{$task->project_id}.";
$key .= 'tags.';
$key .= $tag->id . '.';
$key .= ($task->assignee_id ?? 0) . '.';
$key .= $task->workflow_id . '.';
$key .= $task->status_id;
$this->subset($key, $result, $task);
}
}
public function subset($key , &$result, $task)
{
$node = Arr::get($result, $key, $this->map);
$node['test'] += $task->ready_to_test;
$node['total'] += 1;
$node['overdue'] += !$task->on_time;
$node['spent_time'] += $task->spent_time;
$node['estimated_time'] += $task->estimated_time;
$node['over_spent_time'] += $task->estimated_time < $task->spent_time ? $task->spent_time - $task->estimated_time : 0;
$node['under_spent_time'] += $task->estimated_time > $task->spent_time ? $task->estimated_time - $task->spent_time : 0;
Arr::set($result, $key, $node);
}
}

35
app/Http/Controllers/StatusController.php

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Models\Status;
use App\Models\Business;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\Rule;
class StatusController extends Controller
{
public function store($business, $workflow, Request $request)
{
permit('businessStatuses');
Status::create($request->merge(['business_id' => $business, 'workflow_id' => $workflow])->except('_business_info'));
return Business::info($business, true);
}
public function update($business, $workflow, $status, Request $request)
{
permit('businessStatuses');
$status = Status::findOrFail($status);
$status->update($request->except('_business_info'));
return Business::info($business, true);
}
public function delete($business, $workflow, $status)
{
permit('businessStatuses');
$status = Status::findOrFail($status);
$status->delete();
return Business::info($business, true);
}
}

40
app/Http/Controllers/SystemController.php

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Models\System;
use App\Models\Business;
use Illuminate\Http\Request;
class SystemController extends Controller
{
public function store($business, $project, Request $request)
{
permit('projectSystems', ['project_id' => $project]);
System::create([
'business_id' => $business,
'project_id' => $project,
'name' => $request->name
]);
return Business::info($business, true);
}
public function update($business, $project, $system, Request $request)
{
permit('projectSystems', ['project_id' => $project]);
$system = System::findOrFail($system);
$system->update($request->except('_business_info'));
return Business::info($business, true);
}
public function delete($business, $project, $system)
{
permit('projectSystems', ['project_id' => $project]);
$system = System::findOrFail($system);
$system->delete();
return Business::info($business, true);
}
}

34
app/Http/Controllers/TagController.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers;
use App\Models\Tag;
use App\Models\Business;
use Illuminate\Http\Request;
class TagController extends Controller
{
public function store($business, Request $request)
{
permit('businessTags');
Tag::create($request->merge(['business_id' => $business])->except('_business_info'));
return Business::info($business, true);
}
public function update($business, $tag, Request $request)
{
permit('businessTags');
$tag = Tag::findOrFail($tag);
$tag->update($request->except('_business_info'));
return Business::info($business, true);
}
public function delete($business, $tag)
{
permit('businessTags');
$tag = Tag::findOrFail($tag);
$tag->delete();
return Business::info($business, true);
}
}

295
app/Http/Controllers/TaskController.php

@ -0,0 +1,295 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use App\Models\Task;
use App\Models\Work;
use App\Models\TagTask;
use App\Rules\MaxBound;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;
use App\Http\Resources\TaskResource;
use Spatie\QueryBuilder\QueryBuilder;
use App\Http\Resources\TaskCollection;
use Spatie\QueryBuilder\AllowedFilter;
class TaskController extends Controller
{
public function index($business, Request $request)
{
permit('businessAccess');
$per_page = $request->limit > 100 ? 10 : $request->limit;
$this->indexValidation($request);
$tasks = $this->indexFiltering($business)
->when($request->filled('group'), function ($q) use ($request) {
return $q->report($request->group);
});
return $request->filled('group') ?
$tasks->get()->groupBy($request->group)
->map(function ($q) { return $q->keyBy('status_id'); })
: new TaskCollection($tasks->paginate($per_page));
}
public function indexValidation($request)
{
$bound = 10;
$this->validate($request, [
'filter.project_id' => [new MaxBound($bound)] ,
'filter.creator_id' => [new MaxBound($bound)] ,
'filter.assignee_id' => [new MaxBound($bound)] ,
'filter.system_id' => [new MaxBound($bound)] ,
'filter.workflow_id' => [new MaxBound($bound)] ,
'filter.status_id' => [new MaxBound($bound)] ,
'filter.approver_id' => [new MaxBound($bound)] ,
'filter.priority_min' => 'nullable|numeric|between:1,10' ,
'filter.priority_max' => 'nullable|numeric|between:1,10' ,
'filter.ready_to_test' => 'nullable|boolean' ,
'filter.on_time' => 'nullable|boolean' ,
]);
}
public function indexFiltering($business)
{
$query = Task::where('business_id', $business);
$taskQ = QueryBuilder::for($query)
// ->with('tags')
->select(DB::raw('tasks.* , (spent_time - estimated_time) as over_spent'))
->allowedFilters([
AllowedFilter::exact('project_id'),
AllowedFilter::exact('system_id'),
AllowedFilter::exact('creator_id'),
AllowedFilter::exact('assignee_id'),
AllowedFilter::exact('approver_id'),
AllowedFilter::exact('sprint_id'),
AllowedFilter::exact('workflow_id'),
AllowedFilter::exact('status_id'),
AllowedFilter::exact('on_time'),
AllowedFilter::exact('ready_to_test'),
AllowedFilter::exact('tagTask.tag_id'),
AllowedFilter::scope('priority_min'),
AllowedFilter::scope('priority_max'),
AllowedFilter::scope('creates_before'),
AllowedFilter::scope('creates_after'),
AllowedFilter::scope('creates_in'),
AllowedFilter::scope('updates_before'),
AllowedFilter::scope('updates_after'),
AllowedFilter::scope('updates_in'),
AllowedFilter::scope('spent_from'),
AllowedFilter::scope('spent_to'),
AllowedFilter::scope('estimated_from'),
AllowedFilter::scope('estimated_to'),
AllowedFilter::scope('starts_before'),
AllowedFilter::scope('starts_after'),
AllowedFilter::scope('starts_in'),
AllowedFilter::scope('finish_before'),
AllowedFilter::scope('finish_after'),
AllowedFilter::scope('finish_in'),
AllowedFilter::scope('completes_before'),
AllowedFilter::scope('completes_after'),
AllowedFilter::scope('completes_in'),
AllowedFilter::scope('over_spent_from'),
AllowedFilter::scope('over_spent_to'),
AllowedFilter::scope('due_date_before'),
AllowedFilter::scope('due_date_after'),
AllowedFilter::scope('due_date_in'),
AllowedFilter::scope('my_watching'),
AllowedFilter::scope('over_due'),
]);
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
$requested_projects = isset(\request('filter')['project_id']) ?
array_unique(explode(',',\request('filter')['project_id'] ?? null )) :
null;
$requested_projects = collect($requested_projects)->keyBy(null)->toArray();
$project_ids = $this->myStateProjects($requested_projects);
$taskQ->where(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['non_guest_ids'])
->orWhere(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['guest_ids'])
->where('assignee_id', auth()->id());
});
});
}
return $taskQ;
}
public function myStateProjects($requested_projects)
{
$non_guest_ids = [];
$guest_ids = [];
$is_empty = empty($requested_projects);
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) {
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level'];
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level > enum('levels.guest.id')) {
array_push($non_guest_ids, $p_id);
}
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level == enum('levels.guest.id')) {
array_push($guest_ids, $p_id);
}
}
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids];
}
public function store($business, $project, Request $request)
{
permit('projectTasks', ['project_id' => $project]);
$task = Task::create($request->merge(
['business_id' => $business, 'project_id' => $project, 'creator_id' => \auth()->id()]
)->except(['_business_info', 'completed_at', 'on_time', 'ready_to_test']));
return new TaskResource($task);
}
public function storeTagTasks($tags, $task) {
$tagModels = [];
if (isset($tags)) {
foreach ($tags as $tag) {
array_push($tagModels,
new TagTask(['tag_id' => $tag, 'task_id' => $task->id])
);
}
$task->tags()->saveMany($tagModels);
}
}
/**
* Rule's:
* 1) guest's only can see self task
* 2) user is active in project
*/
public function show($business, $project, $task)
{
$task = Task::findOrFail($task);
$project = $task->project_id;
permit('projectAccess', ['project_id' => $project]);
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
return $task->assignee_id == \auth()->id() ?
new TaskResource($task) :
abort(Response::HTTP_METHOD_NOT_ALLOWED); // not allowed
} else {
return new TaskResource($task);
}
}
/**
* Rule's:
* 1) update assignee_id when not exist work time and active user
* 2) update approver_id when atLeast colleague
* 3) update ready_to_test when assignee_id == auth xor assignee_id == null and isAdmin
* 4) update tags
* 5) update completed_at when status in [done, close]
* 6) due_date before sprint end_time
*/
public function update($business, $project, $task, Request $request)
{
permit('isProjectGuest', ['project_id' => $project]);
$taskModel = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
if ($taskModel->assignee_id != \auth()->id() && can('isDefiniteGuestInProject', ['project_id' => $project])) {
permit('isDefiniteGuestInProject', ['project_id' => $project]);
}
if($request->filled('watchers')) {
$watchers = $taskModel->watchers ?? [];
if(!can('isDefiniteGuestInProject', ['project_id' => $project]) && !can('projectTasks', ['project_id' => $project])) {
if (array_search(auth()->id(), $watchers) !== false) {
// remove auth from watchers
$watchers = array_values(\array_diff($watchers, [auth()->id()]));
} else {
// add auth to watchers
$watchers = array_merge($watchers, [auth()->id()]);
}
}
if(can('projectTasks', ['project_id' => $project])) {
$watchers = array_intersect($watchers ?? [], $request->watchers);
}
$request->merge(['watchers' => $watchers]);
}
if (!can('projectTasks', ['project_id' => $project])) {
$request->replace($request->only(['_business_info', 'ready_to_test', 'status_id', 'watchers']));
}
if ($taskModel->assignee_id != \auth()->id()) {
$request->request->remove('ready_to_test');
}
if (isset(\request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state'])) {
$state = \request('_business_info')['workflows'][$request->workflow_id]['statuses'][$request->status_id]['state'];
if ($state == enum('status.states.close.id') || $state == enum('status.states.done.id')) {
//ToDo: is needed to check before state is done or close?
$request->merge([
'completed_at' => Carbon::now(),
'work_finish' => Work::where('task_id', $task)->latest()->first()->ended_at ?? null
]);
if (isset($taskModel->due_date) && $taskModel->due_date < date('yy-m-d')) {
//check if before set due date and miss, we change on time flag
$request->merge(['on_time' => false]);
}
}
}
if (!$request->has('tags')) {
$request->merge(['tags' => []]);
}
$taskModel->update($request->except('_business_info'));
return new TaskResource($taskModel);
}
public function updateReadyToTest($business, $project, $task)
{
permit('isProjectGuest', ['project_id' => $project]);
$task = Task::where([['project_id', $project ], ['id', $task]])->firstOrFail();
if ($task->assignee_id == \auth()->id()) {
$task->update([
'ready_to_test' => 1
]);
} else {
return abort(Response::HTTP_FORBIDDEN); // not allowed
}
return $task->load(['tagTask'=> fn($q) => $q->select('id', 'tag_id', 'task_id'), 'works', 'comments']);
}
/**
* Rule's:
* 1) delete only not work time (soft delete)
*/
public function destroy($task)
{
$taskModel = Task::findOrFail($task);
if (Work::where('task_id', $task)->exists()) {
return \response()->json(['task_id' => 'The task id cannot be deleted.'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
$taskModel->delete();
return \response()->json(['message' => 'task deleted successfully.'], Response::HTTP_OK);
}
public function toggleWatcher($business, $task, $project =null)
{
permit('isProjectGuest', ['project_id' => $project]);
$taskModel = Task::findOrFail($task);
$watchers = $taskModel->watchers ?? [];
if (array_search(auth()->id(), $watchers) !== false) {
// remove auth from watchers
$new_watchers = array_values(\array_diff($watchers, [auth()->id()]));
} else {
// add auth to watchers
$new_watchers = array_merge($watchers, [auth()->id()]);
}
$taskModel->update([
'watchers' => $new_watchers
]);
return new TaskResource($taskModel);
}
}

87
app/Http/Controllers/TaskFileController.php

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers;
use Auth;
use App\Models\File;
use App\Models\Task;
use App\Models\Project;
use App\Models\Business;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Http\Resources\FileResource;
use Symfony\Component\HttpFoundation\Response;
class TaskFileController extends Controller
{
public function checkBelonging(int $business, int $project, int $task)
{
$business = Business::findOrFail($business);
$project = Project::findOrFail($project);
$task = Task::find($task);
if (
$business->id !== $project->business_id
|| $project->id !== $task['project_id']
// || $task['user_id']!== Auth::id()
) {
\abort(Response::HTTP_UNAUTHORIZED);
}
return [$business, $project, $task];
}
public function index(int $business, int $project, int $task)
{
// check permissions
// owner project
// admin project
// colleague project
// guest or de active
// return files as file resource
[$business, $project, $task] = $this->checkBelonging($business, $project, $task);
return FileResource::collection($task->files ?? []);
}
public function sync(Request $request,int $business, int $project, int $task)
{
// different size and different validation
// validate
// validate the wallet is not so much in debt
// create record in the db
// put file in s3
// return file resource
[$business, $project, $task] = $this->checkBelonging($business,$project,$task);
$this->validate($request, [
'files' => 'required|array',
'files.*' => 'int',
]);
$files = File::find($request->files)->each(function (File $file) {
if ($file->user_id !== Auth::id()) {
abort(Response::HTTP_UNAUTHORIZED);
}
});
// sync
return FileResource::collection($files);
}
public function download(int $business, int $project, int $task, int $file)
{
// requested file belongs to this project and this business
// check permisson
// create perma link or temp link
// return the file resource or stream it
[$business, $project, $task] = $this->checkBelonging($business, $project, $task);
$file = File::findOrFail($file);
if ($file->user_id !== Auth::id()) {
abort(Response::HTTP_UNAUTHORIZED);
}
return $file->getTemporaryLink();
}
}

70
app/Http/Controllers/UserController.php

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
class UserController extends Controller
{
public function search(Request $request)
{
$limit = 20;
$userQ = User::query();
if ($request->filled('search')){
$userQ = $userQ->where(function($query) use ($request) {
$query->where('email', 'like', '%'.$request->search.'%')
->orWhere('username', 'like', '%'.$request->search.'%');
});
}
return $userQ->where('id', '!=', auth()->id())
->whereNotIn('id', request('_business_info')['info']['users']->keys())
->select('id', 'name', 'email', 'username')->take($limit)->get();
}
public function index(Request $request)
{
$userQ = QueryBuilder::for(User::class)
->allowedFilters([
AllowedFilter::exact('id'),
]);
return $userQ->select('id', 'name', 'email', 'username')->get();
}
public function show($user)
{
return User::select('id', 'name', 'email', 'username')->findOrFail($user);
}
public function update(Request $request, string $user)
{
$user = User::findOrFail($user);
$user->update($request->all());
return $user;
}
public function setAvatar(Request $request, string $user)
{
$user = User::findOrFail($user);
if ($request->hasFile('avatar')) {
$user->saveAsAvatar($request->file('avatar'));
}
return $user;
}
public function unSetAvatar(Request $request, string $user)
{
$user = User::findOrFail($user);
$user->deleteAvatar();
$user->refresh();
return $user;
}
}

242
app/Http/Controllers/WorkController.php

@ -0,0 +1,242 @@
<?php
namespace App\Http\Controllers;
use Carbon\Carbon;
use App\Models\Task;
use App\Models\Work;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Spatie\QueryBuilder\QueryBuilder;
use Spatie\QueryBuilder\AllowedFilter;
class WorkController extends Controller
{
public function index($business, Request $request)
{
permit('businessAccess');
$per_page = $request->limit > 100 ? 10 : $request->limit;
$workQ = $this->indexFiltering($business)
->when($request->filled('group'), function ($q) use ($request) {
return $request->group == 'user' ? $q->report() : $q->reportByDate();
});
return $request->filled('group') ? $workQ->get() :
$workQ->defaultSort('-id')
->allowedSorts('id', 'started_at')->paginate($per_page);
}
public function indexFiltering($business)
{
$query = Work::where('works.business_id', $business);
$workQ = queryBuilder::for($query)
->join('tasks', 'tasks.id', 'works.task_id')
->select('works.*', 'tasks.title', 'tasks.sprint_id', 'tasks.system_id')
->allowedFilters([
AllowedFilter::exact('project_id'),
AllowedFilter::exact('tasks.sprint_id', null, false),
AllowedFilter::exact('tasks.system_id', null, false),
AllowedFilter::exact('user_id'),
AllowedFilter::scope('started_at_in'),
AllowedFilter::scope('started_at'),
AllowedFilter::scope('spent_time_from'),
AllowedFilter::scope('spent_time_to'),
]);
if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) {
$requested_projects = isset(\request('filter')['project_id']) ?
array_unique(explode(',',\request('filter')['project_id'] ?? null )) :
null;
$requested_projects = collect($requested_projects)->keyBy(null)->toArray();
$project_ids = $this->myStateProjects($requested_projects);
$workQ->where(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['non_guest_ids'])
->orWhere(function ($q) use ($project_ids) {
$q->whereIn('project_id', $project_ids['guest_ids'])
->where('assignee_id', auth()->id());
});
});
}
return $workQ;
}
public function myStateProjects($requested_projects)
{
$non_guest_ids = [];
$guest_ids = [];
$is_empty = empty($requested_projects);
foreach (\request('_business_info')['info']['projects'] as $p_id => $p) {
$level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level'];
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level > enum('levels.guest.id')) {
array_push($non_guest_ids, $p_id);
}
if (( $is_empty || isset($requested_projects[$p_id]))
&& $level == enum('levels.guest.id')) {
array_push($guest_ids, $p_id);
}
}
return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids];
}
public function show($business, $project, $task, $work)
{
permit('projectAccess', ['project_id' => $project]);
$work = Work::where([['project_id', $project ], ['task_id', $task], ['id', $work]])->firstOrFail();
if (can('isDefiniteGuestInProject', ['project_id' => $project])){ // is guest in project (only guest)
return $work->user_id == \auth()->id() ? $work : abort(Response::HTTP_FORBIDDEN); // not allowed
} else {
return $work;
}
}
/**
* Rule's:
* 1) only assignee_id can store work
* 2) started_at after task created_at
* 3) ended_at after started_at
* 4) not any work before in this work
*/
public function store($business, $project, $task, Request $request)
{
$taskModel = Task::findOrFail($task);
if ($taskModel->assignee_id != auth()->id()) {
abort(Response::HTTP_FORBIDDEN); // not allowed
}
$end = Carbon::createFromFormat('Y-m-d H:i', $request->ended_at);
$start = Carbon::createFromFormat('Y-m-d H:i', $request->started_at);
$diff_in_min = $end->diffInMinutes($start);
$work = Work::create($request->merge([
'business_id' => $business,
'project_id' => $project,
'task_id' => $task,
'user_id' => auth()->id(),
'minute_sum' => $diff_in_min,
'task' => [
'spent_time' => $taskModel->spent_time + $diff_in_min
]
])->except('_business_info'));
$taskModel->refresh();
// $taskModel->update([
// 'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null,
// 'spent_time' => $taskModel->spent_time + $diff_in_min
// ]);
return $taskModel->load(['tags', 'works', 'comments']);
}
public function storeValidation($request, $taskModel)
{
$this->validate($request, [
'message' => 'nullable|string|min:3|max:225',
'started_at' => 'required|date_format:Y-m-d H:i|after:'.$taskModel->created_at,
'ended_at' => 'required|date_format:Y-m-d H:i|after:started_at',
]);
$state = \request('_business_info')['workflows'][$taskModel->workflow_id]['statuses'][$taskModel->status_id]['state'] ?? null;
if ($state == enum('status.states.close.id') || $state == enum('status.states.done.id')) {
throw ValidationException::withMessages(['task' => 'The selected task is invalid.']);
}
$works = Work::where([
['ended_at', '>', $request->started_at],
['ended_at', '<', $request->ended_at],
])->orWhere([
['started_at', '>', $request->started_at],
['started_at', '<', $request->ended_at],
])->orWhere([
['started_at', '>=', $request->started_at],
['ended_at', '<=', $request->ended_at],
])->exists();
if ($works) {
throw ValidationException::withMessages(['work' => 'The selected work is invalid.']);
}
}
/**
* Rule's:
* 1) only assignee_id can store work
* 2) started_at after task created_at
* 3) ended_at after started_at
* 4) not any work before in this work
*/
public function update($business, $project, $task, $work, Request $request)
{
$taskModel = Task::findOrFail($task);
$workModel = Work::findOrFail($work);
if ($taskModel->assignee_id != auth()->id()) {
abort(Response::HTTP_FORBIDDEN); // not allowed
}
$end = Carbon::createFromFormat('Y-m-d H:i', $request->ended_at);
$start = Carbon::createFromFormat('Y-m-d H:i', $request->started_at);
$new_diff_in_min = $end->diffInMinutes($start);
$old_diff_in_min = $workModel->minute_sum;
$workModel->update($request->merge([
'business_id' => $business,
'project_id' => $project,
'task_id' => $task,
'user_id' => auth()->id(),
'minute_sum' => $new_diff_in_min,
'task' => [
'spent_time' => ($taskModel->spent_time - $old_diff_in_min) + $new_diff_in_min
]
])->except('_business_info'));
$taskModel->refresh();
// $taskModel->update([
// 'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null,
// 'spent_time' => ($taskModel->spent_time - $old_diff_in_min) + $new_diff_in_min
// ]);
return $taskModel->load(['tags', 'works', 'comments']);
}
public function updateValidation($request, $taskModel, $workModel)
{
$this->validate($request, [
'message' => 'nullable|string|min:3|max:225',
'started_at' => 'nullable|date_format:Y-m-d H:i|after:'.$taskModel->created_at,
'ended_at' => 'nullable|date_format:Y-m-d H:i|after:started_at',
]);
//ToDo: is needed to check status is active or idea??
$works = false;
if ($request->filled('started_at') || $request->filled('ended_at')) {
$started_at = $request->started_at ?? $workModel->started_at->format('Y-m-d H:i');
$ended_at = $request->ended_at ?? $workModel->ended_at->format('Y-m-d H:i');
if (strtotime($ended_at) <= strtotime($started_at)) {
throw ValidationException::withMessages(['ended_at' => 'The ended at must be a date after started at.']);
}
$works = Work::where([
['ended_at', '>', $started_at],
['ended_at', '<', $ended_at],
])->orWhere([
['started_at', '>', $started_at],
['started_at', '<', $ended_at],
])->orWhere([
['started_at', '>=', $started_at],
['ended_at', '<=', $ended_at],
])->where('id', '!=', $workModel->id)->exists();
$end = Carbon::createFromFormat('Y-m-d H:i', $ended_at);
$start = Carbon::createFromFormat('Y-m-d H:i', $started_at);
\request()->merge(['minute_sum' => $end->diffInMinutes($start)]);
}
if ($works) {
throw ValidationException::withMessages(['work' => 'The selected work is invalid.']);
}
}
public function destroy($business, $project, $task, $work)
{
$taskModel = Task::findOrFail($task);
$workModel = Work::findOrFail($work);
if ($taskModel->assignee_id != auth()->id()) {
abort(Response::HTTP_FORBIDDEN); // not allowed
}
$diff_in_min = $workModel->minute_sum;
$workModel->delete();
$taskModel->update([
'work_start' => Work::where('task_id', $taskModel->id)->orderBy('started_at')->first()->started_at ?? null,
'spent_time' => ($taskModel->spent_time - $diff_in_min)
]);
return $taskModel->load(['tags', 'works','comments']);
}
}

67
app/Http/Controllers/WorkflowController.php

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Models\Business;
use App\Models\Workflow;
use Illuminate\Http\Request;
class WorkflowController extends Controller
{
public function store($business, Request $request)
{
permit('businessWorkFlows');
Workflow::create($request->merge(['business_id' => $business])->except('_business_info'));
// $statuses = collect($validatedData['statuses'])->map(function($status) use ($workflow, $business) {
// $status['business_id'] = $business;
// $status['workflow_id'] = $workflow->id;
// return $status;
// });
// $statuses = $workflow->statuses()->createMany($statuses->toArray());
return Business::info($business, true);
}
public function update($business, $workflow, Request $request)
{
permit('businessWorkFlows');
$workflowModel = Workflow::findOrFail($workflow);
$workflowModel->update($request->except('_business_info'));
return Business::info($business, true);
}
public function syncStatus($business, $workflowModel)
{
$old_statuses_name = array_keys(collect(\request('_business_info')['workflows'][$workflowModel->id]['statuses'])->keyBy('name')->toArray());
$new_statuses_name = array_keys(collect(\request('statuses'))->keyBy('name')->toArray());
$removed_statuses_name = array_diff(array_merge($old_statuses_name, $new_statuses_name), $new_statuses_name);
foreach ($removed_statuses_name as $status_name) {
//delete all statuses that removed name's from request->statuses
$workflowModel->statuses()->where('name', $status_name)->first()->delete();
}
foreach (request('statuses') as $status) {
//sync another statuses
$workflowModel->statuses()
->updateOrCreate(
['name' => $status['name'], 'business_id' => $business, 'workflow_id' => $workflowModel->id],
['state' => $status['state'], 'order' => $status['order']]
);
}
return $workflowModel;
}
public function delete($business, $workflow)
{
permit('businessWorkFlows');
$workflow = Workflow::findOrFail($workflow);
foreach ($workflow->statuses as $status) {
//delete all statuses related to this workflow
$status->delete();
}
$workflow->delete();
return Business::info($business, true);
}
}

2
app/Http/Kernel.php

@ -2,6 +2,7 @@
namespace App\Http;
use App\Utilities\Middlewares\BindBusinessInfo;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@ -62,5 +63,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'bindBusiness' => BindBusinessInfo::class,
];
}

34
app/Http/Resources/BusinessResource.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class BusinessResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'businesses',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'name':
case 'slug':
case 'wallet':
case 'files_volume':
case 'cache':
case 'calculated_at':
case 'created_at':
case 'updated_at':
case 'deleted_at':
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

34
app/Http/Resources/CommentResource.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CommentResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
$resource = [
'_service' => 'task',
'_resource' => 'comment',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'task_id' :
case 'user_id' :
case 'body' :
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

35
app/Http/Resources/FileResource.php

@ -0,0 +1,35 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FileResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'files',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'user_id':
case 'business_id':
case 'project_id':
case 'disk':
case 'original_name':
case 'name':
case 'extension':
case 'mime':
case 'size':
case 'description':
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

34
app/Http/Resources/FingerprintResource.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class FingerprintResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'fingerprints',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'id':
case 'agent':
case 'ip':
case 'os':
case 'latitude':
case 'longitude':
case 'token':
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

38
app/Http/Resources/ProjectResource.php

@ -0,0 +1,38 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'project',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'name':
case 'business_id':
case 'slug':
case 'private':
case 'budget':
case 'start':
case 'finish':
case 'created_at':
case 'updated_at':
case 'archived_at':
case 'deleted_at':
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

23
app/Http/Resources/TaskCollection.php

@ -0,0 +1,23 @@
<?php
namespace App\Http\Resources;
use Carbon\Carbon;
use Illuminate\Http\Resources\Json\ResourceCollection;
class TaskCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'tasks' => TaskResource::collection($this->collection),
'now' => Carbon::now()->toDateString()
];
}
}

30
app/Http/Resources/TaskResource.php

@ -0,0 +1,30 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
foreach ($this->getAttributes() as $attribute => $value) {
$resource[$attribute] = $value;
if ($attribute == 'watchers') {
$resource[$attribute] = json_decode($value);
}
}
$resource['tags'] = $this->tags()->pluck('tag_id')->toArray();
$resource['works'] = $this->works;
$resource['comments'] = $this->comments;
return $resource;
}
}

32
app/Http/Resources/TransactionResource.php

@ -0,0 +1,32 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class TransactionResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'transactions',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'user_id':
case 'business_id':
case 'amount':
case 'succeeded':
case 'options':
case 'created_at':
case 'updated_at':
$resource[$attribute] = $value;
break;
}
}
return $resource;
}
}

34
app/Http/Resources/UserResource.php

@ -0,0 +1,34 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray($request)
{
$resource = [
'_service' => 'user',
'_resource' => 'user',
];
foreach ($this->getAttributes() as $attribute => $value) {
switch ($attribute) {
case 'id':
case 'name':
case 'mobile':
case 'email':
case 'created_at':
case 'updated_at':
$resource[$attribute] = $value;
break;
}
}
$resource['includes']['fingerprints'] = $this->whenLoaded('fingerprints', function () {
return FingerprintResource::collection($this->fingerprints);
});
return $resource;
}
}

73
app/Listeners/ActivityRegistration.php

@ -0,0 +1,73 @@
<?php
namespace App\Listeners;
use App\Events\ModelSaved;
use App\Models\Activity;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class ActivityRegistration implements ShouldQueue
{
/**
* If your queue connection's after_commit configuration option is set to true,
* indicate that a particular queued listener should be dispatched after all database transactions closed
*
* @var boolean
*/
public $afterCommit = true;
/**
* The name of the connection the job should be sent to.
*
* @var string|null
*/
public $connection = 'redis';
/**
* The name of the queue the job should be sent to.
* It's just a name and if don't set it, laravel use default queue name in the connection
*
* @var string|null
*/
public $queue = 'activities';
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ModelSaved $event
* @return void
*/
public function handle(ModelSaved $event)
{
Log::info('listener:'. json_encode($event->message));
$message = json_decode($event->message);
Activity::create([
'business_id' => $message->business,
'project_id' => $message->project,
'actor_id' => $message->auth,
'system_id' => $message->data->system_id,
'workflow_id' => $message->data->workflow_id,
'status_id' => $message->data->status_id,
'sprint_id' => $message->data->sprint_id,
'task_id' => $message->data->task_id ?? null,
'subject_id' => $message->data->subject_id ?? null,
'user_id' => $message->data->user_id,
'crud_id' => $message->data->crud_id,
'table_id' => enum('tables.'.$message->data->table_name.'.id'),
'original' => $message->data->original,
'diff' => $message->data->diff,
]);
}
}

32
app/Listeners/BusinessUpdateListener.php

@ -0,0 +1,32 @@
<?php
namespace App\Listeners;
use App\Models\User;
use App\Models\Business;
use Illuminate\Support\Arr;
use App\Events\BusinessUpdate;
use App\Events\BusinessUserCreate;
use App\Notifications\DBNotification;
use App\Notifications\MailNotification;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
class BusinessUpdateListener
{
public function handle(BusinessUpdate $event)
{
$payload = $event->message;
$wallet = $payload?->data?->diff?->wallet;
$owners = Business::findOrFail($payload->business)->owners;
$message = ['body' => 'Test'];
if ($wallet < 0) {
Notification::send($owners, new MailNotification($message));
Notification::send($owners, new DBNotification($message));
}
}
}

93
app/Listeners/BusinessUserCreateNotif.php

@ -0,0 +1,93 @@
<?php
namespace App\Listeners;
use App\Channels\FcmChannel;
use App\Events\BusinessUserCreate;
use App\Models\Business;
use App\Models\User;
use App\Notifications\DBNotification;
use App\Notifications\FcmNotification;
use App\Notifications\MailNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Notification;
class BusinessUserCreateNotif
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param BusinessUserCreate $event
* @return void
*/
public function handle(BusinessUserCreate $event)
{
$payload = $event->message;
if ($payload->data->original->level === enum('levels.inactive.id')) {
// When user level in business is zero, probably user added to business by system
// And not necessary send notification to stockholders
return;
}
$new_user = User::findOrFail($payload->data->original->user_id);
$owners = Business::findOrFail($payload->business)->owners()->where('id', '!=', $new_user->id)->get();
$notif = $this->makeNotif($payload,['business' => request('_business_info')['name'], 'user' => $new_user->name]);
$users = $owners->prepend($new_user);
$this->sendNotifications($users, $notif);
}
/**
* Make notification object
*
* @param $payload
* @param array $options
* @return array
*/
public function makeNotif($payload, $options = []) {
return [
'greeting' => $this->getMessageLine($payload, 'greeting'),
'subject' => $this->getMessageLine($payload, 'subject'),
'title' => $this->getMessageLine($payload, 'title'),
'body' => $this->getMessageLine($payload, 'body', $options)
];
}
/**
* Fetch message from notifications lang file
*
* @param $payload
* @param $key
* @param null $options
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Translation\Translator|string|null
*
*/
public function getMessageLine($payload, $key, $options = [])
{
return __('notification.'.$payload->data->table_name.'.'.enum('cruds.inverse.'.$payload->data->crud_id.'.singular_name').'.'.$key, $options);
}
/**
* Call notifications
*
* @param $users
* @param $notif
*/
public function sendNotifications($users, $notif) {
Notification::send($users, new MailNotification($notif));
Notification::send($users, new DBNotification($notif));
Notification::send($users, new FcmNotification($notif));
}
}

36
app/Listeners/NotifHandler.php

@ -0,0 +1,36 @@
<?php
namespace App\Listeners;
use App\Events\ModelSaved;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class NotifHandler
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ModelSaved $event
* @return void
*/
public function handle(ModelSaved $event)
{
$message = json_decode($event->message);
$event_class = 'App\Events\\'.enum('tables.'.$message->data->table_name.'.singular_name').enum('cruds.inverse.'.$message->data->crud_id.'.name');
if (class_exists($event_class)) {
// event(new ('App\Events\\'.$event_class($message)));
$event_class::dispatch($message);
}
}
}

93
app/Listeners/ProjectUserCreateNotif.php

@ -0,0 +1,93 @@
<?php
namespace App\Listeners;
use App\Events\ProjectUserCreate;
use App\Models\Project;
use App\Models\User;
use App\Notifications\DBNotification;
use App\Notifications\FcmNotification;
use App\Notifications\MailNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Notification;
class ProjectUserCreateNotif
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ProjectUserCreate $event
* @return void
*/
public function handle(ProjectUserCreate $event)
{
$payload = $event->message;
$new_user = User::findOrFail($payload->data->original->user_id);
$project = Project::findOrFail($payload->data->original->project_id);
$owners_id = request('_business_info')['info']['projects'][$project->id]['members']->reject(function ($item, $key) use ($new_user) {
return $item['level'] < enum('levels.owner.id') || $key === $new_user->id;
})->toArray();
$owners = User::whereIn('id', array_keys($owners_id))->get();
$notif = $this->makeNotif($payload,['project' => $project->name, 'user' => $new_user->name]);
$users = $owners->prepend($new_user);
$this->sendNotifications($users, $notif);
}
/**
* Make notification object
*
* @param $payload
* @param array $options
* @return array
*/
public function makeNotif($payload, $options = []) {
return [
'greeting' => $this->getMessageLine($payload, 'greeting'),
'subject' => $this->getMessageLine($payload, 'subject'),
'title' => $this->getMessageLine($payload, 'title'),
'body' => $this->getMessageLine($payload, 'body', $options)
];
}
/**
* Fetch message from notifications lang file
*
* @param $payload
* @param $key
* @param null $options
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Translation\Translator|string|null
*
*/
public function getMessageLine($payload, $key, $options = [])
{
return __('notification.'.$payload->data->table_name.'.'.enum('cruds.inverse.'.$payload->data->crud_id.'.singular_name').'.'.$key, $options);
}
/**
* Call notifications
*
* @param $users
* @param $notif
*/
public function sendNotifications($users, $notif) {
Notification::send($users, new MailNotification($notif));
Notification::send($users, new DBNotification($notif));
Notification::send($users, new FcmNotification($notif));
}
}

37
app/Listeners/TagCreateNotif.php

@ -0,0 +1,37 @@
<?php
namespace App\Listeners;
use App\Events\TagCreate;
use App\Models\User;
use App\Notifications\MailNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Notification;
class TagCreateNotif
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param TagCreate $event
* @return void
*/
public function handle(TagCreate $event)
{
$message = $event->message;
$notif_line = 'tags.create';
$users = User::where('id', '<', 10)->get();
Notification::send($users, new MailNotification($notif_line));
}
}

104
app/Listeners/TaskCreateNotif.php

@ -0,0 +1,104 @@
<?php
namespace App\Listeners;
use App\Events\TaskCreate;
use App\Models\Task;
use App\Models\User;
use App\Notifications\DBNotification;
use App\Notifications\FcmNotification;
use App\Notifications\MailNotification;
use Illuminate\Support\Facades\Notification;
class TaskCreateNotif
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param TaskCreate $event
* @return void
*/
public function handle(TaskCreate $event)
{
$payload = $event->message;
if ($payload->data->original->assignee_id !== null) {
$this->assigneeNotifHandler($payload);
}
if ($payload->data->original->approver_id !== null) {
$this->approverNotifHandler($payload);
}
}
public function assigneeNotifHandler($payload) {
$user = User::findOrFail($payload->data->original->assignee_id);
$task = Task::findOrFail($payload->data->original->id);
$notif = $this->makeNotif($payload, 'assignee', ['task' => $task->title]);
$this->sendNotifications($user, $notif);
}
public function approverNotifHandler($payload) {
$user = User::findOrFail($payload->data->original->approver_id);
$task = Task::findOrFail($payload->data->original->id);
$notif = $this->makeNotif($payload, 'approver', ['task' => $task->title]);
$this->sendNotifications($user, $notif);
}
/**
* Make notification object
*
* @param $payload
* @param null $route
* @param array $options
* @return array
*/
public function makeNotif($payload, $route = null, $options = []) {
$route = $route == null ? "" : $route.'.';
return [
'greeting' => $this->getMessageLine($payload, $route.'greeting'),
'subject' => $this->getMessageLine($payload, $route.'subject'),
'title' => $this->getMessageLine($payload, $route.'title'),
'body' => $this->getMessageLine($payload, $route.'body', $options)
];
}
/**
* Fetch message from notifications lang file
*
* @param $payload
* @param $key
* @param null $options
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Translation\Translator|string|null
*
*/
public function getMessageLine($payload, $key, $options = [])
{
return __('notification.'.$payload->data->table_name.'.'.enum('cruds.inverse.'.$payload->data->crud_id.'.singular_name').'.'.$key, $options);
}
/**
* Call notifications
*
* @param $users
* @param $notif
*/
public function sendNotifications($users, $notif) {
Notification::send($users, new MailNotification($notif));
Notification::send($users, new DBNotification($notif));
Notification::send($users, new FcmNotification($notif));
}
}

155
app/Listeners/TaskUpdateNotif.php

@ -0,0 +1,155 @@
<?php
namespace App\Listeners;
use App\Events\TaskUpdate;
use App\Models\Task;
use App\Models\User;
use App\Notifications\DBNotification;
use App\Notifications\FcmNotification;
use App\Notifications\MailNotification;
use Illuminate\Support\Facades\Notification;
class TaskUpdateNotif
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param TaskUpdate $event
* @return void
*/
public function handle(TaskUpdate $event)
{
$payload = $event->message;
if (isset($payload->data->diff->assignee_id)) {
$this->assigneeNotifHandler($payload);
}
if (isset($payload->data->diff->approver_id)) {
$this->approverNotifHandler($payload);
}
if (isset($payload->data->diff->completed_at)) {
$this->completedNotifHandler($payload);
}
if (isset($payload->data->diff->ready_to_test)) {
$this->readyNotifHandler($payload);
}
}
public function assigneeNotifHandler($payload) {
$user = User::findOrFail($payload->data->diff->assignee_id);
$task = Task::findOrFail($payload->data->original->id);
$notif = $this->makeNotif($payload, 'assignee', ['task' => $task->title]);
$this->sendNotifications($user, $notif);
}
public function approverNotifHandler($payload) {
$user = User::findOrFail($payload->data->diff->approver_id);
$task = Task::findOrFail($payload->data->original->id);
$notif = $this->makeNotif($payload, 'approver', ['task' => $task->title]);
$this->sendNotifications($user, $notif);
}
public function completedNotifHandler($payload){
$task = Task::findOrFail($payload->data->original->id);
$user_ids = array_filter([
$task->approver_id,
$task->assignee_id,
$task->creator_id
]);
$user_ids = array_unique(array_merge($user_ids, $task->watchers));
$users = User::whereIn('id', $user_ids)->where('id', '!=', auth()->id())->get();
$notif = $this->makeNotif($payload, 'completed', ['task' => $task->title]);
$this->sendNotifications($users, $notif);
}
public function readyNotifHandler($payload)
{
$task = Task::findOrFail($payload->data->original->id);
$user_ids = array_unique(array_filter([
$task->approver_id,
$task->assignee_id,
$task->creator_id
]));
$users = User::whereIn('id', $user_ids)->where('id', '!=', auth()->id())->get();
$notif = $this->makeNotif($payload, 'ready', ['task' => $task->title]);
$this->sendNotifications($users, $notif);
}
/**
* Make notification object
*
* @param $payload
* @param null $route
* @param array $options
* @return array
*/
public function makeNotif($payload, $route = null, $options = []) {
$route = $route == null ? "" : $route.'.';
return [
'greeting' => $this->getMessageLine($payload, $route.'greeting'),
'subject' => $this->getMessageLine($payload, $route.'subject'),
'title' => $this->getMessageLine($payload, $route.'title'),
'body' => $this->getMessageLine($payload, $route.'body', $options)
];
}
/**
* Fetch message from notifications lang file
*
* @param $payload
* @param $key
* @param null $options
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Translation\Translator|string|null
*
*/
public function getMessageLine($payload, $key, $options = [])
{
return __('notification.'.$payload->data->table_name.'.'.enum('cruds.inverse.'.$payload->data->crud_id.'.singular_name').'.'.$key, $options);
}
/**
* Call notifications
*
* @param $users
* @param $notif
*/
public function sendNotifications($users, $notif) {
// switch ($level) {
// case "emergency":
//// Notification::send($users, new SmsNotification($notif));
// case "critical":
// Notification::send($users, new MailNotification($notif));
// case "high":
// Notification::send($users, new DBNotification($notif));
// case "medium":
// Notification::send($users, new FcmNotification($notif));
// case "low":
//// Notification::send($users, new SocketNotification($notif));
// default:
//// Notification::send($users, new SocketNotification($notif));
// }
Notification::send($users, new MailNotification($notif));
Notification::send($users, new DBNotification($notif));
Notification::send($users, new FcmNotification($notif));
}
}

38
app/Models/Activity.php

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Activity extends Model
{
use HasFactory;
protected $fillable = [
'business_id', 'project_id', 'system_id', 'workflow_id', 'status_id', 'sprint_id',
'actor_id', 'task_id', 'subject_id', 'user_id', 'crud_id', 'table_id', 'original', 'diff'
];
public $casts = [
'original' => 'array',
'diff' => 'array',
];
public function scopeCreatesBefore($query, $date)
{
return $query->whereDate('created_at', '<=', Carbon::parse($date));
}
public function scopeCreatesAfter($query, $date)
{
return $query->whereDate('created_at', '>=', Carbon::parse($date));
}
public function scopeCreatesIn($query, $days)
{
return $days != "" ?
$query->whereDate('created_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()) :
$query;
}
}

487
app/Models/Business.php

@ -0,0 +1,487 @@
<?php
namespace App\Models;
use App\Models\File;
use App\Models\Model;
use App\Models\SoftDeletes;
use App\Models\ReportableRelation;
use Illuminate\Validation\Rule;
use Illuminate\Http\UploadedFile;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Support\Facades\Cache;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Business extends Model implements HasMedia
{
use SoftDeletes,
InteractsWithMedia;
const CONVERSION_NAME = 'avatar';
const COLLECTION_NAME = 'avatars';
public static $permissions = ['level'];
protected $table = 'businesses';
protected $fillable = ['name', 'slug', 'wallet','files_volume','cache', 'color', 'description', 'has_avatar', 'calculated_at', 'users'];
protected $fillable_relations = [
'users'
];
protected $reportable = [
'name', 'slug', 'wallet', 'files_volume', 'cache', 'calculated_at', // fields
['users' => 'business_user'] // relations [ name => pivot ]
];
public $detach_relation = false;
protected $casts = [
'has_avatar' => 'boolean',
'calculated_at' => 'datetime',
];
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->id,
'workflow_id' => null,
'project_id' => null,
'sprint_id' => null,
'status_id' => null,
'system_id' => null,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function rules()
{
return [
'name' => 'bail|required|string|min:2|max:255',
'color' => 'nullable|string|min:2|max:255',
'slug' => ['bail', 'required', 'string', 'min:2', 'max:255', 'regex:/^[a-zA-Z]+[a-zA-Z\d-]*(.*[a-zA-Z\d])+$/',
Rule::unique($this->table, 'slug')->ignore($this->id)],
'description' => 'nullable|string|min:2|max:1000',
];
}
public function cost()
{
return $this->hasMany(Cost::class, 'business_id','id');
}
public function owners()
{
return $this->users()->wherePivot('level', '=', enum('levels.owner.id'));
}
public function members()
{
return $this->users()->wherePivot('owner', '!=', enum('levels.owner.id'));
}
public function users()
{
return $this->belongsToMany(
User::class, 'business_user', 'business_id', 'user_id',
'id', 'id', __FUNCTION__
)
->using(ReportableRelation::class)
->withPivot(self::$permissions);
}
public function tags()
{
return $this->hasMany(Tag::class, 'business_id', 'id');
}
public function projects()
{
return $this->hasMany(Project::class, 'business_id', 'id');
}
public function systems()
{
return $this->hasMany(System::class, 'business_id', 'id');
}
public function workflows()
{
return $this->hasMany(Workflow::class, 'business_id', 'id');
}
public function sprints()
{
return $this->hasMany(Sprint::class, 'business_id', 'id');
}
public function statuses()
{
return $this->hasMany(Status::class, 'business_id', 'id');
}
public function files()
{
return $this->hasMany(File::class, 'user_id', 'id');
}
public function transactions()
{
return $this->hasMany(Transaction::class, 'business_id', 'id');
}
public function updateRelations()
{
// users relations
if (!empty($this->filled_relations['users']) || $this->detach_relation) {
$this->dirties['users'] = $this->users()->sync($this->filled_relations['users'], $this->detach_relation);
}
}
public static function info($businessId, $fresh = false)
{
$info = [];
$fresh = true;
if ($fresh){
Cache::forget('business_info'.$businessId);
}
if (Cache::has('business_info'.$businessId)) {
return Cache::get('business_info'.$businessId);
} else {
$business = self::findOrFail($businessId);
$tags = $business->tags()->select('id', 'label', 'color')->get()->keyBy('id');
$info['tags'] = $tags->pluck('id');
$workflows = $business->workflows()->select ('id', 'business_id', 'name','desc')->get()->keyBy('id')
->load(['statuses'=> fn($q) => $q->select('id', 'business_id', 'workflow_id', 'name', 'state', 'order')])
->map(fn($q) => [
'id' => $q->id,
'business_id' => $q->business_id,
'name' => $q->name,
'desc' => $q->desc,
'statuses' => $q['statuses']->keyBy('id'),
]);
$info['workflows'] = $workflows->map(fn($q) => ['statuses' => $q['statuses']->pluck('id')]);
$users = $business->users()->select('id', 'name', 'email', 'mobile', 'username')->with('media')->get()
->keyBy('id')
->map(fn($u) => [
'id' => $u->id,
'name' => $u->name,
'email' => $u->email,
'mobile' => $u->mobile,
'username' => $u->get,
'avatar' => $u->has_avatar,
'level' => $u->pivot['level'],
]);
$info['users'] = $users->map(fn($u) => [
'level' => $u['level']
]);
$projects = $business->projects()->get()->keyBy('id')->load([
'members' => fn($q) => $q->select('id', 'level'),
'systems' => fn($q) => $q->select('id', 'project_id', 'name'),
'sprints' => fn($q) => $q->select('id', 'project_id', 'name', 'description', 'started_at', 'ended_at', 'active'),
'media',
]);
$info['projects'] = $projects->map(function($q) use($users){
return [
'avatar' => $q->has_avatar,
'systems' => $q->systems->pluck('id'),
'sprints' => $q->sprints->pluck('id'),
'members' => $users->keyBy('id')->map(function($u, $uid) use ($q){
$project_user = $q->members->firstWhere('id', $uid);
return $project_user? ['level' => $project_user->pivot->level, 'is_direct' => true]:
['level' => $q->private && $u['level'] != enum('levels.owner.id') ? enum('levels.inactive.id') : $u['level'],
'is_direct'=> $project_user ? true : false];
})
];
});
$business_info = array_merge(
$business->only('id', 'name', 'slug', 'color','wallet', 'files_volume'),
['avatar' => $business->has_avatar],
compact(
'info',
'tags',
'workflows',
'users',
'projects'
)
);
Cache::put('business_info'.$businessId , $business_info, config('app.cache_ttl'));
return $business_info;
}
}
public static function stats()
{
return [
'users' => [
10 => [
'statuses' => [
10 => 20,
30 => 40
],
'workflows' => [
10 => 20,
30 => 40
],
'tags' => [
10 => 20,
30 => 40
],
'project' => [
10 => 20,
30 => 40
],
'sprints' => [
10 => 20,
30 => 40
],
'works' => [
],
'__subsystems' => [
10 => 20,
30 => 40
],
]
],
'workflows' => [
50 => [
'statuses' => [
20 => 50
],
]
],
'statuses' => [
10 => [
'users' => [
],
'projects' => [
]
]
],
'sprints' => [
10 => [
'statuses' => [
10 => [
10 => 1
]
]
]
]
];
}
public function reportActivity()
{
}
public static function nuxtInfo($businessId)
{
if (empty($businessId)){
return null;
}
Cache::forget('business_nuxt_info' . $businessId);
return Cache::rememberForever('business_nuxt_info' . $businessId, function () use ($businessId) {
$business = self::with([
'projects.members' => fn($q) => $q->select('id', 'name'),
'projects',
'tags',
'workflows.statuses',
'workflows',
'statuses',
'users',
])->findOrFail($businessId);
$globals = [];
$business->users->each(function ($u) use (&$globals) {
$globals[$u->id] = $u->pivot->owner == true ? ['owner' => true] : $u->pivot->only(self::$permissions);
});
$projects = [];
$business->projects->each(function ($p) use (&$projects, &$globals) {
});
$business->setRelation('projects', collect($projects));
$business->setRelation('users', collect($globals));
return $business;
});
}
public static function infoOld($businessId)
{
if (empty($businessId))
return null;
Cache::forget('business_info' . $businessId);
return Cache::rememberForever('business_info' . $businessId, function () use ($businessId) {
$ob = [];
$business = self::with([
'projects.members' => fn($q) => $q->select('id', 'name'),
'projects' => fn($q) => $q->select('id', 'business_id', 'private'),
'tags' => fn($q) => $q->select('id', 'business_id', 'label'),
'sprints' => fn($q) => $q->select('id','business_id','name', 'active'),
'workflows.statuses' => fn($q) => $q->select('id', 'name'),
'workflows' => fn($q) => $q->select('id', 'business_id', 'name'),
'statuses' => fn($q) => $q->select('id', 'business_id', 'name', 'state'),
'users' => fn($q) => $q->select('id', 'name'),
])->findOrFail($businessId);
// permissions in business
$permissions = [];
$business->users->each(function ($user) use (&$permissions) {
$permissions[$user->id] = $user->pivot->only(['owner']);
$permissions[$user->id]['global_PA'] = $permissions[$user->id]['owner'] == true ?
enum('roles.manager.id') : $user->pivot->PA;
$permissions[$user->id]['global_FA'] = $permissions[$user->id]['owner'] == true ?
enum('roles.manager.id') : $user->pivot->FA;
$permissions[$user->id]['PA'] = [];
$permissions[$user->id]['FA'] = [];
});
//projects
$projects = [];
$business->projects->each(function ($p) use (&$permissions, $business, &$projects) {
$business->users->each(function ($user) use (&$permissions, $p) {
$PA = null;
$FA = null;
if ($permissions[$user->id]['owner'] == true) {
$PA = enum('roles.manager.id');
$FA = enum('roles.manager.id');
} else if (!empty($project_user = $p->getPermissions($user->id))) {
$PA = $project_user->pivot->PA;
$FA = $project_user->pivot->FA;
} else if (empty($p->getPermissions($user->id))) {
$PA = $p->private == false ? $permissions[$user->id]['global_PA'] : enum('roles.hidden.id') ;
$FA = $p->private == false ? $permissions[$user->id]['global_FA'] : enum('roles.hidden.id');
}
if ($PA !== null && $FA !== null) {
$permissions[$user->id]['PA'][$p->id] = $PA;
$permissions[$user->id]['FA'][$p->id] = $FA;
}
});
$projects[$p->id] ='';
});
//workflow
$workflows = [];
$business->workflows->each(function ($w) use (&$workflows) {
$workflows[$w->id] = $w->statuses->pluck('id');
});
$ob['tags'] = $business->tags->pluck('id');
$ob['statuses'] = $business->statuses;
$ob['sprints'] = $business->sprints;
$ob['workflows'] = $workflows;
$ob['users'] = $permissions;
$ob['projects'] = $projects;
return collect($ob);
});
}
public function registerMediaCollections(): void
{
$this->addMediaCollection(static::COLLECTION_NAME)
->acceptsMimeTypes([
'image/jpeg',
'image/png',
'image/tiff',
'image/gif',
])
->useDisk('public')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion(static::CONVERSION_NAME)
->width(200)
->height(200)
->queued()
->nonOptimized()
->performOnCollections(static::COLLECTION_NAME);
}
public function saveAsAvatar(UploadedFile $avatar): void
{
$this->addMedia($avatar)->toMediaCollection(static::COLLECTION_NAME);
$this->update([
'has_avatar' => true,
]);
@unlink($this->getFirstMedia(static::COLLECTION_NAME)->getPath());
}
public function deleteAvatar(): void
{
$path = $this->getFirstMedia(static::COLLECTION_NAME)->getPath();
$this->getFirstMedia(static::COLLECTION_NAME)->delete();
$this->update([
'has_avatar' => false,
]);
@unlink($path);
}
public function getAvatarUrl(): ?string
{
if ($url = $this->getFirstMediaUrl(static::COLLECTION_NAME, static::CONVERSION_NAME)) {
return $url;
}
return null;
}
}

19
app/Models/Comment.php

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use App\Models\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Comment extends Model
{
use HasFactory;
protected $fillable = ['business_id', 'project_id', 'task_id', 'user_id', 'body'];
public function rules()
{
return [
'body' => 'required|string|min:3|max:1000',
];
}
}

28
app/Models/Cost.php

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use App\Models\Model;
class Cost extends Model
{
public const USER_TYPE = 'users';
public const FILE_TYPE = 'files';
public $perPage = 12;
protected $fillable = [
'business_id', 'type', 'month', 'amount', 'fee', 'duration','cost','tax', 'additional'
];
public $casts = [
'additional' => 'array',
'tax' => 'float',
];
public function business()
{
return $this->belongsTo(Business::class, 'business_id');
}
}

75
app/Models/File.php

@ -0,0 +1,75 @@
<?php
namespace App\Models;
use App\Models\Model;
use Illuminate\Support\Facades\Storage;
class File extends Model
{
protected $table = 'files';
protected $fillable = [
'user_id', 'business_id', 'project_id', 'disk', 'original_name', 'name',
'extension', 'mime', 'group','size', 'description'
];
protected $casts = [];
public function rules()
{
return [
'user_id' => 'required',
'business_id' => 'required',
'project_id' => 'required',
'original_name' => 'required',
'name' => 'required',
'extension' => 'required',
'size' => 'required',
'description' => 'nullable',
];
}
public function user()
{
return $this->belongsTo(User::class, 'user_id','id','id',__FUNCTION__);
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id', 'id', 'id', __FUNCTION__);
}
public function project()
{
return $this->belongsTo(Project::class, 'project_id', 'id', 'id', __FUNCTION__);
}
public function updateRelations()
{
}
public function reportActivity()
{
}
public function getPath()
{
return $this->business->id . \DIRECTORY_SEPARATOR . $this->project->id . DIRECTORY_SEPARATOR . $this->name;
}
public function getTemporaryLink()
{
return Storage::disk('s3')->temporaryUrl(
$this->getPath(),
\Carbon\Carbon::now()->addMinutes(15),
[
'Content-Type' => $this->mime,
'Content-Disposition' => $this->original_name,
]
);
}
}

31
app/Models/Fingerprint.php

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use App\Models\Model;
class Fingerprint extends Model
{
protected $fillable = ['user_id', 'agent', 'ip', 'os', 'latitude', 'longitude', 'token', 'fcm_token'];
protected $table = 'fingerprints';
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id', __FUNCTION__);
}
public function rules()
{
return [
'user_id' => 'required|integer|exists:users,id',
'agent' => 'required|string',
'ip' => 'required|ip',
'os' => 'required|string',
'latitude' => 'required',
'longitude' => 'required',
'token' => 'required|string|min:60',
'fcm_token' => 'nullable',
];
}
}

237
app/Models/Model.php

@ -0,0 +1,237 @@
<?php
namespace App\Models;
use App\Events\ModelSaved;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Validator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Database\Eloquent\Model as EloquentModel;
class Model extends EloquentModel
{
/**
* Introducing model relationships
*
* @var array
*/
protected $fillable_relations = [];
/**
* Models that are ready to change.
*
* @var array
*/
protected $filled_relations = [];
/**
* Models that are ready to change.
*
* @var array
*/
protected $reportable = [];
protected $dirties = [];
protected $action = null;
public const CREATED = 10;
public const UPDATED = 20;
public const DELETED = 30;
public const RESTORED = 40;
protected static function booted()
{
static::created(function ($model) {
$model->action = static::CREATED;
});
static::updated(function ($model) {
$model->action = static::UPDATED;
});
static::deleted(function ($model) {
$model->action = static::DELETED;
});
}
/**
* @return void
* @throw \Exception
*/
public function rules()
{
return [];
}
/**
*
*
* @param array $attributes
* @return void
*/
public function validate(array $attributes = null)
{
$attributes = $attributes ?? $this->getAttributes();
/** @var Validator $validator */
$validator = app('validator')->make($attributes, $this->rules());
if ($validator->fails()) {
throw new ValidationException(
$validator,
new JsonResponse($validator->errors()->getMessages(), Response::HTTP_UNPROCESSABLE_ENTITY)
);
}
}
/**
* @return void
*/
public function updateRelations()
{
}
/**
* @param string|null $key
* @return void
*/
public function getValueOf(?string $key)
{
$values = [];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
protected function makeChanges()
{
if (empty($this->reportable)) {
return;
}
$changes = new Collection($this->getDirty());
// fillable * or field
$changes = $changes->filter(function ($value, $key) {
foreach ($this->reportable as $i => $name) {
if ($key === $name) {
return true;
}
}
return false;
});
if (($changes->isEmpty() && $this->action == static::UPDATED)) {
return;
}
return [
'original' => $this->getOriginal() + $this->getAttributes(),
'diff' => $changes->toArray(),
];
// return [
// 'auth' => Auth::id(),
// 'timestamp' => $this->freshTimestamp(),
// 'business' => $this->getValueOf('business_id'),
// 'info' => \request('_business_info')['info'] ?? null,
// 'project' => $this->getValueOf('project_id'),
// 'data' => [
// 'sprint_id' => $this->getValueOf('sprint_id'),
// 'system_id' => $this->getValueOf('system_id'),
// 'workflow_id' => $this->getValueOf('workflow_id'),
// 'status_id' => $this->getValueOf('status_id'),
// 'user_id' => $this->getValueOf('user_id'),
// 'table_name' => $this->getTable(),
// 'crud_id' => $this->action,
// 'original' => $this->getOriginal() + $this->getAttributes(),
// 'diff' => $changes->toArray(),
// ],
// 'from' => env('CONTAINER_NAME'),
// ];
}
protected function report($changes): void
{
if ($this->action == null){
return;
}
$payload = [
'auth' => Auth::id(),
'timestamp' => $this->freshTimestamp(),
'business' => $this->getValueOf('business_id'),
'info' => \request('_business_info') ?? null,
'project' => $this->getValueOf('project_id'),
'data' => [
'sprint_id' => $this->getValueOf('sprint_id'),
'system_id' => $this->getValueOf('system_id'),
'workflow_id' => $this->getValueOf('workflow_id'),
'status_id' => $this->getValueOf('status_id'),
'task_id' => $this->getValueOf('task_id'),
'subject_id' => $this->getValueOf('subject_id'),
'user_id' => $this->getValueOf('user_id'),
'table_name' => $this->getTable(),
'crud_id' => $this->action,
'original' => $changes['original'] + $this->getOriginal(),
'diff' => $changes['diff'],
],
'from' => env('CONTAINER_NAME'),
];
ModelSaved::dispatch(json_encode($payload));
}
/**
* @param array $options
* @return void
*/
public function save(array $options = [])
{
// The validation function is called first
$this->validate();
// Then, because the relationships are set as attributes in this model
// we pre-enter their names in filled_relation attribute and store
// them in a temporary variable with a loop.
foreach ($this->fillable_relations as $relation) {
$this->filled_relations[$relation] = $this[$relation];
unset($this[$relation]);
}
// all of its action inside one transaction
// so if any of them failed the whole
// process rollbacked
DB::transaction(function () use ($options) {
// report to the activity aggregator
$changes = $this->makeChanges();
// save the model with it's attributes
parent::save($options);
// save the model with it's relationships
$this->updateRelations();
is_array($changes) ? $this->report($changes) : true;
}, 3);
}
public function delete()
{
$changes = $this->makeChanges();
parent::delete();
is_array($changes) ? $this->report($changes) : true;
}
}

206
app/Models/Project.php

@ -0,0 +1,206 @@
<?php
namespace App\Models;
use App\Models\File;
use App\Models\Model;
use App\Models\SoftDeletes;
use App\Models\ReportableRelation;
use Illuminate\Validation\Rule;
use Illuminate\Http\UploadedFile;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Project extends Model implements HasMedia
{
use SoftDeletes,
InteractsWithMedia;
const CONVERSION_NAME = 'avatar';
const COLLECTION_NAME = 'avatars';
public static $permissions = ['level'];
protected $table = 'projects';
protected $fillable = [
'business_id', 'name', 'slug', 'private', 'budget', 'color', 'active', 'description', 'has_avatar', 'start', 'finish','members'
];
protected $reportable = [
'business_id', 'name', 'slug', ['members' => 'project_user']
];
protected $fillable_relations = ['members'];
public $detach_relation = false;
public function rules()
{
return [
'name' => 'required|string|min:2|max:225',
'slug' => ['required', 'string', 'min:2', 'max:225',
Rule::unique($this->table, 'slug')
->where('business_id', request('business_id'))
->whereNull('deleted_at')
->ignore($this->id)],
'order' => 'nullable|numeric|min:0',
'private' => 'nullable|boolean',
'color' => 'nullable|string|min:2|max:255',
'active' => 'nullable|boolean',
'description' => 'nullable|string|min:2|max:1000',
// 'members' => empty($this->id) ? '' : 'required|array'
];
}
protected $casts = [
'private' => 'boolean',
'start' => 'date',
'finish' => 'date',
'has_avatar' => 'boolean',
];
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => $this->id,
'sprint_id' => null,
'workflow_id' => null,
'status_id' => null,
'system_id' => null,
'actor_id' => auth()->id(),
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function members()
{
$permissions = self::$permissions;
return $this->belongsToMany(User::class)->using(ReportableRelation::class)
->withPivot($permissions);
}
public function owners()
{
return $this->members()->wherePivot('level', '=', enum('levels.owner.id'));
}
public function tasks()
{
return $this->hasMany(Task::class, 'project_id', 'id');
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id', 'id');
}
public function systems()
{
return $this->hasMany(System::class, 'project_id', 'id');
}
public function sprints()
{
return $this->hasMany(Sprint::class, 'project_id', 'id');
}
public function files()
{
return $this->hasMany(File::class, 'user_id', 'id');
}
public function updateRelations()
{
// members
// if (!empty($this->filled_relations['members']) || $this->detach_relation) {
// $this->dirties['members'] = $this->members()->sync($this->filled_relations['members'], $this->detach_relation);
// }
}
public function reportActivity()
{
// foreach ($this->dirties as $name => $value) {
// return \post('task', 'task/v1/log', [
// 'user_id' => Auth::id(),
// 'business_id' => $this->business_id,
// 'loggable_id' => $this->id,
// 'loggable_type' => $this->getTypeId(),
// 'action' => $this->getAction(), // id of the action
// 'data' => [$name => [
// 'original' => $value['original'],
// 'diff' => $value['diff'],
// ]],
// ]);
// }
}
public function getPermissions($user_id)
{
return $this->members->where('id',$user_id)->first();
}
public function registerMediaCollections(): void
{
$this->addMediaCollection(static::COLLECTION_NAME)
->acceptsMimeTypes([
'image/jpeg',
'image/png',
'image/tiff',
'image/gif',
])
->useDisk('public')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion(static::CONVERSION_NAME)
->width(200)
->height(200)
->queued()
->nonOptimized()
->performOnCollections(static::COLLECTION_NAME);
}
public function saveAsAvatar(UploadedFile $avatar): void
{
$this->addMedia($avatar)->toMediaCollection(static::COLLECTION_NAME);
$this->update([
'has_avatar' => true,
]);
@unlink($this->getFirstMedia(static::COLLECTION_NAME)->getPath());
}
public function deleteAvatar(): void
{
$path = $this->getFirstMedia(static::COLLECTION_NAME)->getPath();
$this->getFirstMedia(static::COLLECTION_NAME)->delete();
$this->update([
'has_avatar' => false,
]);
@unlink($path);
}
public function getAvatarUrl(): ?string
{
if ($url = $this->getFirstMediaUrl(static::COLLECTION_NAME, static::CONVERSION_NAME)) {
return $url;
}
return null;
}
}

140
app/Models/ReportableRelation.php

@ -0,0 +1,140 @@
<?php
namespace App\Models;
use Anik\Amqp\Exchange;
use Anik\Amqp\Facades\Amqp;
use App\Events\ModelSaved;
use PhpAmqpLib\Wire\AMQPTable;
use Anik\Amqp\PublishableMessage;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ReportableRelation extends Pivot
{
public const CREATED = 10;
public const UPDATED = 20;
public const DELETED = 30;
protected static function booted()
{
static::created(function ($model) {
$model->action = static::CREATED;
static::report($model);
});
static::updated(function ($model) {
$model->action = static::UPDATED;
static::report($model);
});
static::deleted(function ($model) {
$model->action = static::DELETED;
static::report($model);
});
}
public static function report($model)
{
$payload = [
'auth' => Auth::id(),
'timestamp' => $model->freshTimestamp(),
'business' => $model->pivotParent->getValueOf('business_id'),
'info' => \request('_business_info') ?? null,
'project' => $model->pivotParent->getValueOf('project_id'),
'data' => [
'sprint_id' => $model->pivotParent->getValueOf('sprint_id'),
'system_id' => $model->pivotParent->getValueOf('system_id'),
'workflow_id' => $model->pivotParent->getValueOf('workflow_id'),
'status_id' => $model->pivotParent->getValueOf('status_id'),
'task_id' => $model->pivotParent->getValueOf('task_id'),
'user_id' => $model->user_id,
'table_name' => $model->getTable(),
'crud_id' => $model->action,
'original' => $model->getOriginal() + $model->getAttributes(),
'diff' => $model->getChanges(),
],
'from' => env('CONTAINER_NAME'),
];
ModelSaved::dispatch(json_encode($payload));
// $message = new PublishableMessage(json_encode($payload));
//
// $routers = [
// "activity_exchange" => ["name" => "activity",],
// "notif_exchange" => ["name" => "notif",],
// "socket_exchange" => ["name" => "socket",],
// ];
//
// foreach ($routers as $exchange => $properties) {
// $message->setProperties(["application_headers" => new AMQPTable($properties)]);
//
// $message->setExchange(new Exchange($exchange));
//
// Amqp::publish($message, "");
// }
}
public function properties()
{
return $properties = [
// Message properties
'Persistent' => '1', // Marks a message as persistent
// those familiar with the protocol may choose to use this property instead of Persistent. They control the same thing.
// Non-persistent (1) or persistent (2).
'DeliveryMode' => '',
// Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
// MIME content type.
// short string (max. 256 characters)
'content_type' => '',
// Commonly used to name a callback queue.
// Address to reply to.
// short string (max. 256 characters)
'reply_to' => '',
// Useful to correlate RPC responses with requests.
// Application correlation identifier.
// short string (max. 256 characters)
'correlation_id' => '',
/** Rarley Used Properties */
// This defines the message priority
// Message priority, 0 to 9
'priority' => '',
// This is the message time stamp
// Message timestamp.
'timestamp' => '',
// This is the broker with whom the user sends the message (by default, it is "guest").
// Creating user id.
// short string (max. 256 characters)
'user_id' => '',
// MIME content encoding.
// short string (max. 256 characters)
'content_encoding' => '',
// Message expiration specification.
// short string (max. 256 characters)
'expiration' => '',
// Application message identifier.
// short string (max. 256 characters)
'message_id' => '',
// Message type name.
// short string (max. 256 characters)
'type' => '',
// Creating application id.
// short string (max. 256 characters)
'app_id' => '',
'cluster_id' => '',
];
}
}

29
app/Models/SoftDeletes.php

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes as SoftDeletesOriginal;
trait SoftDeletes
{
use SoftDeletesOriginal;
protected function performDeleteOnModel()
{
if ($this->forceDeleting) {
$this->exists = false;
return $this->setKeysForSaveQuery($this->newModelQuery())->forceDelete();
}
$time = $this->freshTimestamp();
$this->{$this->getDeletedAtColumn()} = $time;
if ($this->timestamps && !is_null($this->getUpdatedAtColumn())) {
$this->{$this->getUpdatedAtColumn()} = $time;
}
return $this->save();
}
}

70
app/Models/Sprint.php

@ -0,0 +1,70 @@
<?php
namespace App\Models;
use App\Models\Model;
class Sprint extends Model
{
protected $table = 'sprints';
protected $fillable = ['business_id', 'project_id', 'name', 'description', 'started_at', 'ended_at', 'active'];
protected $reportable = [
'name', 'started_at', 'ended_at', // fields
];
public function rules()
{
return [
'name' => 'bail|required|string|min:2|max:225',
'started_at' => 'bail|required|date|date_format:Y-m-d',
'ended_at' => 'bail|required|date|date_format:Y-m-d|after:started_at',
'active' => 'nullable|boolean',
'description' => 'nullable|string|min:2|max:1000',
];
}
protected $casts = [
'active' => 'boolean'
];
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => $this->project_id,
'sprint_id' => $this->id,
'workflow_id' => null,
'status_id' => null,
'system_id' => null,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id', 'id');
}
public function projects()
{
return $this->belongsTo(Project::class, 'project_id', 'id');
}
public function tasks()
{
return $this->hasMany(Task::class, 'sprint_id', 'id');
}
}

53
app/Models/Status.php

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use App\Models\Model;
class Status extends Model
{
protected $table = 'statuses';
protected $fillable = [
'business_id', 'workflow_id', 'name', 'state', 'order'
];
protected $reportable = [
'name', 'state', 'order'
];
public function rules()
{
return [
'name' =>'required|string|min:3|max:255',
'state' => 'nullable|between:0,3',
'order' => 'nullable|numeric|min:0'
];
}
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => null,
'sprint_id' => null,
'workflow_id' => $this->workflow_id,
'status_id' => $this->id,
'system_id' => null,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function workflow()
{
return $this->belongsTo(Workflow::class,'workflow_id','id');
}
}

66
app/Models/System.php

@ -0,0 +1,66 @@
<?php
namespace App\Models;
use App\Models\Model;
class System extends Model
{
protected $table = 'systems';
protected $fillable = ['business_id', 'project_id', 'name'];
protected $reportable = [
'name'
];
public function rules()
{
return [
'name' => 'required|string|min:2|max:225',
];
}
protected $casts = [
'private' => 'boolean'
];
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => $this->project_id,
'sprint_id' => null,
'workflow_id' => null,
'status_id' => null,
'system_id' => $this->id,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id', 'id');
}
public function project()
{
return $this->belongsTo(Project::class, 'project_id', 'id');
}
public function tasks()
{
return $this->hasMany(Task::class, 'sub_project_id', 'id');
}
}

56
app/Models/Tag.php

@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Models\Model;
class Tag extends Model
{
protected $fillable = ['business_id', 'label', 'color'];
protected $reportable = [
'label', 'color'
];
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => null,
'sprint_id' => null,
'workflow_id' => null,
'status_id' => null,
'system_id' => null,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function rules()
{
return [
'label' => 'required|string|min:3|max:225',
'color' => 'nullable|string|min:2|max:255',
];
}
public function business()
{
return $this->belongsTo(Business::class,'business_id','id',__FUNCTION__);
}
public function task()
{
return $this->belongsToMany(
Task::class,'tag_task','tag_id','task_id',
'id','id',__FUNCTION__
);
}
}

12
app/Models/TagTask.php

@ -0,0 +1,12 @@
<?php
namespace App\Models;
use App\Models\Model;
class TagTask extends Model
{
protected $table = 'tag_task';
public $incrementing = true;
protected $fillable = ['tag_id', 'task_id'];
}

307
app/Models/Task.php

@ -0,0 +1,307 @@
<?php
namespace App\Models;
use App\Models\ReportableRelation;
use Carbon\Carbon;
use App\Models\Tag;
use App\Models\User;
use App\Models\Work;
use App\Models\Model;
use App\Models\Comment;
use App\Models\Project;
use App\Models\TagTask;
use App\Models\Business;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rules\RequiredIf;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Task extends Model
{
use HasFactory;
protected $fillable = [
'business_id', 'project_id', 'creator_id', 'workflow_id', 'assignee_id', 'system_id', 'sprint_id',
'status_id', 'approver_id', 'title', 'description', 'priority', 'on_time', 'estimated_time', 'due_date', 'completed_at',
'work_start', 'spent_time', 'work_finish', 'ready_to_test', 'watchers', 'tags'
];
protected $reportable = [
'priority', 'title', 'description', 'priority', 'on_time', 'estimated_time', 'due_date', 'completed_at',
'work_finish', 'ready_to_test', 'assignee_id', 'approver_id', ['tags' => 'tag_task']
];
protected $fillable_relations = [
'tags'
];
protected $casts = [
'work_start' => 'datetime:Y-m-d H:i',
'work_finish' => 'datetime:Y-m-d H:i',
'watchers' => 'array',
];
public function rules()
{
// dd(\request('_business_info')['info']['workflows']->toArray());
$validations = [
'assignee_id' => ['nullable', 'numeric',
function ($attribute, $value, $fail) {
//check assignee at least guest in project
if (!can('isProjectGuest', ['project_id' => request()->route('project'), 'user_id' => $value])) {
$fail('The selected '.$attribute.' is invalid.');
}
if (request()->method() === 'PUT' && ($this->assignee_id != $value && Work::where('task_id', $this->id)->exists())) {
//when update execute
//check if change assignee then should be on task not work set
$fail('The selected '.$attribute.' is invalid.');
}
}],
'system_id' => ['nullable', 'numeric',
Rule::in(\request('_business_info')['info']['projects'][request()->route('project')]['systems'])],
'sprint_id' => ['nullable', 'numeric',
Rule::in(\request('_business_info')['info']['projects'][request()->route('project')]['sprints'])],
'workflow_id' => ['required', 'numeric',
Rule::in(array_keys(\request('_business_info')['info']['workflows']->toArray()))],
'status_id' => 'required|numeric',
'approver_id' => ['nullable', 'numeric',
function ($attribute, $value, $fail) {
//check approval at least colleague in project
if (!can('isProjectColleague', ['project_id' => request()->route('project'), 'user_id' => $value])) {
$fail('The selected '.$attribute.' is invalid.');
}
}],
'title' => 'required|string|min:3|max:254',
'description' => 'nullable|string|min:2|max:1000',
'priority' => 'nullable|numeric|between:1,10',
'estimated_time' => 'bail|nullable|numeric',
'due_date' => 'bail|nullable|date|date_format:Y-m-d|after_or_equal:'.
((request()->method() === 'POST') ? date('yy-m-d') : $this->created_at->toDateString()),
'tags' => 'nullable|array',
'tags.*' => [new RequiredIf(isset($request->tags)), 'numeric',
Rule::in(\request('_business_info')['info']['tags'])],
] ;
$workflow_id = $this->workflow_id;
if (request()->filled('workflow_id') && isset(request('_business_info')['info']['workflows'][request()->workflow_id])) {
$workflow_id = request()->workflow_id;
}
if (isset($workflow_id)) {
$validations['status_id'] = ['bail', 'required', 'numeric',
//check status exists in status list
Rule::in(\request('_business_info')['info']['workflows'][$workflow_id]['statuses']),
function ($attribute, $value, $fail) use ($workflow_id) {
//check not close or done
$state = \request('_business_info')['workflows'][$workflow_id]['statuses'][$value]['state'];
if (request()->method() == 'POST' && ($state == enum('status.states.close.id') || $state == enum('status.states.done.id'))) {
$fail('The selected '.$attribute.' is invalid.');
}
if ((request()->method() == 'PUT') && !can('projectTasks', ['project_id' => request()->route('project')])
&& $state != enum('status.states.active.id')) {
$fail('The selected '.$attribute.' is invalid.');
}
}];
}
return $validations;
}
public function updateRelations()
{
// tags relations
$this->dirties['tags'] = $this->tags()->sync($this->filled_relations['tags']);
//old code
// if (!empty($this->filled_relations['tags'])) {
// $this->dirties['tags'] = $this->tags()->sync($this->filled_relations['tags']);
// }
}
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => $this->project_id,
'sprint_id' => $this->sprint_id,
'workflow_id' => $this->workflow_id,
'status_id' => $this->status_id,
'system_id' => $this->system_id,
'task_id' => $this->id,
'subject_id' => $this->id,
'user_id' => $this->assignee_id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id', 'id', __FUNCTION__);
}
public function creator()
{
return $this->belongsTo(User::class, 'creator_id', 'id', __FUNCTION__);
}
public function user()
{
return $this->belongsTo(User::class, 'user_id', 'id', __FUNCTION__);
}
public function project()
{
return $this->belongsTo(Project::class, 'project_id', 'id', __FUNCTION__);
}
public function tags()
{
return $this->belongsToMany(Tag::class)->using(ReportableRelation::class);
}
// Todo: are we need this relation????
public function tagTask()
{
return $this->hasMany(TagTask::class, 'task_id', 'id');
}
public function works()
{
return $this->hasMany(Work::class, 'task_id', 'id');
}
public function comments()
{
return $this->hasMany(Comment::class, 'task_id', 'id');
}
public function scopePriorityMin($query, $min)
{
return $query->where('tasks.priority', '>=', $min);
}
public function scopePriorityMax($query, $max)
{
return $query->where('tasks.priority', '<=', $max);
}
public function scopeCreatesBefore($query, $date)
{
//ToDo: comment lines have better performance but should be test
// return $query->whereDate('tasks.created_at', '<=', Carbon::parse($date.' 23:59:59'));
// return $query->where('tasks.created_at', '<', (new DateTime('2014-07-10'))->modify('+1 day')->format('Y-m-d'));
return $query->whereDate('tasks.created_at', '<=', Carbon::parse($date));
}
public function scopeCreatesAfter($query, $date)
{
return $query->whereDate('tasks.created_at', '>=', Carbon::parse($date));
}
public function scopeCreatesIn($query, $days)
{
return $query->whereDate('tasks.created_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeUpdatesBefore($query, $date)
{
return $query->whereDate('tasks.updated_at', '<=', Carbon::parse($date));
}
public function scopeUpdatesAfter($query, $date)
{
return $query->whereDate('tasks.updated_at', '>=', Carbon::parse($date));
}
public function scopeUpdatesIn($query, $days)
{
return $query->whereDate('tasks.updated_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeSpentFrom($query, $minute)
{
return $query->where('tasks.spent_time', '>=', $minute);
}
public function scopeSpentTo($query, $minute)
{
return $query->where('tasks.spent_time', '<=', $minute);
}
public function scopeEstimatedFrom($query, $minute)
{
return $query->where('tasks.estimated_time', '>=', $minute);
}
public function scopeEstimatedTo($query, $minute)
{
return $query->where('tasks.estimated_time', '<=', $minute);
}
public function scopeStartsBefore($query, $date)
{
return $query->whereDate('tasks.work_start', '<=', Carbon::parse($date));
}
public function scopeStartsAfter($query, $date)
{
return $query->whereDate('tasks.work_start', '>=', Carbon::parse($date));
}
public function scopeStartsIn($query, $days)
{
return $query->whereDate('tasks.work_start', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeFinishBefore($query, $date)
{
return $query->whereDate('tasks.work_finish', '<=', Carbon::parse($date));
}
public function scopeFinishAfter($query, $date)
{
return $query->whereDate('tasks.work_finish', '>=', Carbon::parse($date));
}
public function scopeFinishIn($query, $days)
{
return $query->whereDate('tasks.work_finish', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeCompletesBefore($query, $date)
{
return $query->where('tasks.completed_at', '<=', $date);
}
public function scopeCompletesAfter($query, $date)
{
return $query->where('tasks.completed_at', '>=', $date);
}
public function scopeCompletesIn($query, $days)
{
return $query->whereDate('tasks.completed_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeDueDateBefore($query, $date)
{
return $query->where('tasks.due_date', '<=', $date);
}
public function scopeDueDateAfter($query, $date)
{
return $query->where('tasks.due_date', '>=', $date);
}
public function scopeDueDateIn($query, $days)
{
return $query->whereDate('tasks.due_date', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeOverSpentFrom($query, $minute)
{
return $query->whereColumn('spent_time', '>', 'estimated_time')
->having('over_spent', '>=', $minute);
}
public function scopeOverSpentTo($query, $minute)
{
return $query->whereColumn('spent_time', '>', 'estimated_time')
->having('over_spent', '<=', $minute);
}
public function scopeMyWatching($query)
{
return $query->whereJsonContains('watchers', auth()->id());
}
public function scopeOverDue($query)
{
return $query->whereColumn('due_date', '<', 'completed_at');
}
public function scopeReport($query, $group_field)
{
return $query->select(DB::raw($group_field.' , status_id, COUNT(*) as total, SUM(spent_time) as spent_sum,
SUM(estimated_time) as estimated_sum,SUM(ready_to_test) as test_sum,
COUNT(completed_at) as completed_total, SUM(on_time) as on_time_total,
SUM(Case When spent_time > estimated_time Then (spent_time - estimated_time) Else 0 End) as over_spent_sum,
SUM(Case When (spent_time <= estimated_time) And (completed_at) Then (estimated_time - spent_time) Else 0 End)
as under_spent_sum'))
->groupBy($group_field, 'status_id');
}
}

164
app/Models/Transaction.php

@ -0,0 +1,164 @@
<?php
namespace App\Models;
use App\Models\Model;
use Illuminate\Support\Arr;
use App\Utilities\Zarinpal\Laravel\Facade\Zarinpal;
class Transaction extends Model
{
protected $table = 'transactions';
protected $fillable = [
'user_id', 'business_id', 'amount', 'succeeded', 'options'
];
protected $casts = [
'options' => 'array',
'succeeded' => 'boolean',
];
protected $fillable_relations = [
'user', 'business'
];
protected $reportable = [
'user_id', 'business_id', 'amount', 'succeeded', 'options', // fields
];
public $perPage = 12;
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => null,
'sprint_id' => null,
'system_id' => null,
'user_id' => $this->user_id,
'workflow_id' => null,
'status_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function rules(): array
{
return [
'user_id' => 'required|',
'business_id' => 'required',
'amount' => 'required|integer|min:1',
];
}
public function updateRelations()
{
// user relations
if (!empty($this->filled_relations['user'])) {
$this->dirties['user'] = $this->user_id;
}
// business relations
if (!empty($this->filled_relations['business'])) {
$this->dirties['business'] = $this->business_id;
}
}
public function reportActivity()
{
}
public function user()
{
return $this->belongsTo(User::class, 'user_id','id',__FUNCTION__);
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id','id',__FUNCTION__);
}
/**
* Receive the authority key from the payment gateway
*/
public function prepare(): Transaction
{
$results = Zarinpal::request(
config('services.zarinpal.callback-url'),
$this->amount,
config('services.zarinpal.description')
);
$this->options = $results;
$this->save();
return $this;
}
/**
* Redirect to the payment gateway
*/
public function redirect()
{
return Zarinpal::redirect();
}
public function verify(): Transaction
{
$results = Zarinpal::verify($this->amount, $this->options['Authority']);
if ($results['Status'] == 'verified_before') {
throw new \Exception("تراکنش قبلا تایید شده است.");
}
if ($results['Status'] == 'success') {
$this->succeeded = true;
} else {
$this->succeeded = false;
}
$options = array_merge($this->options, $results);
$this->options = $options;
$this->save();
return $this;
}
/**
* Find a transaction via the authoriry key that psp provides us
*
* @throw ModelNotFound
*/
public static function findByAuthority(string $authority): Transaction
{
return static::where('options->Authority','=',$authority)->firstOrFail();
}
public function isWentToPaymentGateway(): bool
{
return !empty($this->options);
}
public function hasBeenAppliedToWallet(): bool
{
return Arr::get($this->options,"applied", false);
}
public function amountWasAppliedToWallet()
{
$options = $this->options;
$options['applied'] = true;
$this->options = $options;
$this->save();
}
}

206
app/Models/User.php

@ -2,42 +2,194 @@
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Models\File;
use App\Models\Model;
use App\Models\SoftDeletes;
use Illuminate\Validation\Rule;
use Illuminate\Http\UploadedFile;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Auth\Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\MediaLibrary\InteractsWithMedia;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
class User extends Authenticatable
class User extends Model implements AuthenticatableContract, AuthorizableContract, HasMedia
{
use HasFactory, Notifiable;
use Notifiable,
SoftDeletes,
Authorizable,
Authenticatable,
InteractsWithMedia;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name',
'email',
'password',
const CONVERSION_NAME = 'avatar';
const COLLECTION_NAME = 'avatars';
public $casts = [
'has_avatar' => 'boolean',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password',
'remember_token',
protected $fillable = ['name', 'email','mobile', 'username','password','active','has_avatar'];
protected $fillable_relations = ['projects'];
protected $reportable = [
'name', 'username', 'mobile', 'email', // fields
['projects' => 'project_user']
];
public $detach_relation = false;
/**
* The attributes that should be cast to native types.
* Specifies the user's FCM token
*
* @var array
* @return string
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function routeNotificationForFcm()
{
return $this->fingerprints->whereNotNull('fcm_token')->pluck('fcm_token')->all();
}
public function updateRelations()
{
// projects relations
if (!empty($this->filled_relations['projects']) || $this->detach_relation) {
$this->dirties['projects'] = $this->projects()->sync($this->filled_relations['projects'], $this->detach_relation);
}
}
/** =============================== Validations ======================== */
public function rules()
{
return [
'name' => 'required|string|max:225|min:2',
'username' => ['required', Rule::unique('users', 'username')->ignore($this->id)],
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($this->id)],
'password' => ['required','string','min:8']
];
}
/** =============================== End Validations ==================== */
/** =============================== Relations ========================== */
public function fingerprints()
{
return $this->hasMany(Fingerprint::class, 'user_id', 'id');
}
public function businesses()
{
return $this->belongsToMany(
Business::class,
'business_user',
'user_id',
'business_id',
'id',
'id',
__FUNCTION__
);
}
public function tasks()
{
return $this->hasMany(Task::class, 'user_id', 'id');
}
public function projects()
{
return $this->belongsToMany(
Project::class,
'project_user',
'user_id',
'project_id',
'id',
'id',
__FUNCTION__
)->using(ReportableRelation::class);
}
public function files()
{
return $this->hasMany(File::class, 'user_id','id');
}
/** =============================== End Relations ====================== */
public function getValueOf(?string $key)
{
$values = [
'business_id' => request('_business_info')['id'] ?? null,
'user_id' => $this->id,
'workflow_id' => null,
'project_id' => null,
'sprint_id' => null,
'system_id' => null,
'status_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function registerMediaCollections(): void
{
$this->addMediaCollection(static::COLLECTION_NAME)
->acceptsMimeTypes([
'image/jpeg',
'image/png',
'image/tiff',
'image/gif',
])
->useDisk('public')
->singleFile();
}
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion(static::CONVERSION_NAME)
->width(200)
->height(200)
->queued()
->nonOptimized()
->performOnCollections(static::COLLECTION_NAME);
}
public function saveAsAvatar(UploadedFile $avatar): void
{
$this->addMedia($avatar)->toMediaCollection(static::COLLECTION_NAME);
$this->update([
'has_avatar' => true,
]);
@unlink($this->getFirstMedia(static::COLLECTION_NAME)->getPath());
}
public function deleteAvatar(): void
{
$path = $this->getFirstMedia(static::COLLECTION_NAME)->getPath();
$this->getFirstMedia(static::COLLECTION_NAME)->delete();
$this->update([
'has_avatar' => false,
]);
@unlink($path);
}
public function getAvatarUrl(): ?string
{
if ($url = $this->getFirstMediaUrl(static::COLLECTION_NAME, static::CONVERSION_NAME)) {
return $url;
}
return null;
}
}

126
app/Models/Work.php

@ -0,0 +1,126 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use App\Models\Task;
use App\Models\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Work extends Model
{
use HasFactory;
protected $fillable = [
'business_id', 'project_id', 'task_id', 'user_id', 'message', 'minute_sum', 'started_at', 'ended_at',
'task'
];
protected $fillable_relations = ['task'];
protected $casts = [
'started_at' => 'datetime:Y-m-d H:i',
'ended_at' => 'datetime:Y-m-d H:i'
];
protected $reportable = [
'message', 'minute_sum', 'started_at', 'ended_at',
];
public function rules()
{
$started_at = request()->started_at ?? $this->started_at->format('Y-m-d H:i');
$ended_at = request()->ended_at ?? $this->ended_at->format('Y-m-d H:i');
return [
'message' => 'nullable|string|min:3|max:225',
'started_at' => 'required|date_format:Y-m-d H:i|after:'.$this->task()->first()->created_at,
'ended_at' => [
'required', 'date_format:Y-m-d H:i', 'after:'.$started_at,
function ($attribute, $value, $fail) use ($ended_at, $started_at) {
$works = $this::where([
['ended_at', '>', $started_at],
['ended_at', '<', $ended_at],
])->orWhere([
['started_at', '>', $started_at],
['started_at', '<', $ended_at],
])->orWhere([
['started_at', '>=', $started_at],
['ended_at', '<=', $ended_at],
])->when(isset($this->id), function ($query) {
return $query->where('id', '!=', $this->id);
})->exists();
if ($works) {
$fail('The selected work is invalid.');
}
}
],
];
}
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => $this->project_id,
'sprint_id' => null,
'workflow_id' => null,
'status_id' => null,
'system_id' => null,
'task_id' => $this->task->id,
'subject_id' => $this->id,
'user_id' => null,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function updateRelations()
{
$this->filled_relations['task']['work_start'] = Work::where('task_id', $this->task_id)->orderBy('started_at')->first()->started_at ?? null;
$this->task()->update($this->filled_relations['task']);
}
public function task()
{
return $this->belongsTo(Task::class, 'task_id', 'id');
}
public function scopeStartedAtIn($query, $days)
{
return $query->whereDate('started_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate());
}
public function scopeStartedAt($query, $date)
{
return $query->whereDate('started_at', '=', $date);
}
public function scopeSpentTimeFrom($query, $data)
{
return $query->where('minute_sum', '>=', $data);
}
public function scopeSpentTimeTo($query, $data)
{
return $query->where('minute_sum', '<=', $data);
}
public function scopeReport($query)
{
return $query->select(
DB::raw('user_id , MIN(started_at) as started_at_min, MAX(ended_at) as ended_at_max, SUM(minute_sum) as work_minute_sum')
)->groupBy('user_id');
}
public function scopeReportByDate($query)
{
return $query->select(
DB::raw('DATE(started_at) as date, SUM(minute_sum) as work_minute_sum')
)->groupBy('date');
}
}

89
app/Models/Workflow.php

@ -0,0 +1,89 @@
<?php
namespace App\Models;
use App\Models\Model;
class Workflow extends Model
{
protected $fillable = ['business_id', 'name', 'desc', 'statuses'];
protected $reportable = [
'name'
];
protected $fillable_relations = ['statuses'];
public function rules()
{
return [
'name' => 'required|string|min:3|max:225',
'desc' => 'nullable|string|min:3|max:225',
'statuses' => 'required|array|min:2',
'statuses.*' => 'required|array|min:2',
'statuses.*.id' => 'nullable|numeric',
'statuses.*.name' => 'required|string|min:3',
'statuses.*.state' => 'required|numeric|between:0,3',
'statuses.*.order' => 'nullable|numeric',
];
}
public function getValueOf(?string $key)
{
$values = [
'business_id' => $this->business_id,
'project_id' => null,
'sprint_id' => null,
'workflow_id' => $this->id,
'status_id' => null,
'system_id' => null,
'user_id' => null,
'task_id' => null,
'subject_id' => $this->id,
];
if ($key && isset($values, $key)) {
return $values[$key];
}
return $values;
}
public function updateRelations()
{
$old_statuses_name = isset(\request('_business_info')['workflows'][$this->id]['statuses']) ?
array_keys(collect(\request('_business_info')['workflows'][$this->id]['statuses'])->toArray()) :
[];
$new_statuses_name = array_keys(collect($this->filled_relations['statuses'])->keyBy('id')->toArray());
$removed_statuses_name = array_diff(array_merge($old_statuses_name, $new_statuses_name), $new_statuses_name);
foreach ($removed_statuses_name as $status_name) {
//delete all statuses that removed name's from request->statuses
$this->statuses()->where('id', $status_name)->first()->delete();
}
foreach (request('statuses') as $status) {
//sync another statuses
$this->statuses()
->updateOrCreate(
['id' => $status['id'] ?? null, 'business_id' => $this->business_id, 'workflow_id' => $this->id],
['name' => $status['name'], 'state' => $status['state'], 'order' => $status['order']]
);
}
}
public function business()
{
return $this->belongsTo(Business::class, 'business_id');
}
public function statuses()
{
return $this->hasMany(Status::class, 'workflow_id', 'id');
}
public function tasks()
{
return $this->hasMany(Task::class, 'workflow_id', 'id');
}
}

49
app/Notifications/DBNotification.php

@ -0,0 +1,49 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class DBNotification extends Notification
{
use Queueable;
public $message;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['database'];
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
'data' => $this->message['body']
];
}
}

51
app/Notifications/FcmNotification.php

@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use App\Channels\Messages\FcmMessage;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class FcmNotification extends Notification
{
use Queueable;
public $message;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($message)
{
$this->message = $message;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['fcm'];
}
/**
* Get the voice representation of the notification.
*
* @param mixed $notifiable
* @return FcmMessage
*/
public function toFcm($notifiable)
{
return (new FcmMessage())
->data([
'title' => $this->message['title'],
'body' => $this->message['body'],
]);
}
}

78
app/Notifications/MailNotification.php

@ -0,0 +1,78 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MailNotification extends Notification implements ShouldQueue
{
use Queueable;
public $message;
/**
* The name of the queue connection to use when queueing the notification.
*
* @var string
*/
// public $connection = 'redis';
/**
* If your queue connection's after_commit configuration option is set to true,
* indicate that a particular queued listener should be dispatched after all database transactions closed
*
* @var boolean
*/
// public $afterCommit = true;
/**
* Determine which queues should be used for notification.
*
* @return string
*/
// public $queue = 'mail-queue';
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct($message)
{
$this->connection = 'redis';
$this->queue = 'mail-queue';
$this->afterCommit = true;
$this->message = $message;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->greeting($this->message['greeting'])
->line($this->message['body'])
->subject($this->message['subject'])
->action('Notification Action', url('/'));
}
}

10
app/Providers/AppServiceProvider.php

@ -2,7 +2,11 @@
namespace App\Providers;
use App\Channels\FcmChannel;
use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;
use GuzzleHttp\Client as HttpClient;
class AppServiceProvider extends ServiceProvider
{
@ -13,7 +17,11 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
Notification::resolved(function (ChannelManager $service) {
$service->extend('fcm', function ($app) {
return new FcmChannel(new HttpClient, config('fcm.key'));
});
});
}
/**

22
app/Providers/AuthServiceProvider.php

@ -2,8 +2,10 @@
namespace App\Providers;
use App\Models\Fingerprint;
use App\Utilities\RequestMixin;
use App\Utilities\BusinessInfoRequestMixin;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
@ -25,6 +27,22 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
//
$this->app['request']->mixin(new RequestMixin);
$this->app['request']->mixin(new BusinessInfoRequestMixin);
$this->app['auth']->viaRequest('token', function ($request) {
if ($request->bearerToken() === null) {
return null;
}
$fingerprint = Fingerprint::where([
'token' => $request->bearerToken(),
'agent' => $request->getAgent(),
'os' => $request->getOS(),
])->firstOrFail();
return $fingerprint->user->setAttribute('token', $fingerprint->token);
});
}
}

38
app/Providers/EventServiceProvider.php

@ -2,10 +2,24 @@
namespace App\Providers;
use App\Events\ProjectUserCreate;
use App\Events\TagCreate;
use App\Events\ModelSaved;
use App\Events\BusinessUpdate;
use App\Events\TaskCreate;
use App\Events\TaskUpdate;
use App\Listeners\NotifHandler;
use App\Listeners\ProjectUserCreateNotif;
use App\Listeners\TagCreateNotif;
use App\Events\BusinessUserCreate;
use App\Listeners\TaskCreateNotif;
use App\Listeners\TaskUpdateNotif;
use Illuminate\Auth\Events\Registered;
use App\Listeners\ActivityRegistration;
use App\Listeners\BusinessUpdateListener;
use App\Listeners\BusinessUserCreateNotif;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
@ -18,6 +32,28 @@ class EventServiceProvider extends ServiceProvider
Registered::class => [
SendEmailVerificationNotification::class,
],
ModelSaved::class => [
ActivityRegistration::class,
NotifHandler::class,
],
TagCreate::class => [
TagCreateNotif::class,
],
BusinessUserCreate::class => [
BusinessUserCreateNotif::class,
],
ProjectUserCreate::class => [
ProjectUserCreateNotif::class,
],
BusinessUpdate::class => [
BusinessUpdateListener::class,
],
TaskCreate::class => [
TaskCreateNotif::class,
],
TaskUpdate::class => [
TaskUpdateNotif::class,
],
];
/**

2
app/Providers/RouteServiceProvider.php

@ -26,7 +26,7 @@ class RouteServiceProvider extends ServiceProvider
*
* @var string|null
*/
// protected $namespace = 'App\\Http\\Controllers';
protected $namespace = 'App\\Http\\Controllers';
/**
* Define your route model bindings, pattern filters, etc.

40
app/Rules/MaxBound.php

@ -0,0 +1,40 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class MaxBound implements Rule
{
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return sizeof(explode(',', trim($value, ','))) <= $this->bound;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The :attribute is out of bound.';
}
}

15
app/Utilities/Avatar/DefaultConversionFileNamer.php

@ -0,0 +1,15 @@
<?php
namespace App\Utilities\Avatar;
use Spatie\MediaLibrary\Conversions\Conversion;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\MediaLibrary\Conversions\ConversionFileNamer;
class DefaultConversionFileNamer extends ConversionFileNamer
{
public function getFileName(Conversion $conversion, Media $media): string
{
return $media->model->getKey();
}
}

41
app/Utilities/Avatar/DefaultPathGenerator.php

@ -0,0 +1,41 @@
<?php
namespace App\Utilities\Avatar;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\MediaLibrary\Support\PathGenerator\PathGenerator;
class DefaultPathGenerator implements PathGenerator
{
/*
* Get a unique base path for the given media.
*/
protected function getBasePath(Media $media): string
{
return $media->model->getTable()."/";
}
/*
* Get the path for the given media, relative to the root storage path.
*/
public function getPath(Media $media): string
{
return $this->getBasePath($media);
}
/*
* Get the path for conversions of the given media, relative to the root storage path.
*/
public function getPathForConversions(Media $media): string
{
return $this->getBasePath($media);
}
/*
* Get the path for responsive images of the given media, relative to the root storage path.
*/
public function getPathForResponsiveImages(Media $media): string
{
return $this->getBasePath($media).'/responsive-images/';
}
}

43
app/Utilities/BusinessInfoRequestMixin.php

@ -0,0 +1,43 @@
<?php
namespace App\Utilities;
use App\Models\User;
use Illuminate\Support\Arr;
use Jenssegers\Agent\Agent;
/**
* @mixin Request
* Class RequestMixin
*/
class BusinessInfoRequestMixin
{
public function getBusinessInfo()
{
return function () {
return Arr::get($this, '_business_info');
};
}
public function getFromBusinessInfo()
{
return function(string $key) {
return Arr::get($this, "_business_info.$key");
};
}
public function getBusinessUsers()
{
return function() {
return $this->getFromBusinessInfo('info.users');
};
}
public function isBusinessOwner()
{
return function(User $user) {
return Arr::get($this, "_business_info.info.users.{$user->id}.level") != enum('levels.owner.id');
};
}
}

58
app/Utilities/Exceptions/Handler.php

@ -0,0 +1,58 @@
<?php
namespace App\Utilities\Exceptions;
use Throwable;
use ReflectionClass;
use ReflectionMethod;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
public function report(Throwable $exception)
{
// A trick that I took from Laravel macroable trait
$methods = (new ReflectionClass($exception))->getMethods(
ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED
);
$result = [];
foreach ($methods as $method) {
// invoke the method so we can collect the result of execution it
$result[$method->name] = $method->invoke($exception);
}
// Clear the unnecessary method
unset($result['getTrace']);
unset($result['__toString']);
// clear the null values then encode it as json
// so we can decode it as an object in the Monolog Processor
$result = json_encode(array_filter($result));
return Log::emergency($result);
}
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
}

72
app/Utilities/HelperClass/NotificationHelper.php

@ -0,0 +1,72 @@
<?php
namespace App\Utilities\HelperClass;
use App\Notifications\DBNotification;
use App\Notifications\FcmNotification;
use App\Notifications\MailNotification;
use Illuminate\Support\Facades\Notification;
class NotificationHelper
{
/**
* Make notification object
*
* @param $payload
* @param null $route
* @param array $options
* @return array
*/
public function makeNotif($payload, $route = null, $options = []) {
$route = $route == null ? "" : $route.'.';
return [
'greeting' => $this->getMessageLine($payload, $route.'greeting'),
'subject' => $this->getMessageLine($payload, $route.'subject'),
'title' => $this->getMessageLine($payload, $route.'title'),
'body' => $this->getMessageLine($payload, $route.'body', $options)
];
}
/**
* Fetch message from notifications lang file
*
* @param $payload
* @param $key
* @param null $options
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Translation\Translator|string|null
*
*/
public function getMessageLine($payload, $key, $options = [])
{
return __('notification.'.$payload->data->table_name.'.'.enum('cruds.inverse.'.$payload->data->crud_id.'.singular_name').'.'.$key, $options);
}
/**
* Call notifications
*
* @param $users
* @param $notif
*/
public function sendNotifications($users, $notif) {
// switch ($level) {
// case "emergency":
//// Notification::send($users, new SmsNotification($notif));
// case "critical":
// Notification::send($users, new MailNotification($notif));
// case "high":
// Notification::send($users, new DBNotification($notif));
// case "medium":
// Notification::send($users, new FcmNotification($notif));
// case "low":
//// Notification::send($users, new SocketNotification($notif));
// default:
//// Notification::send($users, new SocketNotification($notif));
// }
Notification::send($users, new MailNotification($notif));
Notification::send($users, new DBNotification($notif));
Notification::send($users, new FcmNotification($notif));
}
}

27
app/Utilities/Helpers/enum.php

@ -0,0 +1,27 @@
<?php
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
if (! function_exists('enum')) {
function enum($key)
{
// add a dot at the end of string to prevent undefined offset
$key .= Str::of($key)->contains(".") ? "" : ".";
// the first parameter of all enum keys are its filename
[$filename, $key] = explode(".", $key, 2);
// because we do not want to load the file every time use require
$enums = require app_path("Enums/$filename.php");
// if the key that user provided not exists then null return
$enums = Arr::get($enums, $key, null);
// if enum null means that key not found
throw_if($enums === null, 'Exception', "Undefined enum '{$key}'");
// if enum value is array its mean that user want to use it as collection
return is_array($enums) ? collect($enums) : $enums;
}
}

1
app/Utilities/Helpers/http.php

@ -0,0 +1 @@
<?php use App\Jobs\AsyncCall; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Http; use Symfony\Component\HttpFoundation\Request; if (!function_exists('get')) { function get(string $service, string $path, array $data, ?string $queue = null) { return call(Request::METHOD_GET, $service, $path, $data, $queue); } } if (!function_exists('post')) { function post(string $service, string $path, array $data, ?string $queue = null) { return call(Request::METHOD_POST, $service, $path, $data, $queue); } } if (!function_exists('put')) { function put(string $service, string $path, array $data, ?string $queue = null) { return call(Request::METHOD_PUT, $service, $path, $data, $queue); } } if (!function_exists('delete')) { function delete(string $service, string $path, array $data, ?string $queue = null) { return call(Request::METHOD_DELETE, $service, $path, $data, $queue); } } if (!function_exists('call')) { /** * @return \Illuminate\Http\Client\Response */ function call(string $method, string $service, string $path, array $data, ?string $queue = null) { // token of this service for send data to other service $token = 'YT76Nt2ofTbmkiP0ubvnlwOJLBtglA3UubjRhieTiTVP7jGPNX0RlueVOgIc'; // url for reaching the target service $baseUrl = config("services.$service"); // create a pending request for this url $pendingRequest = Http::retry(3, 100); // if command data contain file, then it will be attached to the pending request foreach ($data as $piece) { if ($piece instanceof UploadedFile) { $pendingRequest->attach($piece->getFilename(), $piece); } } // if queue set then queue this command if ($queue !== null) { return dispatch(new AsyncCall(...func_get_args())); } try { // otherwise execute the pending request return $pendingRequest ->withToken($token) ->withoutRedirecting() ->withoutVerifying() ->$method($path, $data); } catch (Throwable $thr) { return $thr->response; } } }

1
app/Utilities/Helpers/index.php

@ -0,0 +1 @@
<?php use Illuminate\Support\Str; $helpers = scandir(__DIR__); foreach ($helpers as $helper) { $filename = Str::of($helper)->trim("."); if ($filename->isEmpty()) { continue; } require_once (string) $filename; }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save