first commit
This commit is contained in:
80
app/Services/AssignmentService.php
Normal file
80
app/Services/AssignmentService.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Consultant;
|
||||
use App\Models\DataCentre;
|
||||
use App\Models\DataCentreConsultantAssignment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AssignmentService
|
||||
{
|
||||
/**
|
||||
* Tugaskan perunding kepada pusat data.
|
||||
*
|
||||
* Peraturan: satu pusat data hanya boleh ada SATU perunding aktif pada satu masa.
|
||||
* Penugasan terdahulu ditamatkan (end_date diisi, is_active=false) dan disimpan
|
||||
* dalam sejarah penugasan.
|
||||
*/
|
||||
public function assign(DataCentre $dataCentre, Consultant $consultant, User $officer, ?string $startDate = null, ?string $reason = null): DataCentreConsultantAssignment
|
||||
{
|
||||
return DB::transaction(function () use ($dataCentre, $consultant, $officer, $startDate, $reason) {
|
||||
$start = $startDate ? Carbon::parse($startDate) : now();
|
||||
|
||||
// Tamatkan semua penugasan aktif sedia ada untuk pusat data ini.
|
||||
$dataCentre->assignments()
|
||||
->where('is_active', true)
|
||||
->get()
|
||||
->each(function (DataCentreConsultantAssignment $existing) use ($start) {
|
||||
$existing->update([
|
||||
'is_active' => false,
|
||||
'end_date' => $existing->end_date ?? $start->copy()->subDay(),
|
||||
]);
|
||||
});
|
||||
|
||||
$assignment = $dataCentre->assignments()->create([
|
||||
'consultant_id' => $consultant->id,
|
||||
'start_date' => $start,
|
||||
'reason' => $reason,
|
||||
'assigned_by' => $officer->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$dataCentre->update(['current_consultant_id' => $consultant->id]);
|
||||
|
||||
activity('penugasan')
|
||||
->performedOn($dataCentre)
|
||||
->causedBy($officer)
|
||||
->withProperties([
|
||||
'consultant_id' => $consultant->id,
|
||||
'consultant' => $consultant->nama_perunding,
|
||||
'reason' => $reason,
|
||||
])
|
||||
->log('Perunding ditugaskan kepada pusat data');
|
||||
|
||||
return $assignment;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tamatkan penugasan aktif tanpa menggantikan dengan perunding baharu.
|
||||
*/
|
||||
public function endActive(DataCentre $dataCentre, User $officer, ?string $reason = null): void
|
||||
{
|
||||
DB::transaction(function () use ($dataCentre, $officer, $reason) {
|
||||
$dataCentre->assignments()->where('is_active', true)->update([
|
||||
'is_active' => false,
|
||||
'end_date' => now(),
|
||||
'reason' => $reason,
|
||||
]);
|
||||
$dataCentre->update(['current_consultant_id' => null]);
|
||||
|
||||
activity('penugasan')
|
||||
->performedOn($dataCentre)
|
||||
->causedBy($officer)
|
||||
->log('Penugasan perunding ditamatkan');
|
||||
});
|
||||
}
|
||||
}
|
||||
67
app/Services/DocumentService.php
Normal file
67
app/Services/DocumentService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Submission;
|
||||
use App\Models\SubmissionDocument;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DocumentService
|
||||
{
|
||||
/**
|
||||
* Simpan laporan PDF baharu untuk serahan dengan versi & hash.
|
||||
* Versi sebelumnya ditanda tidak aktif tetapi dikekalkan (sejarah versi).
|
||||
* Fail disimpan pada disk 'local' (di luar direktori public).
|
||||
*/
|
||||
public function store(Submission $submission, UploadedFile $file, User $user): SubmissionDocument
|
||||
{
|
||||
return DB::transaction(function () use ($submission, $file, $user) {
|
||||
$nextVersion = (int) $submission->documents()->max('version') + 1;
|
||||
|
||||
$hash = hash_file('sha256', $file->getRealPath());
|
||||
$path = $file->storeAs(
|
||||
"reports/{$submission->id}",
|
||||
'v'.$nextVersion.'_'.uniqid().'.pdf',
|
||||
'local'
|
||||
);
|
||||
|
||||
// Nyahaktifkan versi aktif sebelumnya.
|
||||
$submission->documents()->where('is_active', true)->update(['is_active' => false]);
|
||||
|
||||
$document = $submission->documents()->create([
|
||||
'original_filename' => $file->getClientOriginalName(),
|
||||
'stored_path' => $path,
|
||||
'file_hash' => $hash,
|
||||
'mime_type' => $file->getClientMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'version' => $nextVersion,
|
||||
'is_active' => true,
|
||||
'uploaded_by' => $user->id,
|
||||
]);
|
||||
|
||||
activity('dokumen')
|
||||
->performedOn($submission)
|
||||
->causedBy($user)
|
||||
->withProperties(['version' => $nextVersion, 'filename' => $document->original_filename])
|
||||
->log('Laporan PDF dimuat naik');
|
||||
|
||||
return $document;
|
||||
});
|
||||
}
|
||||
|
||||
public function setActiveVersion(Submission $submission, SubmissionDocument $document): void
|
||||
{
|
||||
$submission->documents()->where('is_active', true)->update(['is_active' => false]);
|
||||
$document->update(['is_active' => true]);
|
||||
}
|
||||
|
||||
public function download(SubmissionDocument $document)
|
||||
{
|
||||
abort_unless(Storage::disk('local')->exists($document->stored_path), 404, 'Fail laporan tidak dijumpai.');
|
||||
|
||||
return Storage::disk('local')->download($document->stored_path, $document->original_filename);
|
||||
}
|
||||
}
|
||||
204
app/Services/ExpressionEvaluator.php
Normal file
204
app/Services/ExpressionEvaluator.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Penilai ungkapan aritmetik yang selamat (recursive-descent parser).
|
||||
*
|
||||
* Menyokong: + - * / ( ), nombor perpuluhan, dan pemboleh ubah (token) yang
|
||||
* dipetakan daripada array $variables. Tiada eval()/PHP code dilaksanakan,
|
||||
* jadi selamat untuk formula yang dikonfigur oleh pengguna JPP.
|
||||
*/
|
||||
class ExpressionEvaluator
|
||||
{
|
||||
/** @var array<int, array{type:string, value:string}> */
|
||||
protected array $tokens = [];
|
||||
|
||||
protected int $pos = 0;
|
||||
|
||||
/** @var array<string, float> */
|
||||
protected array $variables = [];
|
||||
|
||||
protected bool $guardDivZero = false;
|
||||
|
||||
protected bool $divByZeroHit = false;
|
||||
|
||||
/**
|
||||
* @param array<string, float|int|null> $variables
|
||||
*/
|
||||
public function evaluate(string $expression, array $variables = [], bool $guardDivZero = false): ?float
|
||||
{
|
||||
$this->variables = array_map(fn ($v) => $v === null ? 0.0 : (float) $v, $variables);
|
||||
$this->guardDivZero = $guardDivZero;
|
||||
$this->divByZeroHit = false;
|
||||
$this->tokens = $this->tokenize($expression);
|
||||
$this->pos = 0;
|
||||
|
||||
if (empty($this->tokens)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->parseExpression();
|
||||
|
||||
if ($this->pos < count($this->tokens)) {
|
||||
throw new RuntimeException('Ungkapan formula tidak sah berhampiran: '.($this->tokens[$this->pos]['value'] ?? ''));
|
||||
}
|
||||
|
||||
if ($this->guardDivZero && $this->divByZeroHit) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{type:string, value:string}>
|
||||
*/
|
||||
protected function tokenize(string $expr): array
|
||||
{
|
||||
$tokens = [];
|
||||
$len = strlen($expr);
|
||||
$i = 0;
|
||||
|
||||
while ($i < $len) {
|
||||
$ch = $expr[$i];
|
||||
|
||||
if (ctype_space($ch)) {
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($ch, ['+', '-', '*', '/', '(', ')'], true)) {
|
||||
$tokens[] = ['type' => 'op', 'value' => $ch];
|
||||
$i++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Nombor (termasuk perpuluhan)
|
||||
if (ctype_digit($ch) || ($ch === '.' && $i + 1 < $len && ctype_digit($expr[$i + 1]))) {
|
||||
$num = '';
|
||||
while ($i < $len && (ctype_digit($expr[$i]) || $expr[$i] === '.')) {
|
||||
$num .= $expr[$i];
|
||||
$i++;
|
||||
}
|
||||
$tokens[] = ['type' => 'num', 'value' => $num];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Token / pemboleh ubah: huruf, nombor, underscore (cth: SKOP1, A, D4I)
|
||||
if (ctype_alpha($ch) || $ch === '_') {
|
||||
$tok = '';
|
||||
while ($i < $len && (ctype_alnum($expr[$i]) || $expr[$i] === '_')) {
|
||||
$tok .= $expr[$i];
|
||||
$i++;
|
||||
}
|
||||
$tokens[] = ['type' => 'var', 'value' => $tok];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Aksara tidak sah dalam formula: '$ch'");
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
protected function peek(): ?array
|
||||
{
|
||||
return $this->tokens[$this->pos] ?? null;
|
||||
}
|
||||
|
||||
protected function consume(): ?array
|
||||
{
|
||||
return $this->tokens[$this->pos++] ?? null;
|
||||
}
|
||||
|
||||
/** expression = term (('+' | '-') term)* */
|
||||
protected function parseExpression(): float
|
||||
{
|
||||
$value = $this->parseTerm();
|
||||
|
||||
while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) {
|
||||
$op = $this->consume()['value'];
|
||||
$right = $this->parseTerm();
|
||||
$value = $op === '+' ? $value + $right : $value - $right;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** term = factor (('*' | '/') factor)* */
|
||||
protected function parseTerm(): float
|
||||
{
|
||||
$value = $this->parseFactor();
|
||||
|
||||
while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['*', '/'], true)) {
|
||||
$op = $this->consume()['value'];
|
||||
$right = $this->parseFactor();
|
||||
|
||||
if ($op === '*') {
|
||||
$value *= $right;
|
||||
} else {
|
||||
if ($right == 0.0) {
|
||||
$this->divByZeroHit = true;
|
||||
if ($this->guardDivZero) {
|
||||
$value = 0.0;
|
||||
|
||||
continue;
|
||||
}
|
||||
throw new RuntimeException('Pembahagian dengan sifar dalam formula.');
|
||||
}
|
||||
$value /= $right;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/** factor = number | variable | '(' expression ')' | ('+'|'-') factor */
|
||||
protected function parseFactor(): float
|
||||
{
|
||||
$t = $this->peek();
|
||||
|
||||
if ($t === null) {
|
||||
throw new RuntimeException('Ungkapan formula tidak lengkap.');
|
||||
}
|
||||
|
||||
if ($t['type'] === 'op' && $t['value'] === '(') {
|
||||
$this->consume();
|
||||
$value = $this->parseExpression();
|
||||
$close = $this->consume();
|
||||
if (! $close || $close['value'] !== ')') {
|
||||
throw new RuntimeException('Kurungan tidak seimbang dalam formula.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) {
|
||||
$op = $this->consume()['value'];
|
||||
$value = $this->parseFactor();
|
||||
|
||||
return $op === '-' ? -$value : $value;
|
||||
}
|
||||
|
||||
if ($t['type'] === 'num') {
|
||||
$this->consume();
|
||||
|
||||
return (float) $t['value'];
|
||||
}
|
||||
|
||||
if ($t['type'] === 'var') {
|
||||
$this->consume();
|
||||
|
||||
return $this->variables[$t['value']] ?? 0.0;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Token formula tidak dijangka: '.$t['value']);
|
||||
}
|
||||
}
|
||||
100
app/Services/FormulaService.php
Normal file
100
app/Services/FormulaService.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ChecklistTemplate;
|
||||
use App\Models\Submission;
|
||||
|
||||
/**
|
||||
* Mengira hasil formula seksyen (A, B, C, D, E ...) berdasarkan jawapan serahan.
|
||||
*
|
||||
* Token tersedia kepada formula:
|
||||
* - formula_token bagi setiap checklist_item (cth: SKOP1, EE, RE, D4I)
|
||||
* - result_token bagi setiap formula yang telah dikira sebelumnya (cth: A, B)
|
||||
*
|
||||
* Formula dinilai mengikut turutan `order` supaya C boleh merujuk A & B, dsb.
|
||||
*/
|
||||
class FormulaService
|
||||
{
|
||||
public function __construct(protected ExpressionEvaluator $evaluator) {}
|
||||
|
||||
/**
|
||||
* Kira semua hasil formula bagi satu serahan.
|
||||
*
|
||||
* @return array<int, array{result_token:string,label:?string,value:?float,unit:?string}>
|
||||
*/
|
||||
public function computeForSubmission(Submission $submission): array
|
||||
{
|
||||
$template = $submission->checklistTemplate
|
||||
?: ($submission->checklist_template_id ? ChecklistTemplate::find($submission->checklist_template_id) : ChecklistTemplate::activeTemplate());
|
||||
|
||||
if (! $template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Bina peta token => nilai daripada jawapan item.
|
||||
$variables = [];
|
||||
$answers = $submission->relationLoaded('answers')
|
||||
? $submission->answers
|
||||
: $submission->answers()->with('item')->get();
|
||||
|
||||
foreach ($answers as $answer) {
|
||||
$item = $answer->item;
|
||||
if ($item && $item->formula_token) {
|
||||
$variables[$item->formula_token] = is_numeric($answer->value) ? (float) $answer->value : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->compute($template, $variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kira hasil formula daripada peta pemboleh ubah mentah (berguna untuk ujian).
|
||||
*
|
||||
* @param array<string, float|int|null> $variables
|
||||
* @return array<int, array{result_token:string,label:?string,value:?float,unit:?string}>
|
||||
*/
|
||||
public function compute(ChecklistTemplate $template, array $variables): array
|
||||
{
|
||||
$formulas = $template->relationLoaded('formulas')
|
||||
? $template->formulas->sortBy('order')
|
||||
: $template->formulas()->orderBy('order')->get();
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($formulas as $formula) {
|
||||
$value = $this->evaluator->evaluate($formula->expression, $variables, $formula->guard_div_zero);
|
||||
|
||||
// Hasil ini menjadi token yang boleh dirujuk oleh formula seterusnya.
|
||||
$variables[$formula->result_token] = $value ?? 0.0;
|
||||
|
||||
$results[] = [
|
||||
'result_token' => $formula->result_token,
|
||||
'label' => $formula->label,
|
||||
'value' => $value,
|
||||
'unit' => $formula->unit,
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kira semula dan simpan hasil ke dalam jadual submission_results.
|
||||
*/
|
||||
public function recalculateAndStore(Submission $submission): void
|
||||
{
|
||||
$results = $this->computeForSubmission($submission);
|
||||
|
||||
foreach ($results as $r) {
|
||||
$submission->results()->updateOrCreate(
|
||||
['result_token' => $r['result_token']],
|
||||
[
|
||||
'label' => $r['label'],
|
||||
'value' => $r['value'],
|
||||
'unit' => $r['unit'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Services/ReportingCycleService.php
Normal file
37
app/Services/ReportingCycleService.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\ChecklistTemplate;
|
||||
use App\Models\ReportingCycle;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ReportingCycleService
|
||||
{
|
||||
/**
|
||||
* Bina atribut kitaran daripada tahun pembaharuan.
|
||||
* Peraturan perniagaan:
|
||||
* - reporting_year = renewal_year - 2 (cth: pembaharuan 2028 => pelaporan 2026)
|
||||
* - tempoh pelaporan: 1 Jan - 31 Dis tahun pelaporan
|
||||
* - tarikh tutup lalai: hari/bulan yang dikonfigur JPP, tahun (renewal_year - 1)
|
||||
*/
|
||||
public function buildAttributes(int $renewalYear, ?string $cutOffDate = null): array
|
||||
{
|
||||
$reportingYear = ReportingCycle::reportingYearFor($renewalYear);
|
||||
|
||||
$cutOffDay = (int) AppSetting::get('default_cut_off_day', 1);
|
||||
$cutOffMonth = (int) AppSetting::get('default_cut_off_month', 12);
|
||||
|
||||
// Pembaharuan dibuat sebelum/pada 1 Disember tahun sebelum pembaharuan.
|
||||
$defaultCutOff = Carbon::create($renewalYear - 1, $cutOffMonth, $cutOffDay);
|
||||
|
||||
return [
|
||||
'reporting_year' => $reportingYear,
|
||||
'period_start' => Carbon::create($reportingYear, 1, 1),
|
||||
'period_end' => Carbon::create($reportingYear, 12, 31),
|
||||
'cut_off_date' => $cutOffDate ? Carbon::parse($cutOffDate) : $defaultCutOff,
|
||||
'checklist_template_id' => ChecklistTemplate::activeTemplate()?->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
144
app/Services/SubmissionWorkflowService.php
Normal file
144
app/Services/SubmissionWorkflowService.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ChecklistTemplate;
|
||||
use App\Models\DataCentre;
|
||||
use App\Models\ReportingCycle;
|
||||
use App\Models\Submission;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class SubmissionWorkflowService
|
||||
{
|
||||
public function __construct(protected FormulaService $formulaService) {}
|
||||
|
||||
/**
|
||||
* Dapatkan atau cipta serahan draf untuk pusat data + kitaran tertentu.
|
||||
*/
|
||||
public function findOrCreateDraft(DataCentre $dataCentre, ReportingCycle $cycle): Submission
|
||||
{
|
||||
$consultantId = $dataCentre->current_consultant_id;
|
||||
|
||||
if (! $consultantId) {
|
||||
throw ValidationException::withMessages([
|
||||
'data_centre_id' => 'Pusat data ini tiada perunding aktif. Sila hubungi JPP.',
|
||||
]);
|
||||
}
|
||||
|
||||
return Submission::firstOrCreate(
|
||||
[
|
||||
'data_centre_id' => $dataCentre->id,
|
||||
'reporting_cycle_id' => $cycle->id,
|
||||
],
|
||||
[
|
||||
'consultant_id' => $consultantId,
|
||||
'checklist_template_id' => $cycle->checklist_template_id ?? ChecklistTemplate::activeTemplate()?->id,
|
||||
'status' => 'draf',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan jawapan senarai semak (draf). $answers = [item_id => [value, unit, page_reference, consultant_note]]
|
||||
*/
|
||||
public function saveAnswers(Submission $submission, array $answers): void
|
||||
{
|
||||
DB::transaction(function () use ($submission, $answers) {
|
||||
foreach ($answers as $itemId => $data) {
|
||||
$submission->answers()->updateOrCreate(
|
||||
['checklist_item_id' => $itemId],
|
||||
[
|
||||
'value' => $data['value'] ?? null,
|
||||
'unit' => $data['unit'] ?? null,
|
||||
'page_reference' => $data['page_reference'] ?? null,
|
||||
'consultant_note' => $data['consultant_note'] ?? null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Kira semula nilai formula (A, B, C, D, E ...).
|
||||
$submission->load('answers.item');
|
||||
$this->formulaService->recalculateAndStore($submission);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hantar serahan secara muktamad (draf -> baru).
|
||||
*/
|
||||
public function submit(Submission $submission, User $user): void
|
||||
{
|
||||
if ($submission->is_locked) {
|
||||
throw ValidationException::withMessages(['status' => 'Serahan telah dikunci dan tidak boleh dihantar semula.']);
|
||||
}
|
||||
|
||||
// Mesti ada sekurang-kurangnya satu laporan PDF aktif sebelum hantar.
|
||||
if (! $submission->activeDocument()->exists()) {
|
||||
throw ValidationException::withMessages(['document' => 'Sila muat naik laporan PDF sebelum menghantar serahan.']);
|
||||
}
|
||||
|
||||
$from = $submission->status;
|
||||
// Jika sedang dalam pembetulan, hantar semula sebagai 'dalam_tindakan'.
|
||||
$to = $from === 'pembetulan_perunding' ? 'dalam_tindakan' : 'baru';
|
||||
|
||||
$this->changeStatus($submission, $to, $user, 'Serahan dihantar oleh perunding.', [
|
||||
'submitted_at' => now(),
|
||||
'submitted_by' => $user->id,
|
||||
]);
|
||||
|
||||
$this->formulaService->recalculateAndStore($submission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tukar status serahan dengan rekod sejarah & audit.
|
||||
*/
|
||||
public function changeStatus(Submission $submission, string $to, User $user, ?string $notes = null, array $extra = []): void
|
||||
{
|
||||
DB::transaction(function () use ($submission, $to, $user, $notes, $extra) {
|
||||
$from = $submission->status;
|
||||
|
||||
$submission->fill(array_merge(['status' => $to], $extra));
|
||||
$submission->save();
|
||||
|
||||
$submission->statusHistories()->create([
|
||||
'from_status' => $from,
|
||||
'to_status' => $to,
|
||||
'changed_by' => $user->id,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
|
||||
activity('serahan')
|
||||
->performedOn($submission)
|
||||
->causedBy($user)
|
||||
->withProperties(['from' => $from, 'to' => $to, 'notes' => $notes])
|
||||
->log("Status serahan ditukar daripada {$from} kepada {$to}");
|
||||
});
|
||||
}
|
||||
|
||||
public function lock(Submission $submission, User $user, ?string $reason = null): void
|
||||
{
|
||||
$submission->update([
|
||||
'is_locked' => true,
|
||||
'locked_by' => $user->id,
|
||||
'locked_at' => now(),
|
||||
'lock_reason' => $reason,
|
||||
]);
|
||||
|
||||
activity('serahan')->performedOn($submission)->causedBy($user)
|
||||
->withProperties(['reason' => $reason])->log('Serahan dikunci');
|
||||
}
|
||||
|
||||
public function unlock(Submission $submission, User $user, string $reason): void
|
||||
{
|
||||
$submission->update([
|
||||
'is_locked' => false,
|
||||
'lock_reason' => $reason,
|
||||
'locked_by' => $user->id,
|
||||
'locked_at' => now(),
|
||||
]);
|
||||
|
||||
activity('serahan')->performedOn($submission)->causedBy($user)
|
||||
->withProperties(['reason' => $reason])->log('Serahan dibuka semula');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user