1032 lines
40 KiB
PHP
1032 lines
40 KiB
PHP
<?php
|
|
|
|
namespace Modules\Connector\Http\Controllers\Api\V2;
|
|
|
|
use App\BusinessLocation;
|
|
use App\Contact;
|
|
use App\DocumentAndNote;
|
|
use App\Events\TransactionPaymentAdded;
|
|
use App\InvoiceScheme;
|
|
use App\Media;
|
|
use App\Services\Api\BusinessContextService;
|
|
use App\TaxRate;
|
|
use App\TransactionPayment;
|
|
use App\User;
|
|
use App\Utils\ModuleUtil;
|
|
use App\Utils\TransactionUtil;
|
|
use App\Utils\Util;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Project\Entities\InvoiceLine;
|
|
use Modules\Project\Entities\Project;
|
|
use Modules\Project\Entities\ProjectCategory;
|
|
use Modules\Project\Entities\ProjectTask;
|
|
use Modules\Project\Entities\ProjectTaskComment;
|
|
use Modules\Project\Entities\ProjectTimeLog;
|
|
use Modules\Project\Entities\ProjectTransaction;
|
|
use Modules\Project\Entities\ProjectUser;
|
|
use Modules\Project\Utils\ProjectUtil;
|
|
|
|
class ProjectExtendedController extends BaseController
|
|
{
|
|
public function __construct(
|
|
protected BusinessContextService $contextService,
|
|
protected ModuleUtil $moduleUtil,
|
|
protected ProjectUtil $projectUtil,
|
|
protected Util $commonUtil,
|
|
protected TransactionUtil $transactionUtil
|
|
) {
|
|
}
|
|
|
|
protected function ensureProject(): int
|
|
{
|
|
if (! $this->moduleUtil->isModuleInstalled('Project')) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
|
|
return $this->contextService->getBusinessId(request());
|
|
}
|
|
|
|
protected function isAdmin(int $business_id): bool
|
|
{
|
|
return $this->moduleUtil->is_admin(Auth::user(), $business_id);
|
|
}
|
|
|
|
public function resources(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
|
|
$users = User::forDropdown($business_id, false);
|
|
$categories = ProjectCategory::where('business_id', $business_id)->pluck('name', 'id');
|
|
$customers = Contact::customersDropdown($business_id, false);
|
|
|
|
$projects_query = Project::where('business_id', $business_id);
|
|
if (! $is_admin) {
|
|
$projects_query->whereHas('members', fn ($q) => $q->where('user_id', $user_id));
|
|
}
|
|
$projects = $projects_query->pluck('name', 'id');
|
|
|
|
return $this->success([
|
|
'users' => $users,
|
|
'customers' => $customers,
|
|
'categories' => $categories,
|
|
'projects' => $projects,
|
|
'project_statuses' => Project::statusDropdown(),
|
|
'task_statuses' => ProjectTask::taskStatuses(),
|
|
'priorities' => ProjectTask::prioritiesDropdown(),
|
|
]);
|
|
}
|
|
|
|
public function projectsKanban(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$statuses = Project::statusDropdown();
|
|
|
|
$projects = Project::with('customer', 'members', 'lead', 'categories')
|
|
->where('business_id', $business_id);
|
|
|
|
if (! $is_admin) {
|
|
$projects->whereHas('members', fn ($q) => $q->where('user_id', $user_id));
|
|
}
|
|
|
|
if ($request->filled('status')) {
|
|
$projects->where('status', $request->input('status'));
|
|
}
|
|
|
|
if ($request->filled('end_date')) {
|
|
$end_date = $request->input('end_date');
|
|
if ($end_date === 'overdue') {
|
|
$projects->where('end_date', '<', Carbon::today())->where('status', '!=', 'completed');
|
|
} elseif ($end_date === 'today') {
|
|
$projects->where('end_date', Carbon::today())->where('status', '!=', 'completed');
|
|
} elseif ($end_date === 'less_than_one_week') {
|
|
$projects->whereBetween('end_date', [Carbon::today(), Carbon::today()->addWeek()])
|
|
->where('status', '!=', 'completed');
|
|
}
|
|
}
|
|
|
|
if ($request->filled('category_id')) {
|
|
$category_id = $request->input('category_id');
|
|
$projects->whereHas('categories', fn ($q) => $q->where('id', $category_id));
|
|
}
|
|
|
|
$grouped = $projects->get()->groupBy('status');
|
|
$columns = [];
|
|
|
|
foreach ($statuses as $key => $label) {
|
|
$items = $grouped[$key] ?? collect();
|
|
$cards = [];
|
|
|
|
foreach ($items as $project) {
|
|
$assigned_to = [];
|
|
foreach ($project->members as $member) {
|
|
$assigned_to[] = [
|
|
'name' => $member->user_full_name ?? '',
|
|
'avatar' => $member->media->display_url ?? null,
|
|
];
|
|
}
|
|
|
|
$tags = $project->categories->pluck('name')->values()->all();
|
|
|
|
$cards[] = [
|
|
'id' => $project->id,
|
|
'title' => $project->name,
|
|
'end_date' => $project->end_date,
|
|
'customer' => $project->customer->name ?? '',
|
|
'lead' => $project->lead->user_full_name ?? '',
|
|
'assigned_to' => $assigned_to,
|
|
'tags' => $tags,
|
|
'has_description' => ! empty($project->description),
|
|
];
|
|
}
|
|
|
|
$columns[] = [
|
|
'id' => $key,
|
|
'title' => $label,
|
|
'cards' => $cards,
|
|
];
|
|
}
|
|
|
|
return $this->success(['columns' => $columns]);
|
|
}
|
|
|
|
public function tasksKanban(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$statuses = ProjectTask::taskStatuses();
|
|
|
|
$query = ProjectTask::with(['members', 'project', 'comments'])
|
|
->where('business_id', $business_id);
|
|
|
|
if (empty($request->input('project_id')) && ! $is_admin) {
|
|
$query->whereHas('members', fn ($q) => $q->where('user_id', $user_id));
|
|
}
|
|
|
|
if ($request->filled('project_id')) {
|
|
$query->where('project_id', $request->input('project_id'));
|
|
}
|
|
|
|
if ($request->filled('user_id')) {
|
|
$filter_user = $request->input('user_id');
|
|
$query->whereHas('members', fn ($q) => $q->where('user_id', $filter_user));
|
|
}
|
|
|
|
if ($request->filled('status')) {
|
|
$query->where('status', $request->input('status'));
|
|
}
|
|
|
|
if ($request->filled('priority')) {
|
|
$query->where('priority', $request->input('priority'));
|
|
}
|
|
|
|
if ($request->filled('due_date')) {
|
|
$due = $request->input('due_date');
|
|
if ($due === 'overdue') {
|
|
$query->where('due_date', '<', Carbon::today())->where('status', '!=', 'completed');
|
|
} elseif ($due === 'today') {
|
|
$query->where('due_date', Carbon::today())->where('status', '!=', 'completed');
|
|
} elseif ($due === 'less_than_one_week') {
|
|
$query->whereBetween('due_date', [Carbon::today(), Carbon::today()->addWeek()])
|
|
->where('status', '!=', 'completed');
|
|
}
|
|
}
|
|
|
|
$grouped = $query->get()->groupBy('status');
|
|
$columns = [];
|
|
|
|
foreach ($statuses as $key => $label) {
|
|
$items = $grouped[$key] ?? collect();
|
|
$cards = [];
|
|
|
|
foreach ($items as $task) {
|
|
$assigned_to = [];
|
|
foreach ($task->members as $member) {
|
|
$assigned_to[] = [
|
|
'name' => $member->user_full_name ?? '',
|
|
'avatar' => $member->media->display_url ?? null,
|
|
];
|
|
}
|
|
|
|
$cards[] = [
|
|
'id' => $task->id,
|
|
'title' => $task->subject,
|
|
'subtitle' => $task->task_id,
|
|
'project_id' => $task->project_id,
|
|
'project' => $task->project->name ?? '',
|
|
'due_date' => $task->due_date,
|
|
'priority' => $task->priority,
|
|
'assigned_to' => $assigned_to,
|
|
'tags' => [$task->priority],
|
|
'has_description' => ! empty($task->description),
|
|
'has_comments' => $task->comments->count() > 0,
|
|
];
|
|
}
|
|
|
|
$columns[] = [
|
|
'id' => $key,
|
|
'title' => $label,
|
|
'cards' => $cards,
|
|
];
|
|
}
|
|
|
|
return $this->success(['columns' => $columns]);
|
|
}
|
|
|
|
public function updateProjectStatus(Request $request, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
|
|
$request->validate([
|
|
'status' => 'required|in:not_started,in_progress,on_hold,cancelled,completed',
|
|
]);
|
|
|
|
$project = Project::where('business_id', $business_id)->findOrFail($id);
|
|
$project->status = $request->input('status');
|
|
$project->save();
|
|
|
|
return $this->success($project, __('lang_v1.success'));
|
|
}
|
|
|
|
public function updateTaskStatus(Request $request, $id)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$request->validate([
|
|
'status' => 'required|in:not_started,in_progress,on_hold,cancelled,completed',
|
|
'project_id' => 'required|integer',
|
|
]);
|
|
|
|
$task = ProjectTask::where('project_id', $request->input('project_id'))
|
|
->findOrFail($id);
|
|
$task->status = $request->input('status');
|
|
$task->save();
|
|
|
|
return $this->success($task, __('lang_v1.success'));
|
|
}
|
|
|
|
protected function projectQuery(int $business_id, int $user_id, bool $is_admin)
|
|
{
|
|
$query = Project::with(['customer', 'members', 'lead', 'categories'])
|
|
->where('business_id', $business_id);
|
|
|
|
if (! $is_admin) {
|
|
$query->whereHas('members', fn ($q) => $q->where('user_id', $user_id));
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
public function showProject(Request $request, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
|
|
$project = $this->projectQuery($business_id, $user_id, $is_admin)
|
|
->withCount(['tasks as incomplete_task' => fn ($q) => $q->where('status', '!=', 'completed')])
|
|
->findOrFail($id);
|
|
|
|
$timelog = ProjectTimeLog::where('project_id', $id)
|
|
->select(DB::raw('SUM(TIMESTAMPDIFF(SECOND, start_datetime, end_datetime)) as total_seconds'))
|
|
->first();
|
|
|
|
$invoice_paid = ProjectTransaction::leftJoin('transaction_payments as TP', 'transactions.id', '=', 'TP.transaction_id')
|
|
->where('transactions.business_id', $business_id)
|
|
->where('pjt_project_id', $id)
|
|
->select(DB::raw('SUM(TP.amount) as paid'))
|
|
->value('paid');
|
|
|
|
$invoice_total = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $id)
|
|
->sum('final_total');
|
|
|
|
$is_lead = $this->projectUtil->isProjectLead($user_id, $id);
|
|
$is_lead_or_admin = $is_admin || $is_lead;
|
|
|
|
return $this->success([
|
|
'project' => $project,
|
|
'timelog_total_seconds' => (int) ($timelog->total_seconds ?? 0),
|
|
'invoice_paid' => (float) ($invoice_paid ?? 0),
|
|
'invoice_total' => (float) $invoice_total,
|
|
'is_lead_or_admin' => $is_lead_or_admin,
|
|
'can_pay_invoice' => $this->canPayProjectInvoice($business_id, $user_id, $is_admin, (int) $id),
|
|
'can_crud_task' => $this->projectUtil->canMemberCrudTask($business_id, $is_lead_or_admin ? null : $user_id, $id),
|
|
'can_crud_timelog' => $this->projectUtil->canMemberCrudTimelog($business_id, $user_id, $id),
|
|
]);
|
|
}
|
|
|
|
public function storeProject(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
|
|
$request->validate([
|
|
'name' => 'required|string|max:191',
|
|
'lead_id' => 'required|integer',
|
|
'status' => 'required|in:not_started,in_progress,on_hold,cancelled,completed',
|
|
]);
|
|
|
|
$input = $request->only('name', 'description', 'contact_id', 'status', 'lead_id');
|
|
$input['start_date'] = $request->filled('start_date') ? $this->commonUtil->uf_date($request->input('start_date')) : null;
|
|
$input['end_date'] = $request->filled('end_date') ? $this->commonUtil->uf_date($request->input('end_date')) : null;
|
|
$input['business_id'] = $business_id;
|
|
$input['created_by'] = Auth::id();
|
|
$input['settings'] = [
|
|
'enable_timelog' => 1,
|
|
'enable_invoice' => 1,
|
|
'enable_notes_documents' => 1,
|
|
'members_crud_task' => 0,
|
|
'members_crud_note' => 0,
|
|
'members_crud_timelog' => 0,
|
|
'task_view' => 'list_view',
|
|
'task_id_prefix' => '#',
|
|
];
|
|
|
|
$members = $request->input('user_id', []);
|
|
if (! is_array($members)) {
|
|
$members = [];
|
|
}
|
|
$members[] = $request->input('lead_id');
|
|
|
|
DB::beginTransaction();
|
|
$project = Project::create($input);
|
|
$project->members()->sync(array_unique($members));
|
|
if ($request->filled('category_id')) {
|
|
$project->categories()->sync((array) $request->input('category_id'));
|
|
}
|
|
DB::commit();
|
|
|
|
return $this->success($project->load(['customer', 'members', 'lead', 'categories']), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function updateProject(Request $request, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
|
|
$project = Project::where('business_id', $business_id)->findOrFail($id);
|
|
$input = $request->only('name', 'description', 'contact_id', 'status', 'lead_id');
|
|
$input['start_date'] = $request->filled('start_date') ? $this->commonUtil->uf_date($request->input('start_date')) : $project->start_date;
|
|
$input['end_date'] = $request->filled('end_date') ? $this->commonUtil->uf_date($request->input('end_date')) : $project->end_date;
|
|
|
|
$project->update($input);
|
|
|
|
if ($request->has('user_id')) {
|
|
$members = (array) $request->input('user_id', []);
|
|
$members[] = $request->input('lead_id', $project->lead_id);
|
|
$project->members()->sync(array_unique($members));
|
|
}
|
|
if ($request->has('category_id')) {
|
|
$project->categories()->sync((array) $request->input('category_id'));
|
|
}
|
|
|
|
return $this->success($project->fresh(['customer', 'members', 'lead', 'categories']));
|
|
}
|
|
|
|
public function showTask(Request $request, $projectId, $id)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$task = ProjectTask::with([
|
|
'members', 'createdBy', 'project',
|
|
'comments' => fn ($q) => $q->latest(),
|
|
'comments.media', 'comments.commentedBy',
|
|
'timeLogs', 'timeLogs.user',
|
|
])
|
|
->where('project_id', $projectId)
|
|
->findOrFail($id);
|
|
|
|
return $this->success($task);
|
|
}
|
|
|
|
public function storeTask(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
|
|
$request->validate([
|
|
'subject' => 'required|string|max:191',
|
|
'priority' => 'required|in:low,medium,high,urgent',
|
|
'status' => 'required|in:not_started,in_progress,on_hold,cancelled,completed',
|
|
]);
|
|
|
|
$input = $request->only('subject', 'description', 'priority', 'status', 'custom_field_1', 'custom_field_2', 'custom_field_3', 'custom_field_4');
|
|
$input['project_id'] = $projectId;
|
|
$input['start_date'] = $request->filled('start_date') ? $this->commonUtil->uf_date($request->input('start_date')) : null;
|
|
$input['due_date'] = $request->filled('due_date') ? $this->commonUtil->uf_date($request->input('due_date')) : null;
|
|
$input['created_by'] = Auth::id();
|
|
$input['business_id'] = $business_id;
|
|
$input['task_id'] = $this->projectUtil->generateTaskId($business_id, $projectId);
|
|
|
|
$task = ProjectTask::create($input);
|
|
if ($request->filled('user_id')) {
|
|
$task->members()->sync((array) $request->input('user_id'));
|
|
}
|
|
|
|
return $this->success($task->load('members'), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function updateTask(Request $request, $projectId, $id)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$task = ProjectTask::where('project_id', $projectId)->findOrFail($id);
|
|
$input = $request->only('subject', 'description', 'priority', 'status', 'custom_field_1', 'custom_field_2', 'custom_field_3', 'custom_field_4');
|
|
$input['start_date'] = $request->filled('start_date') ? $this->commonUtil->uf_date($request->input('start_date')) : $task->start_date;
|
|
$input['due_date'] = $request->filled('due_date') ? $this->commonUtil->uf_date($request->input('due_date')) : $task->due_date;
|
|
|
|
$task->update($input);
|
|
if ($request->has('user_id')) {
|
|
$task->members()->sync((array) $request->input('user_id', []));
|
|
}
|
|
|
|
return $this->success($task->fresh(['members', 'comments', 'timeLogs']));
|
|
}
|
|
|
|
public function storeTaskComment(Request $request, $projectId, $taskId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
|
|
$request->validate(['comment' => 'required|string']);
|
|
|
|
$task = ProjectTask::where('project_id', $projectId)->findOrFail($taskId);
|
|
$comment = $task->comments()->create([
|
|
'comment' => $request->input('comment'),
|
|
'commented_by' => Auth::id(),
|
|
]);
|
|
|
|
$file_names = $request->input('file_name', []);
|
|
if (! empty($file_names)) {
|
|
$names = is_array($file_names) ? $file_names : explode(',', $file_names);
|
|
Media::attachMediaToModel($comment, $business_id, $names);
|
|
}
|
|
|
|
return $this->success($comment->load(['commentedBy', 'media']), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function uploadCommentMedia(Request $request)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$request->validate(['file' => 'required|file|max:10240']);
|
|
|
|
$file_name = Media::uploadFile($request->file('file'));
|
|
|
|
return $this->success(['file_name' => $file_name]);
|
|
}
|
|
|
|
public function destroyTaskComment(Request $request, $projectId, $taskId, $commentId)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$comment = ProjectTaskComment::where('project_task_id', $taskId)->findOrFail($commentId);
|
|
$comment->media()->delete();
|
|
$comment->delete();
|
|
|
|
return $this->success(null, __('lang_v1.success'));
|
|
}
|
|
|
|
public function timeLogs(Request $request, $projectId)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
$logs = ProjectTimeLog::where('project_id', $projectId)
|
|
->with(['task', 'user'])
|
|
->orderByDesc('start_datetime')
|
|
->paginate($request->input('per_page', 25));
|
|
|
|
return $this->paginated($logs);
|
|
}
|
|
|
|
public function storeTimeLog(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$is_lead = $this->projectUtil->isProjectLead($user_id, $projectId);
|
|
|
|
$request->validate([
|
|
'start_datetime' => 'required',
|
|
'end_datetime' => 'required',
|
|
]);
|
|
|
|
$input = $request->only('project_task_id', 'note');
|
|
$input['project_id'] = $projectId;
|
|
$input['start_datetime'] = $this->commonUtil->uf_date($request->input('start_datetime'), true);
|
|
$input['end_datetime'] = $this->commonUtil->uf_date($request->input('end_datetime'), true);
|
|
$input['created_by'] = $user_id;
|
|
$input['user_id'] = ($is_admin || $is_lead) ? $request->input('user_id', $user_id) : $user_id;
|
|
|
|
$log = ProjectTimeLog::create($input);
|
|
|
|
return $this->success($log->load(['task', 'user']), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function destroyTimeLog(Request $request, $projectId, $id)
|
|
{
|
|
$this->ensureProject();
|
|
|
|
ProjectTimeLog::where('project_id', $projectId)->where('id', $id)->delete();
|
|
|
|
return $this->success(null, __('lang_v1.success'));
|
|
}
|
|
|
|
protected function ensureInvoiceAccess(int $business_id, int $user_id, bool $is_admin, int $projectId): void
|
|
{
|
|
if ($is_admin || $this->projectUtil->isProjectLead($user_id, $projectId)) {
|
|
return;
|
|
}
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
|
|
protected function canPayProjectInvoice(int $business_id, int $user_id, bool $is_admin, int $projectId): bool
|
|
{
|
|
if ($is_admin || $this->projectUtil->isProjectLead($user_id, $projectId)) {
|
|
return true;
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if ($user->can('sell.payments') || $user->can('purchase.payments')) {
|
|
return $this->projectUtil->isProjectMember($user_id, $projectId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function ensureInvoicePaymentAccess(int $business_id, int $user_id, bool $is_admin, int $projectId): void
|
|
{
|
|
if (! $this->canPayProjectInvoice($business_id, $user_id, $is_admin, $projectId)) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
}
|
|
|
|
protected function documentPermissions(Project $project, int $user_id, bool $is_admin): array
|
|
{
|
|
if ($is_admin || $this->projectUtil->isProjectLead($user_id, $project->id)) {
|
|
return ['view', 'create', 'delete'];
|
|
}
|
|
if ($this->projectUtil->isProjectMember($user_id, $project->id)) {
|
|
if (! empty($project->settings['members_crud_note'])) {
|
|
return ['view', 'create', 'delete'];
|
|
}
|
|
|
|
return ['view'];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
public function invoiceResources(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$tax_dropdown = TaxRate::forBusinessDropdown($business_id, true, true);
|
|
|
|
return $this->success([
|
|
'invoice_schemes' => InvoiceScheme::forDropdown($business_id),
|
|
'default_scheme' => InvoiceScheme::getDefault($business_id),
|
|
'statuses' => ProjectTransaction::invoiceStatuses(),
|
|
'discount_types' => ProjectTransaction::discountTypes(),
|
|
'taxes' => $tax_dropdown['tax_rates'],
|
|
'tax_attributes' => $tax_dropdown['attributes'],
|
|
'locations' => BusinessLocation::forDropdown($business_id),
|
|
'customers' => Contact::customersDropdown($business_id, false),
|
|
]);
|
|
}
|
|
|
|
public function projectInvoices(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoiceAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$invoices = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $projectId)
|
|
->with('contact')
|
|
->orderByDesc('transaction_date')
|
|
->paginate($request->input('per_page', 25));
|
|
|
|
return $this->paginated($invoices);
|
|
}
|
|
|
|
public function showProjectInvoice(Request $request, $projectId, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoiceAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$invoice = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $projectId)
|
|
->with(['contact', 'invoiceLines', 'invoiceLines.tax', 'project', 'payment_lines'])
|
|
->findOrFail($id);
|
|
|
|
$data = $invoice->toArray();
|
|
$data['can_pay_invoice'] = $this->canPayProjectInvoice($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
return $this->success($data);
|
|
}
|
|
|
|
public function storeProjectInvoice(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoiceAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$request->validate([
|
|
'pjt_title' => 'required|string|max:191',
|
|
'contact_id' => 'required|integer',
|
|
'transaction_date' => 'required',
|
|
'status' => 'required|in:final,draft',
|
|
'final_total' => 'required|numeric',
|
|
'lines' => 'required|array|min:1',
|
|
'lines.*.task' => 'required|string',
|
|
'lines.*.rate' => 'required|numeric',
|
|
'lines.*.quantity' => 'required|numeric',
|
|
'lines.*.total' => 'required|numeric',
|
|
]);
|
|
|
|
DB::beginTransaction();
|
|
$input = $request->only('pjt_title', 'contact_id', 'pay_term_number', 'pay_term_type', 'status', 'discount_type', 'staff_note', 'additional_notes', 'location_id');
|
|
$input['business_id'] = $business_id;
|
|
$input['created_by'] = $user_id;
|
|
$input['pjt_project_id'] = $projectId;
|
|
$input['transaction_date'] = $this->commonUtil->uf_date($request->input('transaction_date'));
|
|
$input['invoice_no'] = $this->transactionUtil->getInvoiceNumber($business_id, $input['status'], null, $request->input('invoice_scheme_id'));
|
|
$input['discount_amount'] = $this->commonUtil->num_uf($request->input('discount_amount', 0));
|
|
$input['total_before_tax'] = $this->commonUtil->num_uf($request->input('total_before_tax', $request->input('final_total')));
|
|
$input['final_total'] = $this->commonUtil->num_uf($request->input('final_total'));
|
|
$input['type'] = 'sell';
|
|
$input['sub_type'] = 'project_invoice';
|
|
$input['payment_status'] = 'due';
|
|
|
|
$invoice_lines = [];
|
|
foreach ($request->input('lines', []) as $line) {
|
|
$invoice_lines[] = [
|
|
'task' => $line['task'],
|
|
'rate' => $this->commonUtil->num_uf($line['rate']),
|
|
'tax_rate_id' => $line['tax_rate_id'] ?? null,
|
|
'quantity' => $this->commonUtil->num_uf($line['quantity']),
|
|
'total' => $this->commonUtil->num_uf($line['total']),
|
|
'description' => $line['description'] ?? null,
|
|
];
|
|
}
|
|
|
|
$transaction = ProjectTransaction::create($input);
|
|
$transaction->invoiceLines()->createMany($invoice_lines);
|
|
DB::commit();
|
|
|
|
return $this->success($transaction->load(['contact', 'invoiceLines', 'invoiceLines.tax']), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function updateProjectInvoice(Request $request, $projectId, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoiceAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$request->validate([
|
|
'pjt_title' => 'required|string|max:191',
|
|
'contact_id' => 'required|integer',
|
|
'transaction_date' => 'required',
|
|
'status' => 'required|in:final,draft',
|
|
'final_total' => 'required|numeric',
|
|
'lines' => 'required|array|min:1',
|
|
'lines.*.task' => 'required|string',
|
|
'lines.*.rate' => 'required|numeric',
|
|
'lines.*.quantity' => 'required|numeric',
|
|
'lines.*.total' => 'required|numeric',
|
|
]);
|
|
|
|
DB::beginTransaction();
|
|
$transaction = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $projectId)
|
|
->findOrFail($id);
|
|
|
|
$input = $request->only('pjt_title', 'contact_id', 'pay_term_number', 'pay_term_type', 'status', 'discount_type', 'staff_note', 'additional_notes', 'location_id');
|
|
$input['transaction_date'] = $this->commonUtil->uf_date($request->input('transaction_date'));
|
|
$input['discount_amount'] = $this->commonUtil->num_uf($request->input('discount_amount', 0));
|
|
$input['total_before_tax'] = $this->commonUtil->num_uf($request->input('total_before_tax', $request->input('final_total')));
|
|
$input['final_total'] = $this->commonUtil->num_uf($request->input('final_total'));
|
|
$transaction->update($input);
|
|
|
|
$existing_ids = [];
|
|
$new_lines = [];
|
|
foreach ($request->input('lines', []) as $line) {
|
|
if (! empty($line['id'])) {
|
|
$invoice_line = InvoiceLine::where('transaction_id', $id)->findOrFail($line['id']);
|
|
$invoice_line->update([
|
|
'task' => $line['task'],
|
|
'tax_rate_id' => $line['tax_rate_id'] ?? null,
|
|
'description' => $line['description'] ?? null,
|
|
'rate' => $this->commonUtil->num_uf($line['rate']),
|
|
'quantity' => $this->commonUtil->num_uf($line['quantity']),
|
|
'total' => $this->commonUtil->num_uf($line['total']),
|
|
]);
|
|
$existing_ids[] = $invoice_line->id;
|
|
} else {
|
|
$new_lines[] = [
|
|
'task' => $line['task'],
|
|
'rate' => $this->commonUtil->num_uf($line['rate']),
|
|
'tax_rate_id' => $line['tax_rate_id'] ?? null,
|
|
'quantity' => $this->commonUtil->num_uf($line['quantity']),
|
|
'total' => $this->commonUtil->num_uf($line['total']),
|
|
'description' => $line['description'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
|
|
InvoiceLine::where('transaction_id', $id)->whereNotIn('id', $existing_ids)->delete();
|
|
if (! empty($new_lines)) {
|
|
$transaction->invoiceLines()->createMany($new_lines);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return $this->success($transaction->fresh(['contact', 'invoiceLines', 'invoiceLines.tax', 'payment_lines']));
|
|
}
|
|
|
|
public function storeProjectInvoicePayment(Request $request, $projectId, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoicePaymentAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$request->validate([
|
|
'amount' => 'required|numeric|min:0.01',
|
|
'method' => 'required|string|max:191',
|
|
]);
|
|
|
|
$transaction = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $projectId)
|
|
->with('contact')
|
|
->findOrFail($id);
|
|
|
|
if ($transaction->payment_status === 'paid') {
|
|
return $this->error(__('purchase.amount_already_paid'), 422);
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
$transaction_before = $transaction->replicate();
|
|
$inputs = $request->only(['method', 'note', 'card_number', 'card_holder_name', 'cheque_number', 'bank_account_number']);
|
|
$inputs['paid_on'] = $this->commonUtil->uf_date($request->input('paid_on', now()), true);
|
|
$inputs['transaction_id'] = $transaction->id;
|
|
$inputs['amount'] = $this->commonUtil->num_uf($request->input('amount'));
|
|
$inputs['created_by'] = $user_id;
|
|
$inputs['payment_for'] = $transaction->contact_id;
|
|
$inputs['business_id'] = $business_id;
|
|
|
|
if (! empty($request->input('account_id')) && $inputs['method'] !== 'advance') {
|
|
$inputs['account_id'] = $request->input('account_id');
|
|
}
|
|
|
|
$ref_count = $this->transactionUtil->setAndGetReferenceCount('sell_payment');
|
|
$inputs['payment_ref_no'] = $this->transactionUtil->generateReferenceNumber('sell_payment', $ref_count);
|
|
|
|
$tp = TransactionPayment::create($inputs);
|
|
$inputs['transaction_type'] = $transaction->type;
|
|
event(new TransactionPaymentAdded($tp, $inputs));
|
|
|
|
$transaction->payment_status = $this->transactionUtil->updatePaymentStatus($transaction->id, $transaction->final_total);
|
|
$transaction->save();
|
|
$this->transactionUtil->activityLog($transaction, 'payment_edited', $transaction_before);
|
|
DB::commit();
|
|
|
|
return $this->success($transaction->fresh(['contact', 'invoiceLines', 'payment_lines']), __('purchase.payment_added_success'));
|
|
}
|
|
|
|
public function destroyProjectInvoice(Request $request, $projectId, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
$this->ensureInvoiceAccess($business_id, $user_id, $is_admin, (int) $projectId);
|
|
|
|
$transaction = ProjectTransaction::where('business_id', $business_id)
|
|
->where('pjt_project_id', $projectId)
|
|
->findOrFail($id);
|
|
$transaction->invoiceLines()->delete();
|
|
$transaction->delete();
|
|
|
|
return $this->success(null, __('lang_v1.success'));
|
|
}
|
|
|
|
public function projectDocuments(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
|
|
$project = $this->projectQuery($business_id, $user_id, $is_admin)->findOrFail($projectId);
|
|
$permissions = $this->documentPermissions($project, $user_id, $is_admin);
|
|
if (! in_array('view', $permissions)) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
|
|
$docs = DocumentAndNote::where('business_id', $business_id)
|
|
->where('notable_id', $projectId)
|
|
->where('notable_type', Project::class)
|
|
->where(function ($query) use ($user_id) {
|
|
$query->where('is_private', 0)
|
|
->orWhere(function ($q) use ($user_id) {
|
|
$q->where('is_private', 1)->where('created_by', $user_id);
|
|
});
|
|
})
|
|
->with(['createdBy', 'media'])
|
|
->orderByDesc('created_at')
|
|
->paginate($request->input('per_page', 25));
|
|
|
|
return $this->paginated($docs);
|
|
}
|
|
|
|
public function storeProjectDocument(Request $request, $projectId)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
|
|
$project = $this->projectQuery($business_id, $user_id, $is_admin)->findOrFail($projectId);
|
|
$permissions = $this->documentPermissions($project, $user_id, $is_admin);
|
|
if (! in_array('create', $permissions)) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
|
|
$request->validate(['heading' => 'required|string|max:191']);
|
|
|
|
DB::beginTransaction();
|
|
$input = $request->only('heading', 'description', 'is_private');
|
|
$input['business_id'] = $business_id;
|
|
$input['created_by'] = $user_id;
|
|
$input['is_private'] = ! empty($input['is_private']) ? 1 : 0;
|
|
|
|
$doc = $project->documentsAndnote()->create($input);
|
|
$file_names = $request->input('file_name', []);
|
|
if (! empty($file_names)) {
|
|
$names = is_array($file_names) ? $file_names : explode(',', $file_names);
|
|
Media::attachMediaToModel($doc, $business_id, $names);
|
|
}
|
|
DB::commit();
|
|
|
|
return $this->success($doc->load(['createdBy', 'media']), __('lang_v1.success'), 201);
|
|
}
|
|
|
|
public function destroyProjectDocument(Request $request, $projectId, $id)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_id = Auth::id();
|
|
$is_admin = $this->isAdmin($business_id);
|
|
|
|
$project = $this->projectQuery($business_id, $user_id, $is_admin)->findOrFail($projectId);
|
|
$permissions = $this->documentPermissions($project, $user_id, $is_admin);
|
|
if (! in_array('delete', $permissions)) {
|
|
abort(403, 'Unauthorized action.');
|
|
}
|
|
|
|
$doc = DocumentAndNote::where('business_id', $business_id)
|
|
->where('notable_id', $projectId)
|
|
->where('notable_type', Project::class)
|
|
->findOrFail($id);
|
|
$doc->media()->delete();
|
|
$doc->delete();
|
|
|
|
return $this->success(null, __('lang_v1.success'));
|
|
}
|
|
|
|
public function employeeTimeLogReport(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$user_ids = (array) $request->input('user_id', []);
|
|
$project_ids = (array) $request->input('project_id', []);
|
|
$start_date = $request->input('start_date');
|
|
$end_date = $request->input('end_date');
|
|
|
|
$query = ProjectUser::with(['projects' => function ($q) use ($project_ids) {
|
|
if (! empty($project_ids)) {
|
|
$q->whereIn('pjt_projects.id', $project_ids);
|
|
}
|
|
}, 'projects.timeLogs' => function ($q) use ($start_date, $end_date) {
|
|
if (! empty($start_date) && ! empty($end_date)) {
|
|
if ($start_date === $end_date) {
|
|
$q->whereDate('start_datetime', $start_date);
|
|
} else {
|
|
$q->whereBetween('start_datetime', [$start_date, $end_date]);
|
|
}
|
|
}
|
|
}, 'projects.timeLogs.task'])
|
|
->where('business_id', $business_id);
|
|
|
|
if (! empty($user_ids)) {
|
|
$query->whereIn('id', $user_ids);
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($query->get() as $user) {
|
|
$user_projects = [];
|
|
$user_total = 0;
|
|
foreach ($user->projects as $project) {
|
|
$logs = [];
|
|
$project_seconds = 0;
|
|
foreach ($project->timeLogs as $timeLog) {
|
|
if ($timeLog->user_id != $user->id) {
|
|
continue;
|
|
}
|
|
$seconds = $timeLog->end_datetime && $timeLog->start_datetime
|
|
? Carbon::parse($timeLog->start_datetime)->diffInSeconds(Carbon::parse($timeLog->end_datetime))
|
|
: 0;
|
|
$project_seconds += $seconds;
|
|
$logs[] = [
|
|
'id' => $timeLog->id,
|
|
'task' => $timeLog->task->subject ?? null,
|
|
'start_datetime' => $timeLog->start_datetime,
|
|
'end_datetime' => $timeLog->end_datetime,
|
|
'duration_seconds' => $seconds,
|
|
'note' => $timeLog->note,
|
|
];
|
|
}
|
|
if (empty($logs)) {
|
|
continue;
|
|
}
|
|
$user_total += $project_seconds;
|
|
$user_projects[] = [
|
|
'id' => $project->id,
|
|
'name' => $project->name,
|
|
'time_logs' => $logs,
|
|
'total_seconds' => $project_seconds,
|
|
];
|
|
}
|
|
if (! empty($user_projects)) {
|
|
$rows[] = [
|
|
'id' => $user->id,
|
|
'name' => $user->user_full_name,
|
|
'projects' => $user_projects,
|
|
'total_seconds' => $user_total,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $this->success(['users' => $rows]);
|
|
}
|
|
|
|
public function projectTimeLogReport(Request $request)
|
|
{
|
|
$business_id = $this->ensureProject();
|
|
$project_ids = (array) $request->input('project_id', []);
|
|
$start_date = $request->input('start_date');
|
|
$end_date = $request->input('end_date');
|
|
|
|
$query = Project::with(['timeLogs' => function ($q) use ($start_date, $end_date) {
|
|
if (! empty($start_date) && ! empty($end_date)) {
|
|
if ($start_date === $end_date) {
|
|
$q->whereDate('start_datetime', $start_date);
|
|
} else {
|
|
$q->whereBetween('start_datetime', [$start_date, $end_date]);
|
|
}
|
|
}
|
|
}, 'timeLogs.task', 'timeLogs.user'])
|
|
->where('business_id', $business_id);
|
|
|
|
if (! empty($project_ids)) {
|
|
$query->whereIn('id', $project_ids);
|
|
}
|
|
|
|
$rows = [];
|
|
foreach ($query->get() as $project) {
|
|
$logs = [];
|
|
$total_seconds = 0;
|
|
foreach ($project->timeLogs as $timeLog) {
|
|
$seconds = $timeLog->end_datetime && $timeLog->start_datetime
|
|
? Carbon::parse($timeLog->start_datetime)->diffInSeconds(Carbon::parse($timeLog->end_datetime))
|
|
: 0;
|
|
$total_seconds += $seconds;
|
|
$logs[] = [
|
|
'id' => $timeLog->id,
|
|
'user' => $timeLog->user->user_full_name ?? null,
|
|
'task' => $timeLog->task->subject ?? null,
|
|
'start_datetime' => $timeLog->start_datetime,
|
|
'end_datetime' => $timeLog->end_datetime,
|
|
'duration_seconds' => $seconds,
|
|
'note' => $timeLog->note,
|
|
];
|
|
}
|
|
if (! empty($logs)) {
|
|
$rows[] = [
|
|
'id' => $project->id,
|
|
'name' => $project->name,
|
|
'time_logs' => $logs,
|
|
'total_seconds' => $total_seconds,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $this->success(['projects' => $rows]);
|
|
}
|
|
}
|