Compare commits
14 Commits
fix/nginx-
...
feat/revis
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
720e314bbd | ||
|
|
0b1589d173 | ||
|
|
e3956ae0e4 | ||
|
|
748ac8a77e | ||
|
|
e52c4d1d27 | ||
|
|
cec11d6385 | ||
|
|
b632996052 | ||
|
|
e59841fd23 | ||
|
|
e468672bbe | ||
|
|
685c6df82e | ||
|
|
cfef3775d7 | ||
|
|
956df5cfe6 | ||
|
|
fa554446ca | ||
|
|
0ef03fe7cb |
316
app/Exports/StockProductsExport.php
Normal file
316
app/Exports/StockProductsExport.php
Normal file
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use Maatwebsite\Excel\Concerns\WithColumnWidths;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class StockProductsExport implements FromCollection, WithHeadings, WithStyles, WithColumnWidths
|
||||
{
|
||||
protected $reportData;
|
||||
|
||||
public function __construct($reportData)
|
||||
{
|
||||
// Validate and sanitize report data
|
||||
if (!is_array($reportData)) {
|
||||
throw new \InvalidArgumentException('Report data must be an array');
|
||||
}
|
||||
|
||||
if (!isset($reportData['data']) || !isset($reportData['dealers'])) {
|
||||
throw new \InvalidArgumentException('Report data must contain "data" and "dealers" keys');
|
||||
}
|
||||
|
||||
// Ensure dealers is a collection
|
||||
if (!($reportData['dealers'] instanceof Collection)) {
|
||||
$reportData['dealers'] = collect($reportData['dealers']);
|
||||
}
|
||||
|
||||
// Ensure data is an array
|
||||
if (!is_array($reportData['data'])) {
|
||||
$reportData['data'] = [];
|
||||
}
|
||||
|
||||
$this->reportData = $reportData;
|
||||
|
||||
// Debug: Log the structure of report data
|
||||
Log::info('StockProductsExport constructor', [
|
||||
'has_data' => isset($reportData['data']),
|
||||
'has_dealers' => isset($reportData['dealers']),
|
||||
'data_count' => isset($reportData['data']) ? count($reportData['data']) : 0,
|
||||
'dealers_count' => isset($reportData['dealers']) ? count($reportData['dealers']) : 0,
|
||||
'dealers' => isset($reportData['dealers']) ? $reportData['dealers']->pluck('name')->toArray() : []
|
||||
]);
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
try {
|
||||
$data = collect();
|
||||
$no = 1;
|
||||
|
||||
foreach ($this->reportData['data'] as $row) {
|
||||
$exportRow = [
|
||||
'no' => $no++,
|
||||
'kode_produk' => $row['product_code'] ?? '',
|
||||
'nama_produk' => $row['product_name'] ?? '',
|
||||
'kategori' => $row['category_name'] ?? '',
|
||||
'satuan' => $row['unit'] ?? ''
|
||||
];
|
||||
|
||||
// Add dealer columns
|
||||
foreach ($this->reportData['dealers'] as $dealer) {
|
||||
try {
|
||||
$dealerKey = "dealer_{$dealer->id}";
|
||||
// Clean dealer name for array key to avoid special characters
|
||||
$cleanDealerName = $this->cleanDealerName($dealer->name);
|
||||
$exportRow[$cleanDealerName] = $row[$dealerKey] ?? 0;
|
||||
|
||||
Log::info('Processing dealer column', [
|
||||
'original_name' => $dealer->name,
|
||||
'clean_name' => $cleanDealerName,
|
||||
'dealer_key' => $dealerKey,
|
||||
'value' => $row[$dealerKey] ?? 0
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error processing dealer column', [
|
||||
'dealer_id' => $dealer->id,
|
||||
'dealer_name' => $dealer->name,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// Use a safe fallback name
|
||||
$exportRow['Dealer_' . $dealer->id] = $row["dealer_{$dealer->id}"] ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add total stock
|
||||
$exportRow['total_stok'] = $row['total_stock'] ?? 0;
|
||||
|
||||
$data->push($exportRow);
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in collection method', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Return empty collection as fallback
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
try {
|
||||
$headings = [
|
||||
'No',
|
||||
'Kode Produk',
|
||||
'Nama Produk',
|
||||
'Kategori',
|
||||
'Satuan'
|
||||
];
|
||||
|
||||
// Add dealer headings
|
||||
foreach ($this->reportData['dealers'] as $dealer) {
|
||||
try {
|
||||
$cleanName = $this->cleanDealerName($dealer->name);
|
||||
$headings[] = $cleanName;
|
||||
|
||||
Log::info('Processing dealer heading', [
|
||||
'original_name' => $dealer->name,
|
||||
'clean_name' => $cleanName
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error processing dealer heading', [
|
||||
'dealer_id' => $dealer->id,
|
||||
'dealer_name' => $dealer->name,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// Use a safe fallback name
|
||||
$headings[] = 'Dealer_' . $dealer->id;
|
||||
}
|
||||
}
|
||||
|
||||
// Add total heading
|
||||
$headings[] = 'Total Stok';
|
||||
|
||||
return $headings;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in headings method', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Return basic headings as fallback
|
||||
return ['No', 'Kode Produk', 'Nama Produk', 'Kategori', 'Satuan', 'Total Stok'];
|
||||
}
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet)
|
||||
{
|
||||
try {
|
||||
$lastColumn = $sheet->getHighestColumn();
|
||||
$lastRow = $sheet->getHighestRow();
|
||||
|
||||
// Validate column and row values
|
||||
if (!$lastColumn || !$lastRow || $lastRow < 1) {
|
||||
Log::warning('Invalid sheet dimensions', ['lastColumn' => $lastColumn, 'lastRow' => $lastRow]);
|
||||
return $sheet;
|
||||
}
|
||||
|
||||
// Style header row
|
||||
$sheet->getStyle('A1:' . $lastColumn . '1')->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'color' => ['rgb' => 'FFFFFF'],
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '4472C4'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Style all cells
|
||||
$sheet->getStyle('A1:' . $lastColumn . $lastRow)->applyFromArray([
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
'alignment' => [
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Center align specific columns
|
||||
$sheet->getStyle('A:A')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle('D:D')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
// Right align numeric columns (dealer columns and total)
|
||||
$dealerStartCol = 'E';
|
||||
$dealerCount = count($this->reportData['dealers']);
|
||||
|
||||
if ($dealerCount > 0) {
|
||||
$dealerEndCol = chr(ord('E') + $dealerCount - 1);
|
||||
$totalCol = chr(ord($dealerStartCol) + $dealerCount);
|
||||
|
||||
// Validate column letters
|
||||
if (ord($dealerEndCol) <= ord('Z') && ord($totalCol) <= ord('Z')) {
|
||||
$sheet->getStyle($dealerStartCol . ':' . $dealerEndCol)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||
$sheet->getStyle($totalCol . ':' . $totalCol)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||
|
||||
// Bold total column
|
||||
$sheet->getStyle($totalCol . '1:' . $totalCol . $lastRow)->getFont()->setBold(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-size columns safely
|
||||
foreach (range('A', $lastColumn) as $column) {
|
||||
try {
|
||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Failed to auto-size column', ['column' => $column, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
return $sheet;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in styles method', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return $sheet;
|
||||
}
|
||||
}
|
||||
|
||||
public function columnWidths(): array
|
||||
{
|
||||
try {
|
||||
$widths = [
|
||||
'A' => 8, // No
|
||||
'B' => 15, // Kode Produk
|
||||
'C' => 30, // Nama Produk
|
||||
'D' => 15, // Kategori
|
||||
'E' => 10, // Satuan
|
||||
];
|
||||
|
||||
// Add dealer column widths safely
|
||||
$currentCol = 'F';
|
||||
$dealerCount = count($this->reportData['dealers']);
|
||||
|
||||
for ($i = 0; $i < $dealerCount; $i++) {
|
||||
// Validate column letter
|
||||
if (ord($currentCol) <= ord('Z')) {
|
||||
$widths[$currentCol] = 15;
|
||||
$currentCol = chr(ord($currentCol) + 1);
|
||||
} else {
|
||||
Log::warning('Too many dealer columns, stopping at column Z');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add total column width if we haven't exceeded Z
|
||||
if (ord($currentCol) <= ord('Z')) {
|
||||
$widths[$currentCol] = 15;
|
||||
}
|
||||
|
||||
return $widths;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in columnWidths method', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Return basic widths as fallback
|
||||
return [
|
||||
'A' => 8, // No
|
||||
'B' => 15, // Kode Produk
|
||||
'C' => 30, // Nama Produk
|
||||
'D' => 15, // Kategori
|
||||
'E' => 10, // Satuan
|
||||
'F' => 15 // Total Stok
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean dealer name to make it safe for array keys and Excel headers
|
||||
*/
|
||||
private function cleanDealerName($dealerName)
|
||||
{
|
||||
// Remove or replace special characters that can cause issues with Excel
|
||||
$cleanName = preg_replace('/[^a-zA-Z0-9\s\-_]/', '', $dealerName);
|
||||
$cleanName = trim($cleanName);
|
||||
|
||||
// If name becomes empty, use a default
|
||||
if (empty($cleanName)) {
|
||||
$cleanName = 'Dealer';
|
||||
}
|
||||
|
||||
// Limit length to avoid Excel issues
|
||||
if (strlen($cleanName) > 31) {
|
||||
$cleanName = substr($cleanName, 0, 31);
|
||||
}
|
||||
|
||||
return $cleanName;
|
||||
}
|
||||
}
|
||||
435
app/Exports/TechnicianReportExport.php
Normal file
435
app/Exports/TechnicianReportExport.php
Normal file
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Services\TechnicianReportService;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use Maatwebsite\Excel\Concerns\WithColumnWidths;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Events\AfterSheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class TechnicianReportExport implements FromCollection, WithHeadings, WithStyles, WithColumnWidths, WithEvents
|
||||
{
|
||||
protected $dealerId;
|
||||
protected $startDate;
|
||||
protected $endDate;
|
||||
protected $technicianReportService;
|
||||
protected $mechanics;
|
||||
protected $headings;
|
||||
protected $filterInfo;
|
||||
|
||||
public function __construct($dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
$this->dealerId = $dealerId;
|
||||
$this->startDate = $startDate;
|
||||
$this->endDate = $endDate;
|
||||
$this->technicianReportService = new TechnicianReportService();
|
||||
|
||||
// Get mechanics and prepare headings
|
||||
$this->prepareHeadings();
|
||||
$this->prepareFilterInfo();
|
||||
}
|
||||
|
||||
private function prepareHeadings()
|
||||
{
|
||||
try {
|
||||
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||
$this->dealerId,
|
||||
$this->startDate,
|
||||
$this->endDate
|
||||
);
|
||||
|
||||
$this->mechanics = $reportData['mechanics'];
|
||||
|
||||
// Build headings - simplified structure
|
||||
$this->headings = [
|
||||
'No',
|
||||
'Nama Pekerjaan',
|
||||
'Kode Pekerjaan',
|
||||
'Kategori'
|
||||
];
|
||||
|
||||
// Add mechanic columns (only total, no completed/pending)
|
||||
foreach ($this->mechanics as $mechanic) {
|
||||
$mechanicName = $this->cleanName($mechanic->name);
|
||||
$this->headings[] = $mechanicName;
|
||||
}
|
||||
|
||||
// Add total column at the end
|
||||
$this->headings[] = 'Total';
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error preparing headings: ' . $e->getMessage());
|
||||
$this->headings = ['Error preparing data'];
|
||||
$this->mechanics = collect();
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareFilterInfo()
|
||||
{
|
||||
$this->filterInfo = [];
|
||||
|
||||
// Dealer filter
|
||||
if ($this->dealerId) {
|
||||
$dealer = \App\Models\Dealer::find($this->dealerId);
|
||||
$dealerName = $dealer ? $dealer->name : 'Unknown Dealer';
|
||||
$this->filterInfo[] = "Dealer: {$dealerName}";
|
||||
} else {
|
||||
// Check user access for "Semua Dealer"
|
||||
$user = auth()->user();
|
||||
if ($user && $user->role_id) {
|
||||
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||
if ($role) {
|
||||
$technicianReportService = new \App\Services\TechnicianReportService();
|
||||
if ($technicianReportService->isAdminRole($role)) {
|
||||
$this->filterInfo[] = "Dealer: Semua Dealer (Admin)";
|
||||
} else if ($role->dealers->count() > 0) {
|
||||
$dealerNames = $role->dealers->pluck('name')->implode(', ');
|
||||
$this->filterInfo[] = "Dealer: Semua Dealer (Pivot: {$dealerNames})";
|
||||
} else {
|
||||
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||
}
|
||||
} else {
|
||||
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||
}
|
||||
} else {
|
||||
$this->filterInfo[] = "Dealer: Semua Dealer";
|
||||
}
|
||||
}
|
||||
|
||||
// Date range filter
|
||||
if ($this->startDate && $this->endDate) {
|
||||
$startDateFormatted = Carbon::parse($this->startDate)->format('d/m/Y');
|
||||
$endDateFormatted = Carbon::parse($this->endDate)->format('d/m/Y');
|
||||
$this->filterInfo[] = "Periode: {$startDateFormatted} - {$endDateFormatted}";
|
||||
} elseif ($this->startDate) {
|
||||
$startDateFormatted = Carbon::parse($this->startDate)->format('d/m/Y');
|
||||
$this->filterInfo[] = "Tanggal Mulai: {$startDateFormatted}";
|
||||
} elseif ($this->endDate) {
|
||||
$endDateFormatted = Carbon::parse($this->endDate)->format('d/m/Y');
|
||||
$this->filterInfo[] = "Tanggal Akhir: {$endDateFormatted}";
|
||||
} else {
|
||||
$this->filterInfo[] = "Periode: Semua Periode";
|
||||
}
|
||||
|
||||
// Export date
|
||||
$exportDate = Carbon::now()->format('d/m/Y H:i:s');
|
||||
$this->filterInfo[] = "Tanggal Export: {$exportDate}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean name for Excel compatibility
|
||||
*/
|
||||
private function cleanName($name)
|
||||
{
|
||||
// Remove special characters and limit length
|
||||
$cleaned = preg_replace('/[^a-zA-Z0-9\s]/', '', $name);
|
||||
$cleaned = trim($cleaned);
|
||||
|
||||
// Limit to 31 characters (Excel sheet name limit)
|
||||
if (strlen($cleaned) > 31) {
|
||||
$cleaned = substr($cleaned, 0, 31);
|
||||
}
|
||||
|
||||
return $cleaned ?: 'Unknown';
|
||||
}
|
||||
|
||||
public function collection()
|
||||
{
|
||||
try {
|
||||
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||
$this->dealerId,
|
||||
$this->startDate,
|
||||
$this->endDate
|
||||
);
|
||||
$data = [];
|
||||
$no = 1;
|
||||
$columnTotals = [];
|
||||
foreach ($this->mechanics as $mechanic) {
|
||||
$columnTotals["mechanic_{$mechanic->id}_total"] = 0;
|
||||
}
|
||||
$columnTotals['row_total'] = 0;
|
||||
foreach ($reportData['data'] as $row) {
|
||||
$rowTotal = 0;
|
||||
$exportRow = [
|
||||
$no++,
|
||||
$row['work_name'],
|
||||
$row['work_code'],
|
||||
$row['category_name']
|
||||
];
|
||||
foreach ($this->mechanics as $mechanic) {
|
||||
$mechanicTotal = $row["mechanic_{$mechanic->id}_total"] ?? 0;
|
||||
$exportRow[] = $mechanicTotal;
|
||||
$rowTotal += $mechanicTotal;
|
||||
$columnTotals["mechanic_{$mechanic->id}_total"] += $mechanicTotal;
|
||||
}
|
||||
$exportRow[] = $rowTotal;
|
||||
$columnTotals['row_total'] += $rowTotal;
|
||||
$data[] = $exportRow;
|
||||
}
|
||||
// Add total row
|
||||
$totalRow = ['', 'TOTAL', '', ''];
|
||||
foreach ($this->mechanics as $mechanic) {
|
||||
$totalRow[] = $columnTotals["mechanic_{$mechanic->id}_total"];
|
||||
}
|
||||
$totalRow[] = $columnTotals['row_total'];
|
||||
$data[] = $totalRow;
|
||||
return collect($data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in collection: ' . $e->getMessage());
|
||||
return collect([['Error loading data']]);
|
||||
}
|
||||
}
|
||||
|
||||
public function headings(): array
|
||||
{
|
||||
return $this->headings;
|
||||
}
|
||||
|
||||
public function styles(Worksheet $sheet)
|
||||
{
|
||||
try {
|
||||
$lastColumn = $sheet->getHighestColumn();
|
||||
$lastRow = $sheet->getHighestRow();
|
||||
|
||||
// Calculate positions
|
||||
$titleRow = 1;
|
||||
$headerRow = 1; // Headers are now in row 2
|
||||
$dataStartRow = 2; // Data starts in row 3
|
||||
|
||||
// Calculate total row position (after data)
|
||||
$dataRows = count($this->technicianReportService->getTechnicianReportData($this->dealerId, $this->startDate, $this->endDate)['data']);
|
||||
$totalRow = $dataStartRow + $dataRows;
|
||||
$filterStartRow = $totalRow + 2; // After total row + empty row
|
||||
|
||||
// Style the title row (row 1)
|
||||
$sheet->getStyle('A' . $titleRow . ':' . $lastColumn . $titleRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 16,
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Header styling (row 2)
|
||||
$sheet->getStyle('A' . $headerRow . ':' . $lastColumn . $headerRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'color' => ['rgb' => 'FFFFFF'],
|
||||
'size' => 10,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '2E5BBA'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Data styling (starting from row 3)
|
||||
if ($lastRow > $headerRow) {
|
||||
$dataEndRow = $totalRow;
|
||||
$sheet->getStyle('A' . $dataStartRow . ':' . $lastColumn . $dataEndRow)->applyFromArray([
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Left align text columns
|
||||
$sheet->getStyle('B' . $dataStartRow . ':D' . $dataEndRow)->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
// Style the total row
|
||||
$sheet->getStyle('A' . $totalRow . ':' . $lastColumn . $totalRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 11,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'F2F2F2'],
|
||||
],
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Style the export information section
|
||||
if ($filterStartRow <= $lastRow) {
|
||||
$exportInfoRow = $totalRow + 2; // After total row + empty row
|
||||
$filterEndRow = $lastRow;
|
||||
|
||||
// Style the "INFORMASI EXPORT" title
|
||||
$sheet->getStyle('A' . $exportInfoRow . ':' . $lastColumn . $exportInfoRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 12,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'E6E6E6'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
|
||||
// Style the filter info rows
|
||||
$filterInfoStartRow = $exportInfoRow + 3; // After title + empty + "Filter yang Digunakan:"
|
||||
$sheet->getStyle('A' . $filterInfoStartRow . ':' . $lastColumn . $filterEndRow)->applyFromArray([
|
||||
'font' => [
|
||||
'size' => 10,
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||
'vertical' => Alignment::VERTICAL_TOP,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
// Auto-size columns
|
||||
foreach (range('A', $lastColumn) as $column) {
|
||||
if ($column === 'A') {
|
||||
// Set specific width for column A (No) - don't auto-size
|
||||
$sheet->getColumnDimension($column)->setWidth(5);
|
||||
} else {
|
||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error applying styles: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function columnWidths(): array
|
||||
{
|
||||
$widths = [
|
||||
'A' => 5, // No - reduced from 8 to 5
|
||||
'B' => 30, // Nama Pekerjaan
|
||||
'C' => 15, // Kode Pekerjaan
|
||||
'D' => 20, // Kategori
|
||||
];
|
||||
|
||||
// Add widths for mechanic columns
|
||||
$currentColumn = 'E';
|
||||
foreach ($this->mechanics as $mechanic) {
|
||||
$widths[$currentColumn++] = 15; // Mechanic total
|
||||
}
|
||||
|
||||
// Add width for total column
|
||||
$widths[$currentColumn] = 15; // Total
|
||||
|
||||
return $widths;
|
||||
}
|
||||
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
AfterSheet::class => function(AfterSheet $event) {
|
||||
$sheet = $event->sheet->getDelegate();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
// Header styling ONLY for row 1
|
||||
$sheet->getStyle('A1:' . $highestColumn . '1')->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'color' => ['rgb' => 'FFFFFF'],
|
||||
'size' => 10,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => '2E5BBA'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_CENTER,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
// Total row styling (only last row)
|
||||
$sheet->getStyle('A' . $highestRow . ':' . $highestColumn . $highestRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 11,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'F2F2F2'],
|
||||
],
|
||||
'borders' => [
|
||||
'allBorders' => [
|
||||
'borderStyle' => Border::BORDER_THIN,
|
||||
'color' => ['rgb' => '000000'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
// Export info below table
|
||||
$infoStartRow = $highestRow + 2;
|
||||
$sheet->setCellValue('A' . $infoStartRow, 'INFORMASI EXPORT');
|
||||
$sheet->getStyle('A' . $infoStartRow . ':' . $highestColumn . $infoStartRow)->applyFromArray([
|
||||
'font' => [
|
||||
'bold' => true,
|
||||
'size' => 12,
|
||||
],
|
||||
'fill' => [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'startColor' => ['rgb' => 'E6E6E6'],
|
||||
],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||
'vertical' => Alignment::VERTICAL_CENTER,
|
||||
],
|
||||
]);
|
||||
$sheet->setCellValue('A' . ($infoStartRow + 2), 'Filter yang Digunakan:');
|
||||
$row = $infoStartRow + 3;
|
||||
foreach ($this->filterInfo as $info) {
|
||||
$sheet->setCellValue('A' . $row, $info);
|
||||
$row++;
|
||||
}
|
||||
$sheet->getStyle('A' . ($infoStartRow + 2) . ':A' . ($row-1))->applyFromArray([
|
||||
'font' => [ 'size' => 10 ],
|
||||
'alignment' => [
|
||||
'horizontal' => Alignment::HORIZONTAL_LEFT,
|
||||
'vertical' => Alignment::VERTICAL_TOP,
|
||||
],
|
||||
]);
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use App\Models\Work;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
@@ -37,7 +39,21 @@ class AdminController extends Controller
|
||||
$month = $request->month;
|
||||
$dealer = $request->dealer;
|
||||
$year = $request->year;
|
||||
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::all();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
||||
// dd($ajax_url);
|
||||
return view('dashboard', compact('month','year', 'ajax_url', 'dealer', 'dealer_datas'));
|
||||
@@ -72,16 +88,47 @@ class AdminController extends Controller
|
||||
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
||||
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$sql .= " and t.dealer_id = '". $dealer ."'";
|
||||
}
|
||||
|
||||
$dealer_work_trx = DB::statement($sql);
|
||||
|
||||
// Get dealers based on user role - only change this part
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::all();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||
}else{
|
||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||
if($dealer_datas->count() > 0) {
|
||||
$allowedDealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
if(!in_array($dealer, $allowedDealerIds)) {
|
||||
// If dealer is not allowed, reset to 'all'
|
||||
$dealer = 'all';
|
||||
}
|
||||
} else {
|
||||
// If no dealers are allowed, reset to 'all'
|
||||
$dealer = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
// Build dealer filter based on user role
|
||||
$dealerFilter = '';
|
||||
if($dealer_datas->count() > 0) {
|
||||
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
$dealerFilter = " and s.dealer_id IN (" . implode(',', $dealerIds) . ")";
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."'". $dealerFilter ." GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."'". $dealerFilter ." GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||
} else {
|
||||
$dealer_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id, \", @sql, \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.dealer_id ORDER BY s.dealer_id ASC\"), CONCAT(\"SELECT d.name as DEALER, d.id as dealer_id \", \"FROM transactions s JOIN dealers d ON d.id = s.dealer_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`dealer_id` ORDER BY s.`dealer_id` ASC\"))");
|
||||
}
|
||||
|
||||
$dealer_work_trx = DB::statement("PREPARE stmt FROM @sql");
|
||||
@@ -128,10 +175,12 @@ class AdminController extends Controller
|
||||
|
||||
$prev_mth_start = date('Y-m-d', strtotime(date($year.'-'. $request->month .'-1')." -1 month"));
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && $year == date('Y')) {
|
||||
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}else{
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
||||
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}
|
||||
|
||||
$prev_month_trx = [];
|
||||
@@ -143,6 +192,11 @@ class AdminController extends Controller
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$prev_month = $prev_month->where('dealer_id', $request->dealer);
|
||||
$now_month = $now_month->where('dealer_id', $request->dealer);
|
||||
} else if($dealer_datas->count() > 0) {
|
||||
// Filter by allowed dealers based on user role
|
||||
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
$prev_month = $prev_month->whereIn('dealer_id', $dealerIds);
|
||||
$now_month = $now_month->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
$prev_month_trx[] = $prev_month->sum('qty');
|
||||
@@ -160,6 +214,36 @@ class AdminController extends Controller
|
||||
return view('dashboard_data', compact('theads', 'work_trx', 'month', 'year', 'dealer_names', 'dealer_trx', 'dealer', 'totals'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is admin type
|
||||
*/
|
||||
private function isAdminRole($role)
|
||||
{
|
||||
if (!$role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define admin role names that should have access to all dealers
|
||||
$adminRoleNames = [
|
||||
'admin'
|
||||
];
|
||||
|
||||
// Check if role name contains admin keywords (but not "area")
|
||||
$roleName = strtolower(trim($role->name));
|
||||
foreach ($adminRoleNames as $adminName) {
|
||||
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Role with "area" in name should use pivot dealers, not all dealers
|
||||
if (strpos($roleName, 'area') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function dealer_work_trx(Request $request) {
|
||||
$dealer_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request) {
|
||||
if(isset($request->month)) {
|
||||
@@ -227,10 +311,12 @@ class AdminController extends Controller
|
||||
foreach($works as $work1) {
|
||||
$prev_mth_start = date('Y-m-d', strtotime(date('Y-'. $request->month .'-1')." -1 month"));
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && date('Y') == date('Y')) {
|
||||
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}else{
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
||||
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}
|
||||
|
||||
// dd($prev_mth_end);
|
||||
@@ -348,10 +434,12 @@ class AdminController extends Controller
|
||||
foreach($works as $work1) {
|
||||
$prev_mth_start = date('Y-m-d', strtotime(date($request->year.'-'. $request->month .'-1')." -1 month"));
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
if($request->month == date('m')) {
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
if($request->month == date('m') && $request->year == date('Y')) {
|
||||
// Jika bulan sekarang, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}else{
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t');
|
||||
// Jika bulan lain, ambil total bulan sebelumnya yang lengkap
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('t', strtotime($prev_mth_start));
|
||||
}
|
||||
|
||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||
|
||||
@@ -287,7 +287,11 @@ class ApiController extends Controller
|
||||
|
||||
public function logout()
|
||||
{
|
||||
Auth::user()->tokens()->delete();
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth('sanctum')->user();
|
||||
if ($user) {
|
||||
$user->tokens()->delete();
|
||||
}
|
||||
return response()->json([
|
||||
'message' => 'Logout success',
|
||||
'status' => true,
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\User;
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
@@ -50,11 +51,39 @@ class LoginController extends Controller
|
||||
*/
|
||||
protected function authenticated(Request $request, $user)
|
||||
{
|
||||
$user = Privilege::where('menu_id', 10)->where('role_id', Auth::user()->role_id)->where('view', 1)->first();
|
||||
// Get user's role_id
|
||||
$roleId = Auth::user()->role_id;
|
||||
|
||||
if ($user != null) {
|
||||
return redirect()->route('dashboard');
|
||||
}else{
|
||||
if (!$roleId) {
|
||||
// User has no role, redirect to default
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
// Check if user has access to adminarea menu
|
||||
if (!User::roleCanAccessMenu($roleId, 'adminarea')) {
|
||||
// User doesn't have admin area access, redirect to default home
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
// User has admin area access, get first accessible menu (excluding adminarea and mechanicarea)
|
||||
$firstMenu = Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||
->where('privileges.role_id', $roleId)
|
||||
->where('privileges.view', 1)
|
||||
->whereNotIn('menus.link', ['adminarea', 'mechanicarea'])
|
||||
->select('menus.*', 'privileges.view', 'privileges.create', 'privileges.update', 'privileges.delete')
|
||||
->orderBy('menus.id')
|
||||
->first();
|
||||
|
||||
if (!$firstMenu) {
|
||||
// User has no accessible menus (excluding adminarea/mechanicarea), redirect to default
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to redirect to the first accessible menu
|
||||
return redirect()->route($firstMenu->link);
|
||||
} catch (\Exception $e) {
|
||||
// Route doesn't exist, fallback to default home
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
237
app/Http/Controllers/KPI/TargetsController.php
Normal file
237
app/Http/Controllers/KPI/TargetsController.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\KPI;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\KPI\StoreKpiTargetRequest;
|
||||
use App\Http\Requests\KPI\UpdateKpiTargetRequest;
|
||||
use App\Models\KpiTarget;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TargetsController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of KPI targets
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$targets = KpiTarget::with(['user', 'user.role'])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(15);
|
||||
|
||||
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||
$mechanics = User::with('dealer')
|
||||
->where('role_id', 3)
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
// If no mechanics found, get all users as fallback
|
||||
if ($mechanics->isEmpty()) {
|
||||
$mechanics = User::with('dealer')
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
}
|
||||
|
||||
return view('kpi.targets.index', compact('targets', 'mechanics'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new KPI target
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||
$mechanics = User::with('dealer')
|
||||
->where('role_id', 3)
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
// Debug: Log the mechanics found
|
||||
Log::info('Mechanics found for KPI target creation:', [
|
||||
'count' => $mechanics->count(),
|
||||
'mechanics' => $mechanics->pluck('name', 'id')->toArray()
|
||||
]);
|
||||
|
||||
// If no mechanics found, get all users as fallback
|
||||
if ($mechanics->isEmpty()) {
|
||||
$mechanics = User::with('dealer')
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
Log::warning('No mechanics found, using all users as fallback', [
|
||||
'count' => $mechanics->count()
|
||||
]);
|
||||
}
|
||||
|
||||
return view('kpi.targets.create', compact('mechanics'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created KPI target
|
||||
*/
|
||||
public function store(StoreKpiTargetRequest $request)
|
||||
{
|
||||
try {
|
||||
// Log the validated data
|
||||
Log::info('Creating KPI target with data:', $request->validated());
|
||||
|
||||
// Check if user already has an active target and deactivate it
|
||||
$existingTarget = KpiTarget::where('user_id', $request->user_id)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if ($existingTarget) {
|
||||
Log::info('Deactivating existing active KPI target', [
|
||||
'user_id' => $request->user_id,
|
||||
'existing_target_id' => $existingTarget->id
|
||||
]);
|
||||
|
||||
// Deactivate the existing target
|
||||
$existingTarget->update(['is_active' => false]);
|
||||
}
|
||||
|
||||
$target = KpiTarget::create($request->validated());
|
||||
|
||||
Log::info('KPI target created successfully', [
|
||||
'target_id' => $target->id,
|
||||
'user_id' => $target->user_id
|
||||
]);
|
||||
|
||||
return redirect()->route('kpi.targets.index')
|
||||
->with('success', 'Target KPI berhasil ditambahkan');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create KPI target', [
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $request->validated()
|
||||
]);
|
||||
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', 'Gagal menambahkan target KPI: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified KPI target
|
||||
*/
|
||||
public function show(KpiTarget $target)
|
||||
{
|
||||
$target->load(['user.dealer', 'achievements']);
|
||||
|
||||
return view('kpi.targets.show', compact('target'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified KPI target
|
||||
*/
|
||||
public function edit(KpiTarget $target)
|
||||
{
|
||||
// Debug: Check if target is loaded correctly
|
||||
if (!$target) {
|
||||
abort(404, 'Target KPI tidak ditemukan');
|
||||
}
|
||||
|
||||
// Load target with user relationship
|
||||
$target->load('user');
|
||||
|
||||
// Get mechanics using role_id 3 (mechanic) with dealer relationship
|
||||
$mechanics = User::with('dealer')
|
||||
->where('role_id', 3)
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
// If no mechanics found, get all users as fallback
|
||||
if ($mechanics->isEmpty()) {
|
||||
$mechanics = User::with('dealer')
|
||||
->orderBy('name', 'asc')
|
||||
->limit(50)
|
||||
->get();
|
||||
}
|
||||
|
||||
// Ensure data types are correct for comparison
|
||||
$target->user_id = (int)$target->user_id;
|
||||
|
||||
return view('kpi.targets.edit', compact('target', 'mechanics'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified KPI target
|
||||
*/
|
||||
public function update(UpdateKpiTargetRequest $request, KpiTarget $target)
|
||||
{
|
||||
try {
|
||||
$target->update($request->validated());
|
||||
|
||||
return redirect()->route('kpi.targets.index')
|
||||
->with('success', 'Target KPI berhasil diperbarui');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->withInput()
|
||||
->with('error', 'Gagal memperbarui target KPI: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified KPI target
|
||||
*/
|
||||
public function destroy(KpiTarget $target)
|
||||
{
|
||||
try {
|
||||
$target->delete();
|
||||
|
||||
return redirect()->route('kpi.targets.index')
|
||||
->with('success', 'Target KPI berhasil dihapus');
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()
|
||||
->with('error', 'Gagal menghapus target KPI: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle active status of KPI target
|
||||
*/
|
||||
public function toggleStatus(KpiTarget $target)
|
||||
{
|
||||
try {
|
||||
$target->update(['is_active' => !$target->is_active]);
|
||||
|
||||
$status = $target->is_active ? 'diaktifkan' : 'dinonaktifkan';
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "Target KPI berhasil {$status}",
|
||||
'is_active' => $target->is_active
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Gagal mengubah status target KPI'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI targets for specific user
|
||||
*/
|
||||
public function getUserTargets(User $user)
|
||||
{
|
||||
$targets = $user->kpiTargets()
|
||||
->with('achievements')
|
||||
->orderBy('year', 'desc')
|
||||
->orderBy('month', 'desc')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $targets
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Yajra\DataTables\Facades\DataTables;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Models\Role;
|
||||
|
||||
class ReportController extends Controller
|
||||
{
|
||||
@@ -36,14 +37,42 @@ class ReportController extends Controller
|
||||
$request['sa'] = 'all';
|
||||
}
|
||||
|
||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request) {
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($role) {
|
||||
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||
if(isset($request->month)) {
|
||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||
}
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$q = $q->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$q = $q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$q = $q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$q = $q->where('user_sa_id', '=', $request->sa);
|
||||
@@ -52,8 +81,27 @@ class ReportController extends Controller
|
||||
return $q;
|
||||
})->orderBy('id', 'ASC')->get();
|
||||
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
// Get SA users based on dealer access
|
||||
if($dealer_datas->count() > 0) {
|
||||
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||
} else {
|
||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||
}
|
||||
$sa = $request->sa;
|
||||
$dealer = $request->dealer;
|
||||
$month = $request->month;
|
||||
@@ -82,8 +130,27 @@ class ReportController extends Controller
|
||||
$request['sa'] = 'all';
|
||||
}
|
||||
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::orderBy('id', 'ASC')->get();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
// Get SA users based on dealer access
|
||||
if($dealer_datas->count() > 0) {
|
||||
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||
} else {
|
||||
$sa_datas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||
}
|
||||
|
||||
$sa = $request->sa;
|
||||
$dealer = $request->dealer;
|
||||
@@ -126,12 +193,41 @@ class ReportController extends Controller
|
||||
$sa = $request->sa;
|
||||
$year = $request->year;
|
||||
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($role) {
|
||||
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$dealer_work_trx = DB::statement("SET @sql = NULL");
|
||||
$sql = "SELECT IF(work_id IS NOT NULL, GROUP_CONCAT(DISTINCT CONCAT('SUM(IF(work_id = \"', work_id,'\", qty,\"\")) AS \"',CONCAT(w.name, '|',w.id),'\"')), 's.work_id') INTO @sql FROM transactions t JOIN works w ON w.id = t.work_id WHERE month(t.date) = '". $month ."' and year(t.date) = '". $year ."' and t.deleted_at is null";
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$dealerIdsStr = implode(',', $dealerIds);
|
||||
$sql .= " and t.dealer_id IN (". $dealerIdsStr .")";
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$sql .= " and t.dealer_id = '". $dealer ."'";
|
||||
}
|
||||
} else {
|
||||
$sql .= " and t.dealer_id = '". $dealer ."'";
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$sql .= " and t.user_sa_id = '". $sa ."'";
|
||||
@@ -139,17 +235,35 @@ class ReportController extends Controller
|
||||
|
||||
$sa_work_trx = DB::statement($sql);
|
||||
|
||||
// Validate dealer access before building the main query
|
||||
$dealerFilter = "";
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$dealerFilter = " and s.dealer_id = '". $dealer ."'";
|
||||
}
|
||||
} else {
|
||||
$dealerFilter = " and s.dealer_id = '". $dealer ."'";
|
||||
}
|
||||
} else if($allowedDealers->count() > 0) {
|
||||
// If no specific dealer requested, filter by allowed dealers
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$dealerIdsStr = implode(',', $dealerIds);
|
||||
$dealerFilter = " and s.dealer_id IN (". $dealerIdsStr .")";
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
}else{
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.dealer_id = '". $dealer ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as sa_id \", \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
}
|
||||
}else{
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." and s.user_sa_id = '". $sa ."' GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
}else{
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
$sa_work_trx = DB::statement("SET @sql = IF(@sql != 's.work_id' ,CONCAT(\"SELECT sa.name as SA, sa.id as sa_id, \", @sql, \"FROM transactions s JOIN users sa ON sa.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.user_sa_id ORDER BY s.user_sa_id ASC\"), CONCAT(\"SELECT sa.name as SA, sa.id as user_sa_id \", \"FROM transactions s JOIN dealers d ON d.id = s.user_sa_id WHERE month(s.date) = '". $month ."' and year(s.date) = '". $year ."' and s.deleted_at is null". $dealerFilter ." GROUP BY s.`user_sa_id` ORDER BY s.`user_sa_id` ASC\"))");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,21 +332,69 @@ class ReportController extends Controller
|
||||
$request['month'] = date('m');
|
||||
}
|
||||
|
||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request) {
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($role) {
|
||||
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$works = Work::select('id', 'name')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||
if(isset($request->month)) {
|
||||
$q->whereMonth('date', '=', $request->month);
|
||||
}
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$q->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$q->where('user_sa_id', '=', $request->sa);
|
||||
}
|
||||
})->get();
|
||||
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::all();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
// Get SA users based on dealer access
|
||||
if($dealer_datas->count() > 0) {
|
||||
$dealerIds = $dealer_datas->pluck('id')->toArray();
|
||||
$sas = User::select('id', 'name')->where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||
} else {
|
||||
$sas = User::select('id', 'name')->where('role_id', 4)->get();
|
||||
}
|
||||
|
||||
$trxs = [];
|
||||
foreach($sas as $key => $sa) {
|
||||
@@ -244,9 +406,23 @@ class ReportController extends Controller
|
||||
$d = $d->whereMonth('date', '=', $request->month);
|
||||
}
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$d = $d->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$d = $d->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$d = $d->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
$d = $d->where('user_sa_id', '=', $request->sa);
|
||||
@@ -301,36 +477,77 @@ class ReportController extends Controller
|
||||
$month = $request->month;
|
||||
$dealer_id = $request->dealer;
|
||||
$sa_id = $request->sa;
|
||||
$dealers = Dealer::all();
|
||||
$sas = User::where('role_id', 4)->get();
|
||||
|
||||
|
||||
return view('back.report.transaction_sa', compact('sas', 'dealers', 'dealer_id', 'sa_id', 'month', 'trxs', 'works', 'work_count', 'sa_names', 'trx_data'));
|
||||
return view('back.report.transaction_sa', compact('sas', 'dealer_datas', 'dealer_id', 'sa_id', 'month', 'trxs', 'works', 'work_count', 'sa_names', 'trx_data'));
|
||||
}
|
||||
|
||||
public function sa_work_trx(Request $request) {
|
||||
$sa_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request) {
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($role) {
|
||||
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$sa_work_trx = Work::select(DB::raw('works.name AS work_name'), DB::raw("IFNULL(SUM(t.qty), 0) AS qty"), 'works.id AS work_id')->whereHas('transactions', function($q) use($request, $allowedDealers) {
|
||||
if(isset($request->month)) {
|
||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||
}
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$q->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
$q = $q->where('dealer_id', '=', $request->dealer);
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
||||
$q = $q->where('user_sa_id', '=', $request->sa_filter);
|
||||
$q->where('user_sa_id', '=', $request->sa_filter);
|
||||
}
|
||||
|
||||
return $q;
|
||||
})->leftJoin('transactions AS t', function($q) use($request) {
|
||||
})->leftJoin('transactions AS t', function($q) use($request, $allowedDealers) {
|
||||
$q->on('t.work_id', '=', 'works.id');
|
||||
$q->on(DB::raw('MONTH(t.date)'), '=', DB::raw($request->month));
|
||||
$q->on(DB::raw('YEAR(t.date)'), '=', DB::raw(date('Y')));
|
||||
$q->on('t.user_sa_id', '=', DB::raw($request->sa));
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$q->whereIn('t.dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$q->on('t.dealer_id', '=', DB::raw($request->dealer));
|
||||
}
|
||||
} else {
|
||||
$q->on('t.dealer_id', '=', DB::raw($request->dealer));
|
||||
}
|
||||
}
|
||||
if(isset($request->sa_filter) && $request->sa_filter != 'all') {
|
||||
$q->on('t.user_sa_id', '=', DB::raw($request->sa_filter));
|
||||
}
|
||||
@@ -351,14 +568,42 @@ class ReportController extends Controller
|
||||
$request['sa'] = 'all';
|
||||
}
|
||||
|
||||
$sas = User::where('role_id', 4)->whereHas('sa_transactions', function($q) use($request) {
|
||||
// Get dealers based on user role
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($role) {
|
||||
$allowedDealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$sas = User::where('role_id', 4)->whereHas('sa_transactions', function($q) use($request, $allowedDealers) {
|
||||
if(isset($request->month)) {
|
||||
$q = $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||
}
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$q->whereIn('dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->dealer) && $request->dealer != 'all') {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$q->where('dealer_id', '=', $request->dealer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(isset($request->sa) && $request->sa != 'all') {
|
||||
@@ -383,10 +628,22 @@ class ReportController extends Controller
|
||||
$request['year'] = date('Y');
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if($role && $this->isAdminRole($role) && $role->dealers->count() == 0) {
|
||||
$dealer_datas = Dealer::all();
|
||||
} else if($role) {
|
||||
$dealer_datas = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealer_datas = collect();
|
||||
}
|
||||
|
||||
$year = $request->year;
|
||||
$month = $request->month;
|
||||
$dealer = $request->dealer;
|
||||
$dealer_datas = Dealer::all();
|
||||
$ajax_url = route('dashboard_data').'?month='.$month.'&year='.$year.'&dealer='.$dealer;
|
||||
return view('dashboard', compact('month', 'ajax_url', 'dealer', 'dealer_datas', 'year'));
|
||||
}
|
||||
@@ -396,9 +653,30 @@ class ReportController extends Controller
|
||||
$menu = Menu::where('link', 'report.transaction')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$current_user = Auth::user();
|
||||
$current_role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($current_user->role_id);
|
||||
|
||||
// Get dealers based on user role
|
||||
if($current_role && $this->isAdminRole($current_role) && $current_role->dealers->count() == 0) {
|
||||
$dealers = Dealer::all();
|
||||
} else if($current_role) {
|
||||
$dealers = $current_role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$dealers = collect();
|
||||
}
|
||||
|
||||
// Get SA users based on dealer access
|
||||
if($dealers->count() > 0) {
|
||||
$dealerIds = $dealers->pluck('id')->toArray();
|
||||
$sas = User::where('role_id', 4)->whereIn('dealer_id', $dealerIds)->get();
|
||||
$mechanics = User::where('role_id', 3)->whereIn('dealer_id', $dealerIds)->get();
|
||||
} else {
|
||||
$sas = User::where('role_id', 4)->get();
|
||||
$mechanics = User::where('role_id', 3)->get();
|
||||
$dealers = Dealer::all();
|
||||
}
|
||||
|
||||
$works = Work::all();
|
||||
|
||||
return view('back.report.transaction', compact('sas', 'mechanics', 'dealers', 'works'));
|
||||
@@ -410,6 +688,20 @@ class ReportController extends Controller
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
if ($request->ajax()) {
|
||||
// Get dealers based on user role
|
||||
$current_user = Auth::user();
|
||||
$current_role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($current_user->role_id);
|
||||
|
||||
if($current_role && $this->isAdminRole($current_role) && $current_role->dealers->count() == 0) {
|
||||
$allowedDealers = Dealer::all();
|
||||
} else if($current_role) {
|
||||
$allowedDealers = $current_role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
} else {
|
||||
$allowedDealers = collect();
|
||||
}
|
||||
|
||||
$data = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
|
||||
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||
@@ -417,6 +709,13 @@ class ReportController extends Controller
|
||||
->leftJoin('dealers as d', 'd.id', '=', 'transactions.dealer_id')
|
||||
->select('transactions.id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk', 'transactions.dealer_id', 'd.name as dealer_name');
|
||||
|
||||
// Filter by allowed dealers based on user role
|
||||
if($allowedDealers->count() > 0) {
|
||||
|
||||
$dealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
$data->whereIn('transactions.dealer_id', $dealerIds);
|
||||
}
|
||||
|
||||
if(isset($request->date_start)) {
|
||||
$data->where('transactions.date', '>=', $request->date_start);
|
||||
}
|
||||
@@ -434,8 +733,16 @@ class ReportController extends Controller
|
||||
}
|
||||
|
||||
if(isset($request->dealer)) {
|
||||
// Validate that the requested dealer is allowed for this user
|
||||
if($allowedDealers->count() > 0) {
|
||||
$allowedDealerIds = $allowedDealers->pluck('id')->toArray();
|
||||
if(in_array($request->dealer, $allowedDealerIds)) {
|
||||
$data->where('transactions.dealer_id', $request->dealer);
|
||||
}
|
||||
} else {
|
||||
$data->where('transactions.dealer_id', $request->dealer);
|
||||
}
|
||||
}
|
||||
|
||||
$data->orderBy('date', 'DESC');
|
||||
return DataTables::of($data)->addIndexColumn()
|
||||
@@ -565,4 +872,34 @@ class ReportController extends Controller
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is admin type
|
||||
*/
|
||||
private function isAdminRole($role)
|
||||
{
|
||||
if (!$role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define admin role names that should have access to all dealers
|
||||
$adminRoleNames = [
|
||||
'admin'
|
||||
];
|
||||
|
||||
// Check if role name contains admin keywords (but not "area")
|
||||
$roleName = strtolower(trim($role->name));
|
||||
foreach ($adminRoleNames as $adminName) {
|
||||
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Role with "area" in name should use pivot dealers, not all dealers
|
||||
if (strpos($roleName, 'area') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
103
app/Http/Controllers/Reports/ReportStockProductsController.php
Normal file
103
app/Http/Controllers/Reports/ReportStockProductsController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Reports;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Product;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
use App\Services\StockReportService;
|
||||
use App\Exports\StockProductsExport;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Yajra\DataTables\DataTables;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ReportStockProductsController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
|
||||
return view('reports.stock-products');
|
||||
}
|
||||
|
||||
public function getData(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
|
||||
if ($request->ajax()) {
|
||||
$filterDate = $request->get('filter_date');
|
||||
|
||||
$stockService = new StockReportService();
|
||||
$reportData = $stockService->getOptimizedStockReportData($filterDate);
|
||||
|
||||
return DataTables::of($reportData['data'])
|
||||
->addIndexColumn()
|
||||
->addColumn('product_info', function($row) {
|
||||
return "<strong>{$row['product_name']}</strong><br><small class='text-muted'>{$row['product_code']}</small>";
|
||||
})
|
||||
->addColumn('total_stock', function($row) {
|
||||
return number_format($row['total_stock'], 2);
|
||||
})
|
||||
->rawColumns(['product_info'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 'Invalid request'], 400);
|
||||
}
|
||||
|
||||
public function getDealers()
|
||||
{
|
||||
$stockService = new StockReportService();
|
||||
$dealers = $stockService->getDealersBasedOnUserRole();
|
||||
return response()->json($dealers);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
try {
|
||||
$menu = Menu::where('link','reports.stock-product.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
|
||||
$filterDate = $request->get('filter_date');
|
||||
|
||||
$stockService = new StockReportService();
|
||||
$reportData = $stockService->getOptimizedStockReportData($filterDate);
|
||||
|
||||
// Validate report data
|
||||
if (!isset($reportData['data']) || !isset($reportData['dealers'])) {
|
||||
throw new \Exception('Invalid report data structure');
|
||||
}
|
||||
|
||||
// Debug: Log dealer names to identify problematic characters
|
||||
Log::info('Export data validation', [
|
||||
'data_count' => count($reportData['data']),
|
||||
'dealers_count' => count($reportData['dealers']),
|
||||
'dealer_names' => $reportData['dealers']->pluck('name')->toArray(),
|
||||
'first_data_row' => isset($reportData['data'][0]) ? array_keys($reportData['data'][0]) : []
|
||||
]);
|
||||
|
||||
$fileName = 'laporan_stok_produk_' . ($filterDate ?: date('Y-m-d')) . '.xlsx';
|
||||
|
||||
return Excel::download(new StockProductsExport($reportData), $fileName);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Export error: ' . $e->getMessage(), [
|
||||
'filter_date' => $request->get('filter_date'),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return back()->with('error', 'Gagal mengexport data: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
329
app/Http/Controllers/Reports/ReportTechniciansController.php
Normal file
329
app/Http/Controllers/Reports/ReportTechniciansController.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Reports;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Role;
|
||||
use App\Services\TechnicianReportService;
|
||||
use App\Exports\TechnicianReportExport;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Illuminate\Support\Facades\DB; // Added DB facade
|
||||
use App\Models\Dealer; // Added Dealer model
|
||||
|
||||
class ReportTechniciansController extends Controller
|
||||
{
|
||||
protected $technicianReportService;
|
||||
|
||||
public function __construct(TechnicianReportService $technicianReportService)
|
||||
{
|
||||
$this->technicianReportService = $technicianReportService;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$menu = Menu::where('link','reports.technician.index')->first();
|
||||
abort_if(!Gate::allows('view', $menu), 403);
|
||||
|
||||
return view('reports.technician');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dealers for filter dropdown
|
||||
*/
|
||||
public function getDealers()
|
||||
{
|
||||
try {
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
Log::info('Controller: No authenticated user found');
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'User tidak terautentikasi'
|
||||
], 401);
|
||||
}
|
||||
|
||||
Log::info('Controller: Getting dealers for user:', [
|
||||
'user_id' => $user->id,
|
||||
'user_name' => $user->name,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
$dealers = $this->technicianReportService->getDealers();
|
||||
$defaultDealer = $this->technicianReportService->getDefaultDealer();
|
||||
|
||||
Log::info('Controller: Service returned dealers:', [
|
||||
'dealers_count' => $dealers->count(),
|
||||
'dealers' => $dealers->toArray(),
|
||||
'default_dealer' => $defaultDealer ? $defaultDealer->toArray() : null,
|
||||
'default_dealer_id' => $defaultDealer ? $defaultDealer->id : null
|
||||
]);
|
||||
|
||||
// Check if default dealer exists in dealers list
|
||||
if ($defaultDealer && $dealers->count() > 0) {
|
||||
$defaultDealerExists = $dealers->contains('id', $defaultDealer->id);
|
||||
Log::info('Controller: Default dealer validation:', [
|
||||
'default_dealer_id' => $defaultDealer->id,
|
||||
'default_dealer_exists_in_list' => $defaultDealerExists,
|
||||
'available_dealer_ids' => $dealers->pluck('id')->toArray()
|
||||
]);
|
||||
|
||||
// If default dealer doesn't exist in list, use first dealer from list
|
||||
if (!$defaultDealerExists) {
|
||||
Log::info('Controller: Default dealer not in list, using first dealer from list');
|
||||
$defaultDealer = $dealers->first();
|
||||
Log::info('Controller: New default dealer:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||
}
|
||||
} else if ($defaultDealer === null && $dealers->count() > 0) {
|
||||
// Admin without default dealer - no need to set default
|
||||
Log::info('Controller: Admin without default dealer, no default will be set');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $dealers,
|
||||
'default_dealer' => $defaultDealer ? $defaultDealer->id : null
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Controller: Error getting dealers: ' . $e->getMessage(), [
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal mengambil data dealer: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get technician report data for DataTable
|
||||
*/
|
||||
public function getData(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->input('dealer_id');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'User tidak terautentikasi'
|
||||
], 401);
|
||||
}
|
||||
|
||||
Log::info('Requesting technician report data:', [
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
$reportData = $this->technicianReportService->getTechnicianReportData(
|
||||
$dealerId,
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
|
||||
Log::info('Technician report data response:', [
|
||||
'data_count' => count($reportData['data']),
|
||||
'mechanics_count' => $reportData['mechanics']->count(),
|
||||
'works_count' => $reportData['works']->count(),
|
||||
'mechanics' => $reportData['mechanics']->map(function($mechanic) {
|
||||
return [
|
||||
'id' => $mechanic->id,
|
||||
'name' => $mechanic->name,
|
||||
'role_id' => $mechanic->role_id
|
||||
];
|
||||
})
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => $reportData['data'],
|
||||
'mechanics' => $reportData['mechanics'],
|
||||
'works' => $reportData['works']
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error getting technician report data: ' . $e->getMessage(), [
|
||||
'dealer_id' => $request->input('dealer_id'),
|
||||
'start_date' => $request->input('start_date'),
|
||||
'end_date' => $request->input('end_date'),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal mengambil data laporan teknisi: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get technician report data for Yajra DataTable
|
||||
*/
|
||||
public function getDataTable(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->input('dealer_id');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'error' => 'User tidak terautentikasi'
|
||||
], 401);
|
||||
}
|
||||
|
||||
Log::info('Requesting technician report data for DataTable:', [
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
$reportData = $this->technicianReportService->getTechnicianReportDataForDataTable(
|
||||
$dealerId,
|
||||
$startDate,
|
||||
$endDate
|
||||
);
|
||||
|
||||
return $reportData;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error getting technician report data for DataTable: ' . $e->getMessage(), [
|
||||
'dealer_id' => $request->input('dealer_id'),
|
||||
'start_date' => $request->input('start_date'),
|
||||
'end_date' => $request->input('end_date'),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Gagal mengambil data laporan teknisi: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export technician report to Excel
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
try {
|
||||
$dealerId = $request->input('dealer_id');
|
||||
$startDate = $request->input('start_date');
|
||||
$endDate = $request->input('end_date');
|
||||
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'User tidak terautentikasi'
|
||||
], 401);
|
||||
}
|
||||
|
||||
Log::info('Exporting technician report', [
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
// Validate dealer access for export
|
||||
if ($dealerId) {
|
||||
// User is trying to export specific dealer
|
||||
if ($user->dealer_id) {
|
||||
// User has specific dealer_id, check if they can access the requested dealer
|
||||
if ($user->dealer_id != $dealerId) {
|
||||
if ($user->role_id) {
|
||||
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||
], 403);
|
||||
}
|
||||
} else {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
} else if ($user->role_id) {
|
||||
// User has role, check if they can access the requested dealer
|
||||
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Anda tidak memiliki akses untuk export data dealer ini'
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// User is trying to export "Semua Dealer" - check if they have permission
|
||||
if ($user->role_id) {
|
||||
$role = \App\Models\Role::with('dealers')->find($user->role_id);
|
||||
if ($role) {
|
||||
// Check if role is admin type
|
||||
$technicianReportService = new \App\Services\TechnicianReportService();
|
||||
if ($technicianReportService->isAdminRole($role)) {
|
||||
// Admin can export all dealers
|
||||
Log::info('Admin user exporting all dealers');
|
||||
} else {
|
||||
// Non-admin with pivot dealers - can only export pivot dealers
|
||||
if ($role->dealers->count() > 0) {
|
||||
Log::info('User with pivot dealers exporting pivot dealers only');
|
||||
} else {
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Anda tidak memiliki akses untuk export data semua dealer'
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ($user->dealer_id) {
|
||||
// User with specific dealer_id cannot export all dealers
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Anda hanya dapat export data dealer Anda sendiri'
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
|
||||
return Excel::download(new TechnicianReportExport($dealerId, $startDate, $endDate), 'laporan_teknisi_' . date('Y-m-d') . '.xlsx');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error exporting technician report: ' . $e->getMessage(), [
|
||||
'dealer_id' => $request->input('dealer_id'),
|
||||
'start_date' => $request->input('start_date'),
|
||||
'end_date' => $request->input('end_date')
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => 'Gagal export laporan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Models\Menu;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Models\Dealer;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
@@ -14,10 +15,11 @@ class RolePrivilegeController extends Controller
|
||||
public function index() {
|
||||
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
$roles = Role::all();
|
||||
$roles = Role::with('dealers')->get();
|
||||
$menus = Menu::all();
|
||||
$users = User::all();
|
||||
return view('back.roleprivileges', compact('roles', 'users', 'menus'));
|
||||
$dealers = Dealer::all();
|
||||
return view('back.roleprivileges', compact('roles', 'users', 'menus', 'dealers'));
|
||||
}
|
||||
|
||||
public function store(Request $request) {
|
||||
@@ -117,4 +119,36 @@ class RolePrivilegeController extends Controller
|
||||
User::where('role_id', $id)->update(['role_id' => 0]);
|
||||
return redirect()->back()->with('success', 'Berhasil Hapus Role');
|
||||
}
|
||||
|
||||
public function assignDealer(Request $request, $id) {
|
||||
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$request->validate([
|
||||
'dealers' => 'required|array',
|
||||
'dealers.*' => 'exists:dealers,id'
|
||||
]);
|
||||
|
||||
$role = Role::findOrFail($id);
|
||||
|
||||
// Sync dealers (this will replace existing assignments)
|
||||
$role->dealers()->sync($request->dealers);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Berhasil assign dealer ke role'
|
||||
]);
|
||||
}
|
||||
|
||||
public function getAssignedDealers($id) {
|
||||
$menu = Menu::where('link', 'roleprivileges.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$role = Role::findOrFail($id);
|
||||
$assignedDealers = $role->dealers()->pluck('dealers.id')->toArray();
|
||||
|
||||
return response()->json([
|
||||
'assignedDealers' => $assignedDealers
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,28 @@ class TransactionController extends Controller
|
||||
->where('active', true)
|
||||
->get();
|
||||
|
||||
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products'));
|
||||
// Get KPI data for current user using KPI service
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
|
||||
// Auto-calculate current month KPI achievement including claimed transactions
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||
|
||||
// Get current month period name
|
||||
$currentMonthName = now()->translatedFormat('F Y');
|
||||
|
||||
$kpiData = [
|
||||
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||
'percentage' => $kpiSummary['current_percentage'],
|
||||
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||
'period' => $currentMonthName,
|
||||
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||
];
|
||||
|
||||
return view('transaction.index', compact('now', 'wash_work', 'work_works', 'user_sas', 'count_transaction_users', 'count_transaction_dealers', 'mechanic', 'products', 'kpiData'));
|
||||
}
|
||||
|
||||
public function workcategory($category_id)
|
||||
@@ -81,37 +102,60 @@ class TransactionController extends Controller
|
||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||
->leftJoin('categories as cat', 'cat.id', '=', 'w.category_id')
|
||||
->select('transactions.id as transaction_id', 'transactions.status', 'transactions.user_id as user_id', 'transactions.user_sa_id as user_sa_id', 'users.name as username', 'sa.name as sa_name', 'cat.name as category_name', 'w.name as workname', 'transactions.qty as qty', 'transactions.date as date', 'transactions.police_number as police_number', 'transactions.warranty as warranty', 'transactions.spk as spk')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->where('users.dealer_id', Auth::user()->dealer_id);
|
||||
|
||||
|
||||
|
||||
$transaction_works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) {
|
||||
return $q->whereDate('date', '=', date('Y-m-d'))->where('dealer_id', Auth::user()->dealer_id);
|
||||
return $q->whereNull('deleted_at')->whereDate('date', '=', date('Y-m-d'))->where('dealer_id', Auth::user()->dealer_id);
|
||||
})->get();
|
||||
|
||||
$tm1 = [];
|
||||
foreach($transaction_works as $index => $work) {
|
||||
$transaction_sas = Transaction::leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||
->select(DB::raw('SUM(transactions.qty) as qty'), 'sa.name as sa_name')
|
||||
->whereNull('transactions.deleted_at')
|
||||
->where('sa.dealer_id', Auth::user()->dealer_id)
|
||||
->where('work_id', $work->id)
|
||||
->whereDate('transactions.date', '=', date('Y-m-d'))->groupBy('transactions.user_sa_id')->get();
|
||||
|
||||
// Initialize data array for this work
|
||||
$tm1[$work['shortname']]['data'] = [];
|
||||
$daily_total = 0;
|
||||
|
||||
foreach($transaction_sas as $sa) {
|
||||
$tm1[$work['shortname']]['data'][] = $sa['sa_name'].":".$sa['qty'];
|
||||
$daily_total += $sa['qty'];
|
||||
}
|
||||
|
||||
$month_share_data = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', Auth::user()->dealer->id)->whereMonth('date', date('m'))->whereYear('date', date('Y'))->where('work_id', $work->id)->groupBy('user_sa_id')->get();
|
||||
$tm1[$work['shortname']]['total_title'] = "*[PERIODE 1 - ". Carbon::now()->translatedFormat('d F Y') ."]*\n\n";
|
||||
|
||||
$sum_month_share_trx = 0;
|
||||
$tm_month = [];
|
||||
foreach($month_share_data as $m_trx) {
|
||||
$tm_month[] = $m_trx->name.":".$m_trx->qty." Unit\n";
|
||||
$sum_month_share_trx += $m_trx->qty;
|
||||
// Add daily total even if no data
|
||||
if (empty($tm1[$work['shortname']]['data'])) {
|
||||
$tm1[$work['shortname']]['data'][] = "Tidak ada data:0";
|
||||
}
|
||||
|
||||
$tm1[$work['shortname']]['total_body'] = $tm_month;
|
||||
$tm1[$work['shortname']]['total_total'] = "*TOTAL : ". $sum_month_share_trx." Unit*";
|
||||
// Remove monthly data section since this is daily report
|
||||
// $month_share_data = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')
|
||||
// ->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')
|
||||
// ->where('transactions.dealer_id', Auth::user()->dealer->id)
|
||||
// ->whereMonth('date', date('m'))
|
||||
// ->whereYear('date', date('Y'))
|
||||
// ->where('work_id', $work->id)
|
||||
// ->groupBy('user_sa_id')
|
||||
// ->get();
|
||||
|
||||
// Remove the period title since this is for daily report, not monthly
|
||||
// $tm1[$work['shortname']]['total_title'] = "*[PERIODE 1 - ". Carbon::now()->translatedFormat('d F Y') ."]*\n\n";
|
||||
|
||||
// $sum_month_share_trx = 0;
|
||||
// $tm_month = [];
|
||||
// foreach($month_share_data as $m_trx) {
|
||||
// $tm_month[] = $m_trx->name.":".$m_trx->qty." Unit\n";
|
||||
// $sum_month_share_trx += $m_trx->qty;
|
||||
// }
|
||||
|
||||
// $tm1[$work['shortname']]['total_body'] = $tm_month;
|
||||
// $tm1[$work['shortname']]['total_total'] = "*TOTAL : ". $sum_month_share_trx." Unit*";
|
||||
}
|
||||
|
||||
if(isset($request->date_start)) {
|
||||
@@ -135,7 +179,29 @@ class TransactionController extends Controller
|
||||
$sas = User::where('role_id', 4)->get();
|
||||
$dealers = Dealer::all();
|
||||
$works = Work::all();
|
||||
return view('transaction.lists', compact('transaction_dealers', 'transaction_mechanics', 'mechanic', 'sas', 'dealers', 'works', 'date_start', 'date_end'));
|
||||
|
||||
// Get KPI data for current user using KPI service
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
|
||||
// Auto-calculate current month KPI achievement including claimed transactions
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||
|
||||
// Get current month period name
|
||||
$currentMonthName = now()->translatedFormat('F Y');
|
||||
|
||||
$kpiData = [
|
||||
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||
'percentage' => $kpiSummary['current_percentage'],
|
||||
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||
'period' => $currentMonthName,
|
||||
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||
];
|
||||
|
||||
return view('transaction.lists', compact('transaction_dealers', 'transaction_mechanics', 'mechanic', 'sas', 'dealers', 'works', 'date_start', 'date_end', 'kpiData'));
|
||||
}
|
||||
|
||||
public function cmp($a, $b){
|
||||
@@ -169,7 +235,7 @@ class TransactionController extends Controller
|
||||
$mechanic = User::leftJoin('dealers as d', 'd.id', '=', 'users.dealer_id')
|
||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||
->where('users.id', Auth::user()->id)->first();
|
||||
$d = Transaction::leftJoin('works as w', 'w.id', '=', 'transactions.work_id')->select('transactions.*', 'w.name as work_name', 'w.shortname as shortname')->where('dealer_id', $id);
|
||||
$d = Transaction::leftJoin('works as w', 'w.id', '=', 'transactions.work_id')->select('transactions.*', 'w.name as work_name', 'w.shortname as shortname')->whereNull('transactions.deleted_at')->where('dealer_id', $id);
|
||||
|
||||
|
||||
if(isset($request->date_start)) {
|
||||
@@ -316,13 +382,13 @@ class TransactionController extends Controller
|
||||
$id = Auth::user()->dealer_id;
|
||||
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
||||
if(isset($request->month)) {
|
||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||
}
|
||||
})->get();
|
||||
|
||||
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
||||
if(isset($request->month)) {
|
||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'))->where('dealer_id', $id);
|
||||
}
|
||||
})->get();
|
||||
|
||||
@@ -331,7 +397,7 @@ class TransactionController extends Controller
|
||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||
->where('users.id', Auth::user()->id)->first();
|
||||
|
||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', date('Y'))->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->whereNull('deleted_at')->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', date('Y'))->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||
$dates = $this->array_value_recursive('date', $dates);
|
||||
|
||||
$month_trxs = [];
|
||||
@@ -342,7 +408,7 @@ class TransactionController extends Controller
|
||||
$prev_mth = explode('-', $prev_mth_start);
|
||||
$prev_mth_end = $prev_mth[0].'-'.$prev_mth[1].'-'.date('d');
|
||||
|
||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||
$yesterday_month_trx = Transaction::whereNull('deleted_at')->where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||
|
||||
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
||||
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
||||
@@ -356,7 +422,7 @@ class TransactionController extends Controller
|
||||
$date_works = [];
|
||||
$share_works = [];
|
||||
foreach ($works as $key2 => $work) {
|
||||
$d = Transaction::where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||
$d = Transaction::whereNull('deleted_at')->where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||
|
||||
if(isset($request->month)) {
|
||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||
@@ -399,7 +465,7 @@ class TransactionController extends Controller
|
||||
foreach($sas as $key => $sa) {
|
||||
$sa_works = [];
|
||||
foreach ($works as $key2 => $work) {
|
||||
$d = Transaction::where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||
$d = Transaction::whereNull('deleted_at')->where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||
|
||||
if(isset($request->month)) {
|
||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', date('Y'));
|
||||
@@ -471,13 +537,13 @@ class TransactionController extends Controller
|
||||
$id = Auth::user()->dealer_id;
|
||||
$works = Work::select('id', 'name', 'shortname')->whereHas('transactions', function($q) use($request, $id) {
|
||||
if(isset($request->month)) {
|
||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||
}
|
||||
})->get();
|
||||
|
||||
$sas = User::select('id', 'name')->whereHas('sa_transactions', function($q) use($request, $id) {
|
||||
if(isset($request->month)) {
|
||||
return $q->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||
return $q->whereNull('deleted_at')->whereMonth('date', '=', $request->month)->whereYear('date', $request->year)->where('dealer_id', $id);
|
||||
}
|
||||
})->get();
|
||||
|
||||
@@ -486,7 +552,7 @@ class TransactionController extends Controller
|
||||
->select('d.name as dealer_name', 'users.name', 'users.id', 'users.role', 'users.email', 'd.dealer_code', 'd.address')
|
||||
->where('users.id', Auth::user()->id)->first();
|
||||
|
||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', $request->year)->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||
$dates = Transaction::select(DB::raw('DATE(`date`) as date'))->whereNull('deleted_at')->where('dealer_id', $id)->whereMonth('date', $request->month)->whereYear('date', $request->year)->groupBy(DB::raw('DATE(`date`)'))->get()->toArray();
|
||||
// print_r($dates);die;
|
||||
$dates = $this->array_value_recursive('date', $dates);
|
||||
|
||||
@@ -503,7 +569,7 @@ class TransactionController extends Controller
|
||||
}
|
||||
|
||||
// dd($prev_mth_end);
|
||||
$yesterday_month_trx = Transaction::where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||
$yesterday_month_trx = Transaction::whereNull('deleted_at')->where('work_id', $work1->id)->where('dealer_id', $id)->whereDate('date', '>=', $prev_mth_start)->whereDate('date', '<=', $prev_mth_end)->sum('qty');
|
||||
|
||||
if(array_key_exists($work1->id, $yesterday_month_trxs_total)) {
|
||||
$yesterday_month_trxs_total[$work1->id] += $yesterday_month_trx;
|
||||
@@ -516,7 +582,7 @@ class TransactionController extends Controller
|
||||
$date_works = [];
|
||||
$share_works = [];
|
||||
foreach ($works as $key2 => $work) {
|
||||
$d = Transaction::where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||
$d = Transaction::whereNull('deleted_at')->where('work_id', $work->id)->where('dealer_id', $id)->whereDate('date', $date);
|
||||
|
||||
if(isset($request->month)) {
|
||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
||||
@@ -556,14 +622,14 @@ class TransactionController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$this_month_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', $id)->whereMonth('date', date('m'))->whereYear('date', $request->year)->groupBy('user_sa_id')->get();
|
||||
$today_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->where('transactions.dealer_id', $id)->whereDate('date', date('Y-m-d'))->groupBy('user_sa_id')->get();
|
||||
$this_month_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->whereNull('transactions.deleted_at')->where('transactions.dealer_id', $id)->whereMonth('date', date('m'))->whereYear('date', $request->year)->groupBy('user_sa_id')->get();
|
||||
$today_trxs = Transaction::select(DB::raw('SUM(qty) as qty'), 'u.name')->join('users AS u', 'u.id', '=', 'transactions.user_sa_id')->whereNull('transactions.deleted_at')->where('transactions.dealer_id', $id)->whereDate('date', date('Y-m-d'))->groupBy('user_sa_id')->get();
|
||||
|
||||
$trxs = [];
|
||||
foreach($sas as $key => $sa) {
|
||||
$sa_works = [];
|
||||
foreach ($works as $key2 => $work) {
|
||||
$d = Transaction::where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||
$d = Transaction::whereNull('deleted_at')->where('user_sa_id', $sa->id)->where('work_id', $work->id)->where('dealer_id', $id);
|
||||
|
||||
if(isset($request->month)) {
|
||||
$d = $d->whereMonth('date', '=', $request->month)->whereYear('date', $request->year);
|
||||
@@ -875,7 +941,7 @@ class TransactionController extends Controller
|
||||
"warranty" => $request->warranty,
|
||||
"user_sa_id" => $request->user_sa_id,
|
||||
"date" => $request->date,
|
||||
"status" => 'completed', // Mark as completed to trigger stock reduction
|
||||
"status" => 0, // pending (0) - Mark as pending initially
|
||||
"created_at" => date('Y-m-d H:i:s'),
|
||||
"updated_at" => date('Y-m-d H:i:s')
|
||||
];
|
||||
@@ -898,6 +964,10 @@ class TransactionController extends Controller
|
||||
$this->stockService->reduceStockForTransaction($transaction);
|
||||
}
|
||||
|
||||
// Recalculate KPI achievement after creating transactions
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
DB::commit();
|
||||
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
|
||||
|
||||
@@ -997,4 +1067,270 @@ class TransactionController extends Controller
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get claim transactions for DataTable - Only for mechanics
|
||||
*/
|
||||
public function getClaimTransactions(Request $request)
|
||||
{
|
||||
// Only allow mechanics to access this endpoint
|
||||
if (Auth::user()->role_id != 3) {
|
||||
return response()->json([
|
||||
'draw' => intval($request->input('draw')),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => []
|
||||
]);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id'
|
||||
]);
|
||||
|
||||
try {
|
||||
$query = Transaction::leftJoin('users', 'users.id', '=', 'transactions.user_id')
|
||||
->leftJoin('users as sa', 'sa.id', '=', 'transactions.user_sa_id')
|
||||
->leftJoin('works as w', 'w.id', '=', 'transactions.work_id')
|
||||
->select([
|
||||
'transactions.id',
|
||||
'transactions.date',
|
||||
'transactions.spk',
|
||||
'transactions.police_number',
|
||||
'transactions.qty',
|
||||
'transactions.status',
|
||||
'transactions.claimed_at',
|
||||
'transactions.claimed_by',
|
||||
'w.name as work_name',
|
||||
'sa.name as sa_name',
|
||||
'users.name as mechanic_name'
|
||||
])
|
||||
->whereNull('transactions.deleted_at')
|
||||
->where('transactions.dealer_id', $request->dealer_id)
|
||||
->whereIn('transactions.status', [0, 1]) // Only pending and completed transactions
|
||||
->orderBy('transactions.date', 'desc');
|
||||
|
||||
// Handle DataTables server-side processing
|
||||
$total = $query->count();
|
||||
|
||||
// Search functionality
|
||||
if ($request->has('search') && !empty($request->search['value'])) {
|
||||
$searchValue = $request->search['value'];
|
||||
$query->where(function($q) use ($searchValue) {
|
||||
$q->where('transactions.spk', 'like', "%{$searchValue}%")
|
||||
->orWhere('transactions.police_number', 'like', "%{$searchValue}%")
|
||||
->orWhere('w.name', 'like', "%{$searchValue}%")
|
||||
->orWhere('sa.name', 'like', "%{$searchValue}%")
|
||||
->orWhere('users.name', 'like', "%{$searchValue}%");
|
||||
});
|
||||
}
|
||||
|
||||
$filteredTotal = $query->count();
|
||||
|
||||
// Pagination
|
||||
$start = $request->input('start', 0);
|
||||
$length = $request->input('length', 15);
|
||||
$query->skip($start)->take($length);
|
||||
|
||||
$transactions = $query->get();
|
||||
|
||||
$data = [];
|
||||
foreach ($transactions as $transaction) {
|
||||
$data[] = [
|
||||
'id' => $transaction->id,
|
||||
'date' => date('d/m/Y', strtotime($transaction->date)),
|
||||
'spk' => $transaction->spk,
|
||||
'police_number' => $transaction->police_number,
|
||||
'work_name' => $transaction->work_name,
|
||||
'qty' => number_format($transaction->qty),
|
||||
'sa_name' => $transaction->sa_name,
|
||||
'status' => $this->getStatusBadge($transaction->status),
|
||||
'action' => $this->getActionButtons($transaction),
|
||||
'claimed_at' => $transaction->claimed_at,
|
||||
'claimed_by' => $transaction->claimed_by
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'draw' => intval($request->input('draw')),
|
||||
'recordsTotal' => $total,
|
||||
'recordsFiltered' => $filteredTotal,
|
||||
'data' => $data
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'error' => 'Error fetching claim transactions: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status badge HTML
|
||||
*/
|
||||
private function getStatusBadge($status)
|
||||
{
|
||||
switch ($status) {
|
||||
case 0: // pending
|
||||
return '<span class="badge badge-warning">Menunggu</span>';
|
||||
case 1: // completed
|
||||
return '<span class="badge badge-success">Closed</span>';
|
||||
default:
|
||||
return '<span class="badge badge-secondary">Tidak Diketahui</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a transaction - Only for mechanics
|
||||
*/
|
||||
public function claim($id)
|
||||
{
|
||||
// Only allow mechanics to claim transactions
|
||||
if (Auth::user()->role_id != 3) {
|
||||
return response()->json([
|
||||
'status' => 403,
|
||||
'message' => 'Hanya mekanik yang dapat mengklaim pekerjaan'
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$transaction = Transaction::whereNull('deleted_at')->find($id);
|
||||
|
||||
if (!$transaction) {
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'message' => 'Transaksi tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if transaction belongs to current user's dealer
|
||||
if ($transaction->dealer_id !== Auth::user()->dealer_id) {
|
||||
return response()->json([
|
||||
'status' => 403,
|
||||
'message' => 'Anda tidak memiliki akses ke transaksi ini'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Check if transaction can be claimed (pending or completed)
|
||||
if (!in_array($transaction->status, [0, 1])) { // pending (0) and completed (1)
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Hanya transaksi yang menunggu atau sudah selesai yang dapat diklaim'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check if transaction is already claimed
|
||||
if (!empty($transaction->claimed_at) || !empty($transaction->claimed_by)) {
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Transaksi ini sudah diklaim sebelumnya'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check if transaction was created by SA (role_id = 4)
|
||||
$creator = User::find($transaction->user_id);
|
||||
if (!$creator || $creator->role_id != 4) {
|
||||
return response()->json([
|
||||
'status' => 400,
|
||||
'message' => 'Hanya transaksi yang dibuat oleh Service Advisor yang dapat diklaim'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Update transaction with claim information
|
||||
$transaction->update([
|
||||
'claimed_at' => now(),
|
||||
'claimed_by' => Auth::user()->id
|
||||
]);
|
||||
|
||||
// Recalculate KPI achievement after claiming
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiService->calculateKpiAchievementWithClaims(Auth::user());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Pekerjaan berhasil diklaim'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Gagal mengklaim pekerjaan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action buttons HTML for claim transactions - Only for mechanics
|
||||
*/
|
||||
private function getActionButtons($transaction)
|
||||
{
|
||||
$buttons = '';
|
||||
|
||||
// Only show buttons for mechanics
|
||||
if (Auth::user()->role_id == 3) {
|
||||
|
||||
// Claim button - show only if not claimed yet
|
||||
if (empty($transaction->claimed_at) && empty($transaction->claimed_by)) {
|
||||
$buttons .= '<button class="btn btn-sm btn-success mr-1" onclick="claimTransaction(' . $transaction->id . ')" title="Klaim Pekerjaan">';
|
||||
$buttons .= 'Klaim';
|
||||
$buttons .= '</button>';
|
||||
} else {
|
||||
if($transaction->claimed_by == Auth::user()->id) {
|
||||
// Check if precheck exists
|
||||
$precheck = \App\Models\Precheck::where('transaction_id', $transaction->id)->first();
|
||||
if (!$precheck) {
|
||||
$buttons .= '<a href="/transaction/prechecks/' . $transaction->id . '" class="btn btn-sm btn-warning mr-1" title="Precheck">';
|
||||
$buttons .= 'Precheck';
|
||||
$buttons .= '</a>';
|
||||
} else {
|
||||
// Check if postcheck exists
|
||||
$postcheck = \App\Models\Postcheck::where('transaction_id', $transaction->id)->first();
|
||||
if (!$postcheck) {
|
||||
$buttons .= '<a href="/transaction/postchecks/' . $transaction->id . '" class="btn btn-sm btn-info mr-1" title="Postcheck">';
|
||||
$buttons .= 'Postcheck';
|
||||
$buttons .= '</a>';
|
||||
} else {
|
||||
$buttons .= '<span class="badge badge-success">Selesai</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
$buttons .= '<span class="badge badge-info">Sudah Diklaim</span>';
|
||||
}
|
||||
}
|
||||
|
||||
return $buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI data for AJAX refresh
|
||||
*/
|
||||
public function getKpiData()
|
||||
{
|
||||
try {
|
||||
$kpiService = app(\App\Services\KpiService::class);
|
||||
$kpiSummary = $kpiService->getKpiSummaryWithClaims(Auth::user());
|
||||
|
||||
$currentMonthName = now()->translatedFormat('F Y');
|
||||
|
||||
$kpiData = [
|
||||
'target' => $kpiSummary['current_target'] ? $kpiSummary['current_target']->target_value : 0,
|
||||
'actual' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->actual_value : 0,
|
||||
'percentage' => $kpiSummary['current_percentage'],
|
||||
'status' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status : 'pending',
|
||||
'status_color' => $kpiSummary['current_achievement'] ? $kpiSummary['current_achievement']->status_color : 'secondary',
|
||||
'period' => $currentMonthName,
|
||||
'has_target' => $kpiSummary['current_target'] ? true : false
|
||||
];
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $kpiData
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Error fetching KPI data: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
188
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
188
app/Http/Controllers/Transactions/PostchecksController.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Transactions;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Postcheck;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PostchecksController extends Controller
|
||||
{
|
||||
public function index(Transaction $transaction)
|
||||
{
|
||||
$acConditions = Postcheck::getAcConditionOptions();
|
||||
$blowerConditions = Postcheck::getBlowerConditionOptions();
|
||||
$evaporatorConditions = Postcheck::getEvaporatorConditionOptions();
|
||||
$compressorConditions = Postcheck::getCompressorConditionOptions();
|
||||
|
||||
return view('transaction.postchecks', compact(
|
||||
'transaction',
|
||||
'acConditions',
|
||||
'blowerConditions',
|
||||
'evaporatorConditions',
|
||||
'compressorConditions'
|
||||
));
|
||||
}
|
||||
|
||||
public function store(Request $request, Transaction $transaction)
|
||||
{
|
||||
$request->validate([
|
||||
'kilometer' => 'required|numeric|min:0',
|
||||
'pressure_high' => 'required|numeric|min:0',
|
||||
'pressure_low' => 'nullable|numeric|min:0',
|
||||
'cabin_temperature' => 'nullable|numeric',
|
||||
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'ac_condition' => 'nullable|in:' . implode(',', Postcheck::getAcConditionOptions()),
|
||||
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'blower_condition' => 'nullable|in:' . implode(',', Postcheck::getBlowerConditionOptions()),
|
||||
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'evaporator_condition' => 'nullable|in:' . implode(',', Postcheck::getEvaporatorConditionOptions()),
|
||||
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'compressor_condition' => 'nullable|in:' . implode(',', Postcheck::getCompressorConditionOptions()),
|
||||
'postcheck_notes' => 'nullable|string',
|
||||
'front_image' => 'required|image|mimes:jpeg,png,jpg|max:2048',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'transaction_id' => $transaction->id,
|
||||
'postcheck_by' => auth()->id(),
|
||||
'postcheck_at' => now(),
|
||||
'police_number' => $transaction->police_number,
|
||||
'spk_number' => $transaction->spk,
|
||||
'kilometer' => $request->kilometer,
|
||||
'pressure_high' => $request->pressure_high,
|
||||
'pressure_low' => $request->pressure_low,
|
||||
'cabin_temperature' => $request->cabin_temperature,
|
||||
'ac_condition' => $request->ac_condition,
|
||||
'blower_condition' => $request->blower_condition,
|
||||
'evaporator_condition' => $request->evaporator_condition,
|
||||
'compressor_condition' => $request->compressor_condition,
|
||||
'postcheck_notes' => $request->postcheck_notes,
|
||||
];
|
||||
|
||||
// Handle file uploads
|
||||
$imageFields = [
|
||||
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||
'blower_image', 'evaporator_image'
|
||||
];
|
||||
|
||||
foreach ($imageFields as $field) {
|
||||
if ($request->hasFile($field) && $request->file($field)->isValid()) {
|
||||
try {
|
||||
$file = $request->file($field);
|
||||
|
||||
// Generate unique filename with transaction ID
|
||||
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
|
||||
|
||||
// Create directory path: transactions/{transaction_id}/postcheck/
|
||||
$directory = 'transactions/' . $transaction->id . '/postcheck';
|
||||
|
||||
// Ensure base storage directory exists
|
||||
$this->ensureStorageDirectoryExists();
|
||||
|
||||
// Ensure transactions directory exists
|
||||
if (!Storage::disk('public')->exists('transactions')) {
|
||||
Storage::disk('public')->makeDirectory('transactions', 0755, true);
|
||||
Log::info('Created transactions directory');
|
||||
}
|
||||
|
||||
// Ensure transaction ID directory exists
|
||||
$transactionDir = 'transactions/' . $transaction->id;
|
||||
if (!Storage::disk('public')->exists($transactionDir)) {
|
||||
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
|
||||
Log::info('Created transaction directory: ' . $transactionDir);
|
||||
}
|
||||
|
||||
// Ensure postcheck directory exists
|
||||
if (!Storage::disk('public')->exists($directory)) {
|
||||
Storage::disk('public')->makeDirectory($directory, 0755, true);
|
||||
Log::info('Created postcheck directory: ' . $directory);
|
||||
}
|
||||
|
||||
// Store file in organized directory structure
|
||||
$path = $file->storeAs($directory, $filename, 'public');
|
||||
|
||||
// Store file path
|
||||
$data[$field] = $path;
|
||||
|
||||
// Store metadata
|
||||
$data[$field . '_metadata'] = [
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'uploaded_at' => now()->toISOString(),
|
||||
'transaction_id' => $transaction->id,
|
||||
'filename' => $filename,
|
||||
];
|
||||
|
||||
Log::info('File uploaded successfully: ' . $path);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Log error for debugging
|
||||
Log::error('File upload failed: ' . $e->getMessage(), [
|
||||
'field' => $field,
|
||||
'file' => $file->getClientOriginalName(),
|
||||
'transaction_id' => $transaction->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Postcheck::create($data);
|
||||
return redirect()->route('transaction')->with('success', 'Postcheck berhasil disimpan');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Postcheck creation failed: ' . $e->getMessage());
|
||||
return back()->withErrors(['error' => 'Gagal menyimpan data postcheck. Silakan coba lagi.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the base storage directory exists
|
||||
*/
|
||||
private function ensureStorageDirectoryExists()
|
||||
{
|
||||
$storagePath = storage_path('app/public');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
if (!mkdir($storagePath, 0755, true)) {
|
||||
Log::error('Failed to create storage directory: ' . $storagePath);
|
||||
throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.');
|
||||
}
|
||||
Log::info('Created storage directory: ' . $storagePath);
|
||||
}
|
||||
|
||||
// Check if directory is writable
|
||||
if (!is_writable($storagePath)) {
|
||||
Log::error('Storage directory is not writable: ' . $storagePath);
|
||||
throw new \Exception(
|
||||
'Storage directory is not writable: ' . $storagePath . '. ' .
|
||||
'Please run one of these commands from your project root: ' .
|
||||
'1) php fix_permissions.php ' .
|
||||
'2) chmod -R 775 storage/ ' .
|
||||
'3) mkdir -p storage/app/public/transactions/{transaction_id}/postcheck'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we can create subdirectories
|
||||
$testDir = $storagePath . '/test_' . time();
|
||||
if (!mkdir($testDir, 0755, true)) {
|
||||
Log::error('Cannot create subdirectories in storage: ' . $storagePath);
|
||||
throw new \Exception(
|
||||
'Cannot create subdirectories in storage. ' .
|
||||
'Please check permissions and run: php fix_permissions.php'
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up test directory
|
||||
rmdir($testDir);
|
||||
Log::info('Storage directory is properly configured: ' . $storagePath);
|
||||
}
|
||||
}
|
||||
188
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
188
app/Http/Controllers/Transactions/PrechecksController.php
Normal file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Transactions;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Precheck;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PrechecksController extends Controller
|
||||
{
|
||||
public function index(Transaction $transaction)
|
||||
{
|
||||
$acConditions = Precheck::getAcConditionOptions();
|
||||
$blowerConditions = Precheck::getBlowerConditionOptions();
|
||||
$evaporatorConditions = Precheck::getEvaporatorConditionOptions();
|
||||
$compressorConditions = Precheck::getCompressorConditionOptions();
|
||||
|
||||
return view('transaction.prechecks', compact(
|
||||
'transaction',
|
||||
'acConditions',
|
||||
'blowerConditions',
|
||||
'evaporatorConditions',
|
||||
'compressorConditions'
|
||||
));
|
||||
}
|
||||
|
||||
public function store(Request $request, Transaction $transaction)
|
||||
{
|
||||
$request->validate([
|
||||
'kilometer' => 'required|numeric|min:0',
|
||||
'pressure_high' => 'required|numeric|min:0',
|
||||
'pressure_low' => 'nullable|numeric|min:0',
|
||||
'cabin_temperature' => 'nullable|numeric',
|
||||
'cabin_temperature_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'ac_condition' => 'nullable|in:' . implode(',', Precheck::getAcConditionOptions()),
|
||||
'ac_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'blower_condition' => 'nullable|in:' . implode(',', Precheck::getBlowerConditionOptions()),
|
||||
'blower_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'evaporator_condition' => 'nullable|in:' . implode(',', Precheck::getEvaporatorConditionOptions()),
|
||||
'evaporator_image' => 'nullable|image|mimes:jpeg,png,jpg|max:2048',
|
||||
'compressor_condition' => 'nullable|in:' . implode(',', Precheck::getCompressorConditionOptions()),
|
||||
'precheck_notes' => 'nullable|string',
|
||||
'front_image' => 'required|image|mimes:jpeg,png,jpg|max:2048',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'transaction_id' => $transaction->id,
|
||||
'precheck_by' => auth()->id(),
|
||||
'precheck_at' => now(),
|
||||
'police_number' => $transaction->police_number,
|
||||
'spk_number' => $transaction->spk,
|
||||
'kilometer' => $request->kilometer,
|
||||
'pressure_high' => $request->pressure_high,
|
||||
'pressure_low' => $request->pressure_low,
|
||||
'cabin_temperature' => $request->cabin_temperature,
|
||||
'ac_condition' => $request->ac_condition,
|
||||
'blower_condition' => $request->blower_condition,
|
||||
'evaporator_condition' => $request->evaporator_condition,
|
||||
'compressor_condition' => $request->compressor_condition,
|
||||
'precheck_notes' => $request->precheck_notes,
|
||||
];
|
||||
|
||||
// Handle file uploads
|
||||
$imageFields = [
|
||||
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||
'blower_image', 'evaporator_image'
|
||||
];
|
||||
|
||||
foreach ($imageFields as $field) {
|
||||
if ($request->hasFile($field) && $request->file($field)->isValid()) {
|
||||
try {
|
||||
$file = $request->file($field);
|
||||
|
||||
// Generate unique filename with transaction ID
|
||||
$filename = time() . '_' . uniqid() . '_' . $transaction->id . '_' . $field . '.' . $file->getClientOriginalExtension();
|
||||
|
||||
// Create directory path: transactions/{transaction_id}/precheck/
|
||||
$directory = 'transactions/' . $transaction->id . '/precheck';
|
||||
|
||||
// Ensure base storage directory exists
|
||||
$this->ensureStorageDirectoryExists();
|
||||
|
||||
// Ensure transactions directory exists
|
||||
if (!Storage::disk('public')->exists('transactions')) {
|
||||
Storage::disk('public')->makeDirectory('transactions', 0755, true);
|
||||
Log::info('Created transactions directory');
|
||||
}
|
||||
|
||||
// Ensure transaction ID directory exists
|
||||
$transactionDir = 'transactions/' . $transaction->id;
|
||||
if (!Storage::disk('public')->exists($transactionDir)) {
|
||||
Storage::disk('public')->makeDirectory($transactionDir, 0755, true);
|
||||
Log::info('Created transaction directory: ' . $transactionDir);
|
||||
}
|
||||
|
||||
// Ensure precheck directory exists
|
||||
if (!Storage::disk('public')->exists($directory)) {
|
||||
Storage::disk('public')->makeDirectory($directory, 0755, true);
|
||||
Log::info('Created precheck directory: ' . $directory);
|
||||
}
|
||||
|
||||
// Store file in organized directory structure
|
||||
$path = $file->storeAs($directory, $filename, 'public');
|
||||
|
||||
// Store file path
|
||||
$data[$field] = $path;
|
||||
|
||||
// Store metadata
|
||||
$data[$field . '_metadata'] = [
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'uploaded_at' => now()->toISOString(),
|
||||
'transaction_id' => $transaction->id,
|
||||
'filename' => $filename,
|
||||
];
|
||||
|
||||
Log::info('File uploaded successfully: ' . $path);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Log error for debugging
|
||||
Log::error('File upload failed: ' . $e->getMessage(), [
|
||||
'field' => $field,
|
||||
'file' => $file->getClientOriginalName(),
|
||||
'transaction_id' => $transaction->id,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return back()->withErrors(['error' => 'Gagal mengupload file: ' . $field . '. Error: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Precheck::create($data);
|
||||
return redirect()->route('transaction')->with('success', 'Precheck berhasil disimpan');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Precheck creation failed: ' . $e->getMessage());
|
||||
return back()->withErrors(['error' => 'Gagal menyimpan data precheck. Silakan coba lagi.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the base storage directory exists
|
||||
*/
|
||||
private function ensureStorageDirectoryExists()
|
||||
{
|
||||
$storagePath = storage_path('app/public');
|
||||
|
||||
if (!is_dir($storagePath)) {
|
||||
if (!mkdir($storagePath, 0755, true)) {
|
||||
Log::error('Failed to create storage directory: ' . $storagePath);
|
||||
throw new \Exception('Cannot create storage directory: ' . $storagePath . '. Please run: php fix_permissions.php or manually create the directory.');
|
||||
}
|
||||
Log::info('Created storage directory: ' . $storagePath);
|
||||
}
|
||||
|
||||
// Check if directory is writable
|
||||
if (!is_writable($storagePath)) {
|
||||
Log::error('Storage directory is not writable: ' . $storagePath);
|
||||
throw new \Exception(
|
||||
'Storage directory is not writable: ' . $storagePath . '. ' .
|
||||
'Please run one of these commands from your project root: ' .
|
||||
'1) php fix_permissions.php ' .
|
||||
'2) chmod -R 775 storage/ ' .
|
||||
'3) mkdir -p storage/app/public/transactions/{transaction_id}/precheck'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we can create subdirectories
|
||||
$testDir = $storagePath . '/test_' . time();
|
||||
if (!mkdir($testDir, 0755, true)) {
|
||||
Log::error('Cannot create subdirectories in storage: ' . $storagePath);
|
||||
throw new \Exception(
|
||||
'Cannot create subdirectories in storage. ' .
|
||||
'Please check permissions and run: php fix_permissions.php'
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up test directory
|
||||
rmdir($testDir);
|
||||
Log::info('Storage directory is properly configured: ' . $storagePath);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class OpnamesController extends Controller
|
||||
$dealers = Dealer::all();
|
||||
if($request->ajax()){
|
||||
$data = Opname::query()
|
||||
->with('user','dealer')
|
||||
->with(['user','dealer', 'details.product'])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
// Filter berdasarkan dealer yang dipilih
|
||||
@@ -76,6 +76,46 @@ class OpnamesController extends Controller
|
||||
|
||||
return "<span class=\"font-weight-bold {$textColorClass}\">{$label}</span>";
|
||||
})
|
||||
->addColumn('stock_info', function ($row) {
|
||||
// Use eager loaded details
|
||||
$details = $row->details;
|
||||
|
||||
if ($details->isEmpty()) {
|
||||
return '<span class="text-muted">Tidak ada data</span>';
|
||||
}
|
||||
|
||||
$totalProducts = $details->count();
|
||||
$matchingProducts = $details->where('difference', 0)->count();
|
||||
$differentProducts = $totalProducts - $matchingProducts;
|
||||
|
||||
$info = [];
|
||||
|
||||
if ($matchingProducts > 0) {
|
||||
$info[] = "<span class='text-success'><i class='fa fa-check-circle'></i> {$matchingProducts} sesuai</span>";
|
||||
}
|
||||
|
||||
if ($differentProducts > 0) {
|
||||
// Get more details about differences
|
||||
$positiveDiff = $details->where('difference', '>', 0)->count();
|
||||
$negativeDiff = $details->where('difference', '<', 0)->count();
|
||||
|
||||
$diffInfo = [];
|
||||
if ($positiveDiff > 0) {
|
||||
$diffInfo[] = "+{$positiveDiff}";
|
||||
}
|
||||
if ($negativeDiff > 0) {
|
||||
$diffInfo[] = "-{$negativeDiff}";
|
||||
}
|
||||
|
||||
$diffText = implode(', ', $diffInfo);
|
||||
$info[] = "<span class='text-danger'><i class='fa fa-exclamation-triangle'></i> {$differentProducts} selisih ({$diffText})</span>";
|
||||
}
|
||||
|
||||
// Add total products info
|
||||
$info[] = "<small class='text-muted'>(Total: {$totalProducts} produk)</small>";
|
||||
|
||||
return '<div class="stock-info-cell">' . implode('<br>', $info) . '</div>';
|
||||
})
|
||||
->addColumn('action', function ($row) use ($menu) {
|
||||
$btn = '<div class="d-flex">';
|
||||
|
||||
@@ -86,7 +126,7 @@ class OpnamesController extends Controller
|
||||
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action', 'status'])
|
||||
->rawColumns(['action', 'status', 'stock_info'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
@@ -124,7 +164,7 @@ class OpnamesController extends Controller
|
||||
$isTransactionForm = $request->has('form') && $request->form === 'opname';
|
||||
|
||||
if ($isTransactionForm) {
|
||||
// Custom validation for transaction form
|
||||
// Simplified validation for transaction form
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'user_id' => 'required|exists:users,id',
|
||||
@@ -140,7 +180,7 @@ class OpnamesController extends Controller
|
||||
'system_stock' => 'required|array',
|
||||
'system_stock.*' => 'required|numeric|min:0',
|
||||
'physical_stock' => 'required|array',
|
||||
'physical_stock.*' => 'required|numeric|min:0'
|
||||
'physical_stock.*' => 'nullable|numeric|min:0'
|
||||
]);
|
||||
|
||||
// Process transaction form data with proper date parsing
|
||||
@@ -199,19 +239,11 @@ class OpnamesController extends Controller
|
||||
$physicalStocks = $request->physical_quantity;
|
||||
}
|
||||
|
||||
// 2. Validasi minimal ada produk yang diisi (termasuk nilai 0)
|
||||
// 2. Simplified validation - all products are valid, set defaults for empty physical stocks
|
||||
$validProductIds = array_filter($productIds);
|
||||
$validSystemStocks = array_filter($systemStocks, function($value) { return $value !== null && $value !== ''; });
|
||||
$validPhysicalStocks = array_filter($physicalStocks, function($value) {
|
||||
return $value !== null && $value !== '' && is_numeric($value);
|
||||
});
|
||||
|
||||
if (empty($validProductIds) || count($validProductIds) === 0) {
|
||||
throw new \Exception('Minimal harus ada satu produk yang diisi untuk opname.');
|
||||
}
|
||||
|
||||
if (count($validPhysicalStocks) === 0) {
|
||||
throw new \Exception('Minimal harus ada satu stock fisik yang diisi (termasuk nilai 0).');
|
||||
throw new \Exception('Minimal harus ada satu produk untuk opname.');
|
||||
}
|
||||
|
||||
// 3. Validasi duplikasi produk
|
||||
@@ -283,19 +315,14 @@ class OpnamesController extends Controller
|
||||
foreach ($productIds as $index => $productId) {
|
||||
if (!$productId) continue;
|
||||
|
||||
// Skip only if physical stock is truly not provided (empty string or null)
|
||||
// Accept 0 as valid input
|
||||
if (!isset($physicalStocks[$index]) || $physicalStocks[$index] === '' || $physicalStocks[$index] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate that physical stock is numeric (including 0)
|
||||
if (!is_numeric($physicalStocks[$index])) {
|
||||
continue;
|
||||
// Set default value to 0 if physical stock is empty or invalid
|
||||
$physicalStockValue = $physicalStocks[$index] ?? null;
|
||||
if ($physicalStockValue === '' || $physicalStockValue === null || !is_numeric($physicalStockValue)) {
|
||||
$physicalStockValue = 0;
|
||||
}
|
||||
|
||||
$systemStock = floatval($systemStocks[$index] ?? 0);
|
||||
$physicalStock = floatval($physicalStocks[$index]);
|
||||
$physicalStock = floatval($physicalStockValue);
|
||||
$difference = $physicalStock - $systemStock;
|
||||
|
||||
$processedCount++;
|
||||
@@ -337,7 +364,7 @@ class OpnamesController extends Controller
|
||||
|
||||
// Validate we have at least one detail to insert
|
||||
if (empty($details)) {
|
||||
throw new \Exception('Tidak ada data stock fisik yang valid untuk diproses.');
|
||||
throw new \Exception('Tidak ada data produk yang valid untuk diproses.');
|
||||
}
|
||||
|
||||
// Bulk insert untuk performa lebih baik
|
||||
@@ -371,13 +398,13 @@ class OpnamesController extends Controller
|
||||
// Redirect back to transaction page with success message and tab indicator
|
||||
return redirect()
|
||||
->route('transaction')
|
||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.")
|
||||
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.")
|
||||
->with('active_tab', 'opname');
|
||||
} else {
|
||||
// Redirect to opname index for regular form
|
||||
return redirect()
|
||||
->route('opnames.index')
|
||||
->with('success', "Opname berhasil disimpan dan disetujui. {$processedCount} produk telah diproses.");
|
||||
->with('success', "Opname berhasil disimpan. {$processedCount} produk telah diproses.");
|
||||
}
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
|
||||
@@ -35,6 +35,13 @@ class WorkController extends Controller
|
||||
</a>';
|
||||
}
|
||||
|
||||
// Set Prices Button
|
||||
if(Gate::allows('view', $menu)) {
|
||||
$btn .= '<a href="'. route('work.set-prices', ['work' => $row->work_id]) .'" class="btn btn-primary btn-sm" title="Set Harga per Dealer">
|
||||
Harga
|
||||
</a>';
|
||||
}
|
||||
|
||||
if(Gate::allows('update', $menu)) {
|
||||
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
|
||||
Edit
|
||||
@@ -157,4 +164,20 @@ class WorkController extends Controller
|
||||
|
||||
return response()->json($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for setting prices per dealer for a specific work.
|
||||
*
|
||||
* @param \App\Models\Work $work
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function showPrices(Work $work)
|
||||
{
|
||||
$menu = Menu::where('link', 'work.index')->first();
|
||||
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
|
||||
|
||||
$dealers = \App\Models\Dealer::all();
|
||||
|
||||
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||
}
|
||||
}
|
||||
|
||||
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
363
app/Http/Controllers/WorkDealerPriceController.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\WorkDealerPrice;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Yajra\DataTables\DataTables;
|
||||
|
||||
class WorkDealerPriceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of work prices for a specific work
|
||||
*/
|
||||
public function index(Request $request, Work $work)
|
||||
{
|
||||
if ($request->ajax()) {
|
||||
$data = WorkDealerPrice::with(['dealer'])
|
||||
->where('work_id', $work->id)
|
||||
->select('work_dealer_prices.*');
|
||||
|
||||
return DataTables::of($data)
|
||||
->addIndexColumn()
|
||||
->addColumn('dealer_name', function($row) {
|
||||
return $row->dealer->name;
|
||||
})
|
||||
->addColumn('formatted_price', function($row) {
|
||||
return $row->formatted_price;
|
||||
})
|
||||
->addColumn('action', function($row) {
|
||||
$btn = '<div class="d-flex flex-row gap-1">';
|
||||
$btn .= '<button class="btn btn-warning btn-sm" onclick="editPrice(' . $row->id . ')" title="Edit Harga">
|
||||
<i class="fa fa-edit"></i>
|
||||
</button>';
|
||||
$btn .= '<button class="btn btn-danger btn-sm" onclick="deletePrice(' . $row->id . ')" title="Hapus Harga">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>';
|
||||
$btn .= '</div>';
|
||||
return $btn;
|
||||
})
|
||||
->rawColumns(['action'])
|
||||
->make(true);
|
||||
}
|
||||
|
||||
$dealers = Dealer::all();
|
||||
return view('back.master.work_prices', compact('work', 'dealers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created price
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'currency' => 'required|string|max:3',
|
||||
'is_active' => 'nullable|in:0,1',
|
||||
], [
|
||||
'work_id.required' => 'ID pekerjaan harus diisi',
|
||||
'work_id.exists' => 'Pekerjaan tidak ditemukan',
|
||||
'dealer_id.required' => 'ID dealer harus diisi',
|
||||
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||
'price.required' => 'Harga harus diisi',
|
||||
'price.numeric' => 'Harga harus berupa angka',
|
||||
'price.min' => 'Harga minimal 0',
|
||||
'currency.required' => 'Mata uang harus diisi',
|
||||
'currency.max' => 'Mata uang maksimal 3 karakter',
|
||||
'is_active.in' => 'Status aktif harus 0 atau 1',
|
||||
]);
|
||||
|
||||
// Check if price already exists for this work-dealer combination (including soft deleted)
|
||||
$existingPrice = WorkDealerPrice::withTrashed()
|
||||
->where('work_id', $request->work_id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->first();
|
||||
|
||||
// Also check for active records to prevent duplicates
|
||||
$activePrice = WorkDealerPrice::where('work_id', $request->work_id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->where('id', '!=', $existingPrice ? $existingPrice->id : 0)
|
||||
->first();
|
||||
|
||||
if ($activePrice) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Use database transaction to prevent race conditions
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
|
||||
if ($existingPrice) {
|
||||
if ($existingPrice->trashed()) {
|
||||
// Restore soft deleted record and update
|
||||
$existingPrice->restore();
|
||||
}
|
||||
|
||||
// Update existing price
|
||||
$existingPrice->update([
|
||||
'price' => $request->price,
|
||||
'currency' => $request->currency,
|
||||
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||
]);
|
||||
$price = $existingPrice;
|
||||
|
||||
$message = 'Harga berhasil diperbarui';
|
||||
} else {
|
||||
// Create new price
|
||||
$price = WorkDealerPrice::create([
|
||||
'work_id' => $request->work_id,
|
||||
'dealer_id' => $request->dealer_id,
|
||||
'price' => $request->price,
|
||||
'currency' => $request->currency,
|
||||
'is_active' => $request->has('is_active') ? (bool)$request->is_active : true,
|
||||
]);
|
||||
|
||||
$message = 'Harga berhasil disimpan';
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => $message
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Illuminate\Database\QueryException $e) {
|
||||
// Handle unique constraint violation
|
||||
if ($e->getCode() == 23000) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Harga untuk dealer ini sudah ada. Silakan edit harga yang sudah ada.'
|
||||
], 422);
|
||||
}
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan database: ' . $e->getMessage()
|
||||
], 500);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified price
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$price = WorkDealerPrice::with(['work', 'dealer'])->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => 'Data harga berhasil diambil'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified price
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$request->validate([
|
||||
'price' => 'required|numeric|min:0',
|
||||
'currency' => 'required|string|max:3',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$price = WorkDealerPrice::findOrFail($id);
|
||||
$price->update($request->all());
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil diperbarui'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified price
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
try {
|
||||
$price = WorkDealerPrice::findOrFail($id);
|
||||
$price->delete(); // Soft delete
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil dihapus'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan saat menghapus harga: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work and dealer
|
||||
*/
|
||||
public function getPrice(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'work_id' => 'required|exists:works,id',
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
]);
|
||||
|
||||
$price = WorkDealerPrice::getPriceForWorkAndDealer(
|
||||
$request->work_id,
|
||||
$request->dealer_id
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $price,
|
||||
'message' => $price ? 'Harga ditemukan' : 'Harga tidak ditemukan'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle status of a price
|
||||
*/
|
||||
public function toggleStatus(Request $request, Work $work)
|
||||
{
|
||||
try {
|
||||
$request->validate([
|
||||
'dealer_id' => 'required|exists:dealers,id',
|
||||
'is_active' => 'required|in:0,1,true,false',
|
||||
], [
|
||||
'dealer_id.required' => 'ID dealer harus diisi',
|
||||
'dealer_id.exists' => 'Dealer tidak ditemukan',
|
||||
'is_active.required' => 'Status aktif harus diisi',
|
||||
'is_active.in' => 'Status aktif harus 0, 1, true, atau false',
|
||||
]);
|
||||
|
||||
// Convert string values to boolean
|
||||
$isActive = filter_var($request->is_active, FILTER_VALIDATE_BOOLEAN);
|
||||
|
||||
// Find existing price (including soft deleted)
|
||||
$existingPrice = WorkDealerPrice::withTrashed()
|
||||
->where('work_id', $work->id)
|
||||
->where('dealer_id', $request->dealer_id)
|
||||
->first();
|
||||
|
||||
if (!$existingPrice) {
|
||||
// Create new record with default price 0 if no record exists
|
||||
$existingPrice = WorkDealerPrice::create([
|
||||
'work_id' => $work->id,
|
||||
'dealer_id' => $request->dealer_id,
|
||||
'price' => 0,
|
||||
'currency' => 'IDR',
|
||||
'is_active' => $isActive,
|
||||
]);
|
||||
} else {
|
||||
// Restore if soft deleted
|
||||
if ($existingPrice->trashed()) {
|
||||
$existingPrice->restore();
|
||||
}
|
||||
|
||||
// Update status
|
||||
$existingPrice->update([
|
||||
'is_active' => $isActive
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'data' => $existingPrice,
|
||||
'message' => 'Status berhasil diubah menjadi ' . ($isActive ? 'Aktif' : 'Nonaktif')
|
||||
]);
|
||||
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'message' => 'Validasi gagal',
|
||||
'errors' => $e->errors()
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk create prices for a work
|
||||
*/
|
||||
public function bulkCreate(Request $request, Work $work)
|
||||
{
|
||||
$request->validate([
|
||||
'prices' => 'required|array',
|
||||
'prices.*.dealer_id' => 'required|exists:dealers,id',
|
||||
'prices.*.price' => 'required|numeric|min:0',
|
||||
'prices.*.currency' => 'required|string|max:3',
|
||||
]);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($request->prices as $priceData) {
|
||||
// Check if price already exists
|
||||
$existingPrice = WorkDealerPrice::where('work_id', $work->id)
|
||||
->where('dealer_id', $priceData['dealer_id'])
|
||||
->first();
|
||||
|
||||
if ($existingPrice) {
|
||||
// Update existing price
|
||||
$existingPrice->update([
|
||||
'price' => $priceData['price'],
|
||||
'currency' => $priceData['currency'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
} else {
|
||||
// Create new price
|
||||
WorkDealerPrice::create([
|
||||
'work_id' => $work->id,
|
||||
'dealer_id' => $priceData['dealer_id'],
|
||||
'price' => $priceData['price'],
|
||||
'currency' => $priceData['currency'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'message' => 'Harga berhasil disimpan'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
DB::rollback();
|
||||
return response()->json([
|
||||
'status' => 500,
|
||||
'message' => 'Terjadi kesalahan: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/StoreKpiTargetRequest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\KPI;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class StoreKpiTargetRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'target_value' => 'required|integer|min:1',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'user_id.required' => 'Mekanik harus dipilih',
|
||||
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
|
||||
'target_value.required' => 'Target nilai harus diisi',
|
||||
'target_value.integer' => 'Target nilai harus berupa angka',
|
||||
'target_value.min' => 'Target nilai minimal 1',
|
||||
'description.max' => 'Deskripsi maksimal 1000 karakter',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => $this->boolean('is_active', true)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal file
59
app/Http/Requests/KPI/UpdateKpiTargetRequest.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\KPI;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class UpdateKpiTargetRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'target_value' => 'required|integer|min:1',
|
||||
'description' => 'nullable|string|max:1000',
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'user_id.required' => 'Mekanik harus dipilih',
|
||||
'user_id.exists' => 'Mekanik yang dipilih tidak valid',
|
||||
'target_value.required' => 'Target nilai harus diisi',
|
||||
'target_value.integer' => 'Target nilai harus berupa angka',
|
||||
'target_value.min' => 'Target nilai minimal 1',
|
||||
'description.max' => 'Deskripsi maksimal 1000 karakter',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'is_active' => $this->boolean('is_active', true)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -48,4 +48,43 @@ class Dealer extends Model
|
||||
->withPivot('quantity')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all work prices for this dealer
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function workPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work
|
||||
*
|
||||
* @param int $workId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForWork($workId)
|
||||
{
|
||||
return $this->workPrices()
|
||||
->where('work_id', $workId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active work prices for this dealer
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function activeWorkPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||
}
|
||||
|
||||
public function roles()
|
||||
{
|
||||
return $this->belongsToMany(Role::class, 'role_dealer');
|
||||
}
|
||||
}
|
||||
|
||||
168
app/Models/KpiAchievement.php
Normal file
168
app/Models/KpiAchievement.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class KpiAchievement extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'kpi_target_id',
|
||||
'target_value',
|
||||
'actual_value',
|
||||
'achievement_percentage',
|
||||
'year',
|
||||
'month',
|
||||
'notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'achievement_percentage' => 'decimal:2',
|
||||
'year' => 'integer',
|
||||
'month' => 'integer'
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'actual_value' => 0,
|
||||
'achievement_percentage' => 0
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user that owns the achievement
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the KPI target for this achievement
|
||||
*/
|
||||
public function kpiTarget(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(KpiTarget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get achievements for specific year and month
|
||||
*/
|
||||
public function scopeForPeriod($query, $year, $month)
|
||||
{
|
||||
return $query->where('year', $year)->where('month', $month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get achievements for current month
|
||||
*/
|
||||
public function scopeCurrentMonth($query)
|
||||
{
|
||||
return $query->where('year', now()->year)->where('month', now()->month);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get achievements within year range
|
||||
*/
|
||||
public function scopeWithinYearRange($query, $startYear, $endYear)
|
||||
{
|
||||
return $query->whereBetween('year', [$startYear, $endYear]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get achievements for specific user
|
||||
*/
|
||||
public function scopeForUser($query, $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get achievement status
|
||||
*/
|
||||
public function getStatusAttribute(): string
|
||||
{
|
||||
if ($this->achievement_percentage >= 100) {
|
||||
return 'exceeded';
|
||||
} elseif ($this->achievement_percentage >= 80) {
|
||||
return 'good';
|
||||
} elseif ($this->achievement_percentage >= 60) {
|
||||
return 'fair';
|
||||
} else {
|
||||
return 'poor';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status color for display
|
||||
*/
|
||||
public function getStatusColorAttribute(): string
|
||||
{
|
||||
return match($this->status) {
|
||||
'exceeded' => 'success',
|
||||
'good' => 'info',
|
||||
'fair' => 'warning',
|
||||
'poor' => 'danger',
|
||||
default => 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get period display name (e.g., "Januari 2024")
|
||||
*/
|
||||
public function getPeriodDisplayName(): string
|
||||
{
|
||||
$monthNames = [
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||
];
|
||||
|
||||
return $monthNames[$this->month] . ' ' . $this->year;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get period start date
|
||||
*/
|
||||
public function getPeriodStartDate(): Carbon
|
||||
{
|
||||
return Carbon::createFromDate($this->year, $this->month, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get period end date
|
||||
*/
|
||||
public function getPeriodEndDate(): Carbon
|
||||
{
|
||||
return Carbon::createFromDate($this->year, $this->month, 1)->endOfMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get target value (from stored value or from relation)
|
||||
*/
|
||||
public function getTargetValueAttribute(): int
|
||||
{
|
||||
// Return stored target value if available, otherwise get from relation
|
||||
return $this->target_value ?? $this->kpiTarget?->target_value ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current target value from relation (for comparison)
|
||||
*/
|
||||
public function getCurrentTargetValueAttribute(): int
|
||||
{
|
||||
return $this->kpiTarget?->target_value ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stored target value differs from current target value
|
||||
*/
|
||||
public function hasTargetValueChanged(): bool
|
||||
{
|
||||
return $this->target_value !== $this->current_target_value;
|
||||
}
|
||||
}
|
||||
61
app/Models/KpiTarget.php
Normal file
61
app/Models/KpiTarget.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class KpiTarget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'target_value',
|
||||
'is_active',
|
||||
'description'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean'
|
||||
];
|
||||
|
||||
protected $attributes = [
|
||||
'is_active' => true
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the user that owns the KPI target
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the achievements for this target
|
||||
*/
|
||||
public function achievements(): HasMany
|
||||
{
|
||||
return $this->hasMany(KpiAchievement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get active targets
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if target is currently active
|
||||
*/
|
||||
public function isCurrentlyActive(): bool
|
||||
{
|
||||
return $this->is_active;
|
||||
}
|
||||
}
|
||||
210
app/Models/Postcheck.php
Normal file
210
app/Models/Postcheck.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Postcheck extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'postcheck_by',
|
||||
'postcheck_at',
|
||||
'police_number',
|
||||
'spk_number',
|
||||
'front_image',
|
||||
'front_image_metadata',
|
||||
'kilometer',
|
||||
'pressure_high',
|
||||
'pressure_low',
|
||||
'cabin_temperature',
|
||||
'cabin_temperature_image',
|
||||
'cabin_temperature_image_metadata',
|
||||
'ac_condition',
|
||||
'ac_image',
|
||||
'ac_image_metadata',
|
||||
'blower_condition',
|
||||
'blower_image',
|
||||
'blower_image_metadata',
|
||||
'evaporator_condition',
|
||||
'evaporator_image',
|
||||
'evaporator_image_metadata',
|
||||
'compressor_condition',
|
||||
'postcheck_notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'postcheck_at' => 'datetime',
|
||||
'kilometer' => 'decimal:2',
|
||||
'pressure_high' => 'decimal:2',
|
||||
'pressure_low' => 'decimal:2',
|
||||
'cabin_temperature' => 'decimal:2',
|
||||
'front_image_metadata' => 'array',
|
||||
'cabin_temperature_image_metadata' => 'array',
|
||||
'ac_image_metadata' => 'array',
|
||||
'blower_image_metadata' => 'array',
|
||||
'evaporator_image_metadata' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the transaction associated with the Postcheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function transaction()
|
||||
{
|
||||
return $this->belongsTo(Transaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who performed the postcheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function postcheckBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'postcheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get front image URL
|
||||
*/
|
||||
public function getFrontImageUrlAttribute()
|
||||
{
|
||||
return $this->front_image ? Storage::disk('public')->url($this->front_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cabin temperature image URL
|
||||
*/
|
||||
public function getCabinTemperatureImageUrlAttribute()
|
||||
{
|
||||
return $this->cabin_temperature_image ? Storage::disk('public')->url($this->cabin_temperature_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AC image URL
|
||||
*/
|
||||
public function getAcImageUrlAttribute()
|
||||
{
|
||||
return $this->ac_image ? Storage::disk('public')->url($this->ac_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blower image URL
|
||||
*/
|
||||
public function getBlowerImageUrlAttribute()
|
||||
{
|
||||
return $this->blower_image ? Storage::disk('public')->url($this->blower_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get evaporator image URL
|
||||
*/
|
||||
public function getEvaporatorImageUrlAttribute()
|
||||
{
|
||||
return $this->evaporator_image ? Storage::disk('public')->url($this->evaporator_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete associated files when model is deleted
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function ($postcheck) {
|
||||
$imageFields = [
|
||||
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||
'blower_image', 'evaporator_image'
|
||||
];
|
||||
|
||||
foreach ($imageFields as $field) {
|
||||
if ($postcheck->$field && Storage::disk('public')->exists($postcheck->$field)) {
|
||||
Storage::disk('public')->delete($postcheck->$field);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AC condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blower condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBlowerConditionOptions()
|
||||
{
|
||||
return ['sudah dibersihkan atau dicuci', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaporator condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getEvaporatorConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the compressor condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCompressorConditionOptions()
|
||||
{
|
||||
return ['sudah dikerjakan', 'sudah diganti'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by transaction
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $transactionId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByTransaction($query, $transactionId)
|
||||
{
|
||||
return $query->where('transaction_id', $transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by user who performed postcheck
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $userId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByUser($query, $userId)
|
||||
{
|
||||
return $query->where('postcheck_by', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by date range
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByDateRange($query, $startDate, $endDate)
|
||||
{
|
||||
return $query->whereBetween('postcheck_at', [$startDate, $endDate]);
|
||||
}
|
||||
}
|
||||
210
app/Models/Precheck.php
Normal file
210
app/Models/Precheck.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class Precheck extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'transaction_id',
|
||||
'precheck_by',
|
||||
'precheck_at',
|
||||
'police_number',
|
||||
'spk_number',
|
||||
'front_image',
|
||||
'front_image_metadata',
|
||||
'kilometer',
|
||||
'pressure_high',
|
||||
'pressure_low',
|
||||
'cabin_temperature',
|
||||
'cabin_temperature_image',
|
||||
'cabin_temperature_image_metadata',
|
||||
'ac_condition',
|
||||
'ac_image',
|
||||
'ac_image_metadata',
|
||||
'blower_condition',
|
||||
'blower_image',
|
||||
'blower_image_metadata',
|
||||
'evaporator_condition',
|
||||
'evaporator_image',
|
||||
'evaporator_image_metadata',
|
||||
'compressor_condition',
|
||||
'precheck_notes'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'precheck_at' => 'datetime',
|
||||
'kilometer' => 'decimal:2',
|
||||
'pressure_high' => 'decimal:2',
|
||||
'pressure_low' => 'decimal:2',
|
||||
'cabin_temperature' => 'decimal:2',
|
||||
'front_image_metadata' => 'array',
|
||||
'cabin_temperature_image_metadata' => 'array',
|
||||
'ac_image_metadata' => 'array',
|
||||
'blower_image_metadata' => 'array',
|
||||
'evaporator_image_metadata' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the transaction associated with the Precheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function transaction()
|
||||
{
|
||||
return $this->belongsTo(Transaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who performed the precheck
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function precheckBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'precheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get front image URL
|
||||
*/
|
||||
public function getFrontImageUrlAttribute()
|
||||
{
|
||||
return $this->front_image ? Storage::disk('public')->url($this->front_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cabin temperature image URL
|
||||
*/
|
||||
public function getCabinTemperatureImageUrlAttribute()
|
||||
{
|
||||
return $this->cabin_temperature_image ? Storage::disk('public')->url($this->cabin_temperature_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AC image URL
|
||||
*/
|
||||
public function getAcImageUrlAttribute()
|
||||
{
|
||||
return $this->ac_image ? Storage::disk('public')->url($this->ac_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blower image URL
|
||||
*/
|
||||
public function getBlowerImageUrlAttribute()
|
||||
{
|
||||
return $this->blower_image ? Storage::disk('public')->url($this->blower_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get evaporator image URL
|
||||
*/
|
||||
public function getEvaporatorImageUrlAttribute()
|
||||
{
|
||||
return $this->evaporator_image ? Storage::disk('public')->url($this->evaporator_image) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete associated files when model is deleted
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function ($precheck) {
|
||||
$imageFields = [
|
||||
'front_image', 'cabin_temperature_image', 'ac_image',
|
||||
'blower_image', 'evaporator_image'
|
||||
];
|
||||
|
||||
foreach ($imageFields as $field) {
|
||||
if ($precheck->$field && Storage::disk('public')->exists($precheck->$field)) {
|
||||
Storage::disk('public')->delete($precheck->$field);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the AC condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getAcConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the blower condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getBlowerConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the evaporator condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getEvaporatorConditionOptions()
|
||||
{
|
||||
return ['kotor', 'berlendir', 'bocor', 'bersih'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the compressor condition options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCompressorConditionOptions()
|
||||
{
|
||||
return ['kotor', 'rusak', 'baik', 'tidak ada'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by transaction
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $transactionId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByTransaction($query, $transactionId)
|
||||
{
|
||||
return $query->where('transaction_id', $transactionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by user who performed precheck
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param int $userId
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByUser($query, $userId)
|
||||
{
|
||||
return $query->where('precheck_by', $userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter by date range
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param string $startDate
|
||||
* @param string $endDate
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeByDateRange($query, $startDate, $endDate)
|
||||
{
|
||||
return $query->whereBetween('precheck_at', [$startDate, $endDate]);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
@@ -11,4 +12,19 @@ class Role extends Model
|
||||
protected $fillable = [
|
||||
'name'
|
||||
];
|
||||
|
||||
public function dealers()
|
||||
{
|
||||
return $this->belongsToMany(Dealer::class, 'role_dealer');
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
}
|
||||
|
||||
public function hasDealer($dealerId)
|
||||
{
|
||||
return $this->dealers()->where('dealers.id', $dealerId)->whereNull('dealers.deleted_at')->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ class Transaction extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
protected $fillable = [
|
||||
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id"
|
||||
"user_id", "user_sa_id", "work_id", "form", "spk", "police_number", "warranty", "date", "qty", "status", "dealer_id",
|
||||
"claimed_at", "claimed_by"
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'claimed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -52,4 +57,24 @@ class Transaction extends Model
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_sa_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the precheck associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function precheck()
|
||||
{
|
||||
return $this->hasOne(Precheck::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the postcheck associated with the transaction
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function postcheck()
|
||||
{
|
||||
return $this->hasOne(Postcheck::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,4 +132,193 @@ class User extends Authenticatable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all KPI targets for the User
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function kpiTargets()
|
||||
{
|
||||
return $this->hasMany(KpiTarget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all KPI achievements for the User
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function kpiAchievements()
|
||||
{
|
||||
return $this->hasMany(KpiAchievement::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is mechanic
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMechanic()
|
||||
{
|
||||
return $this->hasRole('mechanic');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current KPI target (no longer filtered by year/month)
|
||||
*
|
||||
* @return KpiTarget|null
|
||||
*/
|
||||
public function getCurrentKpiTarget()
|
||||
{
|
||||
return $this->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI achievement for specific year and month
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return KpiAchievement|null
|
||||
*/
|
||||
public function getKpiAchievement($year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
return $this->kpiAchievements()
|
||||
->where('year', $year)
|
||||
->where('month', $month)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function accessibleDealers()
|
||||
{
|
||||
if (!$this->role_id) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Load role with dealers
|
||||
if (!$this->relationLoaded('role')) {
|
||||
$this->load('role.dealers');
|
||||
}
|
||||
|
||||
// If user has specific dealer_id, check if role allows access
|
||||
if ($this->dealer_id) {
|
||||
if ($this->role && $this->role->hasDealer($this->dealer_id)) {
|
||||
return Dealer::where('id', $this->dealer_id)->get();
|
||||
}
|
||||
return collect();
|
||||
}
|
||||
|
||||
// If no specific dealer_id, return all dealers accessible by role
|
||||
return $this->role ? $this->role->dealers : collect();
|
||||
}
|
||||
|
||||
public function canAccessDealer($dealerId)
|
||||
{
|
||||
if (!$this->role_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load role with dealers
|
||||
if (!$this->relationLoaded('role')) {
|
||||
$this->load('role.dealers');
|
||||
}
|
||||
|
||||
return $this->role && $this->role->hasDealer($dealerId);
|
||||
}
|
||||
|
||||
public function getPrimaryDealer()
|
||||
{
|
||||
if ($this->dealer_id && $this->canAccessDealer($this->dealer_id)) {
|
||||
return $this->dealer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all accessible menus for a specific role
|
||||
*
|
||||
* @param int $roleId
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getAccessibleMenus($roleId)
|
||||
{
|
||||
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||
->where('privileges.role_id', $roleId)
|
||||
->where('privileges.view', 1)
|
||||
->select('menus.*', 'privileges.view', 'privileges.create', 'privileges.update', 'privileges.delete')
|
||||
->orderBy('menus.id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accessible menus for current user
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public function getMyAccessibleMenus()
|
||||
{
|
||||
if (!$this->role_id) {
|
||||
return collect();
|
||||
}
|
||||
return self::getAccessibleMenus($this->role_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can access specific menu
|
||||
*
|
||||
* @param string $menuLink
|
||||
* @return bool
|
||||
*/
|
||||
public function canAccessMenu($menuLink)
|
||||
{
|
||||
if (!$this->role_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||
->where('privileges.role_id', $this->role_id)
|
||||
->where('menus.link', $menuLink)
|
||||
->where('privileges.view', 1)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role can access specific menu (static method)
|
||||
*
|
||||
* @param int $roleId
|
||||
* @param string $menuLink
|
||||
* @return bool
|
||||
*/
|
||||
public static function roleCanAccessMenu($roleId, $menuLink)
|
||||
{
|
||||
return \App\Models\Privilege::join('menus', 'privileges.menu_id', '=', 'menus.id')
|
||||
->where('privileges.role_id', $roleId)
|
||||
->where('menus.link', $menuLink)
|
||||
->where('privileges.view', 1)
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prechecks performed by this user
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function prechecks()
|
||||
{
|
||||
return $this->hasMany(Precheck::class, 'precheck_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all postchecks performed by this user
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function postchecks()
|
||||
{
|
||||
return $this->hasMany(Postcheck::class, 'postcheck_by');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,52 @@ class Work extends Model
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dealer prices for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function dealerPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific dealer
|
||||
*
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForDealer($dealerId)
|
||||
{
|
||||
return $this->dealerPrices()
|
||||
->where('dealer_id', $dealerId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific dealer (including soft deleted)
|
||||
*
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public function getPriceForDealerWithTrashed($dealerId)
|
||||
{
|
||||
return $this->dealerPrices()
|
||||
->withTrashed()
|
||||
->where('dealer_id', $dealerId)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active prices for this work
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function activeDealerPrices()
|
||||
{
|
||||
return $this->hasMany(WorkDealerPrice::class)->active();
|
||||
}
|
||||
}
|
||||
|
||||
81
app/Models/WorkDealerPrice.php
Normal file
81
app/Models/WorkDealerPrice.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class WorkDealerPrice extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'work_id',
|
||||
'dealer_id',
|
||||
'price',
|
||||
'currency',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'price' => 'decimal:2',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the work associated with the price
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function work()
|
||||
{
|
||||
return $this->belongsTo(Work::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dealer associated with the price
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function dealer()
|
||||
{
|
||||
return $this->belongsTo(Dealer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to get only active prices
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted price with currency
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFormattedPriceAttribute()
|
||||
{
|
||||
return number_format($this->price, 0, ',', '.') . ' ' . $this->currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price for specific work and dealer
|
||||
*
|
||||
* @param int $workId
|
||||
* @param int $dealerId
|
||||
* @return WorkDealerPrice|null
|
||||
*/
|
||||
public static function getPriceForWorkAndDealer($workId, $dealerId)
|
||||
{
|
||||
return static::where('work_id', $workId)
|
||||
->where('dealer_id', $dealerId)
|
||||
->active()
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
public function boot()
|
||||
{
|
||||
Carbon::setLocale('id');
|
||||
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*', 'warehouse_management.*'], function ($view) {
|
||||
View::composer(['layouts.partials.sidebarMenu', 'dashboard', 'dealer_recap', 'back.*', 'warehouse_management.*', 'reports.*', 'kpi.*'], function ($view) {
|
||||
$menuQuery = Menu::all();
|
||||
$menus = [];
|
||||
foreach($menuQuery as $menu) {
|
||||
|
||||
453
app/Services/KpiService.php
Normal file
453
app/Services/KpiService.php
Normal file
@@ -0,0 +1,453 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\KpiTarget;
|
||||
use App\Models\KpiAchievement;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class KpiService
|
||||
{
|
||||
/**
|
||||
* Calculate KPI achievement for a user
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return KpiAchievement|null
|
||||
*/
|
||||
public function calculateKpiAchievement(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
// Get current KPI target (no longer filtered by year/month)
|
||||
$kpiTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$kpiTarget) {
|
||||
Log::info("No KPI target found for user {$user->id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate actual value based on month
|
||||
$actualValue = $this->getActualWorkCount($user, $year, $month);
|
||||
|
||||
// Calculate percentage
|
||||
$achievementPercentage = $kpiTarget->target_value > 0
|
||||
? ($actualValue / $kpiTarget->target_value) * 100
|
||||
: 0;
|
||||
|
||||
// Save or update achievement with target value stored directly
|
||||
return KpiAchievement::updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'year' => $year,
|
||||
'month' => $month
|
||||
],
|
||||
[
|
||||
'kpi_target_id' => $kpiTarget->id,
|
||||
'target_value' => $kpiTarget->target_value, // Store target value directly for historical tracking
|
||||
'actual_value' => $actualValue,
|
||||
'achievement_percentage' => $achievementPercentage
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual work count for a user in specific month
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return int
|
||||
*/
|
||||
private function getActualWorkCount(User $user, $year, $month)
|
||||
{
|
||||
return Transaction::where('user_id', $user->id)
|
||||
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->sum('qty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate KPI report for a user
|
||||
*
|
||||
* @param User $user
|
||||
* @param int|null $year
|
||||
* @param int|null $month
|
||||
* @return array
|
||||
*/
|
||||
public function generateKpiReport(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
$achievements = $user->kpiAchievements()
|
||||
->where('year', $year)
|
||||
->where('month', $month)
|
||||
->orderBy('month')
|
||||
->get();
|
||||
|
||||
$target = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'target' => $target,
|
||||
'achievements' => $achievements,
|
||||
'summary' => $this->calculateSummary($achievements),
|
||||
'period' => [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'period_name' => $this->getMonthName($month) . ' ' . $year
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate summary statistics for achievements
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $achievements
|
||||
* @return array
|
||||
*/
|
||||
private function calculateSummary($achievements)
|
||||
{
|
||||
if ($achievements->isEmpty()) {
|
||||
return [
|
||||
'total_target' => 0,
|
||||
'total_actual' => 0,
|
||||
'average_achievement' => 0,
|
||||
'best_period' => null,
|
||||
'worst_period' => null,
|
||||
'total_periods' => 0,
|
||||
'achievement_rate' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$totalTarget = $achievements->sum('target_value');
|
||||
$totalActual = $achievements->sum('actual_value');
|
||||
$averageAchievement = $achievements->avg('achievement_percentage');
|
||||
$totalPeriods = $achievements->count();
|
||||
$achievementRate = $totalPeriods > 0 ? ($achievements->where('achievement_percentage', '>=', 100)->count() / $totalPeriods) * 100 : 0;
|
||||
|
||||
$bestPeriod = $achievements->sortByDesc('achievement_percentage')->first();
|
||||
$worstPeriod = $achievements->sortBy('achievement_percentage')->first();
|
||||
|
||||
return [
|
||||
'total_target' => $totalTarget,
|
||||
'total_actual' => $totalActual,
|
||||
'average_achievement' => round($averageAchievement, 2),
|
||||
'best_period' => $bestPeriod,
|
||||
'worst_period' => $worstPeriod,
|
||||
'total_periods' => $totalPeriods,
|
||||
'achievement_rate' => round($achievementRate, 2)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI statistics for all mechanics
|
||||
*
|
||||
* @param int|null $year
|
||||
* @param int|null $month
|
||||
* @return array
|
||||
*/
|
||||
public function getMechanicsKpiStats($year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
$mechanics = User::whereHas('role', function($query) {
|
||||
$query->where('name', 'mechanic');
|
||||
})->get();
|
||||
|
||||
$stats = [];
|
||||
foreach ($mechanics as $mechanic) {
|
||||
$report = $this->generateKpiReport($mechanic, $year, $month);
|
||||
$stats[] = [
|
||||
'user' => $mechanic,
|
||||
'summary' => $report['summary'],
|
||||
'target' => $report['target']
|
||||
];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-calculate KPI achievements for all mechanics
|
||||
*
|
||||
* @param int|null $year
|
||||
* @param int|null $month
|
||||
* @return array
|
||||
*/
|
||||
public function autoCalculateAllMechanics($year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
$mechanics = User::whereHas('role', function($query) {
|
||||
$query->where('name', 'mechanic');
|
||||
})->get();
|
||||
|
||||
$results = [];
|
||||
foreach ($mechanics as $mechanic) {
|
||||
try {
|
||||
$achievement = $this->calculateKpiAchievement($mechanic, $year, $month);
|
||||
$results[] = [
|
||||
'user_id' => $mechanic->id,
|
||||
'user_name' => $mechanic->name,
|
||||
'success' => true,
|
||||
'achievement' => $achievement
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to calculate KPI for user {$mechanic->id}: " . $e->getMessage());
|
||||
$results[] = [
|
||||
'user_id' => $mechanic->id,
|
||||
'user_name' => $mechanic->name,
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI trend data for chart
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $months
|
||||
* @return array
|
||||
*/
|
||||
public function getKpiTrendData(User $user, $months = 12)
|
||||
{
|
||||
$endDate = now();
|
||||
$startDate = $endDate->copy()->subMonths($months);
|
||||
|
||||
$achievements = $user->kpiAchievements()
|
||||
->where(function($query) use ($startDate, $endDate) {
|
||||
$query->where(function($q) use ($startDate, $endDate) {
|
||||
$q->where('year', '>', $startDate->year)
|
||||
->orWhere(function($subQ) use ($startDate, $endDate) {
|
||||
$subQ->where('year', $startDate->year)
|
||||
->where('month', '>=', $startDate->month);
|
||||
});
|
||||
})
|
||||
->where(function($q) use ($endDate) {
|
||||
$q->where('year', '<', $endDate->year)
|
||||
->orWhere(function($subQ) use ($endDate) {
|
||||
$subQ->where('year', $endDate->year)
|
||||
->where('month', '<=', $endDate->month);
|
||||
});
|
||||
});
|
||||
})
|
||||
->orderBy('year')
|
||||
->orderBy('month')
|
||||
->get();
|
||||
|
||||
$trendData = [];
|
||||
foreach ($achievements as $achievement) {
|
||||
$trendData[] = [
|
||||
'period' => $achievement->getPeriodDisplayName(),
|
||||
'target' => $achievement->target_value,
|
||||
'actual' => $achievement->actual_value,
|
||||
'percentage' => $achievement->achievement_percentage,
|
||||
'status' => $achievement->status
|
||||
];
|
||||
}
|
||||
|
||||
return $trendData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get month name in Indonesian
|
||||
*
|
||||
* @param int $month
|
||||
* @return string
|
||||
*/
|
||||
private function getMonthName($month)
|
||||
{
|
||||
$monthNames = [
|
||||
1 => 'Januari', 2 => 'Februari', 3 => 'Maret', 4 => 'April',
|
||||
5 => 'Mei', 6 => 'Juni', 7 => 'Juli', 8 => 'Agustus',
|
||||
9 => 'September', 10 => 'Oktober', 11 => 'November', 12 => 'Desember'
|
||||
];
|
||||
|
||||
return $monthNames[$month] ?? 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI summary for dashboard
|
||||
*
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getKpiSummary(User $user)
|
||||
{
|
||||
$currentYear = now()->year;
|
||||
$currentMonth = now()->month;
|
||||
|
||||
// Get current month achievement
|
||||
$currentAchievement = $user->kpiAchievements()
|
||||
->where('year', $currentYear)
|
||||
->where('month', $currentMonth)
|
||||
->first();
|
||||
|
||||
// Get current month target (no longer filtered by year/month)
|
||||
$currentTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
// Get last 6 months achievements
|
||||
$recentAchievements = $user->kpiAchievements()
|
||||
->where(function($query) use ($currentYear, $currentMonth) {
|
||||
$query->where('year', '>', $currentYear - 1)
|
||||
->orWhere(function($q) use ($currentYear, $currentMonth) {
|
||||
$q->where('year', $currentYear)
|
||||
->where('month', '>=', max(1, $currentMonth - 5));
|
||||
});
|
||||
})
|
||||
->orderBy('year', 'desc')
|
||||
->orderBy('month', 'desc')
|
||||
->limit(6)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'current_achievement' => $currentAchievement,
|
||||
'current_target' => $currentTarget,
|
||||
'recent_achievements' => $recentAchievements,
|
||||
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
|
||||
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get claimed transactions count for a mechanic
|
||||
*
|
||||
* @param User $user
|
||||
* @param int|null $year
|
||||
* @param int|null $month
|
||||
* @return int
|
||||
*/
|
||||
public function getClaimedTransactionsCount(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
return Transaction::where('claimed_by', $user->id)
|
||||
->whereNotNull('claimed_at')
|
||||
->whereYear('claimed_at', $year)
|
||||
->whereMonth('claimed_at', $month)
|
||||
->sum('qty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate KPI achievement including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return KpiAchievement|null
|
||||
*/
|
||||
public function calculateKpiAchievementWithClaims(User $user, $year = null, $month = null)
|
||||
{
|
||||
$year = $year ?? now()->year;
|
||||
$month = $month ?? now()->month;
|
||||
|
||||
// Get current KPI target
|
||||
$kpiTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$kpiTarget) {
|
||||
Log::info("No KPI target found for user {$user->id}");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate actual value including claimed transactions
|
||||
$actualValue = $this->getActualWorkCountWithClaims($user, $year, $month);
|
||||
|
||||
// Calculate percentage
|
||||
$achievementPercentage = $kpiTarget->target_value > 0
|
||||
? ($actualValue / $kpiTarget->target_value) * 100
|
||||
: 0;
|
||||
|
||||
// Save or update achievement
|
||||
return KpiAchievement::updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'year' => $year,
|
||||
'month' => $month
|
||||
],
|
||||
[
|
||||
'kpi_target_id' => $kpiTarget->id,
|
||||
'target_value' => $kpiTarget->target_value,
|
||||
'actual_value' => $actualValue,
|
||||
'achievement_percentage' => $achievementPercentage
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual work count including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return int
|
||||
*/
|
||||
private function getActualWorkCountWithClaims(User $user, $year, $month)
|
||||
{
|
||||
// Get transactions created by the user (including pending and completed)
|
||||
$createdTransactions = Transaction::where('user_id', $user->id)
|
||||
->whereIn('status', [0, 1]) // pending (0) and completed (1)
|
||||
->whereYear('date', $year)
|
||||
->whereMonth('date', $month)
|
||||
->sum('qty');
|
||||
|
||||
// Get transactions claimed by the user
|
||||
$claimedTransactions = Transaction::where('claimed_by', $user->id)
|
||||
->whereNotNull('claimed_at')
|
||||
->whereYear('claimed_at', $year)
|
||||
->whereMonth('claimed_at', $month)
|
||||
->sum('qty');
|
||||
|
||||
return $createdTransactions + $claimedTransactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KPI summary including claimed transactions
|
||||
*
|
||||
* @param User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getKpiSummaryWithClaims(User $user)
|
||||
{
|
||||
$currentYear = now()->year;
|
||||
$currentMonth = now()->month;
|
||||
|
||||
// Calculate current month achievement including claims
|
||||
$currentAchievement = $this->calculateKpiAchievementWithClaims($user, $currentYear, $currentMonth);
|
||||
|
||||
// Get current month target
|
||||
$currentTarget = $user->kpiTargets()
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return [
|
||||
'current_achievement' => $currentAchievement,
|
||||
'current_target' => $currentTarget,
|
||||
'current_percentage' => $currentAchievement ? $currentAchievement->achievement_percentage : 0,
|
||||
'is_on_track' => $currentAchievement ? $currentAchievement->achievement_percentage >= 100 : false
|
||||
];
|
||||
}
|
||||
}
|
||||
292
app/Services/StockReportService.php
Normal file
292
app/Services/StockReportService.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Stock;
|
||||
use App\Models\StockLog;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class StockReportService
|
||||
{
|
||||
/**
|
||||
* Get stock report data for all products and dealers on a specific date
|
||||
*/
|
||||
public function getStockReportData($targetDate = null)
|
||||
{
|
||||
$targetDate = $targetDate ? Carbon::parse($targetDate) : now();
|
||||
|
||||
// Get dealers based on user role
|
||||
$dealers = $this->getDealersBasedOnUserRole();
|
||||
|
||||
// Get all active products
|
||||
$products = Product::where('active', true)
|
||||
->with(['category'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($products as $product) {
|
||||
$row = [
|
||||
'product_id' => $product->id,
|
||||
'product_code' => $product->code,
|
||||
'product_name' => $product->name,
|
||||
'category_name' => $product->category ? $product->category->name : '-',
|
||||
'unit' => $product->unit ?? '-',
|
||||
'total_stock' => 0
|
||||
];
|
||||
|
||||
// Calculate stock for each dealer on the target date
|
||||
foreach ($dealers as $dealer) {
|
||||
$stockOnDate = $this->getStockOnDate($product->id, $dealer->id, $targetDate);
|
||||
$row["dealer_{$dealer->id}"] = $stockOnDate;
|
||||
$row['total_stock'] += $stockOnDate;
|
||||
}
|
||||
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'dealers' => $dealers
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stock quantity for a specific product and dealer on a given date
|
||||
*/
|
||||
public function getStockOnDate($productId, $dealerId, $targetDate)
|
||||
{
|
||||
// Get the latest stock log entry before or on the target date
|
||||
$latestStockLog = StockLog::whereHas('stock', function($query) use ($productId, $dealerId) {
|
||||
$query->where('product_id', $productId)
|
||||
->where('dealer_id', $dealerId);
|
||||
})
|
||||
->where('created_at', '<=', $targetDate->endOfDay())
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if ($latestStockLog) {
|
||||
// Return the new_quantity from the latest log entry
|
||||
return $latestStockLog->new_quantity;
|
||||
}
|
||||
|
||||
// If no stock log found, check if there's a current stock record
|
||||
$currentStock = Stock::where('product_id', $productId)
|
||||
->where('dealer_id', $dealerId)
|
||||
->first();
|
||||
|
||||
if ($currentStock) {
|
||||
// Check if the stock was created before or on the target date
|
||||
if ($currentStock->created_at <= $targetDate) {
|
||||
return $currentStock->quantity;
|
||||
}
|
||||
}
|
||||
|
||||
// No stock data available for this date
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized stock data using a single query approach
|
||||
*/
|
||||
public function getOptimizedStockReportData($targetDate = null)
|
||||
{
|
||||
$targetDate = $targetDate ? Carbon::parse($targetDate) : now();
|
||||
|
||||
// Get dealers based on user role
|
||||
$dealers = $this->getDealersBasedOnUserRole();
|
||||
|
||||
// Get all active products with their stock data
|
||||
$products = Product::where('active', true)
|
||||
->with(['category', 'stocks.dealer'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($products as $product) {
|
||||
$row = [
|
||||
'product_id' => $product->id,
|
||||
'product_code' => $product->code,
|
||||
'product_name' => $product->name,
|
||||
'category_name' => $product->category ? $product->category->name : '-',
|
||||
'unit' => $product->unit ?? '-',
|
||||
'total_stock' => 0
|
||||
];
|
||||
|
||||
// Calculate stock for each dealer on the target date
|
||||
foreach ($dealers as $dealer) {
|
||||
$stockOnDate = $this->getOptimizedStockOnDate($product->id, $dealer->id, $targetDate);
|
||||
$row["dealer_{$dealer->id}"] = $stockOnDate;
|
||||
$row['total_stock'] += $stockOnDate;
|
||||
}
|
||||
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'dealers' => $dealers
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dealers based on logged-in user's role
|
||||
*/
|
||||
public function getDealersBasedOnUserRole()
|
||||
{
|
||||
// Get current authenticated user
|
||||
$user = Auth::user();
|
||||
|
||||
if (!$user) {
|
||||
Log::warning('No authenticated user found, returning all dealers');
|
||||
return Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||
}
|
||||
|
||||
Log::info('Getting dealers for user:', [
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
// If user has role, check role type and dealer access
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if ($role) {
|
||||
// Check if role is admin type
|
||||
if ($this->isAdminRole($role)) {
|
||||
// Admin role - check if has pivot dealers
|
||||
if ($role->dealers->count() > 0) {
|
||||
// Admin with pivot dealers - return pivot dealers only
|
||||
Log::info('Admin role with pivot dealers, returning pivot dealers only');
|
||||
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
Log::info('Returning pivot dealers for admin:', $dealers->pluck('name')->toArray());
|
||||
return $dealers;
|
||||
} else {
|
||||
// Admin without pivot dealers - return all dealers
|
||||
Log::info('Admin role without pivot dealers, returning all dealers');
|
||||
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||
Log::info('Returning all dealers for admin:', $allDealers->pluck('name')->toArray());
|
||||
return $allDealers;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-admin role - return dealers from role pivot
|
||||
if ($role->dealers->count() > 0) {
|
||||
Log::info('Non-admin role with dealers, returning role dealers');
|
||||
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get();
|
||||
Log::info('Returning dealers from role:', $dealers->pluck('name')->toArray());
|
||||
return $dealers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user has specific dealer_id but no role dealers, check if they can access their dealer_id
|
||||
if ($user->dealer_id) {
|
||||
Log::info('User has specific dealer_id:', ['dealer_id' => $user->dealer_id]);
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||
Log::info('User can access their dealer_id, returning single dealer');
|
||||
$dealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->orderBy('name')->get();
|
||||
Log::info('Returning dealer:', $dealer->pluck('name')->toArray());
|
||||
return $dealer;
|
||||
} else {
|
||||
Log::info('User cannot access their dealer_id');
|
||||
}
|
||||
}
|
||||
Log::info('User has dealer_id but no role or no access, returning empty');
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Fallback: return all dealers if no restrictions
|
||||
Log::info('No restrictions found, returning all dealers');
|
||||
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get();
|
||||
Log::info('Returning all dealers:', $allDealers->pluck('name')->toArray());
|
||||
return $allDealers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is admin type (should show all dealers if no pivot)
|
||||
*/
|
||||
private function isAdminRole($role)
|
||||
{
|
||||
// Define admin role names that should have access to all dealers
|
||||
$adminRoleNames = [
|
||||
'admin'
|
||||
];
|
||||
|
||||
// Check if role name contains admin keywords (but not "area")
|
||||
$roleName = strtolower(trim($role->name));
|
||||
foreach ($adminRoleNames as $adminName) {
|
||||
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||
Log::info('Role identified as admin type:', ['role_name' => $role->name]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Role with "area" in name should use pivot dealers, not all dealers
|
||||
if (strpos($roleName, 'area') !== false) {
|
||||
Log::info('Role contains "area", treating as area role (use pivot dealers):', ['role_name' => $role->name]);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info('Role is not admin type:', ['role_name' => $role->name]);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized method to get stock on date using subquery
|
||||
*/
|
||||
private function getOptimizedStockOnDate($productId, $dealerId, $targetDate)
|
||||
{
|
||||
try {
|
||||
// Use a subquery to get the latest stock log entry efficiently
|
||||
$latestStockLog = DB::table('stock_logs')
|
||||
->join('stocks', 'stock_logs.stock_id', '=', 'stocks.id')
|
||||
->where('stocks.product_id', $productId)
|
||||
->where('stocks.dealer_id', $dealerId)
|
||||
->where('stock_logs.created_at', '<=', $targetDate->endOfDay())
|
||||
->orderBy('stock_logs.created_at', 'desc')
|
||||
->select('stock_logs.new_quantity')
|
||||
->first();
|
||||
|
||||
if ($latestStockLog) {
|
||||
return $latestStockLog->new_quantity;
|
||||
}
|
||||
|
||||
// If no stock log found, check current stock
|
||||
$currentStock = Stock::where('product_id', $productId)
|
||||
->where('dealer_id', $dealerId)
|
||||
->first();
|
||||
|
||||
if ($currentStock && $currentStock->created_at <= $targetDate) {
|
||||
return $currentStock->quantity;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (\Exception $e) {
|
||||
// Log error and return 0
|
||||
Log::error('Error getting stock on date: ' . $e->getMessage(), [
|
||||
'product_id' => $productId,
|
||||
'dealer_id' => $dealerId,
|
||||
'target_date' => $targetDate
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
798
app/Services/TechnicianReportService.php
Normal file
798
app/Services/TechnicianReportService.php
Normal file
@@ -0,0 +1,798 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Work;
|
||||
use App\Models\User;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\Dealer;
|
||||
use App\Models\Role;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TechnicianReportService
|
||||
{
|
||||
/**
|
||||
* Get technician report data for all works and mechanics on a specific date range
|
||||
*/
|
||||
public function getTechnicianReportData($dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
try {
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return [
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
];
|
||||
}
|
||||
|
||||
// Validate dealer access
|
||||
if ($dealerId) {
|
||||
if ($user->dealer_id) {
|
||||
// User has specific dealer_id, check if they can access the requested dealer
|
||||
if ($user->dealer_id != $dealerId) {
|
||||
if ($user->role_id) {
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return [
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// User has dealer_id but no role, can only access their dealer
|
||||
return [
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
];
|
||||
}
|
||||
}
|
||||
} else if ($user->role_id) {
|
||||
// User has role, check if they can access the requested dealer
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return [
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Getting technician report data', [
|
||||
'user_id' => $user->id,
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
]);
|
||||
|
||||
// Get all works with category in single query
|
||||
$works = Work::with(['category'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Get mechanics based on dealer and role access
|
||||
$mechanics = $this->getMechanicsByDealer($dealerId);
|
||||
|
||||
Log::info('Mechanics found for report:', [
|
||||
'count' => $mechanics->count(),
|
||||
'dealer_id_filter' => $dealerId,
|
||||
'mechanics' => $mechanics->map(function($mechanic) {
|
||||
$roleName = 'Unknown';
|
||||
if ($mechanic->role) {
|
||||
$roleName = is_string($mechanic->role) ? $mechanic->role : $mechanic->role->name;
|
||||
}
|
||||
return [
|
||||
'id' => $mechanic->id,
|
||||
'name' => $mechanic->name,
|
||||
'role_id' => $mechanic->role_id,
|
||||
'role_name' => $roleName,
|
||||
'dealer_id' => $mechanic->dealer_id
|
||||
];
|
||||
})
|
||||
]);
|
||||
|
||||
// Get all transaction data in single optimized query
|
||||
$transactions = $this->getOptimizedTransactionData($dealerId, $startDate, $endDate, $mechanics->pluck('id'), $works->pluck('id'));
|
||||
|
||||
Log::info('Transaction data:', [
|
||||
'transaction_count' => count($transactions),
|
||||
'sample_transactions' => array_slice($transactions, 0, 5, true),
|
||||
'dealer_id_filter' => $dealerId,
|
||||
'is_admin_with_pivot' => $user->role_id ? (function() use ($user) {
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
return $role && $this->isAdminRole($role) && $role->dealers->count() > 0;
|
||||
})() : false
|
||||
]);
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($works as $work) {
|
||||
$row = [
|
||||
'work_id' => $work->id,
|
||||
'work_name' => $work->name,
|
||||
'work_code' => $work->shortname,
|
||||
'category_name' => $work->category ? $work->category->name : '-',
|
||||
'total_tickets' => 0
|
||||
];
|
||||
|
||||
// Calculate totals for each mechanic
|
||||
foreach ($mechanics as $mechanic) {
|
||||
$key = $work->id . '_' . $mechanic->id;
|
||||
$mechanicData = $transactions[$key] ?? ['total' => 0, 'completed' => 0, 'pending' => 0];
|
||||
|
||||
$row["mechanic_{$mechanic->id}_total"] = $mechanicData['total'];
|
||||
|
||||
// Add to totals
|
||||
$row['total_tickets'] += $mechanicData['total'];
|
||||
}
|
||||
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
Log::info('Final data prepared:', [
|
||||
'data_count' => count($data),
|
||||
'sample_data' => array_slice($data, 0, 2)
|
||||
]);
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'mechanics' => $mechanics,
|
||||
'works' => $works
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in getTechnicianReportData: ' . $e->getMessage(), [
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Return empty data structure but with proper format
|
||||
return [
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized transaction data in single query
|
||||
*/
|
||||
private function getOptimizedTransactionData($dealerId = null, $startDate = null, $endDate = null, $mechanicIds = null, $workIds = null)
|
||||
{
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Validate dealer access
|
||||
if ($dealerId) {
|
||||
if ($user->dealer_id) {
|
||||
// User has specific dealer_id, check if they can access the requested dealer
|
||||
if ($user->dealer_id != $dealerId) {
|
||||
if ($user->role_id) {
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
// User has dealer_id but no role, can only access their dealer
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} else if ($user->role_id) {
|
||||
// User has role, check if they can access the requested dealer
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Getting optimized transaction data', [
|
||||
'user_id' => $user->id,
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
]);
|
||||
|
||||
$query = Transaction::select(
|
||||
'work_id',
|
||||
'user_id',
|
||||
'status',
|
||||
DB::raw('COUNT(*) as count')
|
||||
)
|
||||
->groupBy('work_id', 'user_id', 'status');
|
||||
|
||||
if ($dealerId) {
|
||||
$query->where('dealer_id', $dealerId);
|
||||
} else if ($user->role_id) {
|
||||
// Check if admin with pivot dealers and "Semua Dealer" selected
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if ($role && $this->isAdminRole($role) && $role->dealers->count() > 0) {
|
||||
// Admin with pivot dealers and "Semua Dealer" selected - filter by pivot dealers
|
||||
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||
Log::info('Admin with pivot dealers, filtering transactions by pivot dealer IDs:', $accessibleDealerIds->toArray());
|
||||
}
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$query->where('date', '>=', $startDate);
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
$query->where('date', '<=', $endDate);
|
||||
}
|
||||
|
||||
if ($mechanicIds && $mechanicIds->count() > 0) {
|
||||
$query->whereIn('user_id', $mechanicIds);
|
||||
}
|
||||
|
||||
if ($workIds && $workIds->count() > 0) {
|
||||
$query->whereIn('work_id', $workIds);
|
||||
}
|
||||
|
||||
// Remove index hint that doesn't exist
|
||||
$results = $query->get();
|
||||
|
||||
Log::info('Transaction query results', [
|
||||
'results_count' => $results->count()
|
||||
]);
|
||||
|
||||
// Organize data by work_id_user_id key
|
||||
$organizedData = [];
|
||||
|
||||
foreach ($results as $result) {
|
||||
$key = $result->work_id . '_' . $result->user_id;
|
||||
|
||||
if (!isset($organizedData[$key])) {
|
||||
$organizedData[$key] = [
|
||||
'total' => 0,
|
||||
'completed' => 0,
|
||||
'pending' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$organizedData[$key]['total'] += $result->count;
|
||||
|
||||
if ($result->status == 1) {
|
||||
$organizedData[$key]['completed'] += $result->count;
|
||||
} else {
|
||||
$organizedData[$key]['pending'] += $result->count;
|
||||
}
|
||||
}
|
||||
|
||||
return $organizedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||
*/
|
||||
private function getTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
$query = Transaction::where('work_id', $workId)
|
||||
->where('user_id', $mechanicId);
|
||||
|
||||
if ($dealerId) {
|
||||
$query->where('dealer_id', $dealerId);
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$query->where('date', '>=', $startDate);
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
$query->where('date', '<=', $endDate);
|
||||
}
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completed ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||
*/
|
||||
private function getCompletedTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
$query = Transaction::where('work_id', $workId)
|
||||
->where('user_id', $mechanicId)
|
||||
->where('status', 1); // Assuming status 1 is completed
|
||||
|
||||
if ($dealerId) {
|
||||
$query->where('dealer_id', $dealerId);
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$query->where('date', '>=', $startDate);
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
$query->where('date', '<=', $endDate);
|
||||
}
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pending ticket count for a specific work and mechanic (legacy method for backward compatibility)
|
||||
*/
|
||||
private function getPendingTicketCount($workId, $mechanicId, $dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
$query = Transaction::where('work_id', $workId)
|
||||
->where('user_id', $mechanicId)
|
||||
->where('status', 0); // Assuming status 0 is pending
|
||||
|
||||
if ($dealerId) {
|
||||
$query->where('dealer_id', $dealerId);
|
||||
}
|
||||
|
||||
if ($startDate) {
|
||||
$query->where('date', '>=', $startDate);
|
||||
}
|
||||
|
||||
if ($endDate) {
|
||||
$query->where('date', '<=', $endDate);
|
||||
}
|
||||
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all dealers for filter
|
||||
*/
|
||||
public function getDealers()
|
||||
{
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
Log::info('No authenticated user found');
|
||||
return collect();
|
||||
}
|
||||
|
||||
Log::info('Getting dealers for user:', [
|
||||
'user_id' => $user->id,
|
||||
'user_name' => $user->name,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
// If user has role, check role type and dealer access
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
Log::info('Role details:', [
|
||||
'role_id' => $role ? $role->id : null,
|
||||
'role_name' => $role ? $role->name : null,
|
||||
'role_dealers_count' => $role ? $role->dealers->count() : 0,
|
||||
'role_dealers' => $role ? $role->dealers->pluck('id', 'name')->toArray() : []
|
||||
]);
|
||||
|
||||
if ($role) {
|
||||
// Check if role is admin type
|
||||
if ($this->isAdminRole($role)) {
|
||||
// Admin role - check if has pivot dealers
|
||||
if ($role->dealers->count() > 0) {
|
||||
// Admin with pivot dealers - return pivot dealers (for "Semua Dealer" option)
|
||||
Log::info('Admin role with pivot dealers, returning pivot dealers');
|
||||
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get(['dealers.id', 'dealers.name', 'dealers.dealer_code']);
|
||||
Log::info('Returning pivot dealers for admin:', $dealers->toArray());
|
||||
return $dealers;
|
||||
} else {
|
||||
// Admin without pivot dealers - return all dealers
|
||||
Log::info('Admin role without pivot dealers, returning all dealers');
|
||||
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||
Log::info('Returning all dealers for admin:', $allDealers->toArray());
|
||||
return $allDealers;
|
||||
}
|
||||
}
|
||||
|
||||
// Role has dealer relationship (tampilkan dealer berdasarkan pivot)
|
||||
if ($role->dealers->count() > 0) {
|
||||
Log::info('Role has dealers relationship, returning role dealers');
|
||||
$dealers = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->get(['dealers.id', 'dealers.name', 'dealers.dealer_code']);
|
||||
Log::info('Returning dealers from role:', $dealers->toArray());
|
||||
return $dealers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user has specific dealer_id but no role dealers, check if they can access their dealer_id
|
||||
if ($user->dealer_id) {
|
||||
Log::info('User has specific dealer_id:', ['dealer_id' => $user->dealer_id]);
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||
Log::info('User can access their dealer_id, returning single dealer');
|
||||
$dealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||
Log::info('Returning dealer:', $dealer->toArray());
|
||||
return $dealer;
|
||||
} else {
|
||||
Log::info('User cannot access their dealer_id');
|
||||
}
|
||||
}
|
||||
Log::info('User has dealer_id but no role or no access, returning empty');
|
||||
return collect();
|
||||
}
|
||||
|
||||
// Fallback: return all dealers if no restrictions
|
||||
Log::info('No restrictions found, returning all dealers');
|
||||
$allDealers = Dealer::whereNull('deleted_at')->orderBy('name')->get(['id', 'name', 'dealer_code']);
|
||||
Log::info('Returning all dealers:', $allDealers->toArray());
|
||||
return $allDealers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is admin type (should show all dealers)
|
||||
*/
|
||||
public function isAdminRole($role)
|
||||
{
|
||||
// Define admin role names that should have access to all dealers
|
||||
$adminRoleNames = [
|
||||
'admin'
|
||||
];
|
||||
|
||||
// Check if role name contains admin keywords (but not "area")
|
||||
$roleName = strtolower(trim($role->name));
|
||||
foreach ($adminRoleNames as $adminName) {
|
||||
if (strpos($roleName, $adminName) !== false && strpos($roleName, 'area') === false) {
|
||||
Log::info('Role identified as admin type:', ['role_name' => $role->name]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if role has no dealer restrictions (no pivot relationships)
|
||||
// This means role can access all dealers
|
||||
if ($role->dealers->count() === 0) {
|
||||
Log::info('Role has no dealer restrictions, treating as admin type:', ['role_name' => $role->name]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Role with "area" in name should use pivot dealers, not all dealers
|
||||
if (strpos($roleName, 'area') !== false) {
|
||||
Log::info('Role contains "area", treating as area role (use pivot dealers):', ['role_name' => $role->name]);
|
||||
return false;
|
||||
}
|
||||
|
||||
Log::info('Role is not admin type:', ['role_name' => $role->name]);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default dealer for filter (berbasis user role)
|
||||
*/
|
||||
public function getDefaultDealer()
|
||||
{
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Log::info('Getting default dealer for user:', [
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id
|
||||
]);
|
||||
|
||||
// If user has role, check role type and dealer access
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if ($role) {
|
||||
// Check if role is admin type
|
||||
if ($this->isAdminRole($role)) {
|
||||
// Admin role - check if has pivot dealers
|
||||
if ($role->dealers->count() > 0) {
|
||||
// Admin with pivot dealers - return first dealer from pivot
|
||||
Log::info('Admin role with pivot dealers, returning first dealer from pivot');
|
||||
$defaultDealer = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->first();
|
||||
Log::info('Default dealer for admin with pivot:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||
return $defaultDealer;
|
||||
} else {
|
||||
// Admin without pivot dealers - no default dealer (show all dealers without selection)
|
||||
Log::info('Admin role without pivot dealers, no default dealer');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Role has dealer relationship (return first dealer from role dealers)
|
||||
if ($role->dealers->count() > 0) {
|
||||
Log::info('Role has dealers relationship, returning first dealer from role dealers');
|
||||
$defaultDealer = $role->dealers()->whereNull('dealers.deleted_at')->orderBy('name')->first();
|
||||
Log::info('Default dealer from role dealers:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||
return $defaultDealer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If user has specific dealer_id, check if they can access it
|
||||
if ($user->dealer_id) {
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
if ($role && $role->hasDealer($user->dealer_id)) {
|
||||
$defaultDealer = Dealer::where('id', $user->dealer_id)->whereNull('deleted_at')->first();
|
||||
Log::info('User dealer found:', $defaultDealer ? $defaultDealer->toArray() : null);
|
||||
return $defaultDealer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fallback: no default dealer
|
||||
Log::info('No default dealer found');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mechanics for a specific dealer
|
||||
*/
|
||||
public function getMechanicsByDealer($dealerId = null)
|
||||
{
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
Log::info('Getting mechanics by dealer:', [
|
||||
'user_id' => $user->id,
|
||||
'user_role_id' => $user->role_id,
|
||||
'user_dealer_id' => $user->dealer_id,
|
||||
'requested_dealer_id' => $dealerId
|
||||
]);
|
||||
|
||||
$query = User::with('role')->whereHas('role', function($query) {
|
||||
$query->where('name', 'mechanic');
|
||||
});
|
||||
|
||||
// If user has role, check role type and dealer access
|
||||
if ($user->role_id) {
|
||||
$role = Role::with(['dealers' => function($query) {
|
||||
$query->whereNull('dealers.deleted_at'); // Only active dealers
|
||||
}])->find($user->role_id);
|
||||
|
||||
if ($role) {
|
||||
// Check if role is admin type
|
||||
if ($this->isAdminRole($role)) {
|
||||
// Admin role - check if has pivot dealers
|
||||
if ($role->dealers->count() > 0) {
|
||||
// Admin with pivot dealers
|
||||
if ($dealerId) {
|
||||
// Specific dealer selected - get mechanics from that dealer
|
||||
Log::info('Admin with pivot dealers, specific dealer selected:', ['dealer_id' => $dealerId]);
|
||||
$query->where('dealer_id', $dealerId);
|
||||
} else {
|
||||
// "Semua Dealer" selected - get mechanics from all pivot dealers
|
||||
Log::info('Admin with pivot dealers, "Semua Dealer" selected, getting mechanics from all pivot dealers');
|
||||
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||
Log::info('Accessible dealer IDs for admin:', $accessibleDealerIds->toArray());
|
||||
}
|
||||
} else {
|
||||
// Admin without pivot dealers - can access all dealers
|
||||
Log::info('Admin without pivot dealers, can access mechanics from all dealers');
|
||||
if ($dealerId) {
|
||||
$query->where('dealer_id', $dealerId);
|
||||
}
|
||||
// If no dealer_id, show all mechanics (no additional filtering)
|
||||
}
|
||||
} else {
|
||||
// Role has dealer relationship (filter by accessible dealers)
|
||||
if ($role->dealers->count() > 0) {
|
||||
Log::info('Role has dealers relationship, filtering mechanics by accessible dealers');
|
||||
$accessibleDealerIds = $role->dealers->pluck('id');
|
||||
$query->whereIn('dealer_id', $accessibleDealerIds);
|
||||
Log::info('Accessible dealer IDs:', $accessibleDealerIds->toArray());
|
||||
} else {
|
||||
Log::info('Role has no dealers, returning empty');
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ($user->dealer_id) {
|
||||
// User has specific dealer_id but no role, can only access their dealer
|
||||
Log::info('User has dealer_id but no role, can only access their dealer');
|
||||
$query->where('dealer_id', $user->dealer_id);
|
||||
}
|
||||
|
||||
// Apply dealer filter if provided (for non-admin roles)
|
||||
if ($dealerId && !$this->isAdminRole($role ?? null)) {
|
||||
Log::info('Applying dealer filter for non-admin role:', ['dealer_id' => $dealerId]);
|
||||
$query->where('dealer_id', $dealerId);
|
||||
}
|
||||
|
||||
$mechanics = $query->orderBy('name')->get(['id', 'name', 'dealer_id']);
|
||||
|
||||
Log::info('Mechanics found:', [
|
||||
'count' => $mechanics->count(),
|
||||
'mechanics' => $mechanics->map(function($mechanic) {
|
||||
return [
|
||||
'id' => $mechanic->id,
|
||||
'name' => $mechanic->name,
|
||||
'dealer_id' => $mechanic->dealer_id
|
||||
];
|
||||
})->toArray()
|
||||
]);
|
||||
|
||||
return $mechanics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get technician report data for Yajra DataTable
|
||||
*/
|
||||
public function getTechnicianReportDataForDataTable($dealerId = null, $startDate = null, $endDate = null)
|
||||
{
|
||||
try {
|
||||
// Get current authenticated user
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
]);
|
||||
}
|
||||
|
||||
// Validate dealer access
|
||||
if ($dealerId) {
|
||||
if ($user->dealer_id) {
|
||||
// User has specific dealer_id, check if they can access the requested dealer
|
||||
if ($user->dealer_id != $dealerId) {
|
||||
if ($user->role_id) {
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// User has dealer_id but no role, can only access their dealer
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else if ($user->role_id) {
|
||||
// User has role, check if they can access the requested dealer
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
if (!$role || !$role->hasDealer($dealerId)) {
|
||||
// User doesn't have access to this dealer
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Getting technician report data for DataTable', [
|
||||
'user_id' => $user->id,
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate
|
||||
]);
|
||||
|
||||
// Get all works with category
|
||||
$works = Work::with(['category'])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
// Get mechanics based on dealer and role access
|
||||
$mechanics = $this->getMechanicsByDealer($dealerId);
|
||||
|
||||
// Get transaction data
|
||||
$transactions = $this->getOptimizedTransactionData($dealerId, $startDate, $endDate, $mechanics->pluck('id'), $works->pluck('id'));
|
||||
|
||||
Log::info('Transaction data for DataTable:', [
|
||||
'transaction_count' => count($transactions),
|
||||
'dealer_id_filter' => $dealerId,
|
||||
'is_admin_with_pivot' => $user->role_id ? (function() use ($user) {
|
||||
$role = Role::with('dealers')->find($user->role_id);
|
||||
return $role && $this->isAdminRole($role) && $role->dealers->count() > 0;
|
||||
})() : false
|
||||
]);
|
||||
|
||||
$data = [];
|
||||
|
||||
foreach ($works as $work) {
|
||||
$row = [
|
||||
'DT_RowIndex' => count($data) + 1,
|
||||
'work_name' => $work->name,
|
||||
'work_code' => $work->shortname,
|
||||
'category_name' => $work->category ? $work->category->name : '-'
|
||||
];
|
||||
|
||||
// Add mechanic columns
|
||||
foreach ($mechanics as $mechanic) {
|
||||
$key = $work->id . '_' . $mechanic->id;
|
||||
$mechanicData = $transactions[$key] ?? ['total' => 0, 'completed' => 0, 'pending' => 0];
|
||||
|
||||
$row["mechanic_{$mechanic->id}_total"] = $mechanicData['total'];
|
||||
}
|
||||
|
||||
$data[] = $row;
|
||||
}
|
||||
|
||||
Log::info('DataTable response prepared', [
|
||||
'data_count' => count($data),
|
||||
'mechanics_count' => $mechanics->count(),
|
||||
'works_count' => $works->count()
|
||||
]);
|
||||
|
||||
// Create DataTable response
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => count($data),
|
||||
'recordsFiltered' => count($data),
|
||||
'data' => $data,
|
||||
'mechanics' => $mechanics,
|
||||
'works' => $works
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error in getTechnicianReportDataForDataTable: ' . $e->getMessage(), [
|
||||
'dealer_id' => $dealerId,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'draw' => request()->input('draw', 1),
|
||||
'recordsTotal' => 0,
|
||||
'recordsFiltered' => 0,
|
||||
'data' => [],
|
||||
'mechanics' => collect(),
|
||||
'works' => collect()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateKpiTargetsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('kpi_targets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->integer('target_value');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Unique constraint untuk mencegah duplikasi target aktif per user (satu target aktif per user)
|
||||
$table->unique(['user_id', 'is_active'], 'kpi_targets_user_active_unique');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('kpi_targets');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateKpiAchievementsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('kpi_achievements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('kpi_target_id')->constrained()->onDelete('cascade');
|
||||
$table->integer('target_value'); // Menyimpan target value secara langsung untuk historical tracking
|
||||
$table->integer('actual_value')->default(0);
|
||||
$table->decimal('achievement_percentage', 5, 2)->default(0);
|
||||
$table->integer('year');
|
||||
$table->integer('month');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Unique constraint untuk mencegah duplikasi achievement per user per bulan
|
||||
// Note: Tidak menggunakan kpi_target_id karena target sekarang permanen per user
|
||||
$table->unique(['user_id', 'year', 'month']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('kpi_achievements');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddClaimedColumnsToTransactionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->timestamp('claimed_at')->nullable()->after('status');
|
||||
$table->unsignedBigInteger('claimed_by')->nullable()->after('claimed_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('transactions', function (Blueprint $table) {
|
||||
$table->dropColumn(['claimed_at', 'claimed_by']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateWorkDealerPricesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('work_dealer_prices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('work_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('dealer_id')->constrained()->onDelete('cascade');
|
||||
$table->decimal('price', 15, 2)->default(0.00);
|
||||
$table->string('currency', 3)->default('IDR');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->unique(['work_id', 'dealer_id']);
|
||||
$table->index(['dealer_id', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('work_dealer_prices');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateRoleDealerTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('role_dealer', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('role_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('dealer_id')->constrained()->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['role_id', 'dealer_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('role_dealer');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePrechecksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('prechecks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('precheck_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamp('precheck_at')->nullable();
|
||||
$table->string('police_number');
|
||||
$table->string('spk_number');
|
||||
$table->string('front_image', 255)->nullable();
|
||||
$table->json('front_image_metadata')->nullable();
|
||||
$table->decimal('kilometer', 10, 2);
|
||||
$table->decimal('pressure_high', 10, 2);
|
||||
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||
$table->string('cabin_temperature_image', 255)->nullable();
|
||||
$table->json('cabin_temperature_image_metadata')->nullable();
|
||||
$table->enum('ac_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->string('ac_image', 255)->nullable();
|
||||
$table->json('ac_image_metadata')->nullable();
|
||||
$table->enum('blower_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->string('blower_image', 255)->nullable();
|
||||
$table->json('blower_image_metadata')->nullable();
|
||||
$table->enum('evaporator_condition', ['kotor', 'berlendir', 'bocor', 'bersih'])->nullable();
|
||||
$table->string('evaporator_image', 255)->nullable();
|
||||
$table->json('evaporator_image_metadata')->nullable();
|
||||
$table->enum('compressor_condition', ['kotor', 'rusak', 'baik', 'tidak ada'])->nullable();
|
||||
$table->text('precheck_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['transaction_id']);
|
||||
$table->index(['precheck_by']);
|
||||
$table->index(['precheck_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('prechecks');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePostchecksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('postchecks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('transaction_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('postcheck_by')->constrained('users')->onDelete('cascade');
|
||||
$table->timestamp('postcheck_at')->nullable();
|
||||
$table->string('police_number');
|
||||
$table->string('spk_number');
|
||||
$table->string('front_image', 255)->nullable();
|
||||
$table->json('front_image_metadata')->nullable();
|
||||
$table->decimal('kilometer', 10, 2);
|
||||
$table->decimal('pressure_high', 10, 2);
|
||||
$table->decimal('pressure_low', 10, 2)->nullable();
|
||||
$table->decimal('cabin_temperature', 10, 2)->nullable();
|
||||
$table->string('cabin_temperature_image', 255)->nullable();
|
||||
$table->json('cabin_temperature_image_metadata')->nullable();
|
||||
$table->enum('ac_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->string('ac_image', 255)->nullable();
|
||||
$table->json('ac_image_metadata')->nullable();
|
||||
$table->enum('blower_condition', ['sudah dibersihkan atau dicuci', 'sudah diganti'])->nullable();
|
||||
$table->string('blower_image', 255)->nullable();
|
||||
$table->json('blower_image_metadata')->nullable();
|
||||
$table->enum('evaporator_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->string('evaporator_image', 255)->nullable();
|
||||
$table->json('evaporator_image_metadata')->nullable();
|
||||
$table->enum('compressor_condition', ['sudah dikerjakan', 'sudah diganti'])->nullable();
|
||||
$table->text('postcheck_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['transaction_id']);
|
||||
$table->index(['postcheck_by']);
|
||||
$table->index(['postcheck_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('postchecks');
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,18 @@ class MenuSeeder extends Seeder
|
||||
[
|
||||
'name' => 'Histori Stock',
|
||||
'link' => 'stock-audit.index'
|
||||
],
|
||||
[
|
||||
'name' => 'Target',
|
||||
'link' => 'kpi.targets.index'
|
||||
],
|
||||
[
|
||||
'name' => 'Stock Produk',
|
||||
'link' => 'reports.stock-product.index'
|
||||
],
|
||||
[
|
||||
'name' => 'Teknisi',
|
||||
'link' => 'reports.technician.index'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -47,5 +59,7 @@ class MenuSeeder extends Seeder
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Menu::whereIn('link', ['targets.index','product-categories.index'])->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,67 @@
|
||||
"use strict";
|
||||
|
||||
function moneyFormat(n, currency) {
|
||||
n = (n != null) ? n : 0;
|
||||
n = n != null ? n : 0;
|
||||
var v = parseFloat(n).toFixed(0);
|
||||
return currency + " " + v.replace(/./g, function (c, i, a) {
|
||||
return (
|
||||
currency +
|
||||
" " +
|
||||
v.replace(/./g, function (c, i, a) {
|
||||
return i > 0 && c !== "," && (a.length - i) % 3 === 0 ? "." + c : c;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var KTToastr = function () {
|
||||
var KTToastr = (function () {
|
||||
var successToastr = function (msg, title) {
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
"debug": false,
|
||||
"newestOnTop": false,
|
||||
"progressBar": false,
|
||||
"positionClass": "toast-top-right",
|
||||
"preventDuplicates": true,
|
||||
"onclick": null,
|
||||
"showDuration": "500",
|
||||
"hideDuration": "500",
|
||||
"timeOut": "3000",
|
||||
"extendedTimeOut": "3000",
|
||||
"showEasing": "swing",
|
||||
"hideEasing": "swing",
|
||||
"showMethod": "fadeIn",
|
||||
"hideMethod": "fadeOut"
|
||||
closeButton: true,
|
||||
debug: false,
|
||||
newestOnTop: false,
|
||||
progressBar: false,
|
||||
positionClass: "toast-top-right",
|
||||
preventDuplicates: true,
|
||||
onclick: null,
|
||||
showDuration: "500",
|
||||
hideDuration: "500",
|
||||
timeOut: "3000",
|
||||
extendedTimeOut: "3000",
|
||||
showEasing: "swing",
|
||||
hideEasing: "swing",
|
||||
showMethod: "fadeIn",
|
||||
hideMethod: "fadeOut",
|
||||
};
|
||||
var $toast = toastr["success"](msg, title);
|
||||
|
||||
if (typeof $toast === 'undefined') {
|
||||
if (typeof $toast === "undefined") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var errorToastr = function (msg, title) {
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
"debug": false,
|
||||
"newestOnTop": false,
|
||||
"progressBar": false,
|
||||
"positionClass": "toast-top-right",
|
||||
"preventDuplicates": true,
|
||||
"onclick": null,
|
||||
"showDuration": "500",
|
||||
"hideDuration": "500",
|
||||
"timeOut": "3000",
|
||||
"extendedTimeOut": "3000",
|
||||
"showEasing": "swing",
|
||||
"hideEasing": "swing",
|
||||
"showMethod": "fadeIn",
|
||||
"hideMethod": "fadeOut"
|
||||
closeButton: true,
|
||||
debug: false,
|
||||
newestOnTop: false,
|
||||
progressBar: false,
|
||||
positionClass: "toast-top-right",
|
||||
preventDuplicates: true,
|
||||
onclick: null,
|
||||
showDuration: "500",
|
||||
hideDuration: "500",
|
||||
timeOut: "3000",
|
||||
extendedTimeOut: "3000",
|
||||
showEasing: "swing",
|
||||
hideEasing: "swing",
|
||||
showMethod: "fadeIn",
|
||||
hideMethod: "fadeOut",
|
||||
};
|
||||
var $toast = toastr["error"](msg, title);
|
||||
|
||||
if (typeof $toast === 'undefined') {
|
||||
if (typeof $toast === "undefined") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
initSuccess: function (msg, title) {
|
||||
@@ -65,11 +69,29 @@ var KTToastr = function () {
|
||||
},
|
||||
initError: function (msg, title) {
|
||||
errorToastr(msg, title);
|
||||
}
|
||||
},
|
||||
};
|
||||
}();
|
||||
})();
|
||||
|
||||
jQuery(document).ready(function () {
|
||||
var li = $('.kt-menu__item--active');
|
||||
li.closest('li.kt-menu__item--submenu').addClass('kt-menu__item--open');
|
||||
var li = $(".kt-menu__item--active");
|
||||
li.closest("li.kt-menu__item--submenu").addClass("kt-menu__item--open");
|
||||
|
||||
// Initialize Select2 globally
|
||||
if (typeof $.fn.select2 !== "undefined") {
|
||||
$(".select2").select2({
|
||||
theme: "bootstrap4",
|
||||
width: "100%",
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize DatePicker globally
|
||||
if (typeof $.fn.datepicker !== "undefined") {
|
||||
$(".datepicker").datepicker({
|
||||
format: "yyyy-mm-dd",
|
||||
autoclose: true,
|
||||
todayHighlight: true,
|
||||
orientation: "bottom auto",
|
||||
});
|
||||
}
|
||||
});
|
||||
655
public/js/pages/back/master/work-prices.js
Normal file
655
public/js/pages/back/master/work-prices.js
Normal file
@@ -0,0 +1,655 @@
|
||||
"use strict";
|
||||
|
||||
// Class definition
|
||||
var WorkPrices = (function () {
|
||||
// Private variables
|
||||
var workId;
|
||||
var dealersTable;
|
||||
var saveTimeout = {}; // For debouncing save requests
|
||||
|
||||
// Loading overlay functions
|
||||
var showLoadingOverlay = function (message) {
|
||||
if ($("#loading-overlay").length === 0) {
|
||||
$("body").append(`
|
||||
<div id="loading-overlay" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div class="text-light mt-3" style="font-size: 1.1rem; font-weight: 500;">${message}</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
$("#loading-overlay").show();
|
||||
$("#loading-overlay .text-light:last").text(message);
|
||||
}
|
||||
};
|
||||
|
||||
var hideLoadingOverlay = function () {
|
||||
$("#loading-overlay").hide();
|
||||
};
|
||||
|
||||
// Private functions
|
||||
var initTable = function () {
|
||||
dealersTable = $("#dealers-table").DataTable({
|
||||
pageLength: -1, // Show all records
|
||||
lengthChange: false, // Hide length change dropdown
|
||||
searching: true,
|
||||
ordering: true,
|
||||
info: false, // Hide "Showing X of Y entries"
|
||||
responsive: true,
|
||||
dom: '<"top"f>rt<"bottom"p>', // Only show search and pagination
|
||||
paging: false, // Disable pagination
|
||||
});
|
||||
};
|
||||
|
||||
var initEvents = function () {
|
||||
// Get work ID from URL
|
||||
var pathArray = window.location.pathname.split("/");
|
||||
workId = pathArray[pathArray.length - 2]; // work/{id}/set-prices
|
||||
|
||||
// Save single price with debouncing
|
||||
$(document).on("click", ".save-single", function () {
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
|
||||
// Clear existing timeout for this dealer
|
||||
if (saveTimeout[dealerId]) {
|
||||
clearTimeout(saveTimeout[dealerId]);
|
||||
}
|
||||
|
||||
// Set new timeout to prevent rapid clicks
|
||||
saveTimeout[dealerId] = setTimeout(function () {
|
||||
saveSinglePrice(dealerId);
|
||||
}, 300); // 300ms delay
|
||||
});
|
||||
|
||||
// Delete price
|
||||
$(document).on("click", ".delete-price", function () {
|
||||
var priceId = $(this).data("price-id");
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
deletePrice(priceId, dealerId);
|
||||
});
|
||||
|
||||
// Save all prices
|
||||
$("#btn-save-all").on("click", function () {
|
||||
saveAllPrices();
|
||||
});
|
||||
|
||||
// Status toggle
|
||||
$(document).on("change", ".status-input", function () {
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
var isChecked = $(this).is(":checked");
|
||||
var label = $(this).siblings("label");
|
||||
var checkbox = $(this);
|
||||
|
||||
// Update visual immediately
|
||||
if (isChecked) {
|
||||
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||
} else {
|
||||
label
|
||||
.text("Nonaktif")
|
||||
.removeClass("active")
|
||||
.addClass("inactive");
|
||||
}
|
||||
|
||||
// Send AJAX request to update database
|
||||
toggleStatus(dealerId, isChecked, checkbox, label);
|
||||
});
|
||||
|
||||
// Format price input with thousand separator
|
||||
$(document).on("input", ".price-input", function () {
|
||||
var input = $(this);
|
||||
var value = input.val().replace(/[^\d]/g, "");
|
||||
|
||||
if (value === "") {
|
||||
input.val("0");
|
||||
} else {
|
||||
var numValue = parseInt(value);
|
||||
input.val(numValue.toLocaleString("id-ID"));
|
||||
// Don't update original value here - let it be updated only when saving
|
||||
}
|
||||
});
|
||||
|
||||
// Format price inputs on page load
|
||||
$(".price-input").each(function () {
|
||||
var input = $(this);
|
||||
var value = input.val();
|
||||
if (value && value !== "0") {
|
||||
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||
input.val(numValue.toLocaleString("id-ID"));
|
||||
// Store the original numeric value for comparison
|
||||
input.data("original-value", numValue.toString());
|
||||
console.log(
|
||||
"Initialized price for dealer",
|
||||
input.attr("name").replace("price_", ""),
|
||||
":",
|
||||
numValue
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var saveSinglePrice = function (dealerId) {
|
||||
// Prevent multiple clicks
|
||||
var saveButton = $('.save-single[data-dealer-id="' + dealerId + '"]');
|
||||
if (saveButton.hasClass("loading")) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
var priceInput = $('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||
var price = priceInput.val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||
var isActive = statusInput.is(":checked");
|
||||
|
||||
if (!price || parseInt(price) <= 0) {
|
||||
toastr.error("Harga harus lebih dari 0");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get original price from data attribute (without separator)
|
||||
var originalPrice = priceInput.data("original-value") || "0";
|
||||
var currentPrice = parseInt(price);
|
||||
var originalPriceInt = parseInt(originalPrice);
|
||||
|
||||
console.log(
|
||||
"Debug - Original price:",
|
||||
originalPriceInt,
|
||||
"Current price:",
|
||||
currentPrice
|
||||
);
|
||||
|
||||
// If price hasn't actually changed, don't update
|
||||
if (currentPrice === originalPriceInt && originalPrice !== "0") {
|
||||
toastr.info("Harga tidak berubah, tidak perlu update");
|
||||
return;
|
||||
}
|
||||
|
||||
// If price has changed, update original value for next comparison
|
||||
if (currentPrice !== originalPriceInt) {
|
||||
priceInput.data("original-value", currentPrice.toString());
|
||||
console.log(
|
||||
"Price changed from",
|
||||
originalPriceInt,
|
||||
"to",
|
||||
currentPrice
|
||||
);
|
||||
}
|
||||
|
||||
// Disable button and show loading state
|
||||
saveButton.addClass("loading").prop("disabled", true);
|
||||
var originalText = saveButton.text();
|
||||
saveButton.text("Menyimpan...");
|
||||
|
||||
var data = {
|
||||
work_id: parseInt(workId),
|
||||
dealer_id: parseInt(dealerId),
|
||||
price: currentPrice, // Use the validated price
|
||||
currency: "IDR",
|
||||
is_active: isActive ? 1 : 0,
|
||||
};
|
||||
|
||||
// Debug: Log the data being sent
|
||||
console.log("Sending data:", data);
|
||||
console.log("Original price:", originalPriceInt);
|
||||
console.log("Current price:", currentPrice);
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices",
|
||||
method: "POST",
|
||||
data: data,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||
},
|
||||
beforeSend: function () {
|
||||
console.log("Sending AJAX request with data:", data);
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable button
|
||||
saveButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
// Update UI
|
||||
updateRowAfterSave(dealerId, response.data);
|
||||
|
||||
// Ensure consistent formatting after update
|
||||
var updatedPrice = priceInput.val().replace(/[^\d]/g, "");
|
||||
if (updatedPrice && updatedPrice !== "0") {
|
||||
var formattedPrice =
|
||||
parseInt(updatedPrice).toLocaleString("id-ID");
|
||||
priceInput.val(formattedPrice);
|
||||
}
|
||||
|
||||
// Show brief loading message
|
||||
toastr.info(
|
||||
"Data berhasil disimpan, memperbarui tampilan..."
|
||||
);
|
||||
} else {
|
||||
toastr.error(response.message || "Terjadi kesalahan");
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable button
|
||||
saveButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON) {
|
||||
if (xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
if (xhr.responseJSON.errors) {
|
||||
// Show validation errors
|
||||
var errorMessages = [];
|
||||
for (var field in xhr.responseJSON.errors) {
|
||||
var fieldName = field;
|
||||
switch (field) {
|
||||
case "work_id":
|
||||
fieldName = "ID Pekerjaan";
|
||||
break;
|
||||
case "dealer_id":
|
||||
fieldName = "ID Dealer";
|
||||
break;
|
||||
case "price":
|
||||
fieldName = "Harga";
|
||||
break;
|
||||
case "currency":
|
||||
fieldName = "Mata Uang";
|
||||
break;
|
||||
case "is_active":
|
||||
fieldName = "Status Aktif";
|
||||
break;
|
||||
}
|
||||
errorMessages.push(
|
||||
fieldName +
|
||||
": " +
|
||||
xhr.responseJSON.errors[field][0]
|
||||
);
|
||||
}
|
||||
message = errorMessages.join("\n");
|
||||
}
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var saveAllPrices = function () {
|
||||
// Prevent multiple clicks
|
||||
var saveAllButton = $("#btn-save-all");
|
||||
if (saveAllButton.hasClass("loading")) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
var prices = [];
|
||||
var hasValidPrice = false;
|
||||
|
||||
$(".price-input").each(function () {
|
||||
var dealerId = $(this).attr("name").replace("price_", "");
|
||||
var price = $(this).val().replace(/[^\d]/g, ""); // Remove non-numeric characters
|
||||
var statusInput = $('input[name="status_' + dealerId + '"]');
|
||||
var isActive = statusInput.is(":checked");
|
||||
|
||||
if (price && parseInt(price) > 0) {
|
||||
hasValidPrice = true;
|
||||
prices.push({
|
||||
dealer_id: dealerId,
|
||||
price: parseInt(price),
|
||||
currency: "IDR",
|
||||
is_active: isActive,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasValidPrice) {
|
||||
toastr.error("Minimal satu dealer harus memiliki harga yang valid");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button and show loading state
|
||||
saveAllButton.addClass("loading").prop("disabled", true);
|
||||
var originalText = saveAllButton.text();
|
||||
saveAllButton.text("Menyimpan...");
|
||||
|
||||
// Show confirmation
|
||||
$("#confirmMessage").text(
|
||||
"Apakah Anda yakin ingin menyimpan semua harga?"
|
||||
);
|
||||
$("#confirmModal").modal("show");
|
||||
|
||||
$("#confirmAction")
|
||||
.off("click")
|
||||
.on("click", function () {
|
||||
$("#confirmModal").modal("hide");
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/bulk",
|
||||
method: "POST",
|
||||
data: {
|
||||
prices: prices,
|
||||
},
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||
"content"
|
||||
),
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable button
|
||||
saveAllButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Memperbarui data...");
|
||||
|
||||
// Reload page to update all data
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
toastr.error(
|
||||
response.message || "Terjadi kesalahan"
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable button
|
||||
saveAllButton
|
||||
.removeClass("loading")
|
||||
.prop("disabled", false)
|
||||
.text(originalText);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var deletePrice = function (priceId, dealerId) {
|
||||
$("#confirmMessage").text(
|
||||
"Apakah Anda yakin ingin menghapus harga ini? Harga yang dihapus dapat dipulihkan dengan menyimpan ulang."
|
||||
);
|
||||
$("#confirmModal").modal("show");
|
||||
|
||||
$("#confirmAction")
|
||||
.off("click")
|
||||
.on("click", function () {
|
||||
$("#confirmModal").modal("hide");
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Menghapus harga...");
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/" + priceId,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr(
|
||||
"content"
|
||||
),
|
||||
},
|
||||
success: function (response) {
|
||||
hideLoadingOverlay();
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
// Reset the row
|
||||
resetRow(dealerId);
|
||||
} else {
|
||||
toastr.error(
|
||||
response.message || "Terjadi kesalahan"
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
hideLoadingOverlay();
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
toastr.error(message);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Handle delete button click
|
||||
$(document).on("click", ".delete-price", function () {
|
||||
var priceId = $(this).data("price-id");
|
||||
var dealerId = $(this).data("dealer-id");
|
||||
deletePrice(priceId, dealerId);
|
||||
});
|
||||
|
||||
var updateRowAfterSave = function (dealerId, data) {
|
||||
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||
var label = statusInput.siblings("label").find(".status-text");
|
||||
var actionCell = row.find("td:last");
|
||||
|
||||
// Update price input if data contains price
|
||||
if (data.price !== undefined) {
|
||||
// Only update if the price actually changed
|
||||
var currentDisplayValue = priceInput.val().replace(/[^\d]/g, "");
|
||||
var newPrice = parseInt(data.price);
|
||||
|
||||
if (parseInt(currentDisplayValue) !== newPrice) {
|
||||
priceInput.val(newPrice.toLocaleString("id-ID"));
|
||||
// Update the original value for future comparisons
|
||||
priceInput.data("original-value", newPrice.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a new record (price = 0), update the save button
|
||||
if (data.price === 0) {
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Simpan")
|
||||
.removeClass("btn-warning")
|
||||
.addClass("btn-success");
|
||||
}
|
||||
|
||||
// Update status if data contains is_active
|
||||
if (data.is_active !== undefined) {
|
||||
statusInput.prop("checked", data.is_active);
|
||||
if (data.is_active) {
|
||||
label.text("Aktif").removeClass("inactive").addClass("active");
|
||||
} else {
|
||||
label
|
||||
.text("Nonaktif")
|
||||
.removeClass("active")
|
||||
.addClass("inactive");
|
||||
}
|
||||
}
|
||||
|
||||
// Update save button if this is a new price save (not just status toggle)
|
||||
if (data.price !== undefined) {
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Update")
|
||||
.removeClass("btn-success")
|
||||
.addClass("btn-warning");
|
||||
|
||||
// Update delete button
|
||||
if (actionCell.find(".delete-price").length === 0) {
|
||||
// Add delete button if it doesn't exist
|
||||
var deleteBtn =
|
||||
'<button type="button" class="btn btn-sm btn-danger delete-price" data-price-id="' +
|
||||
data.id +
|
||||
'" data-dealer-id="' +
|
||||
dealerId +
|
||||
'" title="Hapus Harga">Hapus</button>';
|
||||
actionCell.find(".d-flex.flex-row.gap-1").append(deleteBtn);
|
||||
} else {
|
||||
// Update existing delete button with new price ID
|
||||
actionCell.find(".delete-price").attr("data-price-id", data.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var toggleStatus = function (dealerId, isActive, checkbox, label) {
|
||||
var data = {
|
||||
dealer_id: parseInt(dealerId),
|
||||
is_active: isActive,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: "/admin/work/" + workId + "/prices/toggle-status",
|
||||
method: "POST",
|
||||
data: data,
|
||||
headers: {
|
||||
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||
},
|
||||
beforeSend: function () {
|
||||
// Show brief loading indicator on checkbox
|
||||
checkbox.prop("disabled", true);
|
||||
},
|
||||
success: function (response) {
|
||||
// Re-enable checkbox
|
||||
checkbox.prop("disabled", false);
|
||||
|
||||
if (response.status === 200) {
|
||||
toastr.success(response.message);
|
||||
|
||||
// Update UI if needed
|
||||
if (response.data) {
|
||||
// If this is a new record, update the row to show save button
|
||||
if (response.data.price === 0) {
|
||||
updateRowAfterSave(dealerId, response.data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toastr.error(response.message || "Terjadi kesalahan");
|
||||
// Revert checkbox state
|
||||
checkbox.prop("checked", !isActive);
|
||||
if (!isActive) {
|
||||
label.text("Aktif");
|
||||
} else {
|
||||
label.text("Nonaktif");
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (xhr) {
|
||||
// Re-enable checkbox
|
||||
checkbox.prop("disabled", false);
|
||||
|
||||
var message = "Terjadi kesalahan";
|
||||
if (xhr.responseJSON) {
|
||||
if (xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
if (xhr.responseJSON.errors) {
|
||||
var errorMessages = [];
|
||||
for (var field in xhr.responseJSON.errors) {
|
||||
var fieldName = field;
|
||||
switch (field) {
|
||||
case "dealer_id":
|
||||
fieldName = "ID Dealer";
|
||||
break;
|
||||
case "is_active":
|
||||
fieldName = "Status Aktif";
|
||||
break;
|
||||
}
|
||||
errorMessages.push(
|
||||
fieldName +
|
||||
": " +
|
||||
xhr.responseJSON.errors[field][0]
|
||||
);
|
||||
}
|
||||
message = errorMessages.join("\n");
|
||||
}
|
||||
}
|
||||
toastr.error(message);
|
||||
|
||||
// Revert checkbox state
|
||||
checkbox.prop("checked", !isActive);
|
||||
if (!isActive) {
|
||||
label.text("Aktif");
|
||||
} else {
|
||||
label.text("Nonaktif");
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
var resetRow = function (dealerId) {
|
||||
var row = $('[data-dealer-id="' + dealerId + '"]');
|
||||
var priceInput = row.find('input[name="price_' + dealerId + '"]');
|
||||
var statusInput = row.find('input[name="status_' + dealerId + '"]');
|
||||
var label = statusInput.siblings("label").find(".status-text");
|
||||
var actionCell = row.find("td:last");
|
||||
|
||||
// Reset price input
|
||||
priceInput.val("0");
|
||||
|
||||
// Reset status
|
||||
statusInput.prop("checked", false);
|
||||
label.text("Nonaktif").removeClass("active").addClass("inactive");
|
||||
|
||||
// Remove delete button and update save button
|
||||
actionCell.find(".delete-price").remove();
|
||||
actionCell
|
||||
.find(".save-single")
|
||||
.text("Simpan")
|
||||
.removeClass("btn-warning")
|
||||
.addClass("btn-success");
|
||||
};
|
||||
|
||||
// Public methods
|
||||
return {
|
||||
init: function () {
|
||||
initTable();
|
||||
initEvents();
|
||||
// Initialize price formatting on page load
|
||||
setTimeout(function () {
|
||||
$(".price-input").each(function () {
|
||||
var value = $(this).val();
|
||||
if (value && value !== "0") {
|
||||
var numValue = parseInt(value.replace(/[^\d]/g, ""));
|
||||
if (!isNaN(numValue)) {
|
||||
$(this).val(numValue.toLocaleString("id-ID"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Cleanup timeouts on page unload
|
||||
$(window).on("beforeunload", function () {
|
||||
for (var dealerId in saveTimeout) {
|
||||
if (saveTimeout[dealerId]) {
|
||||
clearTimeout(saveTimeout[dealerId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// On document ready
|
||||
jQuery(document).ready(function () {
|
||||
WorkPrices.init();
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
(()=>{function e(){$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")}))}function a(){$("#date_to").datepicker("remove"),$("#date_to").val(""),e(),$("#date_to").prop("disabled",!0),console.log("End date picker reset and disabled")}$(document).ready((function(){console.log("Opnames index.js loaded"),void 0!==$.fn.DataTable?(console.log("Initializing Select2..."),void 0!==$.fn.select2?$("#dealer_filter").select2({placeholder:"Pilih...",allowClear:!0,width:"100%"}):console.warn("Select2 not available, using regular select"),function(){if(console.log("Initializing datepickers..."),void 0===$.fn.datepicker)return void console.error("Bootstrap Datepicker not available!");$("#date_from").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){var a;console.log("Start date selected:",e.format()),a=e.format(),console.log("Enabling end date picker with min date:",a),$("#date_to").prop("disabled",!1),$("#date_to").datepicker("remove"),$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},startDate:a,endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")})),console.log("End date picker enabled with startDate:",a)})).on("clearDate",(function(e){console.log("Start date cleared"),a()})),e(),$("#date_to").prop("disabled",!0)}(),setTimeout((function(){!function(){console.log("Initializing DataTable..."),$.fn.DataTable.isDataTable("#opnames-table")&&$("#opnames-table").DataTable().destroy();var e=$("#opnames-table").DataTable({processing:!0,serverSide:!0,destroy:!0,ajax:{url:$("#opnames-table").data("url"),type:"GET",data:function(e){return e.dealer_filter=$("#dealer_filter").val(),e.date_from=$("#date_from").val(),e.date_to=$("#date_to").val(),console.log("AJAX data being sent:",{dealer_filter:e.dealer_filter,date_from:e.date_from,date_to:e.date_to}),e},error:function(e,a,t){console.error("DataTables AJAX error:",a,t),console.error("Response:",e.responseText)}},columnDefs:[{targets:0,width:"15%"},{targets:5,width:"15%",className:"text-center"}],columns:[{data:"created_at",name:"created_at",orderable:!0},{data:"opname_date",name:"opname_date",orderable:!0},{data:"dealer_name",name:"dealer.name",orderable:!0},{data:"user_name",name:"user.name",orderable:!0},{data:"status",name:"status",orderable:!0},{data:"action",name:"action",orderable:!1,searchable:!1}],order:[[4,"desc"]],pageLength:10,responsive:!0,ordering:!0,orderMulti:!1});(function(e){$("#kt_search").on("click",(function(){console.log("Filter button clicked");var a=$("#dealer_filter").val(),t=$("#date_from").val(),o=$("#date_to").val();console.log("Filtering with:",{dealer:a,dateFrom:t,dateTo:o}),e.ajax.reload()})),$("#kt_reset").on("click",(function(){console.log("Reset button clicked"),$("#dealer_filter").val(null).trigger("change.select2"),$("#date_from").datepicker("clearDates"),$("#date_to").datepicker("clearDates"),a(),e.ajax.reload()})),$("#date_from, #date_to").on("keypress",(function(e){13===e.which&&$("#kt_search").click()})),$("#dealer_filter").on("change",(function(){console.log("Dealer filter changed:",$(this).val())}))})(e),function(e){e.on("order.dt",(function(){console.log("Order changed:",e.order())})),e.on("processing.dt",(function(e,a,t){t?console.log("DataTable processing started"):console.log("DataTable processing finished")}))}(e)}()}),100)):console.error("DataTables not available!")}))})();
|
||||
(()=>{function e(){$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")}))}function a(){$("#date_to").datepicker("remove"),$("#date_to").val(""),e(),$("#date_to").prop("disabled",!0),console.log("End date picker reset and disabled")}$(document).ready((function(){console.log("Opnames index.js loaded"),void 0!==$.fn.DataTable?(console.log("Initializing Select2..."),void 0!==$.fn.select2?$("#dealer_filter").select2({placeholder:"Pilih...",allowClear:!0,width:"100%"}):console.warn("Select2 not available, using regular select"),function(){if(console.log("Initializing datepickers..."),void 0===$.fn.datepicker)return void console.error("Bootstrap Datepicker not available!");$("#date_from").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){var a;console.log("Start date selected:",e.format()),a=e.format(),console.log("Enabling end date picker with min date:",a),$("#date_to").prop("disabled",!1),$("#date_to").datepicker("remove"),$("#date_to").datepicker({format:"yyyy-mm-dd",autoclose:!0,todayHighlight:!0,orientation:"bottom left",templates:{leftArrow:'<i class="la la-angle-left"></i>',rightArrow:'<i class="la la-angle-right"></i>'},startDate:a,endDate:new Date,clearBtn:!0}).on("changeDate",(function(e){console.log("End date selected:",e.format())})).on("clearDate",(function(e){console.log("End date cleared")})),console.log("End date picker enabled with startDate:",a)})).on("clearDate",(function(e){console.log("Start date cleared"),a()})),e(),$("#date_to").prop("disabled",!0)}(),setTimeout((function(){!function(){console.log("Initializing DataTable..."),$.fn.DataTable.isDataTable("#opnames-table")&&$("#opnames-table").DataTable().destroy();var e=$("#opnames-table").DataTable({processing:!0,serverSide:!0,destroy:!0,ajax:{url:$("#opnames-table").data("url"),type:"GET",data:function(e){return e.dealer_filter=$("#dealer_filter").val(),e.date_from=$("#date_from").val(),e.date_to=$("#date_to").val(),console.log("AJAX data being sent:",{dealer_filter:e.dealer_filter,date_from:e.date_from,date_to:e.date_to}),e},error:function(e,a,t){console.error("DataTables AJAX error:",a,t),console.error("Response:",e.responseText)}},columnDefs:[{targets:0,width:"15%"},{targets:1,width:"12%"},{targets:2,width:"15%"},{targets:3,width:"12%"},{targets:4,width:"10%"},{targets:5,width:"15%",className:"text-center"},{targets:6,width:"15%",className:"text-center"}],columns:[{data:"created_at",name:"created_at",orderable:!0},{data:"opname_date",name:"opname_date",orderable:!0},{data:"dealer_name",name:"dealer.name",orderable:!0},{data:"user_name",name:"user.name",orderable:!0},{data:"status",name:"status",orderable:!0},{data:"stock_info",name:"stock_info",orderable:!1,searchable:!1},{data:"action",name:"action",orderable:!1,searchable:!1}],order:[[0,"desc"]],pageLength:10,responsive:!0,ordering:!0,orderMulti:!1});(function(e){$("#kt_search").on("click",(function(){console.log("Filter button clicked");var a=$("#dealer_filter").val(),t=$("#date_from").val(),o=$("#date_to").val();console.log("Filtering with:",{dealer:a,dateFrom:t,dateTo:o}),e.ajax.reload()})),$("#kt_reset").on("click",(function(){console.log("Reset button clicked"),$("#dealer_filter").val(null).trigger("change.select2"),$("#date_from").datepicker("clearDates"),$("#date_to").datepicker("clearDates"),a(),e.ajax.reload()})),$("#date_from, #date_to").on("keypress",(function(e){13===e.which&&$("#kt_search").click()})),$("#dealer_filter").on("change",(function(){console.log("Dealer filter changed:",$(this).val())}))})(e),function(e){e.on("order.dt",(function(){console.log("Order changed:",e.order())})),e.on("processing.dt",(function(e,a,t){t?console.log("DataTable processing started"):console.log("DataTable processing finished")}))}(e)}()}),100)):console.error("DataTables not available!")}))})();
|
||||
//# sourceMappingURL=index.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -196,8 +196,13 @@ function initializeDataTable() {
|
||||
},
|
||||
},
|
||||
columnDefs: [
|
||||
{ targets: 0, width: "15%" }, // Opname Date column
|
||||
{ targets: 5, width: "15%", className: "text-center" }, // Action column
|
||||
{ targets: 0, width: "15%" }, // Created At column
|
||||
{ targets: 1, width: "12%" }, // Opname Date column
|
||||
{ targets: 2, width: "15%" }, // Dealer column
|
||||
{ targets: 3, width: "12%" }, // User column
|
||||
{ targets: 4, width: "10%" }, // Status column
|
||||
{ targets: 5, width: "15%", className: "text-center" }, // Stock Info column
|
||||
{ targets: 6, width: "15%", className: "text-center" }, // Action column
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
@@ -225,6 +230,12 @@ function initializeDataTable() {
|
||||
name: "status",
|
||||
orderable: true,
|
||||
},
|
||||
{
|
||||
data: "stock_info",
|
||||
name: "stock_info",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
data: "action",
|
||||
name: "action",
|
||||
@@ -232,7 +243,7 @@ function initializeDataTable() {
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
order: [[4, "desc"]], // Order by created_at desc
|
||||
order: [[0, "desc"]], // Order by created_at desc
|
||||
pageLength: 10,
|
||||
responsive: true,
|
||||
ordering: true,
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>POS | Login</title>
|
||||
<meta name="description" content="Login page example">
|
||||
<meta name="description" content="CKB POS Login Page">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="base-url" content="{{ url('/') }}">
|
||||
|
||||
|
||||
322
resources/views/back/master/work_prices.blade.php
Normal file
322
resources/views/back/master/work_prices.blade.php
Normal file
@@ -0,0 +1,322 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('content')
|
||||
|
||||
<style type="text/css">
|
||||
/* Action button flex layout */
|
||||
.d-flex.flex-row.gap-1 {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 0.25rem !important;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn {
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Ensure DataTables doesn't break flex layout */
|
||||
.dataTables_wrapper .dataTables_scrollBody .d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.d-flex.flex-row.gap-1 {
|
||||
flex-direction: column !important;
|
||||
gap: 0.125rem !important;
|
||||
}
|
||||
|
||||
.d-flex.flex-row.gap-1 .btn {
|
||||
width: 100%;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form control styling */
|
||||
.form-control {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #ced4da;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Status text styling */
|
||||
.status-text {
|
||||
font-weight: 600;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.status-text.active {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-text.inactive {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Input group styling */
|
||||
.input-group-text {
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Price input styling */
|
||||
.price-input {
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.price-input:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Status label styling */
|
||||
.custom-control-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Status label styling */
|
||||
.custom-control-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Alert styling */
|
||||
.alert {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Button loading state */
|
||||
.btn.loading {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Loading overlay */
|
||||
#loading-overlay {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
#loading-overlay .spinner-border {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<span class="kt-portlet__head-icon">
|
||||
<i class="kt-font-brand flaticon2-line-chart"></i>
|
||||
</span>
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Set Harga Pekerjaan: <strong>{{ $work->name }}</strong>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-wrapper">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<button type="button" class="btn btn-bold btn-label-brand" id="btn-save-all">
|
||||
Simpan Semua Harga
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
|
||||
<div class="table-responsive">
|
||||
<!--begin: Datatable -->
|
||||
<table class="table table-striped table-bordered table-hover table-checkable" id="dealers-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">No</th>
|
||||
<th width="25%">Nama Dealer</th>
|
||||
<th width="15%">Kode Dealer</th>
|
||||
<th width="20%">Harga (IDR)</th>
|
||||
<th width="10%">Status</th>
|
||||
<th width="15%">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($dealers as $index => $dealer)
|
||||
@php
|
||||
$existingPrice = $work->getPriceForDealer($dealer->id);
|
||||
@endphp
|
||||
<tr data-dealer-id="{{ $dealer->id }}">
|
||||
<td>{{ $index + 1 }}</td>
|
||||
<td>{{ $dealer->name }}</td>
|
||||
<td>{{ $dealer->dealer_code }}</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Rp</span>
|
||||
</div>
|
||||
<input type="text"
|
||||
class="form-control price-input"
|
||||
name="price_{{ $dealer->id }}"
|
||||
value="{{ $existingPrice ? number_format($existingPrice->price, 0, ',', '.') : '0' }}"
|
||||
placeholder="0"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
data-original-value="{{ $existingPrice ? $existingPrice->price : '0' }}">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
class="status-input"
|
||||
id="status_{{ $dealer->id }}"
|
||||
name="status_{{ $dealer->id }}"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
{{ $existingPrice && $existingPrice->is_active ? 'checked' : '' }}>
|
||||
<label for="status_{{ $dealer->id }}" style="margin-left: 0.5rem; font-weight: 500;">Aktif</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-row gap-1">
|
||||
<button type="button"
|
||||
class="btn btn-sm {{ $existingPrice ? 'btn-warning' : 'btn-success' }} save-single"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
title="{{ $existingPrice ? 'Update Harga' : 'Simpan Harga' }}">
|
||||
{{ $existingPrice ? 'Update' : 'Simpan' }}
|
||||
</button>
|
||||
@if($existingPrice)
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-danger delete-price"
|
||||
data-price-id="{{ $existingPrice->id }}"
|
||||
data-dealer-id="{{ $dealer->id }}"
|
||||
title="Hapus Harga">
|
||||
Hapus
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<!--end: Datatable -->
|
||||
</div>
|
||||
|
||||
@if($dealers->count() == 0)
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-warning text-center">
|
||||
<i class="fa fa-exclamation-triangle mr-2"></i>
|
||||
Tidak ada dealer yang tersedia.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--begin::Modal-->
|
||||
<div class="modal fade" id="confirmModal" tabindex="-1" role="dialog" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel">
|
||||
<i class="fa fa-question-circle mr-2"></i>
|
||||
Konfirmasi
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
<i class="fa fa-times mr-1"></i>
|
||||
Batal
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmAction">
|
||||
<i class="fa fa-check mr-1"></i>
|
||||
Ya, Lanjutkan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--end::Modal-->
|
||||
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script src="{{ url('js/pages/back/master/work-prices.js') }}" type="text/javascript"></script>
|
||||
@endsection
|
||||
@@ -39,6 +39,7 @@
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Nama Role</th>
|
||||
<th>Dealer Tambahan</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -47,13 +48,30 @@
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $role->name }}</td>
|
||||
<td>
|
||||
@if($role->dealers->count() > 0)
|
||||
<div class="dealer-list">
|
||||
@foreach($role->dealers->take(3) as $dealer)
|
||||
<span class="badge badge-info mr-1 mb-1">{{ $dealer->name }}</span>
|
||||
@endforeach
|
||||
@if($role->dealers->count() > 3)
|
||||
<span class="badge badge-secondary">+{{ $role->dealers->count() - 3 }} more</span>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<span class="text-muted">Tidak ada dealer tambahan untuk role ini</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex">
|
||||
@can('update', $menus['roleprivileges.index'])
|
||||
<button class="btn btn-sm btn-bold btn-warning mr-2" onclick="editRole({{$role->id}})"> Edit</button>
|
||||
@endcan
|
||||
@can('delete', $menus['roleprivileges.index'])
|
||||
<button class="btn btn-sm btn-bold btn-danger" onclick="deleteRole({{$role->id}}, '{{$role->name}}')">Hapus</button>
|
||||
<button class="btn btn-sm btn-bold btn-danger mr-2" onclick="deleteRole({{$role->id}}, '{{$role->name}}')">Hapus</button>
|
||||
@endcan
|
||||
@can('create', $menus['roleprivileges.index'])
|
||||
<button class="btn btn-sm btn-bold btn-success" onclick="assignDealer({{$role->id}})"> Tambah Dealer</button>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
@@ -183,6 +201,141 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="assignDealerModal" tabindex="-1" role="dialog" aria-labelledby="assignDealerModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form id="assignDealerForm" method="POST">
|
||||
@csrf
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="assignDealerModalLabel">
|
||||
Assign Dealer ke Role
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-light border">
|
||||
<strong>Petunjuk:</strong> Pilih dealer yang akan di-assign ke role ini. Dealer yang sudah di-assign akan otomatis tercentang.
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="font-weight-bold">
|
||||
Daftar Dealer
|
||||
</label>
|
||||
<div class="dealer-checkboxes border rounded p-3" style="max-height: 350px; overflow-y: auto;">
|
||||
<div class="row">
|
||||
@foreach ($dealers as $dealer)
|
||||
<div class="col-md-6 mb-2">
|
||||
<div class="form-check custom-checkbox">
|
||||
<input class="form-check-input dealer-checkbox" type="checkbox"
|
||||
name="dealers[]" value="{{ $dealer->id }}"
|
||||
id="dealer_{{ $dealer->id }}">
|
||||
<label class="form-check-label d-flex align-items-center" for="dealer_{{ $dealer->id }}">
|
||||
<div class="dealer-info">
|
||||
<div class="dealer-name font-weight-semibold">{{ $dealer->name }}</div>
|
||||
<div class="dealer-code text-muted small">
|
||||
<i class="fas fa-tag mr-1"></i>
|
||||
{{ $dealer->dealer_code }}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mr-2" onclick="selectAllDealers()">
|
||||
Pilih Semua
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="deselectAllDealers()">
|
||||
Hapus Semua
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<span id="selectedCount">0</span> dealer dipilih
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">
|
||||
Batal
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.custom-checkbox .form-check-label {
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-checkbox .form-check-label:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.custom-checkbox .form-check-input:checked + .form-check-label {
|
||||
background-color: #e8f5e8;
|
||||
border-left: 3px solid #28a745;
|
||||
}
|
||||
|
||||
.dealer-info {
|
||||
flex: 1;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dealer-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.dealer-code {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.dealer-checkboxes::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.dealer-checkboxes::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.dealer-checkboxes::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dealer-checkboxes::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Responsive improvements */
|
||||
@media (max-width: 768px) {
|
||||
.dealer-checkboxes .col-md-6 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
@@ -296,17 +449,145 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function assignDealer(roleId) {
|
||||
// Set form action
|
||||
let url = '{{ route("roleprivileges.assignDealer", ":id") }}'.replace(':id', roleId);
|
||||
$("#assignDealerForm").attr("action", url);
|
||||
|
||||
// Reset checkboxes and counter
|
||||
$('.dealer-checkbox').prop('checked', false);
|
||||
updateSelectedCount();
|
||||
|
||||
// Load existing assigned dealers
|
||||
$.ajax({
|
||||
url: '{{ route("roleprivileges.getAssignedDealers", ":id") }}'.replace(':id', roleId),
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
if (response.assignedDealers) {
|
||||
response.assignedDealers.forEach(function(dealerId) {
|
||||
$(`#dealer_${dealerId}`).prop('checked', true);
|
||||
});
|
||||
}
|
||||
updateSelectedCount();
|
||||
},
|
||||
error: function() {
|
||||
console.log('Error loading assigned dealers');
|
||||
updateSelectedCount();
|
||||
}
|
||||
});
|
||||
|
||||
$("#assignDealerModal").modal("show");
|
||||
}
|
||||
|
||||
function selectAllDealers() {
|
||||
$('.dealer-checkbox').prop('checked', true);
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function deselectAllDealers() {
|
||||
$('.dealer-checkbox').prop('checked', false);
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
const selectedCount = $('.dealer-checkbox:checked').length;
|
||||
$('#selectedCount').text(selectedCount);
|
||||
|
||||
// Update submit button state
|
||||
if (selectedCount > 0) {
|
||||
$('#submitBtn').prop('disabled', false).removeClass('btn-secondary').addClass('btn-primary');
|
||||
} else {
|
||||
$('#submitBtn').prop('disabled', true).removeClass('btn-primary').addClass('btn-secondary');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Add event handlers for modal close buttons
|
||||
$('.close, [data-dismiss="modal"]').on("click", function () {
|
||||
$("#roleModal").modal("hide");
|
||||
$("#roleEditModal").modal("hide");
|
||||
$("#assignDealerModal").modal("hide");
|
||||
});
|
||||
|
||||
// Also handle the "Close" button
|
||||
$('.btn-secondary[data-dismiss="modal"]').on("click", function () {
|
||||
$("#roleModal").modal("hide");
|
||||
$("#roleEditModal").modal("hide");
|
||||
$("#assignDealerModal").modal("hide");
|
||||
});
|
||||
|
||||
// Event listener for dealer checkboxes
|
||||
$(document).on('change', '.dealer-checkbox', function() {
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
// Handle form submission for assign dealer
|
||||
$("#assignDealerForm").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate if at least one dealer is selected
|
||||
const selectedDealers = $('.dealer-checkbox:checked').length;
|
||||
if (selectedDealers === 0) {
|
||||
Swal.fire({
|
||||
title: 'Peringatan!',
|
||||
text: 'Silakan pilih minimal satu dealer',
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable submit button and show loading
|
||||
const submitBtn = $('#submitBtn');
|
||||
const originalText = submitBtn.html();
|
||||
submitBtn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i>Menyimpan...');
|
||||
|
||||
let formData = new FormData(this);
|
||||
let url = $(this).attr("action");
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
Swal.fire({
|
||||
title: 'Berhasil!',
|
||||
text: response.message,
|
||||
icon: 'success',
|
||||
confirmButtonText: 'OK'
|
||||
}).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: response.message || 'Terjadi kesalahan',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let message = 'Terjadi kesalahan';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
message = xhr.responseJSON.message;
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Error!',
|
||||
text: message,
|
||||
icon: 'error',
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
},
|
||||
complete: function() {
|
||||
// Re-enable submit button
|
||||
submitBtn.prop('disabled', false).html(originalText);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,8 +24,9 @@
|
||||
</div>
|
||||
|
||||
<div class="kt-portlet__body">
|
||||
<div class="table-responsive">
|
||||
<!--begin: Datatable -->
|
||||
<table class="table table-responsive table-striped table-bordered table-hover table-checkable" id="kt_table">
|
||||
<table class="table table-striped table-bordered table-hover table-checkable" id="kt_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
@@ -41,6 +42,7 @@
|
||||
</table>
|
||||
<!--end: Datatable -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--begin::Modal-->
|
||||
|
||||
252
resources/views/kpi/targets/create.blade.php
Normal file
252
resources/views/kpi/targets/create.blade.php
Normal file
@@ -0,0 +1,252 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('title', 'Tambah Target KPI')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.select2-container .select2-selection {
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.select2-container.select2-container--focus .select2-selection {
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(197, 214, 233, 0.25);
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
padding-left: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Limit Select2 dropdown height */
|
||||
.select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Style for Select2 results */
|
||||
.select2-results__option {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.select2-results__option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Improve Select2 search box */
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||
<div class="kt-portlet kt-portlet--mobile">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Tambah Target KPI
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||
<option value="">Pilih Mekanik</option>
|
||||
@foreach($mechanics as $mechanic)
|
||||
<option value="{{ $mechanic->id }}" {{ old('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||
{{ $mechanic->name }} ({{ $mechanic->dealer->name ?? 'N/A' }})
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($mechanics->isEmpty())
|
||||
<div class="alert alert-warning mt-2">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Tidak ada mekanik yang ditemukan. Pastikan ada user dengan role "mechanic" di sistem.
|
||||
</div>
|
||||
@else
|
||||
<small class="form-text text-muted">
|
||||
Ditemukan {{ $mechanics->count() }} mekanik.
|
||||
@if($mechanics->count() >= 50)
|
||||
Menampilkan 50 mekanik pertama. Gunakan pencarian untuk menemukan mekanik tertentu.
|
||||
@endif
|
||||
</small>
|
||||
@endif
|
||||
@error('user_id')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||
value="{{ old('target_value') }}" min="1" required>
|
||||
@error('target_value')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-control-label">Deskripsi</label>
|
||||
<textarea name="description" id="description" class="form-control" rows="3"
|
||||
placeholder="Deskripsi target (opsional)">{{ old('description') }}</textarea>
|
||||
@error('description')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||
value="1" {{ old('is_active', true) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="is_active">
|
||||
Target Aktif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Simpan Target
|
||||
</button>
|
||||
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('javascripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2 with fallback
|
||||
try {
|
||||
// Initialize Select2 for mechanics with search limit
|
||||
$('#user_id').select2({
|
||||
theme: 'bootstrap4',
|
||||
width: '100%',
|
||||
placeholder: 'Pilih Mekanik',
|
||||
allowClear: true,
|
||||
minimumInputLength: 1,
|
||||
maximumInputLength: 50,
|
||||
maximumResultsForSearch: 10,
|
||||
language: {
|
||||
inputTooShort: function() {
|
||||
return "Masukkan minimal 1 karakter untuk mencari";
|
||||
},
|
||||
inputTooLong: function() {
|
||||
return "Maksimal 50 karakter";
|
||||
},
|
||||
noResults: function() {
|
||||
return "Tidak ada hasil ditemukan";
|
||||
},
|
||||
searching: function() {
|
||||
return "Mencari...";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log('Select2 not available, using regular select');
|
||||
// Fallback: ensure regular select works
|
||||
$('.select2').removeClass('select2').addClass('form-control');
|
||||
}
|
||||
|
||||
// Form validation
|
||||
$('#kpi-form').on('submit', function(e) {
|
||||
var isValid = true;
|
||||
var errors = [];
|
||||
|
||||
// Clear previous errors
|
||||
$('.text-danger').remove();
|
||||
|
||||
// Validate required fields
|
||||
if (!$('#user_id').val()) {
|
||||
errors.push('Mekanik harus dipilih');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||
errors.push('Target nilai harus diisi dan minimal 1');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Validasi Gagal',
|
||||
html: errors.join('<br>'),
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
} else {
|
||||
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
263
resources/views/kpi/targets/edit.blade.php
Normal file
263
resources/views/kpi/targets/edit.blade.php
Normal file
@@ -0,0 +1,263 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('title', 'Edit Target KPI')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.select2-container .select2-selection {
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.select2-container.select2-container--focus .select2-selection {
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--single .select2-selection__rendered {
|
||||
padding-left: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.select2-container .select2-selection--single .select2-selection__arrow {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Ensure Select2 is visible */
|
||||
.select2-container {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.select2-dropdown {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* Fix Select2 width */
|
||||
.select2-container--default .select2-selection--single {
|
||||
height: calc(1.5em + 0.75rem + 2px);
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Limit Select2 dropdown height */
|
||||
.select2-results__options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Style for Select2 results */
|
||||
.select2-results__option {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.select2-results__option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Improve Select2 search box */
|
||||
.select2-search--dropdown .select2-search__field {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||
<div class="kt-portlet kt-portlet--mobile">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Edit Target KPI
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
|
||||
<form id="kpi-form" method="POST" action="{{ route('kpi.targets.update', $target->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="user_id" class="form-control-label">Mekanik <span class="text-danger">*</span></label>
|
||||
<select name="user_id" id="user_id" class="form-control select2" required>
|
||||
<option value="">Pilih Mekanik</option>
|
||||
@foreach($mechanics as $mechanic)
|
||||
@php
|
||||
$isSelected = old('user_id', $target->user_id) == $mechanic->id;
|
||||
@endphp
|
||||
<option value="{{ $mechanic->id }}"
|
||||
{{ $isSelected ? 'selected' : '' }}>
|
||||
{{ $mechanic->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
@error('user_id')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="target_value" class="form-control-label">Target Nilai <span class="text-danger">*</span></label>
|
||||
<input type="number" name="target_value" id="target_value" class="form-control"
|
||||
value="{{ old('target_value', $target->target_value) }}" min="1" required>
|
||||
@error('target_value')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-control-label">Deskripsi</label>
|
||||
<textarea name="description" id="description" class="form-control" rows="3"
|
||||
placeholder="Deskripsi target (opsional)">{{ old('description', $target->description) }}</textarea>
|
||||
@error('description')
|
||||
<span class="text-danger">{{ $message }}</span>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" name="is_active" id="is_active" class="custom-control-input"
|
||||
value="1" {{ old('is_active', $target->is_active) ? 'checked' : '' }}>
|
||||
<label class="custom-control-label" for="is_active">
|
||||
Target Aktif
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Update Target
|
||||
</button>
|
||||
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">Kembali</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('javascripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2 with fallback and delay
|
||||
setTimeout(function() {
|
||||
try {
|
||||
// Initialize Select2 for mechanics with search limit
|
||||
$('#user_id').select2({
|
||||
theme: 'bootstrap4',
|
||||
width: '100%',
|
||||
placeholder: 'Pilih Mekanik',
|
||||
allowClear: true,
|
||||
minimumInputLength: 1,
|
||||
maximumInputLength: 50,
|
||||
maximumResultsForSearch: 10,
|
||||
language: {
|
||||
inputTooShort: function() {
|
||||
return "Masukkan minimal 1 karakter untuk mencari";
|
||||
},
|
||||
inputTooLong: function() {
|
||||
return "Maksimal 50 karakter";
|
||||
},
|
||||
noResults: function() {
|
||||
return "Tidak ada hasil ditemukan";
|
||||
},
|
||||
searching: function() {
|
||||
return "Mencari...";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.log('Select2 not available, using regular select');
|
||||
// Fallback: ensure regular select works
|
||||
$('.select2').removeClass('select2').addClass('form-control');
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Form validation
|
||||
$('#kpi-form').on('submit', function(e) {
|
||||
var isValid = true;
|
||||
var errors = [];
|
||||
|
||||
// Clear previous errors
|
||||
$('.text-danger').remove();
|
||||
|
||||
// Validate required fields
|
||||
if (!$('#user_id').val()) {
|
||||
errors.push('Mekanik harus dipilih');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#target_value').val() || $('#target_value').val() < 1) {
|
||||
errors.push('Target nilai harus diisi dan minimal 1');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Validasi Gagal',
|
||||
html: errors.join('<br>'),
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
} else {
|
||||
alert('Validasi Gagal:\n' + errors.join('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
209
resources/views/kpi/targets/index.blade.php
Normal file
209
resources/views/kpi/targets/index.blade.php
Normal file
@@ -0,0 +1,209 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('content')
|
||||
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||
<div class="kt-portlet kt-portlet--mobile">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Manajemen Target KPI
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('kpi.targets.create') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Tambah Target
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
{{ session('error') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered" id="kpiTargetsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Mekanik</th>
|
||||
<th>Target</th>
|
||||
<th>Status</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($targets as $target)
|
||||
<tr>
|
||||
<td>{{ $loop->iteration }}</td>
|
||||
<td>{{ $target->user->name }}</td>
|
||||
<td>{{ number_format($target->target_value) }}</td>
|
||||
<td>
|
||||
@if($target->is_active)
|
||||
<span class="badge badge-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">Nonaktif</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ route('kpi.targets.show', $target->id) }}"
|
||||
class="btn btn-sm btn-info" title="Detail">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{{ route('kpi.targets.edit', $target->id) }}"
|
||||
class="btn btn-sm btn-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-{{ $target->is_active ? 'warning' : 'success' }}"
|
||||
onclick="toggleStatus({{ $target->id }})"
|
||||
title="{{ $target->is_active ? 'Nonaktifkan' : 'Aktifkan' }}">
|
||||
<i class="fas fa-{{ $target->is_active ? 'pause' : 'play' }}"></i>
|
||||
</button>
|
||||
<form action="{{ route('kpi.targets.destroy', $target->id) }}"
|
||||
method="POST"
|
||||
style="display: inline;"
|
||||
onsubmit="return confirm('Yakin ingin menghapus target ini?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger" title="Hapus">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Tidak ada data target KPI</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($targets->hasPages())
|
||||
<div class="d-flex justify-content-center">
|
||||
{{ $targets->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Modal -->
|
||||
<div class="modal fade" id="filterModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Filter Target KPI</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form action="{{ route('kpi.targets.index') }}" method="GET">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>Mekanik</label>
|
||||
<select name="user_id" class="form-control">
|
||||
<option value="">Semua Mekanik</option>
|
||||
@foreach($mechanics as $mechanic)
|
||||
<option value="{{ $mechanic->id }}"
|
||||
{{ request('user_id') == $mechanic->id ? 'selected' : '' }}>
|
||||
{{ $mechanic->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select name="is_active" class="form-control">
|
||||
<option value="">Semua Status</option>
|
||||
<option value="1" {{ request('is_active') == '1' ? 'selected' : '' }}>Aktif</option>
|
||||
<option value="0" {{ request('is_active') == '0' ? 'selected' : '' }}>Nonaktif</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-primary">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('javascripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
$('#kpiTargetsTable').DataTable({
|
||||
"pageLength": 25,
|
||||
"order": [[0, "asc"]]
|
||||
});
|
||||
|
||||
// Auto hide alerts after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut('slow');
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
function toggleStatus(targetId) {
|
||||
if (confirm('Yakin ingin mengubah status target ini?')) {
|
||||
$.ajax({
|
||||
url: '{{ route("kpi.targets.toggle-status", ":id") }}'.replace(':id', targetId),
|
||||
type: 'POST',
|
||||
data: {
|
||||
_token: '{{ csrf_token() }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil',
|
||||
text: response.message,
|
||||
timer: 2000,
|
||||
showConfirmButton: false
|
||||
}).then(function() {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: response.message
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Error',
|
||||
text: 'Terjadi kesalahan saat mengubah status'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
193
resources/views/kpi/targets/show.blade.php
Normal file
193
resources/views/kpi/targets/show.blade.php
Normal file
@@ -0,0 +1,193 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('content')
|
||||
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor" id="kt_content">
|
||||
<div class="kt-container kt-container--fluid kt-grid__item kt-grid__item--fluid">
|
||||
<div class="kt-portlet kt-portlet--mobile">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Detail Target KPI
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('kpi.targets.edit', $target->id) }}" class="btn btn-warning">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="{{ route('kpi.targets.index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Kembali
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td width="150"><strong>Mekanik</strong></td>
|
||||
<td>: {{ $target->user->name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Email</strong></td>
|
||||
<td>: {{ $target->user->email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Dealer</strong></td>
|
||||
<td>: {{ $target->user->dealer->name ?? 'N/A' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Target Nilai</strong></td>
|
||||
<td>: {{ number_format($target->target_value) }} Pekerjaan</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status</strong></td>
|
||||
<td>:
|
||||
@if($target->is_active)
|
||||
<span class="badge badge-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge badge-secondary">Nonaktif</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td width="150"><strong>Jenis Target</strong></td>
|
||||
<td>: <span class="badge badge-info">Target Permanen</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Berlaku Sejak</strong></td>
|
||||
<td>: {{ $target->created_at->format('d/m/Y') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Dibuat</strong></td>
|
||||
<td>: {{ $target->created_at->format('d/m/Y H:i') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Terakhir Update</strong></td>
|
||||
<td>: {{ $target->updated_at->format('d/m/Y H:i') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Total Pencapaian</strong></td>
|
||||
<td>: {{ $target->achievements->count() }} Bulan</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($target->description)
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<h6><strong>Deskripsi:</strong></h6>
|
||||
<p class="text-muted">{{ $target->description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Achievement History -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5><i class="fas fa-chart-line"></i> Riwayat Pencapaian Bulanan</h5>
|
||||
@if($target->achievements->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Periode</th>
|
||||
<th>Target</th>
|
||||
<th>Aktual</th>
|
||||
<th>Pencapaian</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($target->achievements->sortByDesc('year')->sortByDesc('month') as $achievement)
|
||||
<tr>
|
||||
<td>{{ $achievement->getPeriodDisplayName() }}</td>
|
||||
<td>{{ number_format($achievement->target_value) }}</td>
|
||||
<td>{{ number_format($achievement->actual_value) }}</td>
|
||||
<td>{{ number_format($achievement->achievement_percentage, 1) }}%</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $achievement->status_color }}">
|
||||
@switch($achievement->status)
|
||||
@case('exceeded')
|
||||
Melebihi Target
|
||||
@break
|
||||
@case('good')
|
||||
Baik
|
||||
@break
|
||||
@case('fair')
|
||||
Cukup
|
||||
@break
|
||||
@case('poor')
|
||||
Kurang
|
||||
@break
|
||||
@default
|
||||
Tidak Diketahui
|
||||
@endswitch
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i> Belum ada data pencapaian untuk target ini.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Statistics -->
|
||||
@if($target->achievements->count() > 0)
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<h5><i class="fas fa-chart-bar"></i> Statistik Pencapaian</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body text-center">
|
||||
<h4>{{ $target->achievements->count() }}</h4>
|
||||
<small>Total Pencapaian</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body text-center">
|
||||
<h4>{{ $target->achievements->where('achievement_percentage', '>=', 100)->count() }}</h4>
|
||||
<small>Target Tercapai</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body text-center">
|
||||
<h4>{{ number_format($target->achievements->avg('achievement_percentage'), 1) }}%</h4>
|
||||
<small>Rata-rata Pencapaian</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body text-center">
|
||||
<h4>{{ number_format($target->achievements->max('achievement_percentage'), 1) }}%</h4>
|
||||
<small>Pencapaian Tertinggi</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -48,7 +48,6 @@
|
||||
@if(Gate::check('view', $menus['user.index']) || Gate::check('view', $menus['roleprivileges.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-users" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
<span>Manajemen Pengguna</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +76,6 @@
|
||||
@if(Gate::check('view', $menus['work.index']) || Gate::check('view', $menus['category.index']) || Gate::check('view', $menus['dealer.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-exchange-alt" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
<span>Manajemen Transaksi</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +113,6 @@
|
||||
@if(Gate::check('view', $menus['products.index']) || Gate::check('view', $menus['product_categories.index']) || Gate::check('view', $menus['mutations.index']) || Gate::check('view', $menus['opnames.index']) || Gate::check('view', $menus['stock-audit.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-warehouse" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
<span>Manajemen Gudang</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +168,6 @@
|
||||
@if(Gate::check('view', $menus['report.transaction_sa']) || Gate::check('view', $menus['report.transaction']) || Gate::check('view', $menus['report.transaction_dealer']) || Gate::check('view', $menus['work.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<i class="fa fa-chart-bar" style="margin-right: 8px; font-size: 14px;"></i>
|
||||
<span>Laporan</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,23 +201,42 @@
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
@can('view', $menus['work.index'])
|
||||
@can('view', $menus['reports.stock-product.index'])
|
||||
<li class="kt-menu__item" aria-haspopup="true">
|
||||
<a href="{{ route('work.index') }}" class="kt-menu__link">
|
||||
<a href="{{ route('reports.stock-product.index') }}" class="kt-menu__link">
|
||||
<i class="fa fa-cubes" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||
<span class="kt-menu__link-text">Stok Produk</span>
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
@can('view', $menus['work.index'])
|
||||
@can('view', $menus['reports.technician.index'])
|
||||
<li class="kt-menu__item" aria-haspopup="true">
|
||||
<a href="{{ route('work.index') }}" class="kt-menu__link">
|
||||
<a href="{{ route('reports.technician.index') }}" class="kt-menu__link">
|
||||
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||
<span class="kt-menu__link-text">Teknisi</span>
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
|
||||
{{-- Section Header - Only show if user has access to any submenu --}}
|
||||
@if(Gate::check('view', $menus['kpi.targets.index']))
|
||||
<div class="kt-menu__section" style="padding: 15px 20px; margin-top: 10px; margin-bottom: 5px;">
|
||||
<div style="display: flex; align-items: center; color: #a7abc3; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
<span>KPI</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Submenu Items --}}
|
||||
@can('view', $menus['kpi.targets.index'])
|
||||
<li class="kt-menu__item" aria-haspopup="true">
|
||||
<a href="{{ route('kpi.targets.index') }}" class="kt-menu__link">
|
||||
<i class="fa fa-user-cog" style="display: flex; align-items: center; margin-right: 10px;"></i>
|
||||
<span class="kt-menu__link-text">Target</span>
|
||||
</a>
|
||||
</li>
|
||||
@endcan
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
544
resources/views/reports/stock-products.blade.php
Normal file
544
resources/views/reports/stock-products.blade.php
Normal file
@@ -0,0 +1,544 @@
|
||||
@extends('layouts.backapp')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
/* Loading overlay */
|
||||
#loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
#loading-overlay .spinner-border {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Button loading state */
|
||||
.btn.loading {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Dealer column styling */
|
||||
.dealer-column {
|
||||
background-color: #f8f9fa;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Filter section styling */
|
||||
.form-label.font-weight-bold {
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background-color: #ffffff;
|
||||
border-color: #ced4da;
|
||||
color: #6c757d;
|
||||
border-right: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
border-left: 0;
|
||||
border-right: 1px solid #ced4da;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus + .input-group-append .input-group-text {
|
||||
border-color: #80bdff;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus ~ .input-group-prepend .input-group-text {
|
||||
border-color: #80bdff;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.input-group:hover .form-control {
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.input-group:hover .input-group-text {
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Datepicker styling */
|
||||
.datepicker {
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #ced4da;
|
||||
transition: all 0.15s ease-in-out;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.datepicker:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.datepicker::placeholder {
|
||||
color: #6c757d;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.datepicker[readonly] {
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.datepicker[readonly]:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.row.mb-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.col-md-2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
height: 42px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-label.font-weight-bold {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.row.mb-4 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-group-prepend {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group .form-control {
|
||||
border-left: 1px solid #ced4da;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="kt-content" id="kt_content">
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Laporan Stok Produk
|
||||
</h3>
|
||||
</div>
|
||||
<div class="kt-portlet__head-toolbar">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<button type="button" class="btn btn-bold btn-success btn--sm" id="btn-export-excel">
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kt-portlet__body">
|
||||
<!-- Filter Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label font-weight-bold">Filter Tanggal</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control datepicker" id="filter-date" placeholder="Pilih tanggal" value="{{ date('Y-m-d') }}" autocomplete="off" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group mb-0">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-primary btn-block btn-md" id="btn-filter">
|
||||
Cari
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Table Section --}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered" id="report-stock-products-table">
|
||||
<thead id="table-header">
|
||||
<tr>
|
||||
<th>No</th>
|
||||
<th>Produk</th>
|
||||
<th>Kategori</th>
|
||||
<th>Satuan</th>
|
||||
<th>Total Stok</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let dealers = [];
|
||||
let dataTable;
|
||||
|
||||
// Loading overlay functions
|
||||
var showLoadingOverlay = function (message) {
|
||||
if ($("#loading-overlay").length === 0) {
|
||||
$("body").append(`
|
||||
<div id="loading-overlay" style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
">
|
||||
<div class="spinner-border text-light" role="status" style="width: 3rem; height: 3rem;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<div class="text-light mt-3" style="font-size: 1.1rem; font-weight: 500;">${message}</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
$("#loading-overlay").show();
|
||||
$("#loading-overlay .text-light:last").text(message);
|
||||
}
|
||||
};
|
||||
|
||||
var hideLoadingOverlay = function () {
|
||||
$("#loading-overlay").hide();
|
||||
};
|
||||
|
||||
// Initialize datepicker
|
||||
$('#filter-date').datepicker({
|
||||
format: 'yyyy-mm-dd',
|
||||
autoclose: true,
|
||||
todayHighlight: true
|
||||
});
|
||||
|
||||
// Show initial loading
|
||||
showLoadingOverlay("Memuat data dealer...");
|
||||
|
||||
// Load dealers first
|
||||
loadDealers();
|
||||
|
||||
function updateTableHeader() {
|
||||
let headerRow = $('#table-header tr');
|
||||
// Clear existing dealer columns (keep first 4 columns + total)
|
||||
headerRow.find('th:gt(3):not(:last)').remove();
|
||||
|
||||
// Add dealer columns
|
||||
dealers.forEach(function(dealer) {
|
||||
headerRow.find('th:last').before('<th class="text-center" style="min-width: 120px; background-color: #f8f9fa;">' + dealer.name + '</th>');
|
||||
});
|
||||
|
||||
// Update dealer info display
|
||||
if (dealers.length > 0) {
|
||||
$('#dealer-names').text(dealers.map(d => d.name).join(', '));
|
||||
$('#dealer-info').show();
|
||||
} else {
|
||||
$('#dealer-info').hide();
|
||||
}
|
||||
}
|
||||
|
||||
function loadDealers() {
|
||||
$.ajax({
|
||||
url: '{{ route("reports.stock-product.dealers") }}',
|
||||
type: 'GET',
|
||||
success: function(response) {
|
||||
dealers = response || [];
|
||||
showLoadingOverlay("Memuat data produk...");
|
||||
initializeDataTable();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error loading dealers:', error);
|
||||
dealers = [];
|
||||
hideLoadingOverlay();
|
||||
toastr.error('Gagal memuat data dealer. Silakan refresh halaman.');
|
||||
// Still try to initialize with empty dealers
|
||||
showLoadingOverlay("Memuat data produk...");
|
||||
initializeDataTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializeDataTable() {
|
||||
// Destroy existing datatable if exists
|
||||
if (dataTable) {
|
||||
dataTable.destroy();
|
||||
}
|
||||
|
||||
// Update table header with dealer columns
|
||||
updateTableHeader();
|
||||
|
||||
// Build dealer columns dynamically
|
||||
let dealerColumns = dealers.map(function(dealer) {
|
||||
return {
|
||||
data: `dealer_${dealer.id}`,
|
||||
name: `dealer_${dealer.id}`,
|
||||
title: dealer.name,
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
className: 'text-right',
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return data ? number_format(data, 2) : '-';
|
||||
}
|
||||
return data || 0;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Define all columns
|
||||
let columns = [
|
||||
{
|
||||
data: 'DT_RowIndex',
|
||||
name: 'DT_RowIndex',
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
width: '50px',
|
||||
className: 'text-center'
|
||||
},
|
||||
{
|
||||
data: 'product_info',
|
||||
name: 'product_name',
|
||||
orderable: true,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
data: 'category_name',
|
||||
name: 'category_name',
|
||||
orderable: true,
|
||||
searchable: true
|
||||
},
|
||||
{
|
||||
data: 'unit',
|
||||
name: 'unit',
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
width: '80px',
|
||||
className: 'text-center'
|
||||
}
|
||||
];
|
||||
|
||||
// Add dealer columns
|
||||
columns = columns.concat(dealerColumns);
|
||||
|
||||
// Add total stock column
|
||||
columns.push({
|
||||
data: 'total_stock',
|
||||
name: 'total_stock',
|
||||
orderable: true,
|
||||
searchable: false,
|
||||
className: 'text-right font-weight-bold',
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return number_format(data, 2);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize DataTable
|
||||
dataTable = $('#report-stock-products-table').DataTable({
|
||||
destroy: true,
|
||||
processing: true,
|
||||
serverSide: false, // We'll handle data loading manually
|
||||
ajax: {
|
||||
url: '{{ route("reports.stock-product.data") }}',
|
||||
type: 'GET',
|
||||
data: function(d) {
|
||||
d.filter_date = $('#filter-date').val();
|
||||
},
|
||||
beforeSend: function() {
|
||||
showLoadingOverlay("Memuat data stok produk...");
|
||||
},
|
||||
complete: function() {
|
||||
hideLoadingOverlay();
|
||||
},
|
||||
error: function(xhr, error, thrown) {
|
||||
hideLoadingOverlay();
|
||||
console.error('DataTable AJAX Error:', error, thrown);
|
||||
toastr.error('Gagal memuat data. Silakan coba lagi.');
|
||||
}
|
||||
},
|
||||
columns: columns,
|
||||
order: [[1, 'asc']], // Sort by product name
|
||||
pageLength: 25,
|
||||
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
|
||||
'<"row"<"col-sm-12"tr>>' +
|
||||
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
|
||||
initComplete: function() {
|
||||
// DataTable initialization complete
|
||||
hideLoadingOverlay();
|
||||
|
||||
// Check if data is empty
|
||||
if (dataTable.data().length === 0) {
|
||||
toastr.warning('Tidak ada data stok produk yang ditemukan untuk tanggal yang dipilih.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Filter button click
|
||||
$('#btn-filter').click(function() {
|
||||
if (dataTable) {
|
||||
// Prevent multiple clicks
|
||||
var filterButton = $('#btn-filter');
|
||||
if (filterButton.hasClass('loading')) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
filterButton.addClass('loading').prop('disabled', true);
|
||||
var originalHtml = filterButton.html();
|
||||
filterButton.html('<i class="fa fa-spinner fa-spin"></i> Mencari...');
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Memuat data stok produk...");
|
||||
|
||||
// Reload data
|
||||
dataTable.ajax.reload(function() {
|
||||
// Reset button state
|
||||
filterButton.removeClass('loading').prop('disabled', false).html(originalHtml);
|
||||
hideLoadingOverlay();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Export button click
|
||||
$('#btn-export-excel').click(function() {
|
||||
exportToExcel();
|
||||
});
|
||||
|
||||
// Enter key on date input
|
||||
$('#filter-date').keypress(function(e) {
|
||||
if (e.which == 13) { // Enter key
|
||||
$('#btn-filter').click();
|
||||
}
|
||||
});
|
||||
|
||||
// Number format helper
|
||||
function number_format(number, decimals) {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
}).format(number);
|
||||
}
|
||||
|
||||
// Export to Excel function
|
||||
function exportToExcel() {
|
||||
// Prevent multiple clicks
|
||||
var exportButton = $('#btn-export-excel');
|
||||
if (exportButton.hasClass('loading')) {
|
||||
return; // Already processing
|
||||
}
|
||||
|
||||
let filterDate = $('#filter-date').val();
|
||||
let url = '{{ route("reports.stock-product.export") }}';
|
||||
if (filterDate) {
|
||||
url += '?filter_date=' + filterDate;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
exportButton.addClass('loading').prop('disabled', true);
|
||||
var originalHtml = exportButton.html();
|
||||
exportButton.html('<i class="fa fa-spinner fa-spin"></i> Exporting...');
|
||||
|
||||
// Show loading overlay
|
||||
showLoadingOverlay("Menyiapkan file Excel...");
|
||||
|
||||
// Open in new window
|
||||
let newWindow = window.open(url, '_blank');
|
||||
|
||||
// Reset button and hide overlay after a delay
|
||||
setTimeout(function() {
|
||||
exportButton.removeClass('loading').prop('disabled', false).html(originalHtml);
|
||||
hideLoadingOverlay();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Make exportToExcel available globally
|
||||
window.exportToExcel = exportToExcel;
|
||||
|
||||
// Cleanup loading overlay on page unload
|
||||
$(window).on('beforeunload', function() {
|
||||
hideLoadingOverlay();
|
||||
});
|
||||
|
||||
// Fallback for toastr if not available
|
||||
if (typeof toastr === 'undefined') {
|
||||
window.toastr = {
|
||||
success: function(msg) { console.log('SUCCESS:', msg); },
|
||||
error: function(msg) { console.log('ERROR:', msg); },
|
||||
warning: function(msg) { console.log('WARNING:', msg); },
|
||||
info: function(msg) { console.log('INFO:', msg); }
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
1401
resources/views/reports/technician.blade.php
Normal file
1401
resources/views/reports/technician.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -249,22 +249,72 @@
|
||||
$string = "*[Laporan Harian Mekanik ]*\n";
|
||||
$string .= "*". Auth::user()->dealer->name ."*\n";
|
||||
$string .= "*". $transaction_mechanics['today_date'] ."*\n\n\n";
|
||||
|
||||
// Add KPI Achievement Information
|
||||
if($kpiData['has_target']) {
|
||||
$string .= "*=== KPI ACHIEVEMENT PROGRESS ===*\n";
|
||||
$string .= "*Target ". $kpiData['period'] .": ". number_format($kpiData['target']) ." Pekerjaan*\n";
|
||||
$string .= "*Pencapaian: ". number_format($kpiData['actual']) ." Pekerjaan*\n";
|
||||
$string .= "*Progress: ". $kpiData['percentage'] ."%*\n";
|
||||
|
||||
// Add status message
|
||||
if($kpiData['status'] == 'exceeded') {
|
||||
$string .= "*Status: ✅ Target tercapai!*\n";
|
||||
} elseif($kpiData['status'] == 'good') {
|
||||
$string .= "*Status: 🔵 Performa baik*\n";
|
||||
} elseif($kpiData['status'] == 'fair') {
|
||||
$string .= "*Status: 🟡 Perlu peningkatan*\n";
|
||||
} elseif($kpiData['status'] == 'poor') {
|
||||
$string .= "*Status: 🔴 Perlu perbaikan*\n";
|
||||
} else {
|
||||
$string .= "*Status: ⚪ Belum ada data*\n";
|
||||
}
|
||||
$string .= "\n";
|
||||
}
|
||||
|
||||
$overall_total = 0;
|
||||
|
||||
// Debug: Check if data exists
|
||||
if (!empty($transaction_mechanics['data'])) {
|
||||
foreach ($transaction_mechanics['data'] as $shortname => $trx) {
|
||||
$string .= $shortname."\n";
|
||||
$string .= "*". $shortname ."*\n";
|
||||
$total_qty = 0;
|
||||
|
||||
// Check if data array exists
|
||||
if (isset($trx['data']) && is_array($trx['data'])) {
|
||||
foreach ($trx['data'] as $item) {
|
||||
$total_qty += explode(':', $item)[1];
|
||||
$parts = explode(':', $item);
|
||||
if (count($parts) >= 2) {
|
||||
$qty = intval($parts[1]);
|
||||
$total_qty += $qty;
|
||||
}
|
||||
$string .= $item."\n";
|
||||
}
|
||||
$string .= "*TOTAL".$total_qty."*\n\n";
|
||||
|
||||
$string .= $trx['total_title'];
|
||||
foreach ($trx['total_body'] as $total) {
|
||||
$string .= $total;
|
||||
}
|
||||
|
||||
$string .= $trx['total_total']."\n\n";
|
||||
$string .= "*TOTAL: ".$total_qty." Unit*\n\n";
|
||||
$overall_total += $total_qty;
|
||||
|
||||
// Remove monthly data display since this is daily report
|
||||
// if (isset($trx['total_body']) && is_array($trx['total_body'])) {
|
||||
// foreach ($trx['total_body'] as $total) {
|
||||
// $string .= $total;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (isset($trx['total_total'])) {
|
||||
// $string .= $trx['total_total']."\n\n";
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
$string .= "*Tidak ada data transaksi hari ini*\n\n";
|
||||
}
|
||||
|
||||
// Add overall summary
|
||||
$string .= "*=== RINGKASAN HARIAN ===*\n";
|
||||
$string .= "*Tanggal: ". $transaction_mechanics['today_date'] ."*\n";
|
||||
$string .= "*Dealer: ". Auth::user()->dealer->name ."*\n";
|
||||
$string .= "*Total Keseluruhan: ". $overall_total ." Unit*\n";
|
||||
@endphp
|
||||
{!! $string !!}
|
||||
</div>
|
||||
@@ -335,23 +385,156 @@
|
||||
<script>
|
||||
$("#kt-table").DataTable()
|
||||
|
||||
const shareData = {
|
||||
title: 'Dealer',
|
||||
text: $("#shareThis").html()
|
||||
// Check if Web Share API is supported
|
||||
function isWebShareSupported() {
|
||||
return navigator.share && typeof navigator.share === 'function';
|
||||
}
|
||||
|
||||
// Fallback function for copying to clipboard
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// Use modern clipboard API
|
||||
return navigator.clipboard.writeText(text).then(() => {
|
||||
return true;
|
||||
}).catch(() => {
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
try {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
return Promise.resolve(successful);
|
||||
} catch (err) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main share function
|
||||
async function shareData() {
|
||||
const shareText = $("#shareThis").html();
|
||||
|
||||
if (isWebShareSupported()) {
|
||||
// Use Web Share API
|
||||
try {
|
||||
await navigator.share({
|
||||
title: 'Laporan Harian Mekanik',
|
||||
text: shareText
|
||||
});
|
||||
console.log('Data berhasil dibagikan');
|
||||
showSuccessMessage('Data berhasil dibagikan!');
|
||||
} catch (err) {
|
||||
console.log('Share cancelled or failed:', err);
|
||||
// Fallback to clipboard
|
||||
await copyToClipboardFallback(shareText);
|
||||
}
|
||||
} else {
|
||||
// Fallback for unsupported browsers
|
||||
await copyToClipboardFallback(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard fallback function
|
||||
async function copyToClipboardFallback(text) {
|
||||
const success = await copyToClipboard(text);
|
||||
|
||||
if (success) {
|
||||
showSuccessMessage('Data berhasil disalin ke clipboard! Silakan paste di aplikasi yang diinginkan.');
|
||||
} else {
|
||||
showErrorMessage('Gagal menyalin data. Silakan copy manual dari bawah ini:');
|
||||
showManualCopy(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Show success message
|
||||
function showSuccessMessage(message) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berhasil!',
|
||||
text: message,
|
||||
timer: 3000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showErrorMessage(message) {
|
||||
Swal.fire({
|
||||
icon: 'warning',
|
||||
title: 'Perhatian',
|
||||
text: message,
|
||||
confirmButtonText: 'OK'
|
||||
});
|
||||
}
|
||||
|
||||
// Show manual copy option
|
||||
function showManualCopy(text) {
|
||||
const modal = `
|
||||
<div class="modal fade" id="manualCopyModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Copy Manual Data</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Silakan copy data di bawah ini:</p>
|
||||
<textarea class="form-control" rows="15" readonly style="font-family: monospace; font-size: 12px;">${text}</textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
|
||||
<button type="button" class="btn btn-primary" onclick="copyFromTextarea()">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing modal if any
|
||||
$('#manualCopyModal').remove();
|
||||
|
||||
// Add modal to body
|
||||
$('body').append(modal);
|
||||
$('#manualCopyModal').modal('show');
|
||||
}
|
||||
|
||||
// Copy from textarea function
|
||||
function copyFromTextarea() {
|
||||
const textarea = document.querySelector('#manualCopyModal textarea');
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showSuccessMessage('Data berhasil disalin!');
|
||||
$('#manualCopyModal').modal('hide');
|
||||
} else {
|
||||
showErrorMessage('Gagal menyalin data. Silakan copy manual.');
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorMessage('Gagal menyalin data. Silakan copy manual.');
|
||||
}
|
||||
}
|
||||
|
||||
// Share button click handler
|
||||
const btn = $('#share');
|
||||
const resultPara = $('.result');
|
||||
// Share must be triggered by "user activation"
|
||||
btn.click(async function() {
|
||||
try {
|
||||
await navigator.share(shareData)
|
||||
console.log('Dealer shared successfully')
|
||||
} catch(err) {
|
||||
console.log('Error: ' + err)
|
||||
}
|
||||
})
|
||||
|
||||
btn.click(function() {
|
||||
shareData();
|
||||
});
|
||||
|
||||
function editTransaction(id) {
|
||||
let form_action = $("#editTransaction"+id).attr("data-action")
|
||||
|
||||
810
resources/views/transaction/postchecks.blade.php
Normal file
810
resources/views/transaction/postchecks.blade.php
Normal file
@@ -0,0 +1,810 @@
|
||||
@extends('layouts.frontapp')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 25px 0 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #2c5aa0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 250px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #059669;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#postcheck_notes {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.file-input-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 10px 15px;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row > .col-md-6 {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.mobile-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="mobile-container">
|
||||
<div class="container">
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-8">
|
||||
<a href="/"><img src="{{ asset('logo-ckb.png') }}" style="width: 100%" alt="LOGO CKB"></a>
|
||||
</div>
|
||||
<div class="col-4 text-right my-auto">
|
||||
<a class="btn btn-sm btn-danger mt-3" style="background: red !important;" href="{{ route('logout') }}" onclick="logout(event)">
|
||||
{{ __('Logout') }}
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
|
||||
@csrf
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-center">Form Postcheck</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="/" class="btn btn-warning btn-sm">
|
||||
Kembali
|
||||
</a>
|
||||
<form action="{{ route('postchecks.store', $transaction->id) }}" method="POST" id="postcheckForm" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
||||
|
||||
<!-- Informasi Transaksi -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-info-circle"></i> Informasi Transaksi</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor Polisi</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->police_number }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor SPK</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->spk }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-clipboard-list"></i> Data Dasar</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Tanggal Postcheck</label>
|
||||
<input type="text" class="form-control" value="{{ now()->format('d/m/Y H:i') }}" readonly>
|
||||
<small class="form-text text-muted">Tanggal dan waktu saat ini akan digunakan</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="kilometer">Kilometer <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="kilometer" name="kilometer" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_high">Pressure High (PSI) <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="pressure_high" name="pressure_high" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_low">Pressure Low (PSI)</label>
|
||||
<input type="number" class="form-control" id="pressure_low" name="pressure_low" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Depan -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="file" id="front_image" name="front_image" accept="image/*" class="file-input-hidden" required>
|
||||
<div class="camera-container">
|
||||
<video id="front_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="front_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('front_camera', 'front_canvas', 'front_image', 'front_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'front_image', 'front_preview')">
|
||||
</div>
|
||||
<div id="front_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Suhu Kabin -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-thermometer-half"></i> Foto Suhu Kabin</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature">Suhu Kabin (°C)</label>
|
||||
<input type="number" class="form-control" id="cabin_temperature" name="cabin_temperature" step="0.1" placeholder="Masukkan suhu kabin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature_image">Foto Suhu Kabin</label>
|
||||
<input type="file" id="cabin_temperature_image" name="cabin_temperature_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="cabin_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="cabin_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('cabin_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'cabin_temperature_image', 'cabin_preview')">
|
||||
</div>
|
||||
<div id="cabin_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi AC -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-snowflake"></i> Kondisi AC</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_condition">Kondisi AC</label>
|
||||
<select class="form-control" id="ac_condition" name="ac_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($acConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_image">Foto AC</label>
|
||||
<input type="file" id="ac_image" name="ac_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="ac_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="ac_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')">
|
||||
</div>
|
||||
<div id="ac_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Blower -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-fan"></i> Kondisi Blower</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_condition">Kondisi Blower</label>
|
||||
<select class="form-control" id="blower_condition" name="blower_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($blowerConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_image">Foto Blower</label>
|
||||
<input type="file" id="blower_image" name="blower_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="blower_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="blower_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')">
|
||||
</div>
|
||||
<div id="blower_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Evaporator -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-tint"></i> Kondisi Evaporator</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_condition">Kondisi Evaporator</label>
|
||||
<select class="form-control" id="evaporator_condition" name="evaporator_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($evaporatorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_image">Foto Evaporator</label>
|
||||
<input type="file" id="evaporator_image" name="evaporator_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="evaporator_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="evaporator_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')">
|
||||
</div>
|
||||
<div id="evaporator_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Compressor -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-cogs"></i> Kondisi Compressor</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="compressor_condition">Kondisi Compressor</label>
|
||||
<select class="form-control" id="compressor_condition" name="compressor_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($compressorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="postcheck_notes">Catatan Tambahan</label>
|
||||
<textarea class="form-control" id="postcheck_notes" name="postcheck_notes" rows="3" placeholder="Masukkan catatan tambahan jika ada..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tombol Submit -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-save"></i> Simpan Data</h5>
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Simpan Postcheck
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
let streams = {};
|
||||
|
||||
// Logout function
|
||||
function logout(event){
|
||||
event.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Logout?',
|
||||
text: "Anda akan keluar dari sistem!",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#dedede',
|
||||
confirmButtonText: 'Logout'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
$('#logout-form').submit();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback untuk browser lama
|
||||
if (navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
}
|
||||
|
||||
if (navigator.mediaDevices.getUserMedia === undefined) {
|
||||
navigator.mediaDevices.getUserMedia = function(constraints) {
|
||||
// Coba berbagai versi getUserMedia
|
||||
const getUserMedia = navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia ||
|
||||
navigator.oGetUserMedia;
|
||||
|
||||
if (!getUserMedia) {
|
||||
return Promise.reject(new Error('getUserMedia tidak didukung di browser ini'));
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahan fallback untuk browser yang sangat lama
|
||||
if (navigator.getUserMedia === undefined) {
|
||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||
}
|
||||
|
||||
// Start camera
|
||||
async function startCamera(videoId) {
|
||||
try {
|
||||
const video = document.getElementById(videoId);
|
||||
|
||||
// Cek apakah browser mendukung getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Browser tidak mendukung akses kamera');
|
||||
}
|
||||
|
||||
// Stop stream yang sedang berjalan
|
||||
if (streams[videoId]) {
|
||||
streams[videoId].getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Konfigurasi kamera
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { min: 320, ideal: 640, max: 1280 },
|
||||
height: { min: 240, ideal: 480, max: 720 },
|
||||
aspectRatio: { ideal: 4/3 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
video.srcObject = stream;
|
||||
streams[videoId] = stream;
|
||||
|
||||
// Tunggu video siap
|
||||
video.onloadedmetadata = function() {
|
||||
video.play();
|
||||
};
|
||||
|
||||
video.onerror = function(e) {
|
||||
console.error('Error pada video:', e);
|
||||
alert('Error pada video stream');
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
// Pesan error yang lebih spesifik
|
||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage += 'Izin kamera ditolak. Silakan:\n1. Klik ikon kamera di address bar\n2. Pilih "Allow"\n3. Refresh halaman';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage += 'Kamera tidak ditemukan. Pastikan HP memiliki kamera.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage += 'Kamera sedang digunakan aplikasi lain. Tutup aplikasi kamera lain.';
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
errorMessage += 'Kamera tidak mendukung resolusi yang diminta.';
|
||||
} else if (err.name === 'SecurityError') {
|
||||
errorMessage += 'Akses kamera diblokir. Pastikan menggunakan HTTPS atau localhost.';
|
||||
} else {
|
||||
errorMessage += 'Error: ' + err.message;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture photo and convert to file
|
||||
function capturePhoto(videoId, canvasId, inputId, previewId) {
|
||||
const video = document.getElementById(videoId);
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (!video.srcObject) {
|
||||
alert('Silakan buka kamera terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pastikan video sudah siap
|
||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Convert canvas ke File object
|
||||
canvas.toBlob(function(blob) {
|
||||
// Buat File object
|
||||
const file = new File([blob], `photo_${Date.now()}.jpg`, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
// Assign ke file input
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
fileInput.files = dataTransfer.files;
|
||||
|
||||
// Preview
|
||||
const url = URL.createObjectURL(blob);
|
||||
preview.innerHTML = `
|
||||
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||
<br>
|
||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||
</div>
|
||||
`;
|
||||
}, 'image/jpeg', 0.8);
|
||||
|
||||
} catch (err) {
|
||||
alert('Gagal mengambil foto: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload from gallery
|
||||
function handleFileUpload(input, inputId, previewId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Pilih file gambar');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validasi ukuran file (max 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('Ukuran file maksimal 2MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign ke file input yang sesuai
|
||||
const targetInput = document.getElementById(inputId);
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
targetInput.files = dataTransfer.files;
|
||||
|
||||
// Preview
|
||||
const url = URL.createObjectURL(file);
|
||||
const preview = document.getElementById(previewId);
|
||||
preview.innerHTML = `
|
||||
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||
<br>
|
||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Stop all cameras when page is unloaded
|
||||
window.addEventListener('beforeunload', function() {
|
||||
Object.values(streams).forEach(stream => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('postcheckForm').addEventListener('submit', function(e) {
|
||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
|
||||
if (field.type === 'file') {
|
||||
// Validasi file input
|
||||
if (!field.files || field.files.length === 0) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
} else {
|
||||
// Validasi input biasa
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
810
resources/views/transaction/prechecks.blade.php
Normal file
810
resources/views/transaction/prechecks.blade.php
Normal file
@@ -0,0 +1,810 @@
|
||||
@extends('layouts.frontapp')
|
||||
|
||||
@section('styles')
|
||||
<style>
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 25px 0 15px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header i {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #2c5aa0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(44, 90, 160, 0.25);
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 250px;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 15px 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.photo-preview {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #059669;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#precheck_notes {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.file-input-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 10px 15px;
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.section-header h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 10px 12px;
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.camera-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.photo-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-row > .col-md-6 {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.mobile-container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.camera-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="mobile-container">
|
||||
<div class="container">
|
||||
<div class="row mb-4 mt-4">
|
||||
<div class="col-8">
|
||||
<a href="/"><img src="{{ asset('logo-ckb.png') }}" style="width: 100%" alt="LOGO CKB"></a>
|
||||
</div>
|
||||
<div class="col-4 text-right my-auto">
|
||||
<a class="btn btn-sm btn-danger mt-3" style="background: red !important;" href="{{ route('logout') }}" onclick="logout(event)">
|
||||
{{ __('Logout') }}
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
|
||||
@csrf
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-center">Form Precheck</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
{{ session('success') }}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<ul class="mb-0">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a href="/" class="btn btn-warning btn-sm">
|
||||
Kembali
|
||||
</a>
|
||||
<form action="{{ route('prechecks.store', $transaction->id) }}" method="POST" id="precheckForm" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="hidden" name="transaction_id" value="{{ $transaction->id }}">
|
||||
|
||||
<!-- Informasi Transaksi -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-info-circle"></i> Informasi Transaksi</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor Polisi</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->police_number }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Nomor SPK</label>
|
||||
<input type="text" class="form-control" value="{{ $transaction->spk }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Dasar -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-clipboard-list"></i> Data Dasar</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Tanggal Precheck</label>
|
||||
<input type="text" class="form-control" value="{{ now()->format('d/m/Y H:i') }}" readonly>
|
||||
<small class="form-text text-muted">Tanggal dan waktu saat ini akan digunakan</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="kilometer">Kilometer <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="kilometer" name="kilometer" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_high">Pressure High (PSI) <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="pressure_high" name="pressure_high" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="pressure_low">Pressure Low (PSI)</label>
|
||||
<input type="number" class="form-control" id="pressure_low" name="pressure_low" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Depan -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-camera"></i> Foto Depan Kendaraan <span class="text-danger">*</span></h5>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="file" id="front_image" name="front_image" accept="image/*" class="file-input-hidden" required>
|
||||
<div class="camera-container">
|
||||
<video id="front_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="front_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('front_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('front_camera', 'front_canvas', 'front_image', 'front_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'front_image', 'front_preview')">
|
||||
</div>
|
||||
<div id="front_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Foto Suhu Kabin -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-thermometer-half"></i> Foto Suhu Kabin</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature">Suhu Kabin (°C)</label>
|
||||
<input type="number" class="form-control" id="cabin_temperature" name="cabin_temperature" step="0.1" placeholder="Masukkan suhu kabin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="cabin_temperature_image">Foto Suhu Kabin</label>
|
||||
<input type="file" id="cabin_temperature_image" name="cabin_temperature_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="cabin_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="cabin_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('cabin_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('cabin_camera', 'cabin_canvas', 'cabin_temperature_image', 'cabin_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'cabin_temperature_image', 'cabin_preview')">
|
||||
</div>
|
||||
<div id="cabin_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi AC -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-snowflake"></i> Kondisi AC</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_condition">Kondisi AC</label>
|
||||
<select class="form-control" id="ac_condition" name="ac_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($acConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="ac_image">Foto AC</label>
|
||||
<input type="file" id="ac_image" name="ac_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="ac_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="ac_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('ac_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('ac_camera', 'ac_canvas', 'ac_image', 'ac_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'ac_image', 'ac_preview')">
|
||||
</div>
|
||||
<div id="ac_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Blower -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-fan"></i> Kondisi Blower</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_condition">Kondisi Blower</label>
|
||||
<select class="form-control" id="blower_condition" name="blower_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($blowerConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="blower_image">Foto Blower</label>
|
||||
<input type="file" id="blower_image" name="blower_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="blower_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="blower_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('blower_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('blower_camera', 'blower_canvas', 'blower_image', 'blower_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'blower_image', 'blower_preview')">
|
||||
</div>
|
||||
<div id="blower_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Evaporator -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-tint"></i> Kondisi Evaporator</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_condition">Kondisi Evaporator</label>
|
||||
<select class="form-control" id="evaporator_condition" name="evaporator_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($evaporatorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="evaporator_image">Foto Evaporator</label>
|
||||
<input type="file" id="evaporator_image" name="evaporator_image" accept="image/*" class="file-input-hidden">
|
||||
<div class="camera-container">
|
||||
<video id="evaporator_camera" autoplay playsinline class="camera-video"></video>
|
||||
<canvas id="evaporator_canvas" style="display: none;"></canvas>
|
||||
<div class="camera-controls">
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="startCamera('evaporator_camera')">
|
||||
<i class="fas fa-camera"></i> Buka Kamera
|
||||
</button>
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="capturePhoto('evaporator_camera', 'evaporator_canvas', 'evaporator_image', 'evaporator_preview')">
|
||||
<i class="fas fa-camera-retro"></i> Ambil Foto
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Atau upload foto dari galeri:</small>
|
||||
<input type="file" class="form-control-file mt-1" accept="image/*" onchange="handleFileUpload(this, 'evaporator_image', 'evaporator_preview')">
|
||||
</div>
|
||||
<div id="evaporator_preview" class="photo-preview"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kondisi Compressor -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-cogs"></i> Kondisi Compressor</h5>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="compressor_condition">Kondisi Compressor</label>
|
||||
<select class="form-control" id="compressor_condition" name="compressor_condition">
|
||||
<option value="">Pilih Kondisi</option>
|
||||
@foreach($compressorConditions as $condition)
|
||||
<option value="{{ $condition }}">{{ ucfirst($condition) }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="precheck_notes">Catatan Tambahan</label>
|
||||
<textarea class="form-control" id="precheck_notes" name="precheck_notes" rows="3" placeholder="Masukkan catatan tambahan jika ada..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tombol Submit -->
|
||||
<div class="section-header">
|
||||
<h5><i class="fas fa-save"></i> Simpan Data</h5>
|
||||
</div>
|
||||
<div class="form-group text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Simpan Precheck
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('javascripts')
|
||||
<script>
|
||||
let streams = {};
|
||||
|
||||
// Logout function
|
||||
function logout(event){
|
||||
event.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Logout?',
|
||||
text: "Anda akan keluar dari sistem!",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#dedede',
|
||||
confirmButtonText: 'Logout'
|
||||
}).then((result) => {
|
||||
if (result.value) {
|
||||
$('#logout-form').submit();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback untuk browser lama
|
||||
if (navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
}
|
||||
|
||||
if (navigator.mediaDevices.getUserMedia === undefined) {
|
||||
navigator.mediaDevices.getUserMedia = function(constraints) {
|
||||
// Coba berbagai versi getUserMedia
|
||||
const getUserMedia = navigator.webkitGetUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.msGetUserMedia ||
|
||||
navigator.oGetUserMedia;
|
||||
|
||||
if (!getUserMedia) {
|
||||
return Promise.reject(new Error('getUserMedia tidak didukung di browser ini'));
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahan fallback untuk browser yang sangat lama
|
||||
if (navigator.getUserMedia === undefined) {
|
||||
navigator.getUserMedia = navigator.mediaDevices.getUserMedia;
|
||||
}
|
||||
|
||||
// Start camera
|
||||
async function startCamera(videoId) {
|
||||
try {
|
||||
const video = document.getElementById(videoId);
|
||||
|
||||
// Cek apakah browser mendukung getUserMedia
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error('Browser tidak mendukung akses kamera');
|
||||
}
|
||||
|
||||
// Stop stream yang sedang berjalan
|
||||
if (streams[videoId]) {
|
||||
streams[videoId].getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
// Konfigurasi kamera
|
||||
const constraints = {
|
||||
video: {
|
||||
width: { min: 320, ideal: 640, max: 1280 },
|
||||
height: { min: 240, ideal: 480, max: 720 },
|
||||
aspectRatio: { ideal: 4/3 }
|
||||
}
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
video.srcObject = stream;
|
||||
streams[videoId] = stream;
|
||||
|
||||
// Tunggu video siap
|
||||
video.onloadedmetadata = function() {
|
||||
video.play();
|
||||
};
|
||||
|
||||
video.onerror = function(e) {
|
||||
console.error('Error pada video:', e);
|
||||
alert('Error pada video stream');
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
// Pesan error yang lebih spesifik
|
||||
let errorMessage = 'Tidak dapat mengakses kamera. ';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage += 'Izin kamera ditolak. Silakan:\n1. Klik ikon kamera di address bar\n2. Pilih "Allow"\n3. Refresh halaman';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage += 'Kamera tidak ditemukan. Pastikan HP memiliki kamera.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage += 'Kamera sedang digunakan aplikasi lain. Tutup aplikasi kamera lain.';
|
||||
} else if (err.name === 'OverconstrainedError') {
|
||||
errorMessage += 'Kamera tidak mendukung resolusi yang diminta.';
|
||||
} else if (err.name === 'SecurityError') {
|
||||
errorMessage += 'Akses kamera diblokir. Pastikan menggunakan HTTPS atau localhost.';
|
||||
} else {
|
||||
errorMessage += 'Error: ' + err.message;
|
||||
}
|
||||
|
||||
alert(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture photo and convert to file
|
||||
function capturePhoto(videoId, canvasId, inputId, previewId) {
|
||||
const video = document.getElementById(videoId);
|
||||
const canvas = document.getElementById(canvasId);
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
|
||||
if (!video.srcObject) {
|
||||
alert('Silakan buka kamera terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pastikan video sudah siap
|
||||
if (video.videoWidth === 0 || video.videoHeight === 0) {
|
||||
alert('Video belum siap. Tunggu sebentar dan coba lagi.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Convert canvas ke File object
|
||||
canvas.toBlob(function(blob) {
|
||||
// Buat File object
|
||||
const file = new File([blob], `photo_${Date.now()}.jpg`, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
// Assign ke file input
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
fileInput.files = dataTransfer.files;
|
||||
|
||||
// Preview
|
||||
const url = URL.createObjectURL(blob);
|
||||
preview.innerHTML = `
|
||||
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diambil</small>
|
||||
<br>
|
||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||
</div>
|
||||
`;
|
||||
}, 'image/jpeg', 0.8);
|
||||
|
||||
} catch (err) {
|
||||
alert('Gagal mengambil foto: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload from gallery
|
||||
function handleFileUpload(input, inputId, previewId) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('Pilih file gambar');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validasi ukuran file (max 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('Ukuran file maksimal 2MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign ke file input yang sesuai
|
||||
const targetInput = document.getElementById(inputId);
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
targetInput.files = dataTransfer.files;
|
||||
|
||||
// Preview
|
||||
const url = URL.createObjectURL(file);
|
||||
const preview = document.getElementById(previewId);
|
||||
preview.innerHTML = `
|
||||
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 3px solid #059669; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<div class="mt-2">
|
||||
<small class="text-success"><i class="fas fa-check"></i> Foto berhasil diupload</small>
|
||||
<br>
|
||||
<small class="text-muted">Ukuran: ${(file.size / 1024).toFixed(1)} KB</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Stop all cameras when page is unloaded
|
||||
window.addEventListener('beforeunload', function() {
|
||||
Object.values(streams).forEach(stream => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
});
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('precheckForm').addEventListener('submit', function(e) {
|
||||
const requiredFields = ['kilometer', 'front_image', 'pressure_high'];
|
||||
let isValid = true;
|
||||
|
||||
requiredFields.forEach(fieldId => {
|
||||
const field = document.getElementById(fieldId);
|
||||
|
||||
if (field.type === 'file') {
|
||||
// Validasi file input
|
||||
if (!field.files || field.files.length === 0) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
} else {
|
||||
// Validasi input biasa
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Mohon lengkapi semua field yang wajib diisi');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
@@ -327,7 +327,7 @@
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="company-info">
|
||||
<div class="company-name">PT. CINTA KASIH BERSAMA</div>
|
||||
<div class="company-name">PT. CIPTA KREASI BARU</div>
|
||||
<div class="company-tagline">Warehouse Management System</div>
|
||||
</div>
|
||||
<div class="document-title">Dokumen Mutasi Stock</div>
|
||||
@@ -528,7 +528,7 @@
|
||||
<div class="footer">
|
||||
<div class="print-info">
|
||||
Dicetak pada: {{ now()->format('d F Y H:i:s') }} |
|
||||
Sistem Manajemen Gudang PT. Cinta Kasih Bersama
|
||||
Sistem Manajemen Gudang PT. Cipta Kreasi Baru
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<th>Dealer</th>
|
||||
<th>Pengguna</th>
|
||||
<th>Status</th>
|
||||
<th>Informasi Stock</th>
|
||||
<th>Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -114,6 +115,44 @@
|
||||
letter-spacing: normal;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Stock info column styling */
|
||||
.stock-info-cell {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.stock-info-cell .text-success {
|
||||
color: #28a745 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-info-cell .text-danger {
|
||||
color: #dc3545 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stock-info-cell .text-muted {
|
||||
color: #6c757d !important;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.stock-info-cell i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for stock info column */
|
||||
@media (max-width: 768px) {
|
||||
.stock-info-cell {
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stock-info-cell .text-muted {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="company-info">
|
||||
<div class="company-name">PT. CINTA KASIH BERSAMA</div>
|
||||
<div class="company-name">PT. CIPTA KREASI BARU</div>
|
||||
<div class="company-tagline">Warehouse Management System</div>
|
||||
</div>
|
||||
<div class="document-title">Laporan Stock Opname</div>
|
||||
@@ -420,7 +420,7 @@
|
||||
<div class="footer">
|
||||
<div class="print-info">
|
||||
Dicetak pada: {{ now()->format('d F Y H:i:s') }} |
|
||||
Sistem Manajemen Gudang PT. Cinta Kasih Bersama
|
||||
Sistem Manajemen Gudang PT. Cipta Kreasi Baru
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ table.dataTable thead th.sorting:hover:before {
|
||||
<div class="kt-portlet__head-wrapper">
|
||||
<div class="kt-portlet__head-actions">
|
||||
<a href="{{ route('products.export.dealers_stock') }}" class="btn btn-bold btn-success btn--sm" style="margin-right: 8px;">
|
||||
<i class="flaticon2-download"></i>Export Stok Dealer
|
||||
Export
|
||||
</a>
|
||||
@can('create', $menus['products.index'])
|
||||
<a href="{{ route('products.create') }}" class="btn btn-bold btn-label-brand btn--sm">Tambah</a>
|
||||
|
||||
@@ -29,7 +29,6 @@ input.datepicker {
|
||||
<div class="kt-portlet kt-portlet--mobile" id="kt_blockui_datatable">
|
||||
<div class="kt-portlet__head kt-portlet__head--lg">
|
||||
<div class="kt-portlet__head-label">
|
||||
|
||||
<h3 class="kt-portlet__head-title">
|
||||
Histori Stock
|
||||
</h3>
|
||||
|
||||
@@ -11,12 +11,18 @@ use App\Http\Controllers\WarehouseManagement\OpnamesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductCategoriesController;
|
||||
use App\Http\Controllers\WarehouseManagement\ProductsController;
|
||||
use App\Http\Controllers\WorkController;
|
||||
use App\Http\Controllers\WorkDealerPriceController;
|
||||
use App\Http\Controllers\WarehouseManagement\MutationsController;
|
||||
use App\Http\Controllers\WarehouseManagement\StockAuditController;
|
||||
use App\Http\Controllers\KPI\TargetsController;
|
||||
use App\Http\Controllers\Reports\ReportStockProductsController;
|
||||
use App\Http\Controllers\Reports\ReportTechniciansController;
|
||||
use App\Models\Menu;
|
||||
use App\Models\Privilege;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\Transactions\PrechecksController;
|
||||
use App\Http\Controllers\Transactions\PostchecksController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -165,8 +171,23 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
// Stock Management Routes
|
||||
Route::post('/transaction/check-stock', [TransactionController::class, 'checkStockAvailability'])->name('transaction.check-stock');
|
||||
Route::get('/transaction/stock-prediction', [TransactionController::class, 'getStockPrediction'])->name('transaction.stock-prediction');
|
||||
|
||||
// Claim Transactions Route
|
||||
Route::get('/transaction/get-claim-transactions', [TransactionController::class, 'getClaimTransactions'])->name('transaction.get-claim-transactions');
|
||||
Route::post('/transaction/claim/{id}', [TransactionController::class, 'claim'])->name('transaction.claim');
|
||||
|
||||
// Prechecks Routes
|
||||
Route::get('/transaction/prechecks/{transaction}', [PrechecksController::class, 'index'])->name('prechecks.index');
|
||||
Route::post('/transaction/prechecks/{transaction}', [PrechecksController::class, 'store'])->name('prechecks.store');
|
||||
|
||||
// Postchecks Routes
|
||||
Route::get('/transaction/postchecks/{transaction}', [PostchecksController::class, 'index'])->name('postchecks.index');
|
||||
Route::post('/transaction/postchecks/{transaction}', [PostchecksController::class, 'store'])->name('postchecks.store');
|
||||
});
|
||||
|
||||
// KPI Data Route - accessible to all authenticated users
|
||||
Route::get('/transaction/get-kpi-data', [TransactionController::class, 'getKpiData'])->name('transaction.get-kpi-data');
|
||||
|
||||
Route::group(['prefix' => 'admin', 'middleware' => 'adminRole'], function() {
|
||||
Route::get('/dashboard2', [AdminController::class, 'dashboard2'])->name('dashboard2');
|
||||
Route::post('/dealer_work_trx', [AdminController::class, 'dealer_work_trx'])->name('dealer_work_trx');
|
||||
@@ -181,6 +202,23 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::resource('category', CategoryController::class);
|
||||
Route::resource('work', WorkController::class);
|
||||
|
||||
// Work Dealer Prices Routes
|
||||
Route::prefix('work/{work}/prices')->name('work.prices.')->controller(WorkDealerPriceController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
Route::post('/', 'store')->name('store');
|
||||
Route::get('{price}/edit', 'edit')->name('edit');
|
||||
Route::put('{price}', 'update')->name('update');
|
||||
Route::delete('{price}', 'destroy')->name('destroy');
|
||||
Route::post('bulk', 'bulkCreate')->name('bulk-create');
|
||||
Route::post('toggle-status', 'toggleStatus')->name('toggle-status');
|
||||
});
|
||||
|
||||
// Route untuk halaman set harga (menggunakan WorkController)
|
||||
Route::get('work/{work}/set-prices', [WorkController::class, 'showPrices'])->name('work.set-prices');
|
||||
|
||||
// API route untuk mendapatkan harga
|
||||
Route::get('work/get-price', [WorkDealerPriceController::class, 'getPrice'])->name('work.get-price');
|
||||
|
||||
// Work Products Management Routes
|
||||
Route::prefix('work/{work}/products')->name('work.products.')->controller(App\Http\Controllers\WorkProductController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('index');
|
||||
@@ -202,6 +240,8 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::get('/roleprivileges/{id}/edit', [RolePrivilegeController::class, 'edit'])->name('roleprivileges.edit');
|
||||
Route::put('/roleprivileges/{id}/update', [RolePrivilegeController::class, 'update'])->name('roleprivileges.update');
|
||||
Route::delete('/roleprivileges/{id}/delete', [RolePrivilegeController::class, 'delete'])->name('roleprivileges.delete');
|
||||
Route::post('/roleprivileges/{id}/assign-dealer', [RolePrivilegeController::class, 'assignDealer'])->name('roleprivileges.assignDealer');
|
||||
Route::get('/roleprivileges/{id}/assigned-dealers', [RolePrivilegeController::class, 'getAssignedDealers'])->name('roleprivileges.getAssignedDealers');
|
||||
|
||||
Route::get('/report/transaction', [ReportController::class, 'transaction'])->name('report.transaction');
|
||||
Route::post('/report/transaction/data', [ReportController::class, 'transaction_data'])->name('report.transaction_data');
|
||||
@@ -282,6 +322,28 @@ Route::group(['middleware' => 'auth'], function() {
|
||||
Route::get('{stockLog}/detail', 'getDetail')->name('detail');
|
||||
});
|
||||
});
|
||||
|
||||
// KPI Routes for Admins
|
||||
Route::prefix('kpi')->middleware(['adminRole'])->group(function () {
|
||||
// Target Management
|
||||
Route::resource('targets', TargetsController::class, ['as' => 'kpi']);
|
||||
Route::post('/targets/{target}/toggle-status', [TargetsController::class, 'toggleStatus'])->name('kpi.targets.toggle-status');
|
||||
Route::get('/targets/user/{user}', [TargetsController::class, 'getUserTargets'])->name('kpi.targets.user');
|
||||
});
|
||||
|
||||
Route::prefix('reports')->group(function () {
|
||||
Route::get('stock-products', [ReportStockProductsController::class, 'index'])->name('reports.stock-product.index');
|
||||
Route::get('stock-products/data', [ReportStockProductsController::class, 'getData'])->name('reports.stock-product.data');
|
||||
Route::get('stock-products/dealers', [ReportStockProductsController::class, 'getDealers'])->name('reports.stock-product.dealers');
|
||||
Route::get('stock-products/export', [ReportStockProductsController::class, 'export'])->name('reports.stock-product.export');
|
||||
|
||||
Route::get('technician', [ReportTechniciansController::class, 'index'])->name('reports.technician.index');
|
||||
Route::get('technician/data', [ReportTechniciansController::class, 'getData'])->name('reports.technician.data');
|
||||
Route::get('technician/datatable', [ReportTechniciansController::class, 'getDataTable'])->name('reports.technician.datatable');
|
||||
Route::get('technician/dealers', [ReportTechniciansController::class, 'getDealers'])->name('reports.technician.dealers');
|
||||
Route::get('technician/export', [ReportTechniciansController::class, 'export'])->name('reports.technician.export');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Auth::routes();
|
||||
|
||||
Reference in New Issue
Block a user