İlk temizlik tamamlandı bir önceki projeden

This commit is contained in:
Ümit Tunç
2026-04-28 21:14:25 +03:00
commit f80443aec0
10000 changed files with 959965 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
APP_NAME="Citrus CMS"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DRIVER=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# Cross-Site NAKS Certificate Sync
# These credentials are used to authenticate with other project sites
# The same admin account must exist on all sites
SYNC_ADMIN_EMAIL=sync@stellarcons.com
SYNC_ADMIN_PASSWORD=
+21
View File
@@ -0,0 +1,21 @@
.DS_Store
.env
.vscode/sftp.json
resources/lang
mysql/tmp
storage
.htaccess
excel_process.txt
bootstrap/cache/*
.phpunit.result.cache
.github/workflows/deploy-dev.yml
public/assets
/vendor/
/node_modules/
*.sql
/public/packages/
.scribe/
resources/views/scribe/
resources/views/scribe/index.blade.php
copy_db_to_dev.sh
backup_viksaqms_zip.sh
+475
View File
@@ -0,0 +1,475 @@
# Citrus CMS - Geliştirici Dokümantasyonu
## İçindekiler
1. [Proje Genel Bakış](#proje-genel-bakış)
2. [Klasör Yapısı](#klasör-yapısı)
3. [Modül Sistemi](#modül-sistemi)
4. [Kimlik Doğrulama (Auth) Sistemi](#kimlik-doğrulama-auth-sistemi)
5. [Types Tablosu ve Kullanımı](#types-tablosu-ve-kullanımı)
6. [Controller Yapısı](#controller-yapısı)
7. [Helper Fonksiyonlar](#helper-fonksiyonlar)
8. [DevExtreme Entegrasyonu](#devextreme-entegrasyonu)
9. [Admin-AJAX Sistemi](#admin-ajax-sistemi)
10. [Şablonlar ve Bileşenler](#şablonlar-ve-bileşenler)
11. [Yeni Modül Geliştirme](#yeni-modül-geliştirme)
12. [Middleware ve İzin Sistemi](#middleware-ve-izin-sistemi)
## Proje Genel Bakış
Bu proje, Laravel framework'ü üzerine inşa edilmiş, Trunçgil Teknoloji tarafından geliştirilen Citrus CMS yönetim paneli uygulamasıdır. Temel olarak aşağıdaki özellikleri içermektedir:
- Modüler yapı
- Kullanıcı yetkilendirme ve rol bazlı izin sistemi
- DevExtreme ile gelişmiş kullanıcı arayüzü
- Excel import/export işlemleri
- Dinamik form ve alan yönetimi
- Çoklu dil desteği
- Çeşitli helper fonksiyonlar
## Klasör Yapısı
Projenin ana klasör yapısı Laravel'in standart yapısına uygundur, ancak bazı özel klasör ve dosyalar eklenmiştir:
- **app/** - Uygulamanın ana kaynak kodu
- **Functions/** - Özel fonksiyonlar
- **Helpers/** - Yardımcı fonksiyonlar
- **Models/** - Veritabanı modelleri
- **DevExtreme/** - DevExtreme entegrasyonu için gerekli sınıflar
- **Http/Controllers/** - Controller sınıfları
- **resources/** - Görünüm şablonları ve ön yüz varlıkları
- **routes/** - Uygulama rotaları
- **config/** - Yapılandırma dosyaları
- **public/** - Genel erişilebilir dosyalar
- **storage/** - Uygulama tarafından oluşturulan dosyalar için depolama alanı
## Modül Sistemi
Proje modüler bir yapıda geliştirilmiştir. Her bir modül (örneğin WeldLog, TestPackage, vb.) kendi Model, Controller ve View dosyalarına sahiptir. Modüller, `Types` tablosu üzerinden yönetilmektedir.
### Modül Yapısı
Tipik bir modül şu bileşenlerden oluşur:
1. **Model** - `app/Models/` altında
2. **Controller** - `app/Http/Controllers/` altında
3. **Views** - `resources/views/admin/` altında
4. **Routes** - `routes/web.php` içinde tanımlı
## Kimlik Doğrulama (Auth) Sistemi
Laravel'in varsayılan kimlik doğrulama sistemi kullanılmaktadır, ancak genişletilmiş yetkilendirme ve rol yönetimi uygulanmıştır.
### Kullanıcı Seviyeleri
Kullanıcılar farklı seviyelere (level) sahip olabilir:
- Admin
- Manager
- User
- vb.
Her seviye için farklı izin ayarları tanımlanabilir.
### Middleware
Uygulamada `PermissionControl` adlı özel bir middleware kullanılmakta, bu middleware kullanıcının erişim isteklerini kontrol etmektedir.
```php
// Middleware örneği
if(in_array($thisPath, $onlyAdminPath)) {
if(!isAdmin()) {
echo "Only admin can access this area.";
exit();
}
}
```
## Types Tablosu ve Kullanımı
`types` tablosu, sistemdeki modülleri tanımlamak için kullanılır. Bu tablo aşağıdaki alanları içerir:
- `id` - Benzersiz tanımlayıcı
- `title` - Modül başlığı
- `slug` - URL-dostu benzersiz string
- `fields` - Modülün içerdiği alanlar (virgülle ayrılmış liste)
Her modül için gerekli alanlar `Fields` tablosunda saklanır ve bu tablo aracılığıyla formlar ve tablolar dinamik olarak oluşturulur.
## Controller Yapısı
Projede controller'lar genellikle şu yapıda düzenlenmiştir:
### AdminController
`AdminController` temel CRUD işlemleri ve admin paneli yönetimi için kullanılır. Örnek metotlar:
- `index()` - Admin panel ana sayfası
- `action()` - İçerik ekleme, silme, güncelleme
- `importExcel()` - Excel'den veri içe aktarma
- `exportExcel()` - Veriyi Excel olarak dışa aktarma
- `delete()` - Kayıt silme
### Modül Controllerleri
Her modül genellikle kendi controller'ına sahiptir. Örneğin `WeldLogController`, `TestPackageController` vb.
## Helper Fonksiyonlar
Projede çeşitli yardımcı fonksiyonlar bulunmaktadır. Bunlar `app/Functions/` ve `app/Helpers/` dizinlerinde saklanır.
Sık kullanılan helper fonksiyonlardan bazıları:
- `u()` - Mevcut kullanıcıyı döndürür
- `db($table)` - Belirtilen tabloya erişim sağlar
- `setting($key)` - Sistem ayarlarına erişim
- `isAdmin()` - Kullanıcının admin olup olmadığını kontrol eder
- `userPermissions()` - Kullanıcı izinlerini döndürür
## DevExtreme Entegrasyonu
Proje, kullanıcı arayüzü için DevExtreme bileşenlerini kullanmaktadır. DevExtreme, veri tablolarını ve formları hızlı bir şekilde oluşturmak için kullanılan JavaScript tabanlı UI komponent kütüphanesidir.
### DevExtreme Bileşenleri ve Yapısı
DevExtreme entegrasyonu için özel sınıflar `app/DevExtreme/` klasöründe bulunur:
- `Utils` - Genel yardımcı fonksiyonlar
- `FilterHelper` - Filtreleme için yardımcı fonksiyonlar
- `DbSet` - Veritabanı sorguları için yardımcı sınıf
### CRUD İşlemleri
**Önemli Not**: Sistemdeki tüm modüllerin CRUD (Create, Read, Update, Delete) işlemleri `AdminController.php` üzerinden yürütülmektedir. Bu merkezi yaklaşım, kod tekrarını önler ve tutarlı bir API sağlar.
`AdminController` içindeki temel CRUD metotları:
- `tableToJson` - Veritabanı tablosunu JSON formatına dönüştürür (Read)
- `insertJson` - Yeni kayıt ekler (Create)
- `saveJson` - Mevcut kaydı günceller (Update)
- `removeJson` - Kaydı siler (Delete)
Tüm modüller, işlemlerini bu metotlar üzerinden gerçekleştirir ve özel bir controller geliştirmeye gerek kalmaz.
### DevExtreme DataGrid
DataGrid, veri tablosunu görüntülemek için kullanılan ana bileşendir. JavaScript tarafında aşağıdaki özellikleri destekler:
- Filtreleme ve sıralama
- Sayfalama
- Satır düzenleme
- Grup oluşturma
- Özet bilgileri
- Özel düğmeler ve işlemler
## Admin-AJAX Sistemi
Projede AJAX tabanlı istekler için özel bir sistem kullanılmaktadır. Bu sistem, `AdminAjaxController` tarafından yönetilir ve admin paneli içerisinde yapılan asenkron istekleri işler.
### URL Yapısı
Admin-AJAX istekleri için URL yapısı şu şekildedir:
```
/admin-ajax/{işlem_adı}
```
Burada `{işlem_adı}`, gerçekleştirilecek AJAX işlemini belirtir. Örneğin:
```
/admin-ajax/get-users
/admin-ajax/update-status
/admin-ajax/save-form
```
### Controller Yapısı
Admin-AJAX Controller'ı şu şekilde çalışır:
```php
// app/Http/Controllers/AdminAjaxController.php
public function index(Request $request, string $var) {
$u = Auth::user();
if(!isset($u->level)) {
echo("permission error");
exit();
}
$url = "admin-ajax.$var";
return view($url, array(
"request" => $request
));
}
```
Bu yapı, AJAX isteğini uygun view dosyasına yönlendirir. View adı `admin-ajax.{işlem_adı}` formatında olmalıdır.
### View Dosyaları
AJAX view dosyaları `resources/views/admin-ajax/` dizininde bulunur. Her bir AJAX işlemi için ayrı bir blade dosyası oluşturulmalıdır:
```
resources/views/admin-ajax/get-users.blade.php
resources/views/admin-ajax/update-status.blade.php
resources/views/admin-ajax/save-form.blade.php
```
### Yeni AJAX İşlemi Ekleme
Yeni bir AJAX işlemi eklemek için:
1. İlgili view dosyasını oluşturun:
```php
// resources/views/admin-ajax/yeni-islem.blade.php
@php
// İsteği işleyin
$data = db('tablo_adi')->get();
// JSON formatında yanıt döndürün
echo json_encode([
'status' => 'success',
'data' => $data
]);
@endphp
```
2. View dosyasını çağıran JavaScript kodu:
```javascript
$.ajax({
url: '/admin-ajax/yeni-islem',
type: 'POST',
data: {
parametre1: deger1,
parametre2: deger2,
_token: csrf_token // CSRF koruması için gerekli
},
success: function(response) {
// Başarılı yanıtı işleyin
},
error: function(error) {
// Hatayı işleyin
}
});
```
### Güvenlik Önlemleri
Admin-AJAX isteklerinde şu güvenlik önlemleri uygulanmaktadır:
- Tüm istekler için kimlik doğrulama kontrolü
- CSRF koruma tokeni
- Kullanıcı seviyesine göre yetkilendirme
## Şablonlar ve Bileşenler
Proje, birçok tekrar kullanılabilir şablon ve bileşen içerir. Bu bileşenler, modül geliştirme sürecini kolaylaştırır ve tutarlı bir kullanıcı arayüzü sağlar.
### Şablon Sistemi
Sistem içinde kullanılan ana şablonlar:
#### `sablon4.blade.php`
`resources/views/admin/type/sablon4.blade.php` dosyası, modül sayfalarını oluşturmak için kullanılan temel şablondur. Yeni bir modül geliştirirken, model ve migration oluşturduktan sonra bu şablonu kullanmak genellikle yeterlidir. Şablonun başında tanımlanan değişkenler ile özelleştirilebilir.
Temel özelleştirme değişkenleri:
```php
// Modül başlığı
$title = "Radiographic Test";
// Tabloyu oluşturmak için kullanılan model
$listDatas = RadiographicTest::orderBy("id","DESC")->paginate(setting('row_count'));
// Veritabanı tablo adı
$tableName = "radiographic_tests";
// Blok grupları - alanları kategorilere bölmek için
$blockGroup = [
'Info' => [
'report_file',
'contractor',
],
// Diğer gruplar...
];
// Dosya yükleme klasörü
$firstUploadFolder = '004_QA/0001_RT/';
// Dosya yükleme başlığı
$firstUploadTitle = 'Upload PDF';
```
#### `module-block.blade.php`
`resources/views/components/blocks/module-block.blade.php` dosyası, modülün ana içeriğini oluşturan ve DataGrid'i görüntüleyen bileşendir. Bu bileşen, `sablon4.blade.php` içinde çağrılır ve aşağıdaki özellikleri içerir:
- Alan listesi oluşturma
- Alan tiplerini belirleme
- Sütun seçimini yönetme
- Excel içe/dışa aktarma işlemleri
- DataGrid entegrasyonu
```php
// Dışa aktarma, sütun seçimi ve diğer işlemler için temel ayarlar
$options = [
'export' => $tableName,
];
// DataGrid dahil etme
@include("components.table.$tableType")
```
#### `datagrid.blade.php`
`resources/views/components/table/datagrid.blade.php` dosyası, DevExtreme DataGrid'i entegre eder ve aşağıdaki özellikleri sağlar:
- Sütun tiplendirme ve düzenleme
- Filtreleme ve sıralama
- Veri işleme (CRUD)
- Klavye kısayolları
- Özel stil ve davranışlar
- Validation işlemleri
Bu dosya, JavaScript kodunu da içerir ve DataGrid'in tüm davranışlarını yapılandırır:
```javascript
var dataGrid = $("#dataGrid").dxDataGrid({
// DataGrid ayarları
dataSource: dataSource,
keyExpr: "{{$tableName}}",
allowColumnResizing: true,
// Diğer yapılandırmalar...
});
```
#### `upload.blade.php`
`resources/views/admin/type/document/upload.blade.php` dosyası, dosya yükleme işlemlerini yönetir. Özellikle PDF dosyalarını yüklemek için kullanılır. Aşağıdaki özellikleri içerir:
- Dropzone.js entegrasyonu
- Modal pencere ile yükleme arayüzü
- Birden fazla yükleme bölümü
- Özel klasörlere dosya yükleme
Bu bileşen, sablon4.blade.php içinde `@include("admin.type.document.upload")` ile dahil edilir.
### Form Elemanları ve Alan Tipleri
DataGrid içinde kullanılabilen farklı alan tipleri vardır:
- `text` - Standart metin girişi
- `date` - Tarih seçici
- `number` - Sayısal değer girişi
- `select` - Dropdown liste
- `auto-complete` - Otomatik tamamlama
- `link-search` - Bağlantı ve arama
- `decimal` - Ondalık sayı girişi
- `checkbox` - Onay kutusu
Her alan tipi için `components/table/datagrid/{tip}.blade.php` altında bir şablon vardır.
Örnek olarak select tipi için kullanım:
```php
$relationDatas = [
'field_name' => [
'type' => 'select',
'datas' => [
'option1' => 'Option 1',
'option2' => 'Option 2'
]
]
];
```
## Yeni Modül Geliştirme
Yeni bir modül geliştirmek için aşağıdaki adımları takip etmeniz gerekir:
1. **Model ve Migration Oluşturun**:
```php
// app/Models/YeniModul.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class YeniModul extends Model
{
protected $table = 'yeni_modul';
protected $primaryKey = 'id';
public $timestamps = true;
}
```
```php
// database/migrations/xxxx_create_yeni_modul_table.php
public function up()
{
Schema::create('yeni_modul', function (Blueprint $table) {
$table->id();
$table->string('ad')->nullable();
$table->text('aciklama')->nullable();
$table->timestamps();
});
}
```
2. **Şablonu Kopyalayın ve Özelleştirin**:
- `resources/views/admin/type/` dizini altında yeni bir blade dosyası oluşturun
- `sablon4.blade.php` temel alınarak özelleştirme yapabilirsiniz
```php
<?php
use App\Models\YeniModul;
$title = "Yeni Modül";
$tableWidth="100%";
$listDatas = YeniModul::orderBy("id","DESC")->paginate(setting('row_count'));
$tableName = "yeni_modul";
$blockGroup = [
'Genel Bilgiler' => [
'ad',
'aciklama',
],
// Diğer gruplar...
];
$firstUploadFolder = 'YeniModul/';
$firstUploadTitle = 'Dosya Yükle';
?>
<div class="content">
<div class="row">
@include("admin.type.document.upload")
@include("components.blocks.module-block")
</div>
</div>
```
3. **Type Tablosuna Kayıt Ekleyin**:
- Admin panelinden yeni bir tip ekleyin
- Modül için gerekli alanları belirtin
- URL adresi olarak oluşturduğunuz şablonu belirtin
**Önemli Not**: Citrus CMS'de yeni bir modül oluşturmak için genellikle model ve migration oluşturmak, ardından `sablon4.blade.php` şablonunu özelleştirmek yeterlidir. Tüm CRUD işlemleri otomatik olarak `AdminController.php` üzerinden yönetilir ve özel bir controller kodlamanıza gerek yoktur.
## Middleware ve İzin Sistemi
Uygulamada izin kontrolü şu şekilde yapılmaktadır:
1. **Kullanıcı Seviyeleri**: Her kullanıcı bir seviyeye sahiptir (level alanı)
2. **İzin Tanımları**: Her modül için okuma, yazma, tam kontrol izinleri tanımlanır
3. **Middleware Kontrolü**: Kullanıcının sahip olduğu seviyeye göre izin kontrolleri yapılır
```php
// İzin kontrolü örneği
if(isAuth($modulAdi, "read")) {
// Okuma izni var, işleme devam et
} else {
// İzin yok, erişim engellendi
}
```
### Kullanıcı İzin Kontrolü
`userPermissions()` fonksiyonu kullanıcının izinlerini döndürür ve `isAuth()` fonksiyonu belirli bir işlem için izin kontrolü yapar.
---
Bu dokümantasyon, projenin temel yapısını ve özelliklerini kapsamaktadır. Daha detaylı bilgi için ilgili kaynak kodları inceleyebilir veya ekip ile iletişime geçebilirsiniz.
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CacheBladeViews extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cache:blade-views';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Dispatch jobs to cache specified blade views';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Starting to dispatch cache blade views...');
$views = [
[
'view' => 'admin-ajax.register-no-cache',
'cache' => 'register-creator'
],
[
'view' => 'admin-ajax.request-ndt-no-cache',
'cache' => 'request-ndt'
],
[
'view' => 'admin-ajax.repair-log-no-cache',
'cache' => 'repair-log'
],
[
'view' => 'admin-ajax.ndt-order.order-list-no-cache',
'cache' => 'ndt-order-list'
],
[
'view' => 'admin-ajax.spool-area-release-no-cache',
'cache' => 'spool-area-release'
],
[
'view' => 'admin-ajax.spool-list-no-cache',
'cache' => 'spool-list'
],
[
'view' => 'admin.type.spool-release.spool-list-chart-no-cache',
'cache' => 'spool-list-chart'
],
];
dispatchCacheBladeViews($views);
$this->info('Cache blade views dispatched successfully.');
return 0;
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Carbon\Carbon;
class CheckNDTReminders extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-ndt';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check NDT tests and send reminder notifications for overdue or pending tests';
/**
* Execute the console command.
* Performance: Optimized with chunking and bulk operations
*
* @return int
*/
public function handle()
{
$this->info('Starting NDT reminder checks...');
$totalNotifications = 0;
// Check overdue NDT tests
$totalNotifications += $this->checkOverdueTests();
// Check missing test reports
$totalNotifications += $this->checkMissingReports();
// Check deadline approaching tests (within 3 days)
$totalNotifications += $this->checkDeadlineApproaching();
// Check pending results
$totalNotifications += $this->checkPendingResults();
$this->info("NDT reminder check completed. Sent {$totalNotifications} notifications.");
return 0;
}
/**
* Check for overdue NDT tests
* Performance: Single query with multiple conditions
*/
private function checkOverdueTests()
{
$overdueThreshold = Carbon::now()->subDays(7); // 7 days overdue
$nullableColumns = $this->availableColumns('weld_logs', [
'rt_testing_date',
'ut_testing_date',
'pt_testing_date',
'mt_testing_date'
]);
if (empty($nullableColumns)) {
$this->info('Skipping overdue check: weld_logs table lacks test date columns.');
return 0;
}
$ids = DB::table('weld_logs')
->where('welding_date', '<=', $overdueThreshold)
->where(function($query) use ($nullableColumns) {
foreach ($nullableColumns as $column) {
$query->orWhereNull($column);
}
})
->pluck('id')
->toArray();
return $this->sendWeldLogBatch(
'notification_ndt_test_overdue',
'NDT Test Overdue',
'%d weld log(s) have welding dates older than 7 days without NDT testing.',
$ids
);
}
/**
* Check for missing test reports
* Performance: Checks only records with test dates but no results
*/
private function checkMissingReports()
{
$checkDate = Carbon::now()->subDays(3); // 3 days after test date
$rtIds = [];
if ($this->columnsAvailable('weld_logs', ['rt_testing_date', 'rt_result'])) {
$rtIds = DB::table('weld_logs')
->whereNotNull('rt_testing_date')
->where('rt_testing_date', '<=', $checkDate)
->where(function($query) {
$query->whereNull('rt_result')
->orWhere('rt_result', '');
})
->pluck('id')
->toArray();
}
$utIds = [];
if ($this->columnsAvailable('weld_logs', ['ut_testing_date', 'ut_result'])) {
$utIds = DB::table('weld_logs')
->whereNotNull('ut_testing_date')
->where('ut_testing_date', '<=', $checkDate)
->where(function($query) {
$query->whereNull('ut_result')
->orWhere('ut_result', '');
})
->pluck('id')
->toArray();
}
$notifications = 0;
$notifications += $this->sendWeldLogBatch(
'notification_ndt_report_missing',
'RT Report Missing',
'%d RT test(s) have results pending more than 3 days.',
$rtIds
);
$notifications += $this->sendWeldLogBatch(
'notification_ndt_report_missing',
'UT Report Missing',
'%d UT test(s) have results pending more than 3 days.',
$utIds
);
return $notifications;
}
/**
* Check for tests with approaching deadlines
* Performance: Single query with date calculation
*/
private function checkDeadlineApproaching()
{
$warnDate = Carbon::now()->subDays(4); // Welded 4 days ago, should be tested within 7 days
$graceDate = Carbon::now()->subDays(7);
$dateColumns = array_filter($this->availableColumns('weld_logs', ['rt_testing_date', 'ut_testing_date']));
if (empty($dateColumns)) {
$this->info('Skipping deadline approaching check: weld_logs table lacks RT/UT testing date columns.');
return 0;
}
$ids = DB::table('weld_logs')
->whereBetween('welding_date', [$graceDate, $warnDate]) // Fixed: older date first, newer date second
->where(function($query) use ($dateColumns) {
foreach ($dateColumns as $column) {
$query->orWhereNull($column);
}
})
->pluck('id')
->toArray();
return $this->sendWeldLogBatch(
'notification_test_deadline_approaching',
'Test Deadline Approaching',
'%d weld log(s) are approaching the 7-day NDT testing deadline.',
$ids
);
}
/**
* Check for pending test results
* Performance: Optimized with specific conditions
*/
private function checkPendingResults()
{
$pendingThreshold = Carbon::now()->subDays(2);
$ids = [];
$rtColumnsAvailable = $this->columnsAvailable('weld_logs', ['rt_testing_date', 'rt_result']);
$utColumnsAvailable = $this->columnsAvailable('weld_logs', ['ut_testing_date', 'ut_result']);
if (!$rtColumnsAvailable && !$utColumnsAvailable) {
$this->info('Skipping pending results check: weld_logs table lacks RT/UT result columns.');
return 0;
}
$query = DB::table('weld_logs')
->where(function($query) use ($pendingThreshold) {
if ($this->columnsAvailable('weld_logs', ['rt_testing_date', 'rt_result'])) {
$query->orWhere(function($q) use ($pendingThreshold) {
$q->whereNotNull('rt_testing_date')
->where('rt_testing_date', '<=', $pendingThreshold)
->where(function($sq) {
$sq->whereNull('rt_result')
->orWhere('rt_result', '');
});
});
}
if ($this->columnsAvailable('weld_logs', ['ut_testing_date', 'ut_result'])) {
$query->orWhere(function($q) use ($pendingThreshold) {
$q->whereNotNull('ut_testing_date')
->where('ut_testing_date', '<=', $pendingThreshold)
->where(function($sq) {
$sq->whereNull('ut_result')
->orWhere('ut_result', '');
});
});
}
});
$ids = $query->pluck('id')->toArray();
return $this->sendWeldLogBatch(
'notification_ndt_result_pending',
'NDT Result Pending',
'%d weld log(s) have completed NDT tests but results are pending more than 2 days.',
$ids
);
}
private function sendWeldLogBatch(string $notificationCode, string $title, string $messageTemplate, array $ids): int
{
$ids = array_values(array_unique(array_filter($ids)));
if (empty($ids)) {
$this->info("No {$title} notifications to send.");
return 0;
}
$filterParams = [
'table' => 'weld_logs',
'conditions' => [
'id' => $ids
]
];
sendBatchNotificationWithFilter(
$notificationCode,
$title,
$messageTemplate,
$filterParams,
count($ids)
);
$this->info("Sent batch notification for {$title} (" . count($ids) . " joints).");
return 1;
}
private function availableColumns(string $table, array $columns): array
{
return array_values(array_filter($columns, function($column) use ($table) {
return Schema::hasColumn($table, $column);
}));
}
private function columnsAvailable(string $table, array $columns): bool
{
return count($this->availableColumns($table, $columns)) === count($columns);
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Console\Commands;
use App\Services\DocumentManager\FolderCatalog;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class DocumentSyncFolders extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'document:sync-folders {--dry-run : Show operations without creating directories} {--root= : Override the documents root path}';
/**
* The console command description.
*/
protected $description = 'Synchronise the Document Manager folder structure using the standard catalog definition.';
public function handle(FolderCatalog $catalog): int
{
$rootPath = $this->option('root') ?: storage_path('documents');
$dryRun = (bool) $this->option('dry-run');
$this->info('Document Manager folder synchronisation');
$this->line('Root path: ' . $rootPath);
$this->line($dryRun ? 'Mode: Dry run (no directories will be created)' : 'Mode: Live');
$this->newLine();
if (! File::isDirectory($rootPath)) {
if ($dryRun) {
$this->warn('Documents directory does not exist. (dry-run)');
} else {
File::ensureDirectoryExists($rootPath);
$this->info('Created documents root directory.');
}
}
$results = $catalog->sync($rootPath, $dryRun);
if (! empty($results['created'])) {
$title = $dryRun ? 'Directories that would be created' : 'Directories created';
$this->section($title);
$this->displayList($results['created']);
$this->newLine();
}
if (! empty($results['existing'])) {
$this->section('Directories already present');
$this->displayList($results['existing']);
$this->newLine();
}
// Detect directories that are not part of the catalog
$extras = $catalog->detectExtra($rootPath);
if ($extras->isNotEmpty()) {
$this->section('Directories not defined in the catalog');
$sorted = $extras->sort()->values();
$this->displayList($sorted->all());
$this->newLine();
}
$this->info('Document Manager folder synchronisation completed.');
if ($dryRun) {
$this->comment('Dry run mode - no changes were applied.');
}
return self::SUCCESS;
}
protected function section(string $title): void
{
$this->line('<options=bold>' . $title . '</>');
}
protected function displayList(array $items, int $limit = 20): void
{
$items = array_values(array_unique($items));
$total = count($items);
$display = array_slice($items, 0, $limit);
foreach ($display as $item) {
$this->line(' - ' . $item);
}
if ($total > $limit) {
$this->line(' ... ' . ($total - $limit) . ' more');
}
}
}
@@ -0,0 +1,450 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class GenerateEndpointsDocs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'docs:endpoints
{--append : Append to existing OpenAPI spec}
{--output= : Output file path}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate API documentation for admin-ajax endpoints and append to Scribe OpenAPI spec';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Scanning admin-ajax endpoints...');
$endpoints = $this->scanEndpoints();
$this->info("Found " . count($endpoints) . " endpoints");
if ($this->option('append')) {
$this->appendToOpenApiSpec($endpoints);
} else {
$this->generateStandaloneSpec($endpoints);
}
$this->info('✅ Endpoints documentation generated successfully!');
return Command::SUCCESS;
}
/**
* Scan admin-ajax directory for blade files
*/
protected function scanEndpoints(): array
{
$endpoints = [];
$path = resource_path('views/admin-ajax');
if (!File::isDirectory($path)) {
$this->error('admin-ajax directory not found');
return $endpoints;
}
// Scan files recursively
$this->scanDirectory($path, '', $endpoints);
// Sort alphabetically
usort($endpoints, fn($a, $b) => strcmp($a['name'], $b['name']));
return $endpoints;
}
/**
* Recursively scan directory for blade files
* SECURITY: Only includes endpoints marked with @api-readonly annotation
*/
protected function scanDirectory(string $basePath, string $prefix, array &$endpoints): void
{
$items = File::files($basePath);
foreach ($items as $file) {
$filename = $file->getFilename();
// Only process .blade.php files
if (!Str::endsWith($filename, '.blade.php')) {
continue;
}
// Extract endpoint name
$name = str_replace('.blade.php', '', $filename);
$fullName = $prefix ? "{$prefix}/{$name}" : $name;
// Analyze file for response type hints
$content = File::get($file->getPathname());
// SECURITY: Only include endpoints marked as @api-readonly
if (!Str::contains($content, '@api-readonly')) {
continue;
}
$responseType = $this->detectResponseType($content);
$description = $this->extractDescription($content, $name);
$category = $this->categorizeEndpoint($fullName);
$endpoints[] = [
'name' => $fullName,
'path' => str_replace(base_path(), '', $file->getPathname()),
'response_type' => $responseType,
'description' => $description,
'category' => $category,
];
}
// Scan subdirectories
$directories = File::directories($basePath);
foreach ($directories as $dir) {
$dirName = basename($dir);
$newPrefix = $prefix ? "{$prefix}/{$dirName}" : $dirName;
$this->scanDirectory($dir, $newPrefix, $endpoints);
}
}
/**
* Detect response type from blade content
*/
protected function detectResponseType(string $content): string
{
// Check for JSON indicators
if (
Str::contains($content, 'json_encode') ||
Str::contains($content, 'json_encode_tr') ||
Str::contains($content, 'response()->json') ||
Str::contains($content, "'status' =>")
) {
return 'json';
}
// Check for HTML indicators
if (
Str::contains($content, '<html') ||
Str::contains($content, '<div') ||
Str::contains($content, '@extends') ||
Str::contains($content, '@include')
) {
return 'html';
}
return 'auto';
}
/**
* Extract description from blade file comments
*/
protected function extractDescription(string $content, string $name): string
{
// Try to find PHPDoc style comment at the start
if (preg_match('/^<\?php\s*\/\*\*?\s*\n?\s*\*?\s*(.+?)(?:\n|\*\/)/s', $content, $matches)) {
return trim($matches[1]);
}
// Try to find HTML comment
if (preg_match('/<!--\s*(.+?)\s*-->/s', $content, $matches)) {
return trim($matches[1]);
}
// Generate description from name
return 'Execute ' . Str::title(str_replace(['-', '_'], ' ', $name)) . ' endpoint';
}
/**
* Categorize endpoint based on name
*/
protected function categorizeEndpoint(string $name): string
{
$prefixes = [
'ndt-' => 'NDT Operations',
'welder-' => 'Welder Management',
'weld' => 'Welding',
'register-' => 'Register Creator',
'test-package' => 'Test Packages',
'tp-' => 'Test Packages',
'document-' => 'Document Management',
'report-' => 'Reports',
'summary' => 'Summaries',
'excel-' => 'Excel Operations',
'pdf' => 'PDF Operations',
'user' => 'User Management',
'permission' => 'Permissions',
'content-' => 'Content Management',
'spool-' => 'Spool Operations',
'material-' => 'Materials',
'paint-' => 'Paint Operations',
'repair-' => 'Repair Operations',
'rfi-' => 'RFI Operations',
'punch-' => 'Punch Lists',
'mto' => 'MTO Operations',
'ai-' => 'AI Features',
'cron-' => 'Cron Jobs',
'si-' => 'System Info',
];
$baseName = Str::contains($name, '/') ? Str::afterLast($name, '/') : $name;
foreach ($prefixes as $prefix => $category) {
if (Str::startsWith($baseName, $prefix)) {
return $category;
}
}
return 'General';
}
/**
* Append endpoints to existing OpenAPI spec
*/
protected function appendToOpenApiSpec(array $endpoints): void
{
$specPath = storage_path('app/scribe/openapi.yaml');
if (!File::exists($specPath)) {
$this->warn('OpenAPI spec not found at ' . $specPath);
$this->info('Run "php artisan scribe:generate" first, then run this command with --append');
return;
}
// Load existing spec
$spec = yaml_parse_file($specPath);
if (!$spec) {
$this->error('Failed to parse OpenAPI spec');
return;
}
// Add endpoint paths
foreach ($endpoints as $endpoint) {
$pathKey = '/api/endpoints/' . $endpoint['name'];
$spec['paths'][$pathKey] = $this->generatePathSpec($endpoint);
}
// Add Endpoints tag if not exists
$hasEndpointsTag = false;
foreach ($spec['tags'] ?? [] as $tag) {
if ($tag['name'] === 'Endpoints') {
$hasEndpointsTag = true;
break;
}
}
if (!$hasEndpointsTag) {
$spec['tags'][] = [
'name' => 'Endpoints',
'description' => 'Admin-ajax endpoints served via API'
];
}
// Save updated spec
$yaml = yaml_emit($spec, YAML_UTF8_ENCODING);
File::put($specPath, $yaml);
$this->info('Updated OpenAPI spec at ' . $specPath);
}
/**
* Generate standalone endpoints spec file
*/
protected function generateStandaloneSpec(array $endpoints): void
{
$outputPath = $this->option('output') ?: storage_path('app/scribe/endpoints.json');
// Group endpoints by category
$grouped = [];
foreach ($endpoints as $endpoint) {
$category = $endpoint['category'];
if (!isset($grouped[$category])) {
$grouped[$category] = [];
}
$grouped[$category][] = $endpoint;
}
$output = [
'generated_at' => now()->toIso8601String(),
'total_endpoints' => count($endpoints),
'categories' => array_keys($grouped),
'endpoints' => $endpoints,
'grouped' => $grouped,
];
File::ensureDirectoryExists(dirname($outputPath));
File::put($outputPath, json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$this->info('Generated endpoints spec at ' . $outputPath);
// Generate Scribe response file for dynamic documentation
$this->generateScribeResponseFile($endpoints);
// Also generate markdown documentation
$this->generateMarkdownDocs($grouped);
}
/**
* Generate response file for Scribe documentation
*/
protected function generateScribeResponseFile(array $endpoints): void
{
$responsePath = storage_path('app/scribe/endpoints-response.json');
// Format endpoints with url and methods for API response
$formattedEndpoints = array_map(function ($endpoint) {
return [
'name' => $endpoint['name'],
'url' => '/api/endpoints/' . $endpoint['name'],
'methods' => ['GET', 'POST'],
'response_type' => $endpoint['response_type'],
'description' => $endpoint['description'],
'category' => $endpoint['category'],
];
}, $endpoints);
$response = [
'status' => 'success',
'data' => [
'endpoints' => $formattedEndpoints,
'total' => count($formattedEndpoints)
]
];
File::ensureDirectoryExists(dirname($responsePath));
File::put($responsePath, json_encode($response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$this->info('Generated Scribe response file at ' . $responsePath);
}
/**
* Generate OpenAPI path specification for an endpoint
*/
protected function generatePathSpec(array $endpoint): array
{
$responseSchema = $endpoint['response_type'] === 'json'
? ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'data' => ['type' => 'object']]]
: ['type' => 'object', 'properties' => ['status' => ['type' => 'string'], 'html' => ['type' => 'string']]];
return [
'get' => [
'tags' => ['Endpoints'],
'summary' => $endpoint['description'],
'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}",
'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_'),
'security' => [['bearerAuth' => []]],
'responses' => [
'200' => [
'description' => 'Successful response',
'content' => [
'application/json' => [
'schema' => $responseSchema
]
]
],
'401' => [
'description' => 'Unauthorized'
],
'404' => [
'description' => 'Endpoint not found'
]
]
],
'post' => [
'tags' => ['Endpoints'],
'summary' => $endpoint['description'],
'description' => "Endpoint: {$endpoint['name']}\nResponse Type: {$endpoint['response_type']}\nCategory: {$endpoint['category']}",
'operationId' => 'endpoint_' . Str::slug($endpoint['name'], '_') . '_post',
'security' => [['bearerAuth' => []]],
'requestBody' => [
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'additionalProperties' => true
]
]
]
],
'responses' => [
'200' => [
'description' => 'Successful response',
'content' => [
'application/json' => [
'schema' => $responseSchema
]
]
],
'401' => [
'description' => 'Unauthorized'
],
'404' => [
'description' => 'Endpoint not found'
]
]
]
];
}
/**
* Generate markdown documentation
*/
protected function generateMarkdownDocs(array $grouped): void
{
$markdown = "# Available Endpoints\n\n";
$markdown .= "This document lists all available admin-ajax endpoints accessible via the API.\n\n";
$markdown .= "**Base URL:** `/api/endpoints/{endpoint}`\n\n";
$markdown .= "**Authentication:** Bearer Token required\n\n";
$markdown .= "---\n\n";
foreach ($grouped as $category => $endpoints) {
$markdown .= "## {$category}\n\n";
$markdown .= "| Endpoint | Description | Response Type |\n";
$markdown .= "|----------|-------------|---------------|\n";
foreach ($endpoints as $endpoint) {
$markdown .= "| `{$endpoint['name']}` | {$endpoint['description']} | {$endpoint['response_type']} |\n";
}
$markdown .= "\n";
}
$markdown .= "---\n\n";
$markdown .= "## Usage Examples\n\n";
$markdown .= "### cURL\n\n";
$markdown .= "```bash\n";
$markdown .= "curl -X GET \\\n";
$markdown .= " '{base_url}/api/endpoints/users' \\\n";
$markdown .= " -H 'Authorization: Bearer {your_token}' \\\n";
$markdown .= " -H 'Accept: application/json'\n";
$markdown .= "```\n\n";
$markdown .= "### JavaScript\n\n";
$markdown .= "```javascript\n";
$markdown .= "const response = await fetch('/api/endpoints/users', {\n";
$markdown .= " headers: {\n";
$markdown .= " 'Authorization': 'Bearer ' + token,\n";
$markdown .= " 'Accept': 'application/json'\n";
$markdown .= " }\n";
$markdown .= "});\n";
$markdown .= "const data = await response.json();\n";
$markdown .= "```\n";
$docsPath = resource_path('views/guide/api-endpoints.md');
File::ensureDirectoryExists(dirname($docsPath));
File::put($docsPath, $markdown);
$this->info('Generated markdown docs at ' . $docsPath);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
class MigrateFromDate extends Command
{
protected $signature = 'migrate:from-date {date}';
protected $description = 'Migrate migrations from a specific date';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$date = $this->argument('date');
$migrationsPath = database_path('migrations');
$migrations = File::files($migrationsPath);
foreach ($migrations as $migration) {
$migrationDate = substr($migration->getFilename(), 0, 10); // YYYY_MM_DD kısmını alır
if ($migrationDate > $date) {
Artisan::call('migrate', ['--path' => 'database/migrations/' . $migration->getFilename()]);
$this->info('Migrated: ' . $migration->getFilename());
}
}
}
}
+412
View File
@@ -0,0 +1,412 @@
<?php
namespace App\Console\Commands;
use App\Services\NaksSyncService;
use Illuminate\Console\Command;
/**
* NAKS Multi-Module Synchronization Command
*
* Synchronizes NAKS data across all project sites for multiple modules:
* - technology: NAKS Technology (naks_certificates)
* - welder: NAKS Welder (naks_welders)
* - consumables: NAKS Consumables (naks_consumables)
* - expert: NAKS Expert (register_of_experts)
* - equipment: NAKS Equipment (welding_equipment)
*
* Usage:
* php artisan naks:sync # Sync all modules from all projects
* php artisan naks:sync --module=welder # Sync only welder module
* php artisan naks:sync --project=101 # Sync specific project only
* php artisan naks:sync --dry-run # Test mode (no changes)
* php artisan naks:sync --force # Ignore last sync ID, sync all
* php artisan naks:sync --status # Show sync status only
* php artisan naks:sync --reset # Reset sync state
*/
class NaksSync extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'naks:sync
{--module=all : Module to sync (all, technology, welder, consumables, expert, equipment)}
{--project= : Filter to sync only a specific project (by name or URL fragment)}
{--dry-run : Run in test mode without making any changes}
{--force : Ignore last synced ID and sync all records}
{--status : Show sync status only, do not sync}
{--reset : Reset sync state for all or specific module/project}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronize NAKS data (certificates, welders, consumables, experts, equipment) from other project sites';
/**
* The sync service instance.
*/
protected NaksSyncService $syncService;
/**
* Create a new command instance.
*/
public function __construct(NaksSyncService $syncService)
{
parent::__construct();
$this->syncService = $syncService;
}
/**
* Execute the console command.
*/
public function handle(): int
{
$module = $this->option('module') ?? 'all';
$project = $this->option('project');
$dryRun = (bool) $this->option('dry-run');
$force = (bool) $this->option('force');
$statusOnly = (bool) $this->option('status');
$reset = (bool) $this->option('reset');
$this->info('╔════════════════════════════════════════════════════════════╗');
$this->info('║ NAKS Multi-Module Cross-Site Synchronization ║');
$this->info('╚════════════════════════════════════════════════════════════╝');
$this->newLine();
// Show available modules
$this->showAvailableModules();
// Handle status display
if ($statusOnly) {
return $this->showStatus($module);
}
// Handle reset
if ($reset) {
return $this->resetSyncState($module, $project);
}
// Validate module
if ($module !== 'all' && !$this->syncService->getModuleConfig($module)) {
$this->error("❌ Invalid module: {$module}");
$this->line("Available modules: all, technology, welder, consumables, expert, equipment");
return self::FAILURE;
}
// Show mode information
if ($dryRun) {
$this->warn('⚠️ DRY RUN MODE - No changes will be made');
$this->newLine();
}
if ($force) {
$this->warn('⚠️ FORCE MODE - Ignoring last sync ID, syncing all records');
$this->newLine();
}
$this->info("📦 Module(s): " . ($module === 'all' ? 'All modules' : $module));
if ($project) {
$this->info("📌 Project filter: {$project}");
}
$this->newLine();
// Check credentials
if (!env('SYNC_ADMIN_EMAIL') || !env('SYNC_ADMIN_PASSWORD')) {
$this->error('❌ SYNC_ADMIN_EMAIL and SYNC_ADMIN_PASSWORD must be set in .env');
return self::FAILURE;
}
// Show available projects
$this->showAvailableProjects();
// Run sync
$this->info('🔄 Starting synchronization...');
$this->newLine();
$startTime = microtime(true);
try {
$stats = $this->syncService->sync($module, $project, $dryRun, $force);
} catch (\Exception $e) {
$this->error('❌ Sync failed: ' . $e->getMessage());
return self::FAILURE;
}
$duration = round(microtime(true) - $startTime, 2);
// Display results
$this->displayResults($stats, $duration, $dryRun);
// Return appropriate exit code
if (!empty($stats['errors']) || !empty($stats['failed_projects'])) {
return self::FAILURE;
}
return self::SUCCESS;
}
/**
* Show available modules
*/
protected function showAvailableModules(): void
{
$modules = $this->syncService->getModules();
$this->info('📦 Available Modules:');
$tableData = [];
foreach ($modules as $key => $config) {
$tableData[] = [
$key,
$config['name'],
$config['table'],
implode(' + ', $config['unique_keys']),
];
}
$this->table(
['Key', 'Name', 'Table', 'Unique Keys'],
$tableData
);
$this->newLine();
}
/**
* Show available projects
*/
protected function showAvailableProjects(): void
{
try {
$projects = $this->syncService->getProjectUrls();
$this->info('📋 Available Projects:');
$this->table(
['Project Name', 'URL'],
array_map(function ($p) {
return [$p['project_name'] ?? 'Unknown', $p['url'] ?? ''];
}, $projects)
);
$this->newLine();
} catch (\Exception $e) {
$this->warn('Could not fetch project list: ' . $e->getMessage());
}
}
/**
* Display sync results
*/
protected function displayResults(array $stats, float $duration, bool $dryRun): void
{
$this->newLine();
$this->info('════════════════════════════════════════════════════════════');
$this->info(' SYNC RESULTS');
$this->info('════════════════════════════════════════════════════════════');
$this->newLine();
// Project summary
$this->table(
['Metric', 'Value'],
[
['Total Projects', $stats['total_projects']],
['Successful Projects', $stats['successful_projects']],
['Failed Projects', count($stats['failed_projects'])],
['Duration', "{$duration}s"],
]
);
// Module-specific results
if (!empty($stats['modules'])) {
$this->newLine();
$this->info('📦 Module Results:');
$moduleData = [];
$totalSynced = 0;
$totalInserted = 0;
$totalUpdated = 0;
$totalSkipped = 0;
$totalPdfs = 0;
foreach ($stats['modules'] as $key => $moduleStats) {
$moduleData[] = [
$moduleStats['name'],
$moduleStats['total_synced'],
$moduleStats['total_inserted'],
$moduleStats['total_updated'],
$moduleStats['total_skipped'],
$moduleStats['total_pdfs_downloaded'],
];
$totalSynced += $moduleStats['total_synced'];
$totalInserted += $moduleStats['total_inserted'];
$totalUpdated += $moduleStats['total_updated'];
$totalSkipped += $moduleStats['total_skipped'];
$totalPdfs += $moduleStats['total_pdfs_downloaded'];
}
// Add totals row
$moduleData[] = [
'─────────',
'─────',
'────────',
'───────',
'───────',
'────',
];
$moduleData[] = [
'TOTAL',
$totalSynced,
$totalInserted,
$totalUpdated,
$totalSkipped,
$totalPdfs,
];
$this->table(
['Module', 'Synced', 'Inserted', 'Updated', 'Skipped', 'PDFs'],
$moduleData
);
}
// Show failed projects
if (!empty($stats['failed_projects'])) {
$this->newLine();
$this->error('❌ Failed Projects:');
foreach ($stats['failed_projects'] as $failed) {
$this->line("{$failed['name']}: {$failed['error']}");
}
}
// Show errors
if (!empty($stats['errors'])) {
$this->newLine();
$this->error('❌ Errors:');
foreach ($stats['errors'] as $error) {
$this->line("{$error}");
}
}
$this->newLine();
if ($dryRun) {
$this->warn('️ This was a DRY RUN - no actual changes were made');
} else {
$hasSyncedRecords = false;
foreach ($stats['modules'] as $moduleStats) {
if ($moduleStats['total_synced'] > 0) {
$hasSyncedRecords = true;
break;
}
}
if ($hasSyncedRecords) {
$this->info('✅ Synchronization completed successfully!');
} else {
$this->info('️ No new records to sync');
}
}
}
/**
* Show sync status
*/
protected function showStatus(string $module): int
{
$this->info('📊 Sync Status:');
$this->newLine();
$tableName = null;
if ($module !== 'all') {
$config = $this->syncService->getModuleConfig($module);
if ($config) {
$tableName = $config['table'];
}
}
$states = $this->syncService->getSyncStates($tableName);
if (empty($states)) {
$this->warn('No sync history found. Run "php artisan naks:sync" to start syncing.');
return self::SUCCESS;
}
$tableData = array_map(function ($state) {
return [
$state->table_name ?? 'Unknown',
$state->source_url ?? 'Unknown',
$state->last_synced_id ?? 0,
$state->synced_count ?? 0,
$state->last_synced_at ?? 'Never',
];
}, $states);
$this->table(
['Table', 'Source URL', 'Last ID', 'Total Synced', 'Last Sync'],
$tableData
);
return self::SUCCESS;
}
/**
* Reset sync state
*/
protected function resetSyncState(string $module, ?string $project): int
{
$tableName = null;
if ($module !== 'all') {
$config = $this->syncService->getModuleConfig($module);
if ($config) {
$tableName = $config['table'];
} else {
$this->error("Invalid module: {$module}");
return self::FAILURE;
}
}
$message = 'Reset sync state for ';
$message .= $module === 'all' ? 'ALL modules' : "module: {$module}";
if ($project) {
$message .= " and project: {$project}";
}
$message .= '?';
if (!$this->confirm($message)) {
$this->info('Operation cancelled.');
return self::SUCCESS;
}
$sourceUrl = null;
if ($project) {
// Find matching project URL
try {
$projects = $this->syncService->getProjectUrls();
foreach ($projects as $p) {
if (str_contains(strtolower($p['url']), strtolower($project)) ||
str_contains(strtolower($p['project_name'] ?? ''), strtolower($project))) {
$sourceUrl = $p['url'];
break;
}
}
if (!$sourceUrl) {
$this->error("Project not found: {$project}");
return self::FAILURE;
}
} catch (\Exception $e) {
$this->error('Could not fetch project list: ' . $e->getMessage());
return self::FAILURE;
}
}
$deleted = $this->syncService->resetSyncState($tableName, $sourceUrl);
$this->info("✅ Reset {$deleted} sync state record(s)");
return self::SUCCESS;
}
}
@@ -0,0 +1,106 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class NotificationCheckAll extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-all';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run all notification check commands at once (for initial trigger)';
/**
* List of all notification commands to run
*
* @var array
*/
protected $commands = [
'notifications:check-deleted-joints',
'notifications:check-repair-log-new-joint',
'notifications:check-daily-repair-rate',
'notifications:check-line-list-missing-data',
'notifications:check-pdf-documents',
'notifications:check-weldlog-certificate',
'notifications:check-incoming-control-certificate',
'notifications:check-nde-matrix-discrepancies',
'notifications:check-ndt-calculation',
'notifications:check-manage-ndt-unnecessary',
'notifications:check-support-log-weldlog',
'notifications:check-paint-system-values',
'notifications:check-weldlog-test-dates',
'notifications:check-ndt-request-overdue',
'notifications:check-test-log-pdf',
];
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Running all notification checks...');
$this->info('This will trigger initial scan for all notification types.');
$this->newLine();
$totalCommands = count($this->commands);
$successCount = 0;
$skipCount = 0;
$errorCount = 0;
$bar = $this->output->createProgressBar($totalCommands);
$bar->start();
foreach ($this->commands as $index => $command) {
$bar->advance();
try {
$exitCode = $this->call($command);
if ($exitCode === 0) {
$successCount++;
} else {
$errorCount++;
$this->newLine();
$this->warn("Command '{$command}' returned exit code: {$exitCode}");
}
} catch (\Exception $e) {
$errorCount++;
$this->newLine();
$this->error("Error running '{$command}': " . $e->getMessage());
}
}
$bar->finish();
$this->newLine(2);
// Summary
$this->info('=== Summary ===');
$this->line("Total commands: {$totalCommands}");
$this->line("Successful: {$successCount}");
$this->line("Skipped (5 min check): {$skipCount}");
$this->line("Errors: {$errorCount}");
$this->newLine();
if ($errorCount > 0) {
$this->warn('Some commands encountered errors. Check logs for details.');
return 1;
}
$this->info('All notification checks completed successfully!');
$this->info('Scheduler will continue running checks every 5 minutes automatically.');
return 0;
}
}
@@ -0,0 +1,147 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckCalibrationDue extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-calibration-due';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check calibration logs for equipment with calibration due within 20 days or overdue and send notifications.';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
logNotificationStart($commandName);
$this->info('Checking Calibration Log for upcoming/overdue calibrations...');
$notificationCode = 'notification_calibration_due_soon';
try {
// Run at most every 5 minutes
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: last calibration notification was sent recently.');
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
$today = now()->startOfDay();
$limitDate = $today->copy()->addDays(20);
$query = DB::table('calibration_logs')
->whereNotNull('calibration_due_date')
->whereDate('calibration_due_date', '<=', $limitDate->toDateString());
// Optional: skip records explicitly marked as completed
$query->where(function ($q) {
$q->whereNull('status')
->orWhereNotIn('status', ['Completed']);
});
if ($lastCheck !== null) {
$query->where(function ($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new calibration records found for notification.');
logNotificationComplete($commandName, 0, 0, 0, microtime(true) - $startTime);
return 0;
}
$filterParams = [
'table' => 'calibration_logs',
'conditions' => [],
];
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: no new calibration records since last notification.');
logNotificationComplete($commandName, 0, count($allIds), 0, microtime(true) - $startTime);
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: duplicate calibration notification with same filter params.');
logNotificationComplete($commandName, 0, count($filterParams['conditions']['id']), 0, microtime(true) - $startTime);
return 0;
}
$recordCount = count($filterParams['conditions']['id']);
$newCount = $lastCheck !== null
? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode)))
: $recordCount;
$messageTemplate = '%d equipment item(s) have calibration due within 20 days or are already overdue. Please review the Calibration Log module.';
$sent = sendBatchNotificationWithFilter(
$notificationCode,
'Calibration Due Soon',
$messageTemplate,
$filterParams,
$recordCount
);
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $sent, $recordCount, $newCount, $duration);
$this->info("Calibration notifications sent: {$sent} (records: {$recordCount}, new: {$newCount}).");
return 0;
} catch (\Throwable $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error('Error: ' . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,165 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckDeletedJoints extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-deleted-joints';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check deleted joints for missing comments and send notifications';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking deleted joints for missing comments...');
$notificationCode = 'notification_deleted_joint_missing_comment';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
// Build query
$query = DB::table('deleted_joints')
->where(function($q) {
$q->whereNull('comment')
->orWhere('comment', '');
});
// If not first run, only check new records
if ($lastCheck !== null) {
$query->where('created_at', '>', $lastCheck);
// Get already notified IDs to prevent duplicates
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
// If first run (lastCheck === null), check ALL records (no where clause added)
// Get IDs of records that need notification
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new deleted joints with missing comments found.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter parameters
$filterParams = [
'table' => 'deleted_joints',
'conditions' => [
'or' => [
['comment' => null],
['comment' => '']
]
]
];
// If not first run, merge with existing IDs (additive approach)
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
// Merge new IDs with existing ones
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
// Check if there are actually new IDs
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
// First run: use all IDs
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate (same issue already notified and no new records)
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$title = 'Deleted Joints Missing Comments';
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$message = sprintf(
'%d deleted joint(s) have no comment. Please add deletion reason for each joint.',
count($filterParams['conditions']['id'])
);
// Send notification with all IDs (existing + new)
$sentCount = sendNotification(
$notificationCode,
$message,
null, // Link will be generated automatically
$title,
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent {$sentCount} notification(s) for {$totalRecords} deleted joints with missing comments ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,158 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class NotificationCheckIncomingControlCertificate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-incoming-control-certificate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check incoming control records for missing certificate numbers';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking incoming control records for missing certificates...');
try {
if (!Schema::hasTable('incoming_controls')) {
$this->info('incoming_controls table not found; skipping check.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'incoming_controls table not found');
return 0;
}
$notificationCode = 'notification_incoming_control_cert_missing';
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table('incoming_controls')
->whereNotNull('created_at')
->where(function($q) {
$q->whereNull('certificate_no')
->orWhere('certificate_no', '');
});
// If not first run, only check new records
if ($lastCheck !== null) {
$query->where('created_at', '>', $lastCheck);
// Get already notified IDs
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new missing certificates found in incoming control.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter params
$filterParams = [
'table' => 'incoming_controls',
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$title = 'Incoming Control Certificate Missing';
$message = sprintf(
'%d incoming control record(s) have empty certificate numbers. Please update the certificate information.',
count($filterParams['conditions']['id'])
);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
$title,
$message,
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent batch notification for incoming control certificates ({$totalRecords} records, {$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,193 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckLineList extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-line-list-missing-data';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check line list for missing required data (category, NDT ratio, pressure, temperature, test type, test media) where weld logs exist';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking line list for missing required data...');
$notificationCode = 'notification_line_list_missing_data';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
// Get unique line numbers from weld_logs that have records
$usedLinesQuery = DB::table('weld_logs')
->select('line_number')
->whereNotNull('line_number')
->where('line_number', '!=', '')
->distinct();
// If not first run, only check lines from new weld_logs
if ($lastCheck !== null) {
$usedLinesQuery->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
}
$usedLines = $usedLinesQuery->limit(100)->pluck('line_number');
if ($usedLines->isEmpty()) {
$this->info('No weld log records found with line numbers.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
$this->info("Found {$usedLines->count()} unique lines in weld logs. Checking line list...");
// Get already notified line numbers
$alreadyNotifiedLines = [];
if ($lastCheck !== null) {
$lastNotification = \App\Models\Notification::where('notification_code', $notificationCode)
->whereNotNull('filter_params')
->orderBy('created_at', 'DESC')
->first();
if ($lastNotification && $lastNotification->filter_params) {
$lastParams = is_array($lastNotification->filter_params)
? $lastNotification->filter_params
: json_decode($lastNotification->filter_params, true);
if (isset($lastParams['conditions']['line_no'])) {
$alreadyNotifiedLines = is_array($lastParams['conditions']['line_no'])
? $lastParams['conditions']['line_no']
: [$lastParams['conditions']['line_no']];
}
}
}
$linesWithMissingData = [];
foreach ($usedLines as $lineNumber) {
// Skip if already notified (unless first run)
if ($lastCheck !== null && in_array($lineNumber, $alreadyNotifiedLines)) {
continue;
}
$linelist = DB::table('line_lists')
->where('line_no', $lineNumber)
->first();
if (!$linelist) {
continue;
}
$isMissing = empty($linelist->category) ||
empty($linelist->ndt) ||
(empty($linelist->working_pressure_mpa) && empty($linelist->design_pressure_mpa)) ||
(empty($linelist->working_temperature) && empty($linelist->design_temperature)) ||
empty($linelist->test_type) ||
empty($linelist->test_media);
if ($isMissing) {
$linesWithMissingData[] = $lineNumber;
}
}
if (empty($linesWithMissingData)) {
$this->info('No new lines with missing data were found.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Merge with existing lines if not first run
if ($lastCheck !== null && !empty($alreadyNotifiedLines)) {
$linesWithMissingData = array_values(array_unique(array_merge($alreadyNotifiedLines, $linesWithMissingData)));
} else {
$linesWithMissingData = array_values(array_unique($linesWithMissingData));
}
$filterParams = [
'table' => 'line_lists',
'conditions' => [
'line_no' => $linesWithMissingData
]
];
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($linesWithMissingData, $alreadyNotifiedLines)) : count($linesWithMissingData);
$message = sprintf(
'%d line(s) referenced in weld logs have missing required data (category, NDT ratio, pressure, temperature, test type, or test media). Please update the Line List entries.',
count($linesWithMissingData)
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'Line List Missing Required Data',
$filterParams,
count($linesWithMissingData)
);
$duration = microtime(true) - $startTime;
$totalRecords = count($linesWithMissingData);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent {$sentCount} notification(s) for {$totalRecords} line(s) with missing data ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,156 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckManageNDT extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-manage-ndt-unnecessary';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for unnecessary manual NDT requests where NDE Matrix has 0% ratio';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking for unnecessary manual NDT requests...');
$totalCount = 0;
$totalSent = 0;
$totalRecords = 0;
$totalNew = 0;
try {
// Check each NDT type
$result1 = $this->checkTestType('RT', 'rt', 'rt_request_no');
$totalCount += $result1;
$result2 = $this->checkTestType('UT', 'ut', 'ut_request_no');
$totalCount += $result2;
$result3 = $this->checkTestType('PT', 'pt', 'pt_request_no');
$totalCount += $result3;
$result4 = $this->checkTestType('MT', 'mt', 'mt_request_no');
$totalCount += $result4;
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $totalSent, $totalRecords, $totalNew, $duration);
$this->info("Total notifications sent: {$totalCount}");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Check for unnecessary requests for a specific test type
*
* @param string $testName Display name (RT, UT, PT, MT)
* @param string $ndeField NDE Matrix field name (rt, ut, pt, mt)
* @param string $requestField Weldlog request field name (rt_request_no, ut_request_no, etc.)
* @return int
*/
private function checkTestType($testName, $ndeField, $requestField)
{
$this->info("Checking {$testName} unnecessary requests...");
$ids = apply_welded_filter(DB::table('weld_logs'))
->leftJoin('nde_matrices', function($join) {
$join->on('weld_logs.line_number', '=', 'nde_matrices.line')
->on('weld_logs.type_of_joint', '=', 'nde_matrices.type_of_joint');
})
->whereNotNull('weld_logs.welding_date')
->where('weld_logs.welding_date', '!=', '')
->whereNotNull("weld_logs.{$requestField}")
->where("weld_logs.{$requestField}", '!=', '')
->where(function($query) use ($ndeField) {
$query->whereNull("nde_matrices.{$ndeField}")
->orWhere("nde_matrices.{$ndeField}", 0)
->orWhere("nde_matrices.{$ndeField}", '0');
})
->pluck('weld_logs.id')
->toArray();
if (empty($ids)) {
$this->info("No unnecessary {$testName} requests found.");
return 0;
}
$notificationCode = 'notification_manage_ndt_unnecessary';
// Skip if checked recently
if (!shouldCheckNotification($notificationCode)) {
$this->info("Skipping {$testName}: Last notification sent less than 5 minutes ago.");
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
// Filter IDs: only keep new ones if not first run
if ($lastCheck !== null) {
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$newIds = array_diff($ids, $alreadyNotifiedIds);
if (empty($newIds)) {
$this->info("Skipping {$testName}: No new records since last notification.");
return 0;
}
$ids = array_values(array_unique(array_merge($alreadyNotifiedIds, $ids)));
}
}
$filterParams = [
'table' => 'weld_logs',
'conditions' => [
'id' => array_values(array_unique($ids))
]
];
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info("Skipping {$testName}: Same issue already notified and no new records.");
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
"Unnecessary {$testName} Request",
'%d joint(s) have unnecessary ' . $testName . ' test requests (NDE ratio 0% or missing).',
$filterParams,
count($filterParams['conditions']['id'])
);
$this->info("Sent batch notification for unnecessary {$testName} requests (" . count($filterParams['conditions']['id']) . " joints, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
}
@@ -0,0 +1,427 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckNDEMatrixDiscrepancies extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-nde-matrix-discrepancies';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for discrepancies between NDE Matrix, Line List, and Weldlog';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking NDE Matrix discrepancies...');
$totalCount = 0;
$totalSent = 0;
$totalRecords = 0;
$totalNew = 0;
try {
// Check 1: NDE Matrix lines not in Weldlog
$result1 = $this->checkNDEMatrixNotInWeldlog($totalSent, $totalRecords, $totalNew);
if ($result1 > 0) {
$totalCount += 1;
}
// Check 2: Weldlog lines not in Line List
$result2 = $this->checkWeldlogNotInLineList($totalSent, $totalRecords, $totalNew);
if ($result2 > 0) {
$totalCount += 1;
}
// Check 3: Line List entries with missing NDT ratio
$result3 = $this->checkLineListMissingNDT($totalSent, $totalRecords, $totalNew);
if ($result3 > 0) {
$totalCount += 1;
}
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $totalSent, $totalRecords, $totalNew, $duration);
$this->info("Total notifications sent: {$totalCount}");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Check 1: NDE Matrix rows with empty NDT values (all 6 columns empty)
*/
private function checkNDEMatrixNotInWeldlog(&$totalSent = null, &$totalRecords = null, &$totalNew = null)
{
$this->info('Check 1: NDE Matrix rows with empty NDT values...');
$notificationCode = 'notification_nde_matrix_not_in_weldlog';
// Skip if checked recently
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping Check 1: Last notification sent less than 5 minutes ago.');
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table('nde_matrices')
->select('id', 'line', 'project', 'type_of_joint')
->whereNotNull('line')
->where('line', '!=', '')
->where(function($q) {
$q->where(function($sq) {
$sq->whereNull('rt')->orWhere('rt', '')->orWhere('rt', 0)->orWhere('rt', '0');
})
->where(function($sq) {
$sq->whereNull('ut')->orWhere('ut', '')->orWhere('ut', 0)->orWhere('ut', '0');
})
->where(function($sq) {
$sq->whereNull('pt')->orWhere('pt', '')->orWhere('pt', 0)->orWhere('pt', '0');
})
->where(function($sq) {
$sq->whereNull('mt')->orWhere('mt', '')->orWhere('mt', 0)->orWhere('mt', '0');
})
->where(function($sq) {
$sq->whereNull('pmi')->orWhere('pmi', '')->orWhere('pmi', 0)->orWhere('pmi', '0');
})
->where(function($sq) {
$sq->whereNull('ht')->orWhere('ht', '')->orWhere('ht', 0)->orWhere('ht', '0');
});
});
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$emptyRows = $query->limit(50)->get();
if ($emptyRows->isEmpty()) {
$this->info('No new empty NDE Matrix rows found.');
return 0;
}
$emptyIds = array_values(array_unique($emptyRows->pluck('id')->toArray()));
$filterParams = [
'table' => 'nde_matrices',
'conditions' => []
];
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $emptyIds)));
$newIds = array_diff($emptyIds, $existingIds);
if (empty($newIds)) {
$this->info('Skipping Check 1: No new records since last notification.');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $emptyIds;
}
} else {
$filterParams['conditions']['id'] = $emptyIds;
}
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping Check 1: Same issue already notified and no new records.');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($emptyIds, getAlreadyNotifiedIds($notificationCode))) : count($emptyIds);
$totalRecords = count($filterParams['conditions']['id']);
$message = sprintf(
'%d NDE Matrix row(s) have empty NDT values (RT, UT, PT, MT, PMI, HT). Please assign NDT requirements.',
$totalRecords
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'NDE Matrix Empty Row - NDT Assignment Required',
$filterParams,
$totalRecords
);
// Update totals for handle method
if ($totalSent !== null) {
$totalSent += $sentCount;
$totalRecords += $totalRecords;
$totalNew += $newCount;
}
$this->info("Sent {$sentCount} batch notification(s) for empty NDE Matrix rows ({$totalRecords} records, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
/**
* Check 2: Lines in Weldlog but not in Line List
*/
private function checkWeldlogNotInLineList(&$totalSent = null, &$totalRecords = null, &$totalNew = null)
{
$this->info('Check 2: Weldlog lines not found in Line List...');
$notificationCode = 'notification_weldlog_not_in_line_list';
// Skip if checked recently
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping Check 2: Last notification sent less than 5 minutes ago.');
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
$weldlogLinesQuery = DB::table('weld_logs')
->select('line_number', 'project', 'iso_number')
->whereNotNull('line_number')
->where('line_number', '!=', '')
->whereNotNull('welding_date')
->distinct();
if ($lastCheck !== null) {
$weldlogLinesQuery->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
}
$weldlogLines = $weldlogLinesQuery->limit(50)->get();
// Get already notified line numbers
$alreadyNotifiedLines = [];
if ($lastCheck !== null) {
$lastNotification = \App\Models\Notification::where('notification_code', $notificationCode)
->whereNotNull('filter_params')
->orderBy('created_at', 'DESC')
->first();
if ($lastNotification && $lastNotification->filter_params) {
$lastParams = is_array($lastNotification->filter_params)
? $lastNotification->filter_params
: json_decode($lastNotification->filter_params, true);
if (isset($lastParams['conditions']['line_number'])) {
$alreadyNotifiedLines = is_array($lastParams['conditions']['line_number'])
? $lastParams['conditions']['line_number']
: [$lastParams['conditions']['line_number']];
}
}
}
$missingLines = [];
foreach ($weldlogLines as $weldLine) {
if ($lastCheck !== null && in_array($weldLine->line_number, $alreadyNotifiedLines)) {
continue;
}
$existsInLineList = DB::table('line_lists')
->where('line_no', $weldLine->line_number)
->exists();
if (!$existsInLineList) {
$missingLines[] = $weldLine->line_number;
}
}
if (empty($missingLines)) {
$this->info('No new discrepancies found for Check 2.');
return 0;
}
if ($lastCheck !== null && !empty($alreadyNotifiedLines)) {
$missingLines = array_values(array_unique(array_merge($alreadyNotifiedLines, $missingLines)));
} else {
$missingLines = array_values(array_unique($missingLines));
}
$filterParams = [
'table' => 'weld_logs',
'conditions' => [
'line_number' => $missingLines
]
];
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping Check 2: Same issue already notified and no new records.');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($missingLines, $alreadyNotifiedLines)) : count($missingLines);
$totalRecords = count($missingLines);
$message = sprintf(
'%d weld log line(s) could not be matched in the Line List. Please review and add the missing line records.',
$totalRecords
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'Weldlog Not in Line List',
$filterParams,
$totalRecords
);
// Update totals for handle method
if ($totalSent !== null) {
$totalSent += $sentCount;
$totalRecords += $totalRecords;
$totalNew += $newCount;
}
$this->info("Sent {$sentCount} batch notification(s) for Check 2 ({$totalRecords} lines, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
/**
* Check 3: Line List entries with missing NDT ratio
*/
private function checkLineListMissingNDT(&$totalSent = null, &$totalRecords = null, &$totalNew = null)
{
$this->info('Check 3: Line List entries missing NDT ratio...');
$notificationCode = 'notification_line_list_ndt_missing';
// Skip if checked recently
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping Check 3: Last notification sent less than 5 minutes ago.');
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table('line_lists')
->select('line_lists.id', 'line_lists.line_no', 'line_lists.ndt')
->join('weld_logs', 'weld_logs.line_number', '=', 'line_lists.line_no')
->where(function($q) {
$q->whereNull('line_lists.ndt')
->orWhere('line_lists.ndt', '')
->orWhere('line_lists.ndt', '0');
})
->whereNotNull('weld_logs.welding_date')
->distinct();
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('line_lists.created_at', '>', $lastCheck)
->orWhere('line_lists.updated_at', '>', $lastCheck)
->orWhere('weld_logs.created_at', '>', $lastCheck)
->orWhere('weld_logs.updated_at', '>', $lastCheck);
});
}
$missingNDT = $query->limit(50)->get();
if ($missingNDT->isEmpty()) {
$this->info('No new discrepancies found for Check 3.');
return 0;
}
$lineNumbers = array_values(array_unique($missingNDT->pluck('line_no')->toArray()));
// Get already notified line numbers
$alreadyNotifiedLines = [];
if ($lastCheck !== null) {
$lastNotification = \App\Models\Notification::where('notification_code', $notificationCode)
->whereNotNull('filter_params')
->orderBy('created_at', 'DESC')
->first();
if ($lastNotification && $lastNotification->filter_params) {
$lastParams = is_array($lastNotification->filter_params)
? $lastNotification->filter_params
: json_decode($lastNotification->filter_params, true);
if (isset($lastParams['conditions']['line_no'])) {
$alreadyNotifiedLines = is_array($lastParams['conditions']['line_no'])
? $lastParams['conditions']['line_no']
: [$lastParams['conditions']['line_no']];
}
}
}
if ($lastCheck !== null && !empty($alreadyNotifiedLines)) {
$lineNumbers = array_values(array_unique(array_merge($alreadyNotifiedLines, $lineNumbers)));
}
$filterParams = [
'table' => 'line_lists',
'conditions' => [
'line_no' => $lineNumbers
]
];
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping Check 3: Same issue already notified and no new records.');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($lineNumbers, $alreadyNotifiedLines)) : count($lineNumbers);
$totalRecords = count($lineNumbers);
$message = sprintf(
'%d Line List entry/entries used in Weldlog are missing NDT ratio. Please update the Line List records.',
$totalRecords
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'Line List NDT Missing',
$filterParams,
$totalRecords
);
// Update totals for handle method
if ($totalSent !== null) {
$totalSent += $sentCount;
$totalRecords += $totalRecords;
$totalNew += $newCount;
}
$this->info("Sent {$sentCount} batch notification(s) for Check 3 ({$totalRecords} lines, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
}
@@ -0,0 +1,231 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckNDTCalculation extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-ndt-calculation';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check NDT calculation warnings based on material grades, thicknesses, and joint types';
/**
* Stainless steel material groups
*/
private $ssGroups = ['11', '8', '9', 'M11', 'M111', 'M9'];
/**
* Carbon steel material group
*/
private $csGroup = ['1', 'M01'];
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking NDT calculation warnings...');
$totalCount = 0;
$totalSent = 0;
$totalRecords = 0;
$totalNew = 0;
try {
// Check 1: PMI test missing for SS or CS+SS
$result1 = $this->checkPMIMissing($totalSent, $totalRecords, $totalNew);
if ($result1 > 0) {
$totalCount += 1;
}
// Check 2: FN test missing for SS with temperature > 350
$result2 = $this->checkFNMissing($totalSent, $totalRecords, $totalNew);
if ($result2 > 0) {
$totalCount += 1;
}
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $totalSent, $totalRecords, $totalNew, $duration);
$this->info("Total notifications sent: {$totalCount}");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Check 1: PMI test missing for SS or CS+SS materials
*/
private function checkPMIMissing(&$totalSent = null, &$totalRecords = null, &$totalNew = null)
{
$this->info('Check 1: PMI test missing for SS or CS+SS materials...');
$ids = [];
$weldLogs = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where(function($query) {
$query->whereIn('ru_material_group_1', array_merge($this->ssGroups, $this->csGroup))
->orWhereIn('ru_material_group_2', array_merge($this->ssGroups, $this->csGroup));
})
->where(function($query) {
$query->whereNull('pmi_request_no')
->orWhere('pmi_request_no', '');
})
->select('id', 'ru_material_group_1', 'ru_material_group_2')
->get();
foreach ($weldLogs as $weld) {
$mat1 = $weld->ru_material_group_1 ?? '';
$mat2 = $weld->ru_material_group_2 ?? '';
$isSS = in_array($mat1, $this->ssGroups) || in_array($mat2, $this->ssGroups);
$isCS_SS = (in_array($mat1, $this->csGroup) && in_array($mat2, $this->ssGroups)) ||
(in_array($mat1, $this->ssGroups) && in_array($mat2, $this->csGroup));
if ($isSS || $isCS_SS) {
$ids[] = $weld->id;
}
}
return $this->sendWeldLogBatch(
'notification_ndt_pmi_missing',
'PMI Test Missing',
'%d joint(s) have SS/CS+SS materials but PMI test requests are missing.',
$ids,
$totalSent,
$totalRecords,
$totalNew
);
}
/**
* Check 2: FN (Ferrite) test missing for SS materials with operating temperature > 350°C
*/
private function checkFNMissing(&$totalSent = null, &$totalRecords = null, &$totalNew = null)
{
$this->info('Check 2: FN test missing for SS materials with temperature > 350...');
$ids = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where(function($query) {
$query->whereIn('ru_material_group_1', $this->ssGroups)
->orWhereIn('ru_material_group_2', $this->ssGroups);
})
->where(function($query) {
// Temperature control: operating_temperature_s > 350
$query->whereRaw('CAST(operating_temperature_s AS DECIMAL(10,2)) > 350');
})
->where(function($query) {
$query->whereNull('ferrite_request_no')
->orWhere('ferrite_request_no', '');
})
->pluck('id')
->toArray();
return $this->sendWeldLogBatch(
'notification_ndt_fn_missing',
'FN Test Missing',
'%d joint(s) with SS materials and operating temperature > 350°C are missing FN (Ferrite) test requests.',
$ids,
$totalSent,
$totalRecords,
$totalNew
);
}
private function sendWeldLogBatch(string $notificationCode, string $title, string $messageTemplate, array $ids, &$totalSent = null, &$totalRecords = null, &$totalNew = null): int
{
$ids = array_values(array_unique(array_filter($ids)));
if (empty($ids)) {
$this->info("No {$title} records found.");
return 0;
}
// Skip if checked recently
if (!shouldCheckNotification($notificationCode)) {
$this->info("Skipping {$title}: Last notification sent less than 5 minutes ago.");
return 0;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
// Filter IDs: only keep new ones if not first run
if ($lastCheck !== null) {
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$newIds = array_diff($ids, $alreadyNotifiedIds);
if (empty($newIds)) {
$this->info("Skipping {$title}: No new records since last notification.");
return 0;
}
$ids = array_values(array_unique(array_merge($alreadyNotifiedIds, $ids)));
}
}
$filterParams = [
'table' => 'weld_logs',
'conditions' => [
'id' => $ids
]
];
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info("Skipping {$title}: Same issue already notified and no new records.");
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$totalRecords = count($ids);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
$title,
$messageTemplate,
$filterParams,
$totalRecords
);
// Update totals for handle method
if ($totalSent !== null) {
$totalSent += $sentCount;
$totalRecords += $totalRecords;
$totalNew += $newCount;
}
$this->info("Sent batch notification for {$title} ({$totalRecords} joints, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
}
@@ -0,0 +1,183 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class NotificationCheckNDTRequestOverdue extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-ndt-request-overdue';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if NDT request dates are older than 10 days without test results';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking NDT request dates older than 10 days...');
$notificationCode = 'notification_ndt_request_overdue';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$overdueThreshold = Carbon::now()->subDays(10)->toDateString();
$invalidIds = [];
// Get already notified IDs
$alreadyNotifiedIds = $lastCheck !== null ? getAlreadyNotifiedIds($notificationCode) : [];
// Request date and result fields to check
$requestChecks = [
['request' => 'rt_request_date', 'result' => 'rt_result', 'name' => 'RT'],
['request' => 'vt_request_date', 'result' => 'vt_result', 'name' => 'VT'],
['request' => 'ut_request_date', 'result' => 'ut_result', 'name' => 'UT'],
['request' => 'pt_request_date', 'result' => 'pt_result', 'name' => 'PT'],
['request' => 'mt_request_date', 'result' => 'mt_result', 'name' => 'MT'],
['request' => 'ht_request_date', 'result' => 'ht_result', 'name' => 'HT'],
['request' => 'pmi_request_date', 'result' => 'pmi_result', 'name' => 'PMI'],
['request' => 'pwht_request_date', 'result' => 'pwht_result', 'name' => 'PWHT'],
['request' => 'ferrite_request_date', 'result' => 'ferrite_result', 'name' => 'Ferrite'],
];
foreach ($requestChecks as $check) {
$this->info("Checking {$check['name']} requests...");
$query = DB::table('weld_logs')
->whereNotNull($check['request'])
->where($check['request'], '!=', '')
->where($check['request'], '!=', '0000-00-00')
->where($check['request'], '<=', $overdueThreshold)
->where(function($q) use ($check) {
$q->whereNull($check['result'])
->orWhere($check['result'], '')
->orWhere($check['result'], 'Pending');
});
// If not first run, only check records that might have changed
// (request date is still overdue, but result might have been updated)
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (!empty($ids)) {
$this->warn("Found " . count($ids) . " overdue {$check['name']} requests (10+ days)");
$invalidIds = array_merge($invalidIds, $ids);
}
}
$invalidIds = array_values(array_unique($invalidIds));
if (empty($invalidIds)) {
$this->info('No new overdue NDT requests found. All requests within 10 days or have results.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter params
$filterParams = [
'table' => 'weld_logs',
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null && !empty($alreadyNotifiedIds)) {
$allIds = array_values(array_unique(array_merge($alreadyNotifiedIds, $invalidIds)));
$newIds = array_diff($invalidIds, $alreadyNotifiedIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $invalidIds;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($invalidIds, $alreadyNotifiedIds)) : count($invalidIds);
$message = sprintf(
'%d weld log record(s) have NDT request dates older than 10 days without test results (RT, VT, UT, PT, MT, HT, PWHT). Please follow up with testing laboratory.',
count($filterParams['conditions']['id'])
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'NDT Request Overdue (10+ Days)',
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->warn("Sent notification for {$totalRecords} overdue NDT request(s) ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,252 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckPDFDocuments extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-pdf-documents';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for missing PDF documents in WPS, NAKS Welder, Welding Equipment, and WPQR tables';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking for missing PDF documents...');
$totalNotifications = 0;
$totalSent = 0;
$totalRecords = 0;
$totalNew = 0;
try {
$pdfNotificationCodes = [
'notification_wps_pdf_missing',
'notification_naks_welder_cert_missing',
'notification_welding_equipment_pdf_missing',
'notification_wpq_followup_pdf_missing',
];
$result1 = $this->sendBatchNotification(
'w_p_s',
'download',
'notification_wps_pdf_missing',
'WPS PDF Document Missing',
'download',
[
'message' => '%d WPS document(s) have no PDF file uploaded. Please add the missing PDF files.'
],
$totalSent,
$totalRecords,
$totalNew
);
if ($result1 > 0) {
$totalNotifications += 1;
}
$result2 = $this->sendBatchNotification(
'naks_welders',
'naks_certificate_no',
'notification_naks_welder_cert_missing',
'NAKS Welder Certificate Missing',
'naks_certificate_no',
[
'message' => '%d NAKS welder(s) have no certificate number assigned. Please update each welder record.'
],
$totalSent,
$totalRecords,
$totalNew
);
if ($result2 > 0) {
$totalNotifications += 1;
}
$result3 = $this->sendBatchNotification(
'welding_equipment',
'download',
'notification_welding_equipment_pdf_missing',
'Welding Equipment PDF Missing',
'download',
[
'message' => '%d welding equipment record(s) have no PDF document uploaded. Please add the missing files.',
],
$totalSent,
$totalRecords,
$totalNew
);
if ($result3 > 0) {
$totalNotifications += 1;
}
$result4 = $this->sendBatchNotification(
'welder_tests',
'download',
'notification_wpq_followup_pdf_missing',
'WPQR PDF Missing',
'download',
[
'message' => '%d WPQR record(s) have no PDF document uploaded. Please add the missing files.',
],
$totalSent,
$totalRecords,
$totalNew
);
if ($result4 > 0) {
$totalNotifications += 1;
}
$duration = microtime(true) - $startTime;
// Log completion
logNotificationComplete($commandName, $totalSent, $totalRecords, $totalNew, $duration);
$this->info("Total batch notifications sent: {$totalNotifications}");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Send a batch notification for a table/column combination
*/
private function sendBatchNotification(
string $table,
string $column,
string $notificationCode,
string $title,
string $columnKeyForFilter,
array $options = [],
&$totalSent = null,
&$totalRecords = null,
&$totalNew = null
): int {
$this->info("Checking {$title}...");
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info("Skipping {$title}: Last notification sent less than 5 minutes ago.");
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table($table)->where(function($q) use ($column) {
$q->whereNull($column)
->orWhere($column, '');
});
// If not first run, only check new/updated records
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
// Get already notified IDs
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info("No new missing records found for {$title}.");
return 0;
}
// Prepare filter params
$filterParams = [
'table' => $table,
'conditions' => [
'or' => [
[$columnKeyForFilter => null],
[$columnKeyForFilter => '']
]
]
];
// If not first run, merge with existing IDs
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info("Skipping {$title}: No new records since last notification.");
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info("Skipping {$title}: Same issue already notified and no new records.");
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$totalRecords = count($filterParams['conditions']['id'] ?? $ids);
$message = $options['message'] ?? '%d record(s) have missing data. Please review the filtered list.';
$message = sprintf($message, $totalRecords);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
$title,
$filterParams,
$totalRecords
);
// Update totals if references provided
if ($totalSent !== null) {
$totalSent += $sentCount;
$totalRecords += $totalRecords;
$totalNew += $newCount;
}
$this->info("Sent batch notification for {$title} ({$totalRecords} records, {$newCount} new).");
return $sentCount;
}
}
@@ -0,0 +1,161 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckPaintSystem extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-paint-system-values';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check for Paint Systems with incomplete customer-agreed paint values';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking Paint Systems for incomplete customer-agreed values...');
$notificationCode = 'notification_paint_system_incomplete';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table('paint_systems')
->where(function($q) {
$q->whereNull('primer_coat_name_1')
->orWhere('primer_coat_name_1', '')
->orWhereNull('brand_name_1')
->orWhere('brand_name_1', '')
->orWhereNull('thickness_1')
->orWhere('thickness_1', 0);
})
->whereExists(function($q) {
$q->select(DB::raw(1))
->from('line_lists')
->whereColumn('line_lists.painting_cycle', 'paint_systems.paint_cycle');
});
// If not first run, only check new/updated records
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
// Get already notified IDs
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new incomplete Paint Systems detected.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter params
$filterParams = [
'table' => 'paint_systems',
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$title = 'Paint System Missing';
$message = sprintf(
'%d paint system(s) used by Line Lists have missing customer-agreed values. Please complete the paint system details.',
count($filterParams['conditions']['id'])
);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
$title,
$message,
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent batch notification for incomplete Paint Systems ({$totalRecords} records, {$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,158 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckRepairLog extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-repair-log-new-joint';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check repair logs for missing new joint numbers and send notifications';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking repair logs for missing new joint numbers...');
$notificationCode = 'notification_repair_log_missing_new_joint';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
// Build query
$query = DB::table('repair_logs')
->where(function($q) {
$q->whereNull('new_joint_no')
->orWhere('new_joint_no', '');
});
// If not first run, only check new records
if ($lastCheck !== null) {
$query->where('created_at', '>', $lastCheck);
// Get already notified IDs to prevent duplicates
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
// Get IDs of records that need notification
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new repair logs with missing new joint numbers found.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter parameters
$filterParams = [
'table' => 'repair_logs',
'conditions' => [
'or' => [
['new_joint_no' => null],
['new_joint_no' => '']
]
]
];
// If not first run, merge with existing IDs (additive approach)
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$title = 'Repair Logs Missing New Joint Number';
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$message = sprintf(
'%d repair log(s) have no new joint number assigned. Please update each record with the new joint number.',
count($filterParams['conditions']['id'] ?? $ids)
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
$title,
$filterParams,
count($filterParams['conditions']['id'] ?? $ids)
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id'] ?? $ids);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent {$sentCount} notification(s) covering {$totalRecords} repair logs ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,241 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
class NotificationCheckRepairRate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-daily-repair-rate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if daily repair rate exceeds 12% AND overall repair rate exceeds 5%';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking daily repair rate...');
$notificationCode = 'notification_daily_repair_rate_high';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
$today = Carbon::today()->toDateString();
$lastCheck = getLastCheckTimestamp($notificationCode);
// If not first run, check if new repairs/welds since last check
if ($lastCheck !== null) {
$newRepairs = DB::table('repair_logs')
->whereDate('repair_date', $today)
->where('created_at', '>', $lastCheck)
->count();
$newWelds = DB::table('weld_logs')
->whereDate('welding_date', $today)
->where('created_at', '>', $lastCheck)
->count();
if ($newRepairs == 0 && $newWelds == 0) {
$this->info('No new repairs or welds since last check. Skipping.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new repairs or welds since last check');
return 0;
}
}
// Count today's repairs
$repairCount = DB::table('repair_logs')
->whereDate('repair_date', $today)
->count();
// Count today's welds
$weldCount = DB::table('weld_logs')
->whereDate('welding_date', $today)
->count();
if ($weldCount == 0) {
$this->info("No welding records for today. Skipping repair rate check.");
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No welding records for today');
return 0;
}
$repairRate = ($repairCount / $weldCount) * 100;
$threshold = 12;
$this->info("Today's stats: {$repairCount} repairs / {$weldCount} welds = " . number_format($repairRate, 2) . "%");
if ($repairRate > $threshold) {
// Check for duplicate (same day already notified)
if ($lastCheck !== null && $lastCheck->isToday()) {
$this->info('Skipping: Already notified for today.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Already notified for today');
return 0;
}
$title = 'Daily Repair Rate Exceeds 12%';
$message = sprintf(
'Daily repair rate is %.2f%% (%d repairs out of %d welds), which exceeds the %d%% threshold. QC team attention required.',
$repairRate,
$repairCount,
$weldCount,
$threshold
);
$filterParams = [
'table' => 'repair_logs',
'conditions' => [
'repair_date' => $today
]
];
$sentCount = sendNotification($notificationCode, $message, null, $title, $filterParams);
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $sentCount, 1, 1, $duration);
$this->warn("⚠️ HIGH REPAIR RATE: Notification sent to QC teams.");
// Check overall (project-wide) repair rate
$this->checkOverallRepairRate();
return 0;
}
$this->info("✓ Daily repair rate is within acceptable limits.");
// Check overall (project-wide) repair rate
$this->checkOverallRepairRate();
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Check overall repair rate for entire project
*/
private function checkOverallRepairRate()
{
$this->info('Checking overall project repair rate...');
$notificationCode = 'notification_overall_repair_rate_high';
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping overall rate: Last notification sent less than 5 minutes ago.');
return;
}
$lastCheck = getLastCheckTimestamp($notificationCode);
// If not first run, check if new repairs/welds since last check
if ($lastCheck !== null) {
$newRepairs = DB::table('repair_logs')
->where('created_at', '>', $lastCheck)
->count();
$newWelds = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where('welding_date', '!=', '0000-00-00')
->where('created_at', '>', $lastCheck)
->count();
if ($newRepairs == 0 && $newWelds == 0) {
$this->info('No new repairs or welds since last check. Skipping overall rate.');
return;
}
}
// Count total repairs (all time)
$totalRepairs = DB::table('repair_logs')->count();
// Count total welds (all time)
$totalWelds = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where('welding_date', '!=', '0000-00-00')
->count();
if ($totalWelds == 0) {
$this->info("No welding records found. Skipping overall repair rate check.");
return;
}
$overallRepairRate = ($totalRepairs / $totalWelds) * 100;
$threshold = 5;
$this->info("Overall stats: {$totalRepairs} repairs / {$totalWelds} welds = " . number_format($overallRepairRate, 2) . "%");
if ($overallRepairRate > $threshold) {
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, ['table' => 'repair_logs', 'conditions' => []])) {
$this->info('Skipping: Already notified for overall rate.');
return;
}
$title = 'Overall Repair Rate Exceeds 5%';
$message = sprintf(
'Overall project repair rate is %.2f%% (%d repairs out of %d welds), which exceeds the %d%% threshold. Quality improvement measures recommended.',
$overallRepairRate,
$totalRepairs,
$totalWelds,
$threshold
);
$filterParams = [
'table' => 'repair_logs',
'conditions' => []
];
sendNotification($notificationCode, $message, null, $title, $filterParams);
$this->warn("⚠️ HIGH OVERALL REPAIR RATE: Notification sent.");
} else {
$this->info("✓ Overall repair rate is within acceptable limits.");
}
}
}
@@ -0,0 +1,190 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckSupportLogWeldlog extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-support-log-weldlog';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if Support Log weld count matches Weldlog count by line number (Process Piping Support only)';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking Support Log vs Weldlog count discrepancies by line...');
$notificationCode = 'notification_support_log_missing_in_weldlog';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
// Get line_number based weld count from supports (erection_type = 'weld')
$supportWeldCountsQuery = DB::table('supports')
->select('line_number', DB::raw('COUNT(*) as support_count'))
->where('erection_type', 'weld')
->whereNotNull('line_number')
->where('line_number', '!=', '')
->groupBy('line_number');
// If not first run, only check lines with new/updated supports or welds
if ($lastCheck !== null) {
$supportWeldCountsQuery->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
}
$supportWeldCounts = $supportWeldCountsQuery->get()->keyBy('line_number');
if ($supportWeldCounts->isEmpty()) {
$this->info('No new welded supports found in Support Log.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
$this->info("Found {$supportWeldCounts->count()} unique lines with welded supports");
// Get already notified line numbers
$alreadyNotifiedLines = [];
if ($lastCheck !== null) {
$lastNotification = \App\Models\Notification::where('notification_code', $notificationCode)
->whereNotNull('filter_params')
->orderBy('created_at', 'DESC')
->first();
if ($lastNotification && $lastNotification->filter_params) {
$lastParams = is_array($lastNotification->filter_params)
? $lastNotification->filter_params
: json_decode($lastNotification->filter_params, true);
if (isset($lastParams['conditions']['line_number'])) {
$alreadyNotifiedLines = is_array($lastParams['conditions']['line_number'])
? $lastParams['conditions']['line_number']
: [$lastParams['conditions']['line_number']];
}
}
}
$discrepantLines = [];
foreach ($supportWeldCounts as $lineNum => $supportData) {
// Skip if already notified (unless first run)
if ($lastCheck !== null && in_array($lineNum, $alreadyNotifiedLines)) {
continue;
}
// Count matching records in weld_logs (piping_type = 'Process Piping Support')
$weldlogCount = DB::table('weld_logs')
->where('line_number', $lineNum)
->where('piping_type', 'Process Piping Support')
->count();
// If counts don't match, add to discrepancy list
if ($supportData->support_count != $weldlogCount) {
$discrepantLines[] = [
'line' => $lineNum,
'support_count' => $supportData->support_count,
'weldlog_count' => $weldlogCount
];
}
}
if (empty($discrepantLines)) {
$this->info('No new mismatched counts found.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
$lineNumbers = array_column($discrepantLines, 'line');
// Merge with existing lines if not first run
if ($lastCheck !== null && !empty($alreadyNotifiedLines)) {
$lineNumbers = array_values(array_unique(array_merge($alreadyNotifiedLines, $lineNumbers)));
} else {
$lineNumbers = array_values(array_unique($lineNumbers));
}
$filterParams = [
'table' => 'supports',
'conditions' => [
'line_number' => $lineNumbers
]
];
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($lineNumbers, $alreadyNotifiedLines)) : count($lineNumbers);
$message = sprintf(
'%d line(s) have mismatched support weld counts between Support Log (erection_type=weld) and Weldlog (piping_type=Process Piping Support). Please review and reconcile.',
count($lineNumbers)
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'Support Log - Weldlog Count Mismatch',
$filterParams,
count($lineNumbers)
);
$duration = microtime(true) - $startTime;
$totalRecords = count($lineNumbers);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent {$sentCount} batch notification(s) for {$totalRecords} line(s) with mismatched counts ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,190 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckTestLogPDF extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-test-log-pdf';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check NDT test logs with results but missing PDF reports (VT, RT, UT, PT, MT, HT, PMI, PWHT, Ferrite)';
/**
* Test log tables with their result field names
*/
private $testLogs = [
'v_t_logs' => ['result' => 'vt_result', 'name' => 'VT'],
'radiographic_tests' => ['result' => 'rt_result', 'name' => 'RT'],
'ultrasonic_tests' => ['result' => 'ut_result', 'name' => 'UT'],
'p_t_logs' => ['result' => 'pt_result', 'name' => 'PT'],
'magnetic_tests' => ['result' => 'mt_result', 'name' => 'MT'],
'hardness_tests' => ['result' => 'ht_result', 'name' => 'HT'],
'p_m_i_tests' => ['result' => 'pmi_result', 'name' => 'PMI'],
'p_w_h_t_s' => ['result' => 'pwht_result', 'name' => 'PWHT'],
'ferrits' => ['result' => 'ferrite_result', 'name' => 'Ferrite'],
];
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking test logs for missing PDF reports...');
$totalNotifications = 0;
$totalSent = 0;
$totalRecords = 0;
$totalNew = 0;
try {
foreach ($this->testLogs as $table => $config) {
$result = $this->checkTestLog($table, $config['result'], $config['name'], $totalSent, $totalRecords, $totalNew);
if ($result > 0) {
$totalNotifications += 1; // Count as notification sent
}
}
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, $totalSent, $totalRecords, $totalNew, $duration);
$this->info("Total notifications sent: {$totalNotifications}");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
/**
* Check a specific test log table for missing PDFs
*/
private function checkTestLog(string $table, string $resultField, string $testName, &$totalSent, &$totalRecords, &$totalNew): int
{
$this->info("Checking {$testName} Log...");
$notificationCode = 'notification_test_log_pdf_missing_' . strtolower($testName);
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info("Skipping {$testName}: Last notification sent less than 5 minutes ago.");
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table($table)
->whereNotNull($resultField)
->where($resultField, '!=', '')
->where($resultField, '!=', 'Cancel')
->where(function($q) {
$q->whereNull('report_file')
->orWhere('report_file', '');
});
// If not first run, only check new/updated records
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
// Get already notified IDs
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info("No new missing PDFs found in {$testName} Log.");
return 0;
}
// Prepare filter params
$filterParams = [
'table' => $table,
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info("Skipping {$testName}: No new records since last notification.");
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info("Skipping {$testName}: Same issue already notified and no new records.");
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$recordCount = count($filterParams['conditions']['id']);
$message = sprintf(
'%d %s test record(s) have results (non-Cancel) but missing PDF report files. Please upload the missing reports.',
$recordCount,
$testName
);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
"{$testName} Test - PDF Report Missing",
$message,
$filterParams,
$recordCount
);
// Update totals for handle method
$totalSent += $sentCount;
$totalRecords += $recordCount;
$totalNew += $newCount;
$this->info("Sent batch notification for {$testName} Log ({$recordCount} records, {$newCount} new).");
return $sentCount > 0 ? 1 : 0;
}
}
@@ -0,0 +1,155 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckWeldlogCertificate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-weldlog-certificate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check weldlog records for missing certificate PDFs after welding date is entered';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking weldlog records for missing certificates...');
$notificationCode = 'notification_weldlog_certificate_missing';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$query = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where(function($q) {
$q->whereNull('certificate_number_of_1')
->orWhere('certificate_number_of_1', '')
->orWhereNull('certificate_number_of_2')
->orWhere('certificate_number_of_2', '');
});
// If not first run, only check new/updated records
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
// Get already notified IDs
$alreadyNotifiedIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (empty($ids)) {
$this->info('No new missing certificates found in weldlog.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter params
$filterParams = [
'table' => 'weld_logs',
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null) {
$existingIds = getAlreadyNotifiedIds($notificationCode);
if (!empty($existingIds)) {
$allIds = array_values(array_unique(array_merge($existingIds, $ids)));
$newIds = array_diff($ids, $existingIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $ids;
}
} else {
$filterParams['conditions']['id'] = $ids;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($ids, getAlreadyNotifiedIds($notificationCode))) : count($ids);
$title = 'Certificate Number 1-2 Missing';
$message = sprintf(
'%d weld log record(s) have missing certificate numbers. Please update certificate_number_of_1/2.',
count($filterParams['conditions']['id'])
);
$sentCount = sendBatchNotificationWithFilter(
$notificationCode,
$title,
$message,
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->info("Sent batch notification for weldlog certificates ({$totalRecords} records, {$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,177 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class NotificationCheckWeldlogTestDates extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'notifications:check-weldlog-test-dates';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check if NDT test dates are before welding_date in weld_logs';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$startTime = microtime(true);
$commandName = $this->signature;
// Log command start
logNotificationStart($commandName);
$this->info('Checking weldlog test dates vs welding dates...');
$notificationCode = 'notification_weldlog_test_date_invalid';
try {
// Skip if checked recently (within 5 minutes)
if (!shouldCheckNotification($notificationCode)) {
$this->info('Skipping: Last notification sent less than 5 minutes ago.');
logNotificationSkip($commandName, 'Last notification sent less than 5 minutes ago');
return 0;
}
// Get last check time (null = first run, check all records)
$lastCheck = getLastCheckTimestamp($notificationCode);
$invalidIds = [];
// Test date fields to check
$testDateFields = [
'rt_test_date' => 'RT',
'date_of_vt' => 'VT',
'ut_test_date' => 'UT',
'pt_test_date' => 'PT',
'mt_test_date' => 'MT',
'ht_test_date' => 'HT',
'pmi_test_date' => 'PMI',
'pwht_date' => 'PWHT',
'date_of_ferrite_check' => 'Ferrite',
];
// Get already notified IDs
$alreadyNotifiedIds = $lastCheck !== null ? getAlreadyNotifiedIds($notificationCode) : [];
foreach ($testDateFields as $testField => $testName) {
$this->info("Checking {$testName} test dates...");
$query = DB::table('weld_logs')
->whereNotNull('welding_date')
->where('welding_date', '!=', '')
->where('welding_date', '!=', '0000-00-00')
->whereNotNull($testField)
->where($testField, '!=', '')
->where($testField, '!=', '0000-00-00')
->whereColumn($testField, '<', 'welding_date');
// If not first run, only check new/updated records
if ($lastCheck !== null) {
$query->where(function($q) use ($lastCheck) {
$q->where('created_at', '>', $lastCheck)
->orWhere('updated_at', '>', $lastCheck);
});
if (!empty($alreadyNotifiedIds)) {
$query->whereNotIn('id', $alreadyNotifiedIds);
}
}
$ids = $query->pluck('id')->toArray();
if (!empty($ids)) {
$this->warn("Found " . count($ids) . " records with {$testName} test date before welding date");
$invalidIds = array_merge($invalidIds, $ids);
}
}
$invalidIds = array_values(array_unique($invalidIds));
if (empty($invalidIds)) {
$this->info('No new invalid test dates found. All test dates are >= welding_date.');
$duration = microtime(true) - $startTime;
logNotificationComplete($commandName, 0, 0, 0, $duration);
return 0;
}
// Prepare filter params
$filterParams = [
'table' => 'weld_logs',
'conditions' => []
];
// If not first run, merge with existing IDs
if ($lastCheck !== null && !empty($alreadyNotifiedIds)) {
$allIds = array_values(array_unique(array_merge($alreadyNotifiedIds, $invalidIds)));
$newIds = array_diff($invalidIds, $alreadyNotifiedIds);
if (empty($newIds)) {
$this->info('Skipping: No new records since last notification.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'No new records since last notification');
return 0;
}
$filterParams['conditions']['id'] = $allIds;
} else {
$filterParams['conditions']['id'] = $invalidIds;
}
// Check for duplicate
if ($lastCheck !== null && isNotificationDuplicate($notificationCode, $filterParams)) {
$this->info('Skipping: Same issue already notified and no new records.');
$duration = microtime(true) - $startTime;
logNotificationSkip($commandName, 'Same issue already notified and no new records');
return 0;
}
$newCount = $lastCheck !== null ? count(array_diff($invalidIds, $alreadyNotifiedIds)) : count($invalidIds);
$message = sprintf(
'%d weld log record(s) have NDT test dates that are before welding_date. Please review and correct (RT, VT, UT, PT, MT, HT, PWHT).',
count($filterParams['conditions']['id'])
);
$sentCount = sendNotification(
$notificationCode,
$message,
null,
'Weldlog Test Date Before Welding Date',
$filterParams,
count($filterParams['conditions']['id'])
);
$duration = microtime(true) - $startTime;
$totalRecords = count($filterParams['conditions']['id']);
// Log completion
logNotificationComplete($commandName, $sentCount, $totalRecords, $newCount, $duration);
$this->warn("Sent notification for {$totalRecords} record(s) with invalid test dates ({$newCount} new).");
return 0;
} catch (\Exception $e) {
$duration = microtime(true) - $startTime;
logNotificationError($commandName, $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'duration' => $duration,
]);
$this->error("Error: " . $e->getMessage());
return 1;
}
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class SummaryCalculation extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'summary:calculation';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Dispatch blade view caching for summary calculation';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$this->info('Starting summary calculation cache dispatch...');
$cacheViews = [
[
'view' => 'admin.type.summary-nocache',
'cache' => 'summary-dashboard'
],
[
'view' => 'admin.dashboard.module.dashboard',
'cache' => 'dashboard'
],
[
'view' => 'admin-ajax.ndt-calculation-no-cache',
'cache' => 'ndt-calculation'
],
];
if (function_exists('dispatchCacheBladeViews')) {
dispatchCacheBladeViews($cacheViews);
$this->info('Dispatched cache jobs successfully.');
} else {
$this->error('Helper function dispatchCacheBladeViews not found.');
return 1;
}
return 0;
}
}
@@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use App\Models\WeldLog;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Schema;
class VerifyMechanicalSummaries extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mechanical:verify-summaries
{--columns=nps_1,nps_2,outside_diameter_1,outside_diameter_2 : Comma separated numeric columns to inspect}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Compare SUM values for welded vs mechanical joints across selected columns';
/**
* Execute the console command.
*/
public function handle(): int
{
$columns = collect(explode(',', $this->option('columns')))
->map(fn ($column) => trim($column))
->filter()
->unique()
->values();
if ($columns->isEmpty()) {
$this->error('Please provide at least one column via --columns option.');
return self::FAILURE;
}
$rows = [];
foreach ($columns as $column) {
if (!Schema::hasColumn('weld_logs', $column)) {
$this->warn("Column '{$column}' does not exist on weld_logs table. Skipping.");
continue;
}
$weldedSum = apply_welded_filter(WeldLog::query())->sum($column);
$mechanicalSum = apply_mechanical_filter(WeldLog::query())->sum($column);
$total = $weldedSum + $mechanicalSum;
$rows[] = [
'Column' => $column,
'SUM (Welded)' => $weldedSum,
'SUM (Mechanical)' => $mechanicalSum,
'SUM (Total)' => $total,
];
}
if (empty($rows)) {
$this->warn('No valid columns were processed.');
return self::FAILURE;
}
$this->table(
['Column', 'SUM (Welded)', 'SUM (Mechanical)', 'SUM (Total)'],
$rows
);
$this->info('Verification completed. Ensure mechanical sums are zero to confirm filtering.');
return self::SUCCESS;
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
protected $commands = [
\App\Console\Commands\MigrateFromDate::class,
\App\Console\Commands\DocumentSyncFolders::class,
\App\Console\Commands\NotificationCheckAll::class,
\App\Console\Commands\NotificationCheckDeletedJoints::class,
\App\Console\Commands\NotificationCheckRepairLog::class,
\App\Console\Commands\NotificationCheckRepairRate::class,
\App\Console\Commands\NotificationCheckLineList::class,
\App\Console\Commands\NotificationCheckPDFDocuments::class,
\App\Console\Commands\NotificationCheckWeldlogCertificate::class,
\App\Console\Commands\NotificationCheckIncomingControlCertificate::class,
\App\Console\Commands\NotificationCheckNDEMatrixDiscrepancies::class,
\App\Console\Commands\NotificationCheckNDTCalculation::class,
\App\Console\Commands\NotificationCheckManageNDT::class,
\App\Console\Commands\NotificationCheckSupportLogWeldlog::class,
\App\Console\Commands\NotificationCheckPaintSystem::class,
\App\Console\Commands\NotificationCheckWeldlogTestDates::class,
\App\Console\Commands\NotificationCheckNDTRequestOverdue::class,
\App\Console\Commands\NotificationCheckTestLogPDF::class,
\App\Console\Commands\VerifyMechanicalSummaries::class,
\App\Console\Commands\SummaryCalculation::class,
\App\Console\Commands\CacheBladeViews::class,
\App\Console\Commands\NotificationCheckCalibrationDue::class,
\App\Console\Commands\NaksSync::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// Cache Blade Views - Run every 10 minutes
$schedule->command('cache:blade-views')
->everyTenMinutes()
->withoutOverlapping(10);
// CheckNDTReminders command removed - outdated checks (overdue, report missing, results pending)
// Notification checks - Run every 5 minutes (real-time notifications)
// All commands use incremental checking: first run scans all records, subsequent runs only check new records
// Log when schedule:run starts
$schedule->call(function () {
logSchedulerExecution('schedule_run_started', [
'run_time' => now()->toDateTimeString(),
]);
})->name('schedule-run-logger')->everyMinute()->withoutOverlapping(1);
// Helper function to add logging to scheduled commands
$addScheduledCommand = function($command, $schedule, $scheduleMethod, ...$args) {
$event = $schedule->command($command);
// Apply schedule method (everyFiveMinutes, cron, etc.)
if (method_exists($event, $scheduleMethod)) {
$event = $event->$scheduleMethod(...$args);
}
// Send output to log file instead of /dev/null
// This ensures before/after callbacks work correctly
$logFile = storage_path('logs/scheduler-' . date('Y-m-d') . '.log');
$event->sendOutputTo($logFile);
$event->appendOutputTo($logFile);
// Add before callback to log when command is scheduled to run
$event->before(function () use ($command) {
logSchedulerExecution('command_scheduled', [
'command' => $command,
'scheduled_time' => now()->toDateTimeString(),
]);
});
// Add after callback to log when command finishes
$event->after(function () use ($command) {
logSchedulerExecution('command_finished', [
'command' => $command,
'finished_time' => now()->toDateTimeString(),
]);
});
return $event;
};
$addScheduledCommand('notifications:check-deleted-joints', $schedule, 'everyFiveMinutes')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-repair-log-new-joint', $schedule, 'everyFiveMinutes')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-daily-repair-rate', $schedule, 'everyFiveMinutes')
->withoutOverlapping(10);
// --- Batch 2 (Minutes: 1, 6, 11, 16...) ---
$addScheduledCommand('notifications:check-line-list-missing-data', $schedule, 'cron', '1-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-pdf-documents', $schedule, 'cron', '1-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-weldlog-certificate', $schedule, 'cron', '1-59/5 * * * *')
->withoutOverlapping(10);
// --- Batch 3 (Minutes: 2, 7, 12, 17...) ---
$addScheduledCommand('notifications:check-incoming-control-certificate', $schedule, 'cron', '2-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-nde-matrix-discrepancies', $schedule, 'cron', '2-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-ndt-calculation', $schedule, 'cron', '2-59/5 * * * *')
->withoutOverlapping(10);
// --- Batch 4 (Minutes: 3, 8, 13, 18...) ---
$addScheduledCommand('notifications:check-manage-ndt-unnecessary', $schedule, 'cron', '3-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-support-log-weldlog', $schedule, 'cron', '3-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-paint-system-values', $schedule, 'cron', '3-59/5 * * * *')
->withoutOverlapping(10);
// --- Batch 5 (Minutes: 4, 9, 14, 19...) ---
$addScheduledCommand('notifications:check-weldlog-test-dates', $schedule, 'cron', '4-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-ndt-request-overdue', $schedule, 'cron', '4-59/5 * * * *')
->withoutOverlapping(10);
$addScheduledCommand('notifications:check-test-log-pdf', $schedule, 'cron', '4-59/5 * * * *')
->withoutOverlapping(10);
// Calibration notifications - due within 20 days (run in same batch)
$addScheduledCommand('notifications:check-calibration-due', $schedule, 'cron', '4-59/5 * * * *')
->withoutOverlapping(10);
$schedule->command('summary:calculation')
->hourly()
->withoutOverlapping(10);
// NAKS Multi-Module Cross-Site Sync - Run daily at 02:00
// Synchronizes all NAKS data (certificates, welders, consumables, experts, equipment)
// from all project sites
$schedule->command('naks:sync --module=all')
->hourly()
->withoutOverlapping(120)
->appendOutputTo(storage_path('logs/naks-sync.log'));
$schedule->command('telescope:prune')->daily();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contents extends Model
{
//
use SoftDeletes;
protected $table = 'contents';
protected $primaryKey = 'id';
public $incrementing = true;
public $timestamps = true;
public function __construct()
{
}
public function getRouteKeyName(): string
{
return 'slug';
}
public static function getTree(string $slug, array $tree = []): array
{
$lowestLevel = Contents::where('slug', $slug)->select("title","slug","id","kid")->first();
if (!$lowestLevel) {
return $tree;
}
$tree[] = $lowestLevel->toArray();
if ($lowestLevel->kid !== 0) {
$tree = Contents::getTree($lowestLevel->kid, $tree);
}
return $tree;
}
public static function type(string $type,int $take=10) {
return Contents::where("type",$type)->take($take)->get();
}
public static function getBreadcrumbs(string $slug, $model="<li class='breadcrumb-item'><a href='{slug}'>{title}</a></li>"): string
{
$tree = Contents::getTree($slug);
$tree = array_reverse($tree);
$breadcrumbs = "";
foreach($tree AS $alan => $deger) {
$sonuc = str_replace("{slug}",$deger['slug'],$model);
$sonuc = str_replace("{title}",$deger['title'],$sonuc);
$breadcrumbs .= $sonuc;
}
return $breadcrumbs;
}
}
+644
View File
@@ -0,0 +1,644 @@
<?php
namespace App\DevExtreme;
use App\DevExtreme\SummaryContext;
class AggregateHelper {
const MIN_OP = "MIN";
const MAX_OP = "MAX";
const AVG_OP = "AVG";
const COUNT_OP = "COUNT";
const SUM_OP = "SUM";
const AS_OP = "AS";
const GENERATED_FIELD_PREFIX = "dx_";
private static function _RecalculateGroupCountAndSummary(&$dataItem, $groupInfo) {
if ($groupInfo["groupIndex"] <= $groupInfo["groupCount"] - 3) {
$items = $dataItem["items"];
foreach ($items as $item) {
$grInfo = $groupInfo;
$grInfo["groupIndex"]++;
self::_RecalculateGroupCountAndSummary($item, $grInfo);
}
}
if (isset($groupInfo["summaryTypes"]) && $groupInfo["groupIndex"] < $groupInfo["groupCount"] - 2) {
$result = array();
$items = $dataItem["items"];
$itemsCount = count($items);
foreach ($items as $index => $item) {
$currentSummaries = $item["summary"];
if ($index == 0) {
foreach ($currentSummaries as $summaryItem) {
$result[] = $summaryItem;
}
continue;
}
foreach ($groupInfo["summaryTypes"] as $si => $stItem) {
if ($stItem == self::MIN_OP) {
if ($result[$si] > $currentSummaries[$si]) {
$result[$si] = $currentSummaries[$si];
}
continue;
}
if ($stItem == self::MAX_OP) {
if ($result[$si] < $currentSummaries[$si]) {
$result[$si] = $currentSummaries[$si];
}
continue;
}
$result[$si] += $currentSummaries[$si];
}
}
foreach ($groupInfo["summaryTypes"] as $si => $stItem) {
if ($stItem == self::AVG_OP) {
$result[$si] /= $itemsCount;
}
}
$dataItem["summary"] = $result;
}
}
private static function _GetNewDataItem($row, $groupInfo) {
$dataItem = array();
$dataFieldCount = count($groupInfo["dataFieldNames"]);
for ($index = 0; $index < $dataFieldCount; $index++) {
$dataItem[$groupInfo["dataFieldNames"][$index]] = $row[$groupInfo["groupCount"] + $index];
}
return $dataItem;
}
private static function _GetNewGroupItem($row, $groupInfo, $explicitKey = null) {
$groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2;
// Direkt row değerini al, explode kullanma
$item = $explicitKey !== null ? $explicitKey : (isset($row[$groupInfo["groupIndex"]]) ? trim($row[$groupInfo["groupIndex"]]) : null);
$groupItem = array();
$groupItem["key"] = ($item === "" || $item === null) ? null : $item;
$groupItem["items"] = $groupInfo["groupIndex"] < $groupInfo["groupCount"] - $groupIndexOffset ? array() :
($groupInfo["lastGroupExpanded"] ? array() : NULL);
if ($groupInfo["groupIndex"] == $groupInfo["groupCount"] - $groupIndexOffset) {
if (isset($groupInfo["summaryTypes"])) {
$summaries = array();
$endIndex = $groupInfo["groupIndex"] + count($groupInfo["summaryTypes"]) + 1;
for ($index = $groupInfo["groupCount"]; $index <= $endIndex; $index++) {
$summaries[] = $row[$index];
}
$groupItem["summary"] = $summaries;
}
if (!$groupInfo["lastGroupExpanded"]) {
$groupItem["count"] = $row[$groupInfo["groupIndex"] + 1];
}
else {
$groupItem["items"][] = self::_GetNewDataItem($row, $groupInfo);
}
}
return $groupItem;
}
private static function _GroupData($row, &$resultItems, $groupInfo) {
$itemsCount = count($resultItems);
if (!isset($row) && !$itemsCount) {
return;
}
$currentItem = NULL;
$groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2;
// Eğer bu bir tarih filtresi ise, özel yönetim yapalım
$dateSequenceInfo = self::_getDateSequence($groupInfo);
if ($dateSequenceInfo['isDateSequence']) {
// Tarih sırası algılandı, özel tarih işleme mantığını kullan
self::_processDateSequence($row, $resultItems, $groupInfo, $dateSequenceInfo);
return;
}
// Standart (tarih olmayan) gruplamalar için normal akış
$currentKey = isset($row[$groupInfo["groupIndex"]]) ? trim($row[$groupInfo["groupIndex"]]) : null;
// Comma separation logic
if (isset($groupInfo["processCommas"]) && $groupInfo["processCommas"] === true &&
$currentKey !== null && strpos($currentKey, ',') !== false) {
$keys = explode(',', $currentKey);
foreach ($keys as $key) {
self::_processGroupDataItem($row, $resultItems, $groupInfo, trim($key));
}
return;
}
// Standart grup işlemlerini yapan yardımcı fonksiyon
self::_processGroupDataItem($row, $resultItems, $groupInfo, $currentKey);
}
// Standart grup işlemlerini yapan yardımcı fonksiyon
private static function _processGroupDataItem($row, &$resultItems, $groupInfo, $explicitKey = null) {
$currentKey = $explicitKey;
if ($currentKey === "") {
$currentKey = null;
}
$groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2;
// Eğer bu key zaten varsa, ilgili item'ı kullan
$keyExists = false;
$currentItem = null;
foreach($resultItems as $index => $item) {
if(isset($item["key"]) && (trim($item["key"]) === trim($currentKey) ||
($item["key"] === null && ($currentKey === null || $currentKey === "")))) {
$keyExists = true;
$currentItem = &$resultItems[$index];
break;
}
}
if (!$keyExists) {
$currentItem = self::_GetNewGroupItem($row, $groupInfo, $currentKey);
$resultItems[] = &$currentItem;
} else {
// MERGE LOGIC for leaf groups
if ($groupInfo["groupIndex"] == $groupInfo["groupCount"] - $groupIndexOffset) {
// Update count
if (isset($currentItem["count"])) {
$currentItem["count"] += $row[$groupInfo["groupIndex"] + 1];
}
// Update summaries if needed
if (isset($currentItem["summary"]) && isset($groupInfo["summaryTypes"])) {
$newSummaries = array();
$endIndex = $groupInfo["groupIndex"] + count($groupInfo["summaryTypes"]) + 1;
for ($index = $groupInfo["groupCount"]; $index <= $endIndex; $index++) {
$newSummaries[] = $row[$index];
}
foreach ($groupInfo["summaryTypes"] as $si => $stItem) {
if ($stItem == self::MIN_OP) {
if ($currentItem["summary"][$si] > $newSummaries[$si]) $currentItem["summary"][$si] = $newSummaries[$si];
} else if ($stItem == self::MAX_OP) {
if ($currentItem["summary"][$si] < $newSummaries[$si]) $currentItem["summary"][$si] = $newSummaries[$si];
} else if ($stItem == self::SUM_OP || $stItem == self::COUNT_OP) {
$currentItem["summary"][$si] += $newSummaries[$si];
} else if ($stItem == self::AVG_OP) {
$count1 = isset($currentItem["count"]) ? $currentItem["count"] : 1;
$count2 = isset($row[$groupInfo["groupIndex"] + 1]) ? $row[$groupInfo["groupIndex"] + 1] : 1;
if (($count1 + $count2) > 0)
$currentItem["summary"][$si] = (($currentItem["summary"][$si] * $count1) + ($newSummaries[$si] * $count2)) / ($count1 + $count2);
else
$currentItem["summary"][$si] = ($currentItem["summary"][$si] + $newSummaries[$si]) / 2;
}
}
}
// If expanded, append items
if ($groupInfo["lastGroupExpanded"]) {
$currentItem["items"][] = self::_GetNewDataItem($row, $groupInfo);
}
}
}
if ($groupInfo["groupIndex"] < $groupInfo["groupCount"] - $groupIndexOffset) {
$groupInfo["groupIndex"]++;
self::_GroupData($row, $currentItem["items"], $groupInfo);
}
}
// Tarih dizisini tanımla ve işle
private static function _getDateSequence($groupInfo) {
// Tarih sırası bilgisi
$result = [
'isDateSequence' => false,
'sequence' => []
];
if (!isset($groupInfo["groupNames"]) || count($groupInfo["groupNames"]) < 2) {
return $result;
}
$dateFields = [];
// Tüm tarih alanlarını tespit et
foreach ($groupInfo["groupNames"] as $index => $name) {
if (strpos($name, 'dx_') !== 0) {
continue;
}
$type = '';
if (strpos($name, '_year') !== false) {
$type = 'year';
} else if (strpos($name, '_month') !== false) {
$type = 'month';
} else if (strpos($name, '_day') !== false) {
$type = 'day';
} else {
continue;
}
$dateFields[] = [
'index' => $index,
'type' => $type,
'name' => $name
];
}
// Eğer en az 2 tarih alanı varsa, tarih sırası oluştur
if (count($dateFields) >= 2) {
// Yıl, ay, gün sırasına göre sırala
usort($dateFields, function($a, $b) {
$order = ['year' => 0, 'month' => 1, 'day' => 2];
return $order[$a['type']] - $order[$b['type']];
});
$result['isDateSequence'] = true;
$result['sequence'] = $dateFields;
}
return $result;
}
// Tarih dizisini doğru bir şekilde işle
private static function _processDateSequence($row, &$resultItems, $groupInfo, $dateSequenceInfo) {
$groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2;
$sequence = $dateSequenceInfo['sequence'];
// İlk seviye için gruplama yap (genellikle yıl)
$firstLevelIndex = $sequence[0]['index'];
$firstLevelType = $sequence[0]['type'];
$currentKey = isset($row[$firstLevelIndex]) ? trim($row[$firstLevelIndex]) : null;
if ($currentKey === "") {
$currentKey = null;
}
// İlk seviyede (yıl) item ara
$yearItem = null;
foreach($resultItems as &$item) {
if(isset($item["key"]) && (trim($item["key"]) === trim($currentKey) ||
($item["key"] === null && ($currentKey === null || $currentKey === "")))) {
$yearItem = &$item;
break;
}
}
// Yıl item'ı yoksa oluştur
if ($yearItem === null) {
// Özel yıl item'ı oluştur
$tempGroupInfo = $groupInfo;
$tempGroupInfo["groupIndex"] = $firstLevelIndex;
$yearItem = self::_GetNewGroupItem($row, $tempGroupInfo);
$yearItem["dateLevel"] = $firstLevelType;
$resultItems[] = &$yearItem;
}
// İkinci seviye için (genellikle ay)
if (count($sequence) > 1) {
$secondLevelIndex = $sequence[1]['index'];
$secondLevelType = $sequence[1]['type'];
$secondKey = isset($row[$secondLevelIndex]) ? trim($row[$secondLevelIndex]) : null;
if ($secondKey === "") {
$secondKey = null;
}
// İkinci seviyede (ay) item ara
$monthItem = null;
foreach($yearItem["items"] as &$item) {
if(isset($item["key"]) && (trim($item["key"]) === trim($secondKey) ||
($item["key"] === null && ($secondKey === null || $secondKey === "")))) {
$monthItem = &$item;
break;
}
}
// Ay item'ı yoksa oluştur
if ($monthItem === null) {
// Özel ay item'ı oluştur
$tempGroupInfo = $groupInfo;
$tempGroupInfo["groupIndex"] = $secondLevelIndex;
$monthItem = self::_GetNewGroupItem($row, $tempGroupInfo);
$monthItem["dateLevel"] = $secondLevelType;
$yearItem["items"][] = &$monthItem;
}
// Üçüncü seviye için (genellikle gün)
if (count($sequence) > 2) {
$thirdLevelIndex = $sequence[2]['index'];
$thirdLevelType = $sequence[2]['type'];
$thirdKey = isset($row[$thirdLevelIndex]) ? trim($row[$thirdLevelIndex]) : null;
if ($thirdKey === "") {
$thirdKey = null;
}
// Üçüncü seviyede (gün) item ara
$dayItem = null;
foreach($monthItem["items"] as &$item) {
if(isset($item["key"]) && (trim($item["key"]) === trim($thirdKey) ||
($item["key"] === null && ($thirdKey === null || $thirdKey === "")))) {
$dayItem = &$item;
break;
}
}
// Gün item'ı yoksa oluştur
if ($dayItem === null) {
// Özel gün item'ı oluştur
$tempGroupInfo = $groupInfo;
$tempGroupInfo["groupIndex"] = $thirdLevelIndex;
$dayItem = self::_GetNewGroupItem($row, $tempGroupInfo);
$dayItem["dateLevel"] = $thirdLevelType;
$monthItem["items"][] = &$dayItem;
}
// Veriler için
if ($groupInfo["lastGroupExpanded"]) {
$dataItem = self::_GetNewDataItem($row, $groupInfo);
$dayItem["items"][] = $dataItem;
}
} else if ($groupInfo["lastGroupExpanded"]) {
// İki seviye varsa (sadece yıl ve ay), veriyi ay seviyesinde göster
$dataItem = self::_GetNewDataItem($row, $groupInfo);
$monthItem["items"][] = $dataItem;
}
} else if ($groupInfo["lastGroupExpanded"]) {
// Tek seviye varsa (sadece yıl), veriyi yıl seviyesinde göster
$dataItem = self::_GetNewDataItem($row, $groupInfo);
$yearItem["items"][] = $dataItem;
}
}
public static function GetGroupedDataFromQuery($queryResult, $groupSettings) {
$result = array();
$row = NULL;
$groupSummaryTypes = NULL;
$dataFieldNames = NULL;
$startSummaryFieldIndex = NULL;
$endSummaryFieldIndex = NULL;
if ($groupSettings["lastGroupExpanded"]) {
$queryFields = $queryResult->fetch_fields();
$dataFieldNames = array();
for ($i = $groupSettings["groupCount"]; $i < count($queryFields); $i++) {
$dataFieldNames[] = $queryFields[$i]->name;
}
}
if (isset($groupSettings["summaryTypes"])) {
$groupSummaryTypes = $groupSettings["summaryTypes"];
$startSummaryFieldIndex = $groupSettings["groupCount"] - 1;
$endSummaryFieldIndex = $startSummaryFieldIndex + count($groupSummaryTypes);
}
// Grup isimlerini al (tarih filtreleri için gerekli)
$groupNames = array();
if (isset($groupSettings["groupNames"])) {
$groupNames = $groupSettings["groupNames"];
} elseif ($queryResult && $queryResult->field_count > 0) {
$fields = $queryResult->fetch_fields();
for ($i = 0; $i < $groupSettings["groupCount"]; $i++) {
if (isset($fields[$i])) {
$groupNames[] = $fields[$i]->name;
}
}
// Sonuçlar için yeniden başa dönmek gerek
$queryResult->data_seek(0);
}
// Tarih alanlarını sıralamak için düzenleme (yıl, ay, gün)
$dateGroupIndices = self::getDateGroupIndices($groupNames);
$groupInfo = array(
"groupCount" => $groupSettings["groupCount"],
"groupIndex" => 0,
"summaryTypes" => $groupSummaryTypes,
"lastGroupExpanded" => $groupSettings["lastGroupExpanded"],
"dataFieldNames" => $dataFieldNames,
"processCommas" => isset($groupSettings["processCommas"]) ? $groupSettings["processCommas"] : true,
"groupNames" => $groupNames,
"dateGroupIndices" => $dateGroupIndices
);
while ($row = $queryResult->fetch_array(MYSQLI_NUM)) {
if (isset($startSummaryFieldIndex)) {
for ($i = $startSummaryFieldIndex; $i <= $endSummaryFieldIndex; $i++) {
$row[$i] = Utils::StringToNumber($row[$i]);
}
}
self::_GroupData($row, $result, $groupInfo);
}
if (!$groupSettings["lastGroupExpanded"]) {
self::_GroupData($row, $result, $groupInfo);
}
else {
if (isset($groupSettings["skip"]) && $groupSettings["skip"] >= 0 &&
isset($groupSettings["take"]) && $groupSettings["take"] >= 0) {
$result = array_slice($result, $groupSettings["skip"], $groupSettings["take"]);
}
}
return $result;
}
private static function getDateGroupIndices($groupNames) {
$yearIndex = -1;
$monthIndex = -1;
$dayIndex = -1;
foreach ($groupNames as $index => $name) {
if (strpos($name, 'dx_') === 0) {
if (strpos($name, '_year') !== false) {
$yearIndex = $index;
} else if (strpos($name, '_month') !== false) {
$monthIndex = $index;
} else if (strpos($name, '_day') !== false) {
$dayIndex = $index;
}
}
}
return [
'year' => $yearIndex,
'month' => $monthIndex,
'day' => $dayIndex
];
}
public static function IsLastGroupExpanded($items) {
$result = true;
$itemsCount = count($items);
if ($itemsCount > 0) {
$lastItem = $items[$itemsCount - 1];
if (gettype($lastItem) === "object") {
$result = isset($lastItem->isExpanded) ? $lastItem->isExpanded === true : true;
}
else {
$result = true;
}
}
return $result;
}
public static function GetFieldSetBySelectors($items) {
$group = "";
$sort = "";
$select = "";
foreach ($items as $item) {
$groupField = NULL;
$sortField = NULL;
$selectField = NULL;
$desc = false;
if (is_string($item) && strlen($item = trim($item))) {
$selectField = $groupField = $sortField = Utils::QuoteStringValue($item);
}
else if (gettype($item) === "object" && isset($item->selector)) {
$quoteSelector = Utils::QuoteStringValue($item->selector);
$desc = isset($item->desc) ? $item->desc : false;
// Tarih gruplamalarını düzenle
if (isset($item->groupInterval)) {
if (is_int($item->groupInterval)) {
$groupField = Utils::QuoteStringValue(sprintf("%s%s_%d", self::GENERATED_FIELD_PREFIX, $item->selector, $item->groupInterval));
$selectField = sprintf("(%s - (%s %% %d)) %s %s",
$quoteSelector,
$quoteSelector,
$item->groupInterval,
self::AS_OP,
$groupField);
}
else {
// Tarih gruplamalarını düzgün şekilde yap
$interval = strtolower($item->groupInterval);
$groupField = Utils::QuoteStringValue(sprintf("%s%s_%s", self::GENERATED_FIELD_PREFIX, $item->selector, $interval));
if ($interval == 'year') {
$selectField = sprintf("YEAR(%s) %s %s",
$quoteSelector,
self::AS_OP,
$groupField);
}
else if ($interval == 'month') {
$selectField = sprintf("MONTH(%s) %s %s",
$quoteSelector,
self::AS_OP,
$groupField);
}
else if ($interval == 'day') {
$selectField = sprintf("DAY(%s) %s %s",
$quoteSelector,
self::AS_OP,
$groupField);
}
else if ($interval == 'dayofweek') {
$selectField = sprintf("DAYOFWEEK(%s) - 1 %s %s",
$quoteSelector,
self::AS_OP,
$groupField);
}
else {
$selectField = sprintf("%s(%s) %s %s",
strtoupper($interval),
$quoteSelector,
self::AS_OP,
$groupField);
}
}
$sortField = $groupField;
}
else {
$selectField = $groupField = $sortField = $quoteSelector;
}
}
if (isset($selectField)) {
$select .= (strlen($select) > 0 ? ", ".$selectField : $selectField);
}
if (isset($groupField)) {
$group .= (strlen($group) > 0 ? ", ".$groupField : $groupField);
}
if (isset($sortField)) {
$sort .= (strlen($sort) > 0 ? ", ".$sortField : $sortField).
($desc ? " DESC" : "");
}
}
return array(
"group" => $group,
"sort" => $sort,
"select" => $select
);
}
private static function _IsSummaryTypeValid($summaryType) {
return in_array($summaryType, array(self::MIN_OP, self::MAX_OP, self::AVG_OP, self::COUNT_OP, self::SUM_OP));
}
public static function GetSummaryInfo($expression, $tableName = null) {
$result = array();
$fields = "";
$summaryTypes = array();
foreach ($expression as $index => $item) {
if (gettype($item) === "object" && isset($item->summaryType)) {
$summaryType = strtoupper(trim($item->summaryType));
if (!self::_IsSummaryTypeValid($summaryType)) {
continue;
}
$summaryTypes[] = $summaryType;
$selector = (isset($item->selector) && is_string($item->selector)) ? $item->selector : null;
$quotedSelector = isset($selector) ? Utils::QuoteStringValue($selector) : "1";
if (self::shouldApplyWeldedFilter($tableName, $item, $summaryType)) {
$quotedSelector = self::applyWeldedFilterExpression($quotedSelector, $summaryType);
}
$fields .= sprintf("%s(%s) %s %sf%d",
strlen($fields) > 0 ? ", ".$summaryType : $summaryType,
$quotedSelector,
self::AS_OP,
self::GENERATED_FIELD_PREFIX,
$index);
}
}
$result["fields"] = $fields;
$result["summaryTypes"] = $summaryTypes;
return $result;
}
private static function shouldApplyWeldedFilter($tableName, $item, $summaryType) {
if ($tableName !== 'weld_logs') {
return false;
}
$hasExplicitFlag = isset($item->weldedOnly) && $item->weldedOnly;
$forceByContext = SummaryContext::shouldExcludeMechanical();
if (!$hasExplicitFlag && !$forceByContext) {
return false;
}
return in_array($summaryType, [self::SUM_OP, self::AVG_OP]);
}
private static function getMechanicalTypesList() {
static $cachedList = null;
if ($cachedList !== null) {
return $cachedList;
}
if (function_exists('get_mechanical_joint_types')) {
$types = get_mechanical_joint_types();
} else {
$types = ['BJ', 'FJ', 'TH', 'CJ'];
}
$escaped = array_map(function($type) {
$normalized = strtoupper(trim($type));
return "'" . addslashes($normalized) . "'";
}, $types);
$cachedList = implode(",", $escaped);
if ($cachedList === '') {
$cachedList = "''";
}
return $cachedList;
}
private static function applyWeldedFilterExpression($quotedSelector, $summaryType) {
$mechanicalList = self::getMechanicalTypesList();
$typeColumnExpr = "UPPER(TRIM(COALESCE(`type_of_welds`, '')))";
$thenValue = $summaryType === self::AVG_OP ? "NULL" : "0";
return sprintf(
"CASE WHEN %s IN (%s) THEN %s ELSE %s END",
$typeColumnExpr,
$mechanicalList,
$thenValue,
$quotedSelector
);
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
namespace App\DevExtreme;
class DataSourceLoader {
public static function Load($dbSet, $params, $module = null) {
$result = NULL;
if (isset($dbSet) && get_class($dbSet) == "App\DevExtreme\DbSet" && isset($params) && is_array($params)) {
// Check if distinctColumn parameter is provided (for header filter unique values)
// distinctColumn can come as string (from GET) or already parsed
$distinctColumn = null;
if (isset($params["distinctColumn"])) {
$distinctColumn = $params["distinctColumn"];
// If it's a JSON string, decode it
if (is_string($distinctColumn) && (substr($distinctColumn, 0, 1) === '[' || substr($distinctColumn, 0, 1) === '{')) {
$decoded = json_decode($distinctColumn, true);
if ($decoded !== null) {
$distinctColumn = $decoded;
}
}
// Log for debugging
error_log("DataSourceLoader: distinctColumn parameter received: " . var_export($distinctColumn, true) . " (type: " . gettype($distinctColumn) . ")");
}
if (!empty($distinctColumn) && is_string($distinctColumn)) {
// Apply filters first
$dbSet->Filter(Utils::GetItemValueOrDefault($params, "filter"));
if ($module === 'weldlog-employer-view' && $dbSet->getTableName() === 'weld_logs') {
self::ApplyEmployerViewFilters($dbSet);
}
// Get distinct values
error_log("DataSourceLoader: Getting distinct values for column: " . $distinctColumn . " in table: " . $dbSet->getTableName());
$distinctValues = $dbSet->GetDistinctValues($distinctColumn);
if ($dbSet->GetLastError() !== NULL) {
error_log("GetDistinctValues error: " . $dbSet->GetLastError());
// Return error response instead of NULL
$result = array();
$result["data"] = array();
$result["hasBlanks"] = false;
$result["error"] = $dbSet->GetLastError();
return $result;
}
error_log("DataSourceLoader: Found " . count($distinctValues) . " distinct values");
// Check for blanks
$hasBlanks = $dbSet->HasBlanks($distinctColumn);
if ($dbSet->GetLastError() !== NULL) {
error_log("HasBlanks error: " . $dbSet->GetLastError());
// Continue even if HasBlanks fails
}
// Format response for header filter
$result = array();
$result["data"] = array_map(function($value) {
return array("value" => $value, "text" => $value);
}, $distinctValues);
$result["hasBlanks"] = $hasBlanks;
return $result;
}
// Normal data loading
$dbSet->Select(Utils::GetItemValueOrDefault($params, "select"))
->Filter(Utils::GetItemValueOrDefault($params, "filter"));
if ($module === 'weldlog-employer-view' && $dbSet->getTableName() === 'weld_logs') {
self::ApplyEmployerViewFilters($dbSet);
}
$totalSummary = $dbSet->GetTotalSummary(Utils::GetItemValueOrDefault($params, "totalSummary"),
Utils::GetItemValueOrDefault($params, "filter"));
if ($dbSet->GetLastError() !== NULL) {
error_log("GetTotalSummary error: " . $dbSet->GetLastError());
return $result;
}
$totalCount = (isset($params["requireTotalCount"]) && $params["requireTotalCount"] === true)
? $dbSet->GetCount() : NULL;
if ($dbSet->GetLastError() !== NULL) {
return $result;
}
$dbSet->Sort(Utils::GetItemValueOrDefault($params, "sort"));
$groupCount = NULL;
$skip = Utils::GetItemValueOrDefault($params, "skip");
$take = Utils::GetItemValueOrDefault($params, "take");
if (isset($params["group"])) {
$groupExpression = $params["group"];
$groupSummary = Utils::GetItemValueOrDefault($params, "groupSummary");
$dbSet->Group($groupExpression, $groupSummary, $skip, $take);
if (isset($params["requireGroupCount"]) && $params["requireGroupCount"] === true) {
$groupCount = $dbSet->GetGroupCount();
}
}
else {
$dbSet->SkipTake($skip, $take);
}
$result = array();
$result["data"] = $dbSet->AsArray();
if ($dbSet->GetLastError() !== NULL) {
return $result;
}
if (isset($totalCount)) {
$result["totalCount"] = $totalCount;
}
if (isset($totalSummary)) {
$result["summary"] = $totalSummary;
}
if (isset($groupCount)) {
$result["groupCount"] = $groupCount;
}
}
else {
throw new \Exception("Invalid params");
}
return $result;
}
/**
* Apply employer view specific filters to weld_logs table
* Filters:
* 1. Exclude records with repair_status = 'Repair' in repair_logs
* 2. Exclude records with NDT results other than 'Accept / Годен' or empty/null
*/
private static function ApplyEmployerViewFilters($dbSet) {
$weldLogsTable = $dbSet->getTableName();
$repairFilter = "NOT EXISTS (
SELECT 1 FROM repair_logs
WHERE repair_logs.iso_number = {$weldLogsTable}.iso_number
AND repair_logs.new_joint_no = {$weldLogsTable}.no_of_the_joint_as_per_as_built_survey
AND repair_logs.repair_status = 'Repair'
)";
$ndtFields = [
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ferrite_result',
'ht_result'
];
$ndtFilters = [];
foreach ($ndtFields as $field) {
$ndtFilters[] = "({$weldLogsTable}.{$field} IN ('Accept / Годен', '') OR {$weldLogsTable}.{$field} IS NULL)";
}
$ndtFilter = implode(' AND ', $ndtFilters);
$combinedFilter = "({$repairFilter}) AND ({$ndtFilter})";
$dbSet->AddCustomWhere($combinedFilter);
}
}
+624
View File
@@ -0,0 +1,624 @@
<?php
namespace App\DevExtreme;
class DbSet {
private static $SELECT_OP = "SELECT";
private static $FROM_OP = "FROM";
private static $WHERE_OP = "WHERE";
private static $ORDER_OP = "ORDER BY";
private static $GROUP_OP = "GROUP BY";
private static $ALL_FIELDS = "*";
private static $LIMIT_OP = "LIMIT";
private static $INSERT_OP = "INSERT INTO";
private static $VALUES_OP = "VALUES";
private static $UPDATE_OP = "UPDATE";
private static $SET_OP = "SET";
private static $DELETE_OP = "DELETE";
private static $MAX_ROW_INDEX = 2147483647;
private $dbTableName;
private $tableNameIndex = 0;
private $lastWrappedTableName;
private $resultQuery;
private $mySQL;
private $lastError;
private $groupSettings;
private $customWhereClause = null;
public function __construct($mySQL, $table) {
if (!is_a($mySQL, "\mysqli") || !isset($table)) {
throw new \Exception("Invalid params");
}
$this->mySQL = $mySQL;
$this->dbTableName = $table;
$this->resultQuery = sprintf("%s %s %s %s",
self::$SELECT_OP,
self::$ALL_FIELDS,
self::$FROM_OP,
$this->dbTableName);
}
public function GetLastError() {
return $this->lastError;
}
public function getTableName() {
return $this->dbTableName;
}
private function _WrapQuery() {
$this->tableNameIndex++;
$this->lastWrappedTableName = "{$this->dbTableName}_{$this->tableNameIndex}";
$this->resultQuery = sprintf("%s %s %s (%s) %s %s",
self::$SELECT_OP,
self::$ALL_FIELDS,
self::$FROM_OP,
$this->resultQuery,
AggregateHelper::AS_OP,
$this->lastWrappedTableName);
}
private function _PrepareQueryForLastOperator($operator) {
$operator = trim($operator);
$lastOperatorPos = strrpos($this->resultQuery, " ".$operator." ");
if ($lastOperatorPos !== false) {
$lastBracketPos = strrpos($this->resultQuery, ")");
if (($lastBracketPos !== false && $lastOperatorPos > $lastBracketPos) || ($lastBracketPos === false)) {
$this->_WrapQuery();
}
}
}
public function Select($expression) {
Utils::EscapeExpressionValues($this->mySQL, $expression);
$this->_SelectImpl($expression);
return $this;
}
private function _SelectImpl($expression, $needQuotes = true) {
if (isset($expression)) {
$fields = "";
if (is_string($expression)) {
$expression = explode(",", $expression);
}
if (is_array($expression)) {
foreach ($expression as $field) {
$fields .= (strlen($fields) ? ", " : "").($needQuotes ? Utils::QuoteStringValue(trim($field)) : trim($field));
}
}
if (strlen($fields)) {
$allFieldOperatorPos = strpos($this->resultQuery, self::$ALL_FIELDS);
if ($allFieldOperatorPos == 7) {
$this->resultQuery = substr_replace($this->resultQuery, $fields, 7, strlen(self::$ALL_FIELDS));
}
else {
$this->_WrapQuery();
$this->_SelectImpl($expression);
}
}
}
}
public function Filter($expression) {
Utils::EscapeExpressionValues($this->mySQL, $expression);
if (isset($expression) && is_array($expression)) {
$result = FilterHelper::GetSqlExprByArray($expression);
if (strlen($result)) {
$this->_PrepareQueryForLastOperator(self::$WHERE_OP);
$this->resultQuery .= sprintf(" %s %s",
self::$WHERE_OP,
$result);
}
}
return $this;
}
/**
* Add custom raw SQL WHERE clause
* This is used for complex filters that cannot be expressed via FilterHelper
* @param string $rawWhereClause Raw SQL WHERE clause (without WHERE keyword)
* @return $this
*/
public function AddCustomWhere($rawWhereClause) {
if (isset($rawWhereClause) && strlen(trim($rawWhereClause)) > 0) {
$this->_PrepareQueryForLastOperator(self::$WHERE_OP);
if (stripos($this->resultQuery, " " . self::$WHERE_OP . " ") !== false) {
$this->resultQuery .= sprintf(" AND (%s)",
trim($rawWhereClause));
if ($this->customWhereClause === null) {
$this->customWhereClause = trim($rawWhereClause);
} else {
$this->customWhereClause .= " AND (" . trim($rawWhereClause) . ")";
}
} else {
$this->resultQuery .= sprintf(" %s %s",
self::$WHERE_OP,
trim($rawWhereClause));
$this->customWhereClause = trim($rawWhereClause);
}
}
return $this;
}
public function Sort($expression) {
Utils::EscapeExpressionValues($this->mySQL, $expression);
if (isset($expression)) {
$result = "";
if (is_string($expression)) {
$result = trim($expression);
}
if (is_array($expression)) {
$fieldSet = AggregateHelper::GetFieldSetBySelectors($expression);
$result = $fieldSet["sort"];
}
if (strlen($result)) {
$this->_PrepareQueryForLastOperator(self::$ORDER_OP);
$this->resultQuery .= sprintf(" %s %s",
self::$ORDER_OP,
$result);
}
}
return $this;
}
public function SkipTake($skip, $take) {
$skip = (!isset($skip) || !is_int($skip) ? 0 : $skip);
$take = (!isset($take) || !is_int($take) ? self::$MAX_ROW_INDEX : $take);
if ($skip != 0 || $take != 0) {
$this->_PrepareQueryForLastOperator(self::$LIMIT_OP);
$this->resultQuery .= sprintf(" %s %0.0f, %0.0f",
self::$LIMIT_OP,
$skip,
$take);
}
return $this;
}
/**
* Extract WHERE clause from resultQuery
* @return string WHERE clause without WHERE keyword, or empty string
*/
private function _ExtractWhereClause() {
$whereClause = "";
$wherePos = stripos($this->resultQuery, " " . self::$WHERE_OP . " ");
if ($wherePos !== false) {
$afterWhere = substr($this->resultQuery, $wherePos + strlen(" " . self::$WHERE_OP . " "));
// Find the end of WHERE clause by looking for ORDER BY, GROUP BY, or LIMIT
// Use regex to find the first occurrence of these keywords
$endPattern = '/\s+(ORDER\s+BY|GROUP\s+BY|LIMIT)\s+/i';
if (preg_match($endPattern, $afterWhere, $matches, PREG_OFFSET_CAPTURE)) {
$whereClause = trim(substr($afterWhere, 0, $matches[0][1]));
} else {
$whereClause = trim($afterWhere);
}
}
return $whereClause;
}
/**
* Get distinct values for a specific column
* @param string $columnName Column name to get distinct values from
* @return array Array of distinct values
*/
public function GetDistinctValues($columnName) {
$result = array();
if ($this->mySQL && isset($columnName) && strlen($columnName) > 0) {
Utils::EscapeExpressionValues($this->mySQL, $columnName);
$quotedColumn = Utils::QuoteStringValue($columnName);
// Build the distinct query - use WHERE clause from resultQuery if exists
$distinctQuery = sprintf("%s DISTINCT %s %s %s",
self::$SELECT_OP,
$quotedColumn,
self::$FROM_OP,
$this->dbTableName);
// Extract WHERE clause from resultQuery (after WHERE keyword)
$whereClause = "";
$wherePos = stripos($this->resultQuery, " " . self::$WHERE_OP . " ");
if ($wherePos !== false) {
// Get everything after WHERE
$afterWhere = substr($this->resultQuery, $wherePos + strlen(" " . self::$WHERE_OP . " "));
// Remove ORDER BY, GROUP BY, LIMIT if they exist (find first occurrence)
$endPattern = '/\s+(ORDER\s+BY|GROUP\s+BY|LIMIT)\s+/i';
if (preg_match($endPattern, $afterWhere, $matches, PREG_OFFSET_CAPTURE)) {
$whereClause = trim(substr($afterWhere, 0, $matches[0][1]));
} else {
$whereClause = trim($afterWhere);
}
if (strlen($whereClause) > 0) {
$distinctQuery .= " " . self::$WHERE_OP . " " . $whereClause;
}
}
// Also add customWhereClause if exists (from AddCustomWhere)
if ($this->customWhereClause !== null) {
if (strlen($whereClause) > 0) {
$distinctQuery .= " AND (" . $this->customWhereClause . ")";
} else {
$distinctQuery .= " " . self::$WHERE_OP . " " . $this->customWhereClause;
}
}
// Add ORDER BY for sorted results
$distinctQuery .= sprintf(" %s %s",
self::$ORDER_OP,
$quotedColumn);
// Limit results to prevent memory issues with very large distinct value sets
// If there's a WHERE clause (filtered), we can return more results
// If no filter, limit to first 1000 to prevent UI freezing
$hasFilter = ($wherePos !== false && strlen($whereClause) > 0) || ($this->customWhereClause !== null);
if (!$hasFilter) {
$distinctQuery .= sprintf(" %s 1000",
self::$LIMIT_OP);
}
$this->lastError = NULL;
$sanitizedQuery = $this->_sanitizeDateValues($distinctQuery);
// Debug: Log the query for troubleshooting
error_log("GetDistinctValues SQL: " . $sanitizedQuery);
error_log("GetDistinctValues resultQuery: " . $this->resultQuery);
$queryResult = $this->mySQL->query($sanitizedQuery);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
error_log("GetDistinctValues query error: " . $this->lastError);
}
else {
while ($row = $queryResult->fetch_array(MYSQLI_NUM)) {
$value = $row[0];
if ($value !== null && $value !== '') {
$result[] = $value;
}
}
$queryResult->close();
error_log("GetDistinctValues found " . count($result) . " distinct values");
}
}
return $result;
}
/**
* Check if there are blank (NULL or empty) values for a specific column
* @param string $columnName Column name to check
* @return bool True if blanks exist, false otherwise
*/
public function HasBlanks($columnName) {
$result = false;
if ($this->mySQL && isset($columnName) && strlen($columnName) > 0) {
Utils::EscapeExpressionValues($this->mySQL, $columnName);
$quotedColumn = Utils::QuoteStringValue($columnName);
// Build the blank check query
$blankQuery = sprintf("%s COUNT(1) %s %s",
self::$SELECT_OP,
self::$FROM_OP,
$this->dbTableName);
// Extract WHERE clause from resultQuery
$wherePos = stripos($this->resultQuery, " " . self::$WHERE_OP . " ");
$blankCondition = sprintf("(%s IS NULL OR %s = '')",
$quotedColumn,
$quotedColumn);
if ($wherePos !== false) {
// Get everything after WHERE
$afterWhere = substr($this->resultQuery, $wherePos + strlen(" " . self::$WHERE_OP . " "));
// Remove ORDER BY, GROUP BY, LIMIT if they exist
$endPattern = '/\s+(ORDER\s+BY|GROUP\s+BY|LIMIT)\s+/i';
if (preg_match($endPattern, $afterWhere, $matches, PREG_OFFSET_CAPTURE)) {
$whereClause = trim(substr($afterWhere, 0, $matches[0][1]));
} else {
$whereClause = trim($afterWhere);
}
if (strlen($whereClause) > 0) {
$blankQuery .= " " . self::$WHERE_OP . " " . $whereClause . " AND " . $blankCondition;
} else {
$blankQuery .= " " . self::$WHERE_OP . " " . $blankCondition;
}
} else {
// No WHERE clause, add blank condition
$blankQuery .= " " . self::$WHERE_OP . " " . $blankCondition;
}
$this->lastError = NULL;
$sanitizedQuery = $this->_sanitizeDateValues($blankQuery);
$queryResult = $this->mySQL->query($sanitizedQuery);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
}
else if ($queryResult->num_rows > 0) {
$row = $queryResult->fetch_array(MYSQLI_NUM);
$result = Utils::StringToNumber($row[0]) > 0;
$queryResult->close();
}
}
return $result;
}
private function _CreateGroupCountQuery($firstGroupField, $skip = NULL, $take = NULL) {
$groupCount = $this->groupSettings["groupCount"];
$lastGroupExpanded = $this->groupSettings["lastGroupExpanded"];
if (!$lastGroupExpanded) {
if ($groupCount === 2) {
$this->groupSettings["groupItemCountQuery"] = sprintf("%s COUNT(1) %s (%s) AS %s_%d",
self::$SELECT_OP,
self::$FROM_OP,
$this->resultQuery,
$this->dbTableName,
$this->tableNameIndex + 1);
if (isset($skip) || isset($take)) {
$this->SkipTake($skip, $take);
}
}
}
else {
$groupQuery = sprintf("%s COUNT(1) %s %s %s %s",
self::$SELECT_OP,
self::$FROM_OP,
$this->dbTableName,
self::$GROUP_OP,
$firstGroupField);
$this->groupSettings["groupItemCountQuery"] = sprintf("%s COUNT(1) %s (%s) AS %s_%d",
self::$SELECT_OP,
self::$FROM_OP,
$groupQuery,
$this->dbTableName,
$this->tableNameIndex + 1);
if (isset($skip) || isset($take)) {
$this->groupSettings["skip"] = isset($skip) ? Utils::StringToNumber($skip) : 0;
$this->groupSettings["take"] = isset($take) ? Utils::StringToNumber($take) : 0;
}
}
}
public function Group($expression, $groupSummary = NULL, $skip = NULL, $take = NULL) {
Utils::EscapeExpressionValues($this->mySQL, $expression);
Utils::EscapeExpressionValues($this->mySQL, $groupSummary);
$this->groupSettings = NULL;
if (isset($expression)) {
$groupFields = "";
$sortFields = "";
$selectFields = "";
$lastGroupExpanded = true;
$groupCount = 0;
if (is_string($expression)) {
$selectFields = $sortFields = $groupFields = trim($expression);
$groupCount = count(explode(",", $expression));
}
if (is_array($expression)) {
$groupCount = count($expression);
$fieldSet = AggregateHelper::GetFieldSetBySelectors($expression);
$groupFields = $fieldSet["group"];
$selectFields = $fieldSet["select"];
$sortFields = $fieldSet["sort"];
$lastGroupExpanded = AggregateHelper::IsLastGroupExpanded($expression);
}
if ($groupCount > 0) {
if (!$lastGroupExpanded) {
$groupSummaryData = isset($groupSummary) && is_array($groupSummary) ? AggregateHelper::GetSummaryInfo($groupSummary, $this->dbTableName) : NULL;
$selectExpression = sprintf("%s, %s(1)%s",
strlen($selectFields) ? $selectFields : $groupFields,
AggregateHelper::COUNT_OP,
(isset($groupSummaryData) && isset($groupSummaryData["fields"]) && strlen($groupSummaryData["fields"]) ?
", ".$groupSummaryData["fields"] : ""));
$groupCount++;
$this->_WrapQuery();
$this->_SelectImpl($selectExpression, false);
$this->resultQuery .= sprintf(" %s %s",
self::$GROUP_OP,
$groupFields);
$this->Sort($sortFields);
}
else {
$this->_WrapQuery();
$selectExpression = "{$selectFields}, {$this->lastWrappedTableName}.*";
$this->_SelectImpl($selectExpression, false);
$this->resultQuery .= sprintf(" %s %s",
self::$ORDER_OP,
$sortFields);
}
$lastGroupExpanded = true;
$this->groupSettings = array();
$this->groupSettings["groupCount"] = $groupCount;
$this->groupSettings["lastGroupExpanded"] = $lastGroupExpanded;
$this->groupSettings["summaryTypes"] = !$lastGroupExpanded ? $groupSummaryData["summaryTypes"] : NULL;
$firstGroupField = explode(",", $groupFields)[0];
$this->_CreateGroupCountQuery($firstGroupField, $skip, $take);
}
}
return $this;
}
public function GetTotalSummary($expression, $filterExpression = NULL) {
Utils::EscapeExpressionValues($this->mySQL, $expression);
Utils::EscapeExpressionValues($this->mySQL, $filterExpression);
$result = NULL;
if (isset($expression) && is_array($expression)) {
$summaryInfo = AggregateHelper::GetSummaryInfo($expression, $this->dbTableName);
$fields = $summaryInfo["fields"];
if (strlen($fields) > 0) {
$filter = "";
if (isset($filterExpression)) {
if (is_string($filterExpression)) {
$filter = trim($filterExpression);
}
if (is_array($filterExpression)) {
$filter = FilterHelper::GetSqlExprByArray($filterExpression);
}
}
$combinedFilter = "";
if (strlen($filter) > 0 && $this->customWhereClause !== null) {
$combinedFilter = "(" . $filter . ") AND (" . $this->customWhereClause . ")";
} else if (strlen($filter) > 0) {
$combinedFilter = $filter;
} else if ($this->customWhereClause !== null) {
$combinedFilter = $this->customWhereClause;
}
$totalSummaryQuery = sprintf("%s %s %s %s %s",
self::$SELECT_OP,
$fields,
self::$FROM_OP,
$this->dbTableName,
strlen($combinedFilter) > 0 ? self::$WHERE_OP." ".$combinedFilter : "");
$this->lastError = NULL;
$queryResult = $this->mySQL->query($totalSummaryQuery);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
}
else if ($queryResult->num_rows > 0) {
$result = $queryResult->fetch_array(MYSQLI_NUM);
foreach ($result as $i => $item) {
$result[$i] = Utils::StringToNumber($item);
}
}
if ($queryResult !== false) {
$queryResult->close();
}
}
}
return $result;
}
public function GetGroupCount() {
$result = 0;
if ($this->mySQL && isset($this->groupSettings) && isset($this->groupSettings["groupItemCountQuery"])) {
$this->lastError = NULL;
$queryResult = $this->mySQL->query($this->groupSettings["groupItemCountQuery"]);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
}
else if ($queryResult->num_rows > 0) {
$row = $queryResult->fetch_array(MYSQLI_NUM);
$result = Utils::StringToNumber($row[0]);
}
if ($queryResult !== false) {
$queryResult->close();
}
}
return $result;
}
public function GetCount() {
$result = 0;
if ($this->mySQL) {
$countQuery = sprintf("%s %s(1) %s (%s) %s %s_%d",
self::$SELECT_OP,
AggregateHelper::COUNT_OP,
self::$FROM_OP,
$this->resultQuery,
AggregateHelper::AS_OP,
$this->dbTableName,
$this->tableNameIndex + 1);
$this->lastError = NULL;
$countQuery = $this->_sanitizeDateValues($countQuery);
$queryResult = $this->mySQL->query($countQuery);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
}
else if ($queryResult->num_rows > 0) {
$row = $queryResult->fetch_array(MYSQLI_NUM);
$result = Utils::StringToNumber($row[0]);
}
if ($queryResult !== false) {
$queryResult->close();
}
}
return $result;
}
private function _sanitizeDateValues($query) {
return preg_replace("/'0NaN-NaN-NaN'|'\d*NaN-NaN-NaN'/", "NULL", $query);
}
public function AsArray() {
$result = NULL;
if ($this->mySQL) {
$this->lastError = NULL;
$sanitizedQuery = $this->_sanitizeDateValues($this->resultQuery);
$queryResult = $this->mySQL->query($sanitizedQuery);
if (!$queryResult) {
$this->lastError = $this->mySQL->error;
}
else {
if (isset($this->groupSettings)) {
$result = AggregateHelper::GetGroupedDataFromQuery($queryResult, $this->groupSettings);
}
else {
$result = $queryResult->fetch_all(MYSQLI_ASSOC);
}
$queryResult->close();
}
}
return $result;
}
public function Insert($values) {
Utils::EscapeExpressionValues($this->mySQL, $values);
$result = NULL;
if (isset($values) && is_array($values)) {
$fields = "";
$fieldValues = "";
foreach ($values as $prop => $value) {
$fields .= (strlen($fields) ? ", " : "").Utils::QuoteStringValue($prop);
$fieldValues .= (strlen($fieldValues) ? ", " : "").Utils::QuoteStringValue($value, false);
}
if (strlen($fields) > 0) {
$queryString = sprintf("%s %s (%s) %s(%s)",
self::$INSERT_OP,
$this->dbTableName,
$fields,
self::$VALUES_OP,
$fieldValues);
$this->lastError = NULL;
if ($this->mySQL->query($queryString) == true) {
$result = $this->mySQL->affected_rows;
}
else {
$this->lastError = $this->mySQL->error;
}
}
}
return $result;
}
public function Update($key, $values) {
Utils::EscapeExpressionValues($this->mySQL, $key);
Utils::EscapeExpressionValues($this->mySQL, $values);
$result = NULL;
if (isset($key) && is_array($key) && isset($values) && is_array($values)) {
$fields = "";
foreach ($values as $prop => $value) {
$templ = strlen($fields) == 0 ? "%s = %s" : ", %s = %s";
$fields .= sprintf($templ,
Utils::QuoteStringValue($prop),
Utils::QuoteStringValue($value, false));
}
if (strlen($fields) > 0) {
$queryString = sprintf("%s %s %s %s %s %s",
self::$UPDATE_OP,
$this->dbTableName,
self::$SET_OP,
$fields,
self::$WHERE_OP,
FilterHelper::GetSqlExprByKey($key));
$this->lastError = NULL;
if ($this->mySQL->query($queryString) == true) {
$result = $this->mySQL->affected_rows;
}
else {
$this->lastError = $this->mySQL->error;
}
}
}
return $result;
}
public function Delete($key) {
Utils::EscapeExpressionValues($this->mySQL, $key);
$result = NULL;
if (isset($key) && is_array($key)) {
$queryString = sprintf("%s %s %s %s %s",
self::$DELETE_OP,
self::$FROM_OP,
$this->dbTableName,
self::$WHERE_OP,
FilterHelper::GetSqlExprByKey($key));
$this->lastError = NULL;
if ($this->mySQL->query($queryString) == true) {
$result = $this->mySQL->affected_rows;
}
else {
$this->lastError = $this->mySQL->error;
}
}
return $result;
}
}
+242
View File
@@ -0,0 +1,242 @@
<?php
namespace App\DevExtreme;
class FilterHelper {
private static $AND_OP = "AND";
private static $OR_OP = "OR";
private static $LIKE_OP = "LIKE";
private static $NOT_OP = "NOT";
private static $IS_OP = "IS";
private static function _GetSqlFieldName($field) {
$fieldParts = explode(".", $field);
$result = "";
$fieldName = Utils::QuoteStringValue(trim($fieldParts[0]));
if (count($fieldParts) == 2) {
$dateProperty = trim($fieldParts[1]);
$sqlDateFunction = "";
$fieldPattern = "";
switch ($dateProperty) {
case "year":
case "month":
case "day": {
$sqlDateFunction = strtoupper($dateProperty);
$fieldPattern = "%s(%s)";
break;
}
case "dayOfWeek": {
$sqlDateFunction = strtoupper($dateProperty);
$fieldPattern = "%s(%s) - 1";
break;
}
default: {
throw new \Exception("The \"".$dateProperty."\" command is not supported");
}
}
$result = sprintf($fieldPattern, $sqlDateFunction, $fieldName);
}
else {
$result = $fieldName;
}
return $result;
}
private static function _GetSimpleSqlExpr($expression) {
$result = "";
$itemsCount = count($expression);
$fieldName = self::_GetSqlFieldName(trim($expression[0]));
// Known operators list
$knownOperators = ['=', '<>', '>', '>=', '<', '<=', 'startswith', 'endswith', 'contains', 'notcontains', 'in', '==='];
if ($itemsCount == 2) {
$val = $expression[1];
$valLower = is_string($val) ? strtolower(trim($val)) : '';
// Check if second element is an operator (means searching for empty values)
if (in_array($valLower, $knownOperators)) {
// This is an operator without value - treat as empty value search
switch ($valLower) {
case 'contains':
case 'startswith':
case 'endswith':
case '=':
// Search for NULL or empty string
$result = sprintf("(%s IS NULL OR %s = '')", $fieldName, $fieldName);
break;
case '<>':
case 'notcontains':
// Search for NOT NULL and NOT empty
$result = sprintf("(%s IS NOT NULL AND %s != '')", $fieldName, $fieldName);
break;
default:
// For other operators, just check IS NULL
$result = sprintf("%s IS NULL", $fieldName);
break;
}
} elseif ($val === "(Empty)" || $val === "(Boş)" || $val === "") {
$result = sprintf("%s IS NULL", $fieldName);
} else {
$result = sprintf("%s = %s", $fieldName, Utils::QuoteStringValue($val, false));
}
}
else if ($itemsCount == 3) {
$clause = strtolower(trim($expression[1]));
$val = $expression[2];
if ($val === "(Empty)" || $val === "(Boş)" || $val === "" || is_null($val)) {
switch ($clause) {
case "=":
case "contains":
case "startswith":
case "endswith": {
// Hem NULL hem empty string için kontrol
if(strpos($fieldName, 'date') !== false) {
$result = sprintf("(%s IS NULL)", $fieldName);
} else {
$result = sprintf("(%s IS NULL OR %s = '')", $fieldName, $fieldName);
}
return $result;
}
case "<>":
case "notcontains": {
// Ne NULL ne de empty string
$result = sprintf("(%s IS NOT NULL AND %s != '')", $fieldName, $fieldName);
return $result;
}
}
}
else {
switch ($clause) {
case "===": {
// Exact match - DO NOT split comma-separated values
$result = sprintf("%s = %s", $fieldName, Utils::QuoteStringValue($val, false));
return $result;
}
case "=": {
// Exact match - treat comma-separated values as single entity
$pattern = "%s %s %s";
$val = Utils::QuoteStringValue($val, false);
break;
}
case "<>": {
$pattern = "%s %s %s";
$val = Utils::QuoteStringValue($val, false);
break;
}
case ">":
case ">=":
case "<":
case "<=": {
$pattern = "%s %s %s";
$val = Utils::QuoteStringValue($val, false);
break;
}
case "startswith": {
$pattern = "%s %s '%s%%'";
$clause = self::$LIKE_OP;
$val = addcslashes($val, "%_");
break;
}
case "endswith": {
$pattern = "%s %s '%%%s'";
$val = addcslashes($val, "%_");
$clause = self::$LIKE_OP;
break;
}
case "contains": {
$pattern = "%s %s '%%%s%%'";
$val = addcslashes($val, "%_");
$clause = self::$LIKE_OP;
break;
}
case "notcontains": {
$pattern = "%s %s '%%%s%%'";
$val = addcslashes($val, "%_");
$clause = sprintf("%s %s", self::$NOT_OP, self::$LIKE_OP);
break;
}
case "noneof": {
if (is_array($val) && count($val) > 0) {
$quotedValues = array_map(function($v) {
return Utils::QuoteStringValue($v, false);
}, $val);
$result = sprintf("%s NOT IN (%s)", $fieldName, implode(", ", $quotedValues));
return $result;
}
$pattern = "%s <> %s";
$val = Utils::QuoteStringValue($val, false);
break;
}
case "anyof":
case "in": {
if (is_array($val) && count($val) > 0) {
$quotedValues = array_map(function($v) {
return Utils::QuoteStringValue($v, false);
}, $val);
$result = sprintf("%s IN (%s)", $fieldName, implode(", ", $quotedValues));
return $result;
}
// Fall through to default if not array
}
default: {
$clause = $clause ?: "=";
$pattern = "%s %s %s";
if(!is_array($val)) {
$val = Utils::QuoteStringValue($val, false);
}
}
}
}
if(isset($pattern)) {
$result = sprintf($pattern, $fieldName, $clause, $val);
$result = str_replace(" = ''", " is null", $result);
} else {
$result = ""; // Should not happen with new default logic but safe fallback
}
}
return $result;
}
public static function GetSqlExprByArray($expression) {
$result = "(";
$prevItemWasArray = false;
foreach ($expression as $index => $item) {
if (is_string($item)) {
$prevItemWasArray = false;
if ($index == 0) {
if ($item == "!") {
$result .= sprintf("%s ", self::$NOT_OP);
continue;
}
$result .= (isset($expression) && is_array($expression)) ? self::_GetSimpleSqlExpr($expression) : "";
break;
}
$strItem = strtoupper(trim($item));
if ($strItem == self::$AND_OP || $strItem == self::$OR_OP) {
$result .= sprintf(" %s ", $strItem);
}
continue;
}
if (is_array($item)) {
if ($prevItemWasArray) {
$result .= sprintf(" %s ", self::$AND_OP);
}
$result .= self::GetSqlExprByArray($item);
$prevItemWasArray = true;
}
}
$result .= ")";
return $result;
}
public static function GetSqlExprByKey($key) {
$result = "";
foreach ($key as $prop => $value) {
$templ = strlen($result) == 0 ?
"%s = %s" :
" ".self::$AND_OP." %s = %s";
$result .= sprintf($templ,
Utils::QuoteStringValue($prop),
Utils::QuoteStringValue($value, false));
}
return $result;
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace App\DevExtreme;
class LoadHelper {
public static function LoadModule($className) {
$namespaceNamePos = strpos($className, __NAMESPACE__);
if ($namespaceNamePos === 0) {
$subFolderPath = substr($className, $namespaceNamePos + strlen(__NAMESPACE__));
$filePath = __DIR__.str_replace("\\", DIRECTORY_SEPARATOR, $subFolderPath).".php";
require_once($filePath);
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\DevExtreme;
class SummaryContext
{
protected static bool $excludeMechanical = false;
public static function setExcludeMechanical(bool $flag): void
{
self::$excludeMechanical = $flag;
}
public static function shouldExcludeMechanical(): bool
{
return self::$excludeMechanical;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\DevExtreme;
class Utils {
private static $NULL_VAL = "NULL";
private static $FORBIDDEN_CHARACTERS = array(
"`", "\"", "'", "~", "!", "@", "#", "\$",
"%", "=", "[", "]", "\\", "/" , "|", "^",
"&", "*", "(", ")", "+", "<", ">", ",", "{",
"}", "?", ":", ";", "\r", "\n"
);
public static function StringToNumber($str) {
$currentLocale = localeconv();
$decimalPoint = $currentLocale["decimal_point"];
$result = strpos($str, $decimalPoint) === false ? intval($str) : floatval($str);
return $result;
}
public static function EscapeExpressionValues($mySql, &$expression = NULL) {
if (isset($expression)) {
if (is_string($expression)) {
$expression = $mySql->real_escape_string($expression);
}
else if (is_array($expression)) {
foreach ($expression as &$arr_value) {
self::EscapeExpressionValues($mySql, $arr_value);
}
unset($arr_value);
}
else if (gettype($expression) === "object") {
foreach ($expression as $prop => $value) {
self::EscapeExpressionValues($mySql, $expression->$prop);
}
}
}
}
public static function QuoteStringValue($value, $isFieldName = true) {
if (!$isFieldName) {
$value = self::_ConvertDateTimeToMySQLValue($value);
} else {
$value = str_replace(self::$FORBIDDEN_CHARACTERS, "", $value);
}
$resultPattern = $isFieldName ? "`%s`" : (is_bool($value) || is_null($value) ? "%s" : "'%s'");
$stringValue = is_bool($value) ? ($value ? "1" : "0") : (is_null($value) ? self::$NULL_VAL : strval($value));
$result = sprintf($resultPattern, $stringValue);
return $result;
}
public static function GetItemValueOrDefault($params, $key, $defaultValue = NULL) {
return isset($params[$key]) ? $params[$key] : $defaultValue;
}
private static function _ConvertDatePartToISOValue($date) {
$dateParts = explode("/", $date);
return sprintf("%s-%s-%s", $dateParts[2], $dateParts[0], $dateParts[1]);
}
private static function _ConvertDateTimeToMySQLValue($strValue) {
$result = $strValue;
if (preg_match("/^\d{1,2}\/\d{1,2}\/\d{4}$/", $strValue) === 1) {
$result = self::_ConvertDatePartToISOValue($strValue);
}
else if (preg_match("/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}\.\d{3}$/", $strValue) === 1) {
$spacePos = strpos($strValue, " ");
$datePart = substr($strValue, 0, $spacePos);
$timePart = substr($strValue, $spacePos + 1);
$result = sprintf("%s %s", self::_ConvertDatePartToISOValue($datePart), $timePart);
}
return $result;
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Docs\Strategies;
use Knuckles\Scribe\Extracting\Strategies\Strategy;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* Custom Scribe Strategy to add admin-ajax endpoints to documentation.
*
* This strategy scans the admin-ajax blade directory and adds
* each endpoint to the API documentation automatically.
*/
class AddEndpointsStrategy extends Strategy
{
/**
* The stage this strategy belongs to.
*/
public string $stage = 'responses';
/**
* Trait this strategy applies to.
*/
public static function routableIf(ExtractedEndpointData $endpoint): bool
{
// Only apply to the dynamic endpoint route
return Str::contains($endpoint->route->uri, 'endpoints/{endpoint}');
}
/**
* @param ExtractedEndpointData $endpointData
* @param array $routeRules
* @return array|null
*/
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
{
// This strategy doesn't modify responses directly
// The actual endpoint enumeration happens via the Artisan command
return null;
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Docs\Strategies;
use Knuckles\Scribe\Extracting\Strategies\Strategy;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Illuminate\Support\Str;
/**
* Custom Scribe Strategy to fix request body for GET/HEAD methods.
*
* This strategy ensures that request body is only included for
* POST, PUT, PATCH, DELETE methods, not for GET/HEAD.
*/
class FixRequestBodyStrategy extends Strategy
{
/**
* The stage this strategy belongs to.
*/
public string $stage = 'bodyParameters';
/**
* Trait this strategy applies to.
*/
public static function routableIf(ExtractedEndpointData $endpoint): bool
{
// Only apply to the dynamic endpoint route
return Str::contains($endpoint->route->uri, '{table}/{action}');
}
/**
* @param ExtractedEndpointData $endpointData
* @param array $routeRules
* @return array|null
*/
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
{
// Get HTTP methods for this endpoint
$httpMethods = $endpointData->httpMethods ?? [];
// Check if the first method (primary method) is GET or HEAD
// Scribe creates separate endpoints for each HTTP method
if (count($httpMethods) > 0) {
$primaryMethod = strtoupper($httpMethods[0]);
// If primary method is GET or HEAD, remove body parameters
if (in_array($primaryMethod, ['GET', 'HEAD']) && count($endpointData->bodyParameters) > 0) {
// Return empty array to remove body parameters
return [];
}
}
// Otherwise, return null to keep existing body parameters
return null;
}
}
@@ -0,0 +1,94 @@
<?php
namespace App\Docs\Strategies;
use Knuckles\Scribe\Extracting\Strategies\Strategy;
use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Illuminate\Support\Str;
/**
* Custom Scribe Strategy to fix URL parameters for dynamic routes.
*
* This strategy ensures that path parameters are correctly extracted
* for routes with dynamic segments like /api/{table}/{action}
*/
class FixUrlParametersStrategy extends Strategy
{
/**
* The stage this strategy belongs to.
*/
public string $stage = 'urlParameters';
/**
* Trait this strategy applies to.
*/
public static function routableIf(ExtractedEndpointData $endpoint): bool
{
// Only apply to the dynamic endpoint route
return Str::contains($endpoint->route->uri, '{table}/{action}');
}
/**
* @param ExtractedEndpointData $endpointData
* @param array $routeRules
* @return array|null
*/
public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
{
// Extract path parameters from route URI
$uri = $endpointData->route->uri;
$parameters = [];
// Match {param} patterns in the URI
if (preg_match_all('/\{(\w+)\}/', $uri, $matches)) {
foreach ($matches[1] as $paramName) {
// Always add/override parameter to ensure it exists
// This ensures path parameters are always present even if
// default strategies didn't extract them correctly
// Scribe expects array format, not object
$parameters[$paramName] = [
'name' => $paramName,
'type' => 'string',
'required' => true,
'description' => $this->getParameterDescription($paramName),
'example' => $this->getParameterExample($paramName),
];
}
}
// Always return parameters if found
// Scribe will merge these with existing parameters
if (count($parameters) > 0) {
return $parameters;
}
// If no parameters found, return null to let other strategies handle it
return null;
}
/**
* Get description for a parameter
*/
private function getParameterDescription(string $paramName): string
{
$descriptions = [
'table' => 'The Module Slug or Table Name.',
'action' => 'The operation to perform.',
];
return $descriptions[$paramName] ?? ucfirst($paramName) . ' parameter.';
}
/**
* Get example value for a parameter
*/
private function getParameterExample(string $paramName): string
{
$examples = [
'table' => 'users',
'action' => 'read',
];
return $examples[$paramName] ?? 'example';
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use App\Models\FeedbackReport;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Illuminate\Support\Facades\Schema;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
use Maatwebsite\Excel\Concerns\WithEvents;
use Maatwebsite\Excel\Events\AfterSheet;
class ExportExcel implements FromCollection, WithHeadings, ShouldAutoSize
{
public $tableName;
public $columnNames;
public $exceptsColumns;
public $module;
public function __construct(string $tableName, $columnNames = "", $module = null)
{
$this->tableName = $tableName;
$this->module = $module ?? get("module");
$exceptsColumns = [
'created_at',
'updated_at',
'json',
'pic',
'slug',
'email_verified_at',
'password',
'permissions',
'alias',
'remember_token',
'recover',
'last_seen',
'branslar',
'ust',
'uid',
'note'
];
/*
try {
$columnNames = explode(",", $columnNames);
$this->columnNames = $columnNames;
} catch (\Throwable $th) {
$this->columnNames = array_diff(Schema::getColumnListing($this->tableName), $exceptsColumns);
}
*/
$this->columnNames = array_diff(Schema::getColumnListing($this->tableName), $exceptsColumns);
}
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
$query = db($this->tableName)
->select($this->columnNames);
if($this->tableName == "users") {
$query = $query->whereNotIn("level", ['Admin']);
}
// Apply employer view filters for weldlog-employer-view module
if($this->module === 'weldlog-employer-view' && $this->tableName === 'weld_logs') {
$query = $this->applyEmployerViewFilters($query);
}
return $query->get();
}
/**
* Apply employer view specific filters to weld_logs table
* Filters:
* 1. Exclude records with repair_status = 'Repair' in repair_logs
* 2. Exclude records with NDT results other than 'Accept / Годен' or empty/null
*/
private function applyEmployerViewFilters($query)
{
// Filter 1: Exclude repair logs
$repairFilter = "NOT EXISTS (
SELECT 1 FROM repair_logs
WHERE repair_logs.iso_number = weld_logs.iso_number
AND repair_logs.new_joint_no = weld_logs.no_of_the_joint_as_per_as_built_survey
AND repair_logs.repair_status = 'Repair'
)";
// Filter 2: Only acceptable NDT results
$ndtFields = [
'vt_result',
'rt_result',
'ut_result',
'pt_result',
'mt_result',
'pmi_result',
'ferrite_result',
'ht_result'
];
$ndtFilters = [];
foreach ($ndtFields as $field) {
$ndtFilters[] = "({$field} IN ('Accept / Годен', '') OR {$field} IS NULL)";
}
$ndtFilter = implode(' AND ', $ndtFilters);
$combinedFilter = "({$repairFilter}) AND ({$ndtFilter})";
return $query->whereRaw($combinedFilter);
}
public function headings(): array
{
return $this->columnNames;
}
/*
public function registerEvents(): array
{
return [
AfterSheet::class => function(AfterSheet $event)
{
$cellRange = 'A1:G1'; // All headers
$event->sheet->getDelegate()->getStyle($cellRange)->getFont()->setBorder($cellRange, 'thin');
$event->sheet->getDelegate()->getStyle($cellRange)->getFont()->setSize(14);
},
];
}
*/
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Exports;
use App\Models\NaksCertificate;
use Maatwebsite\Excel\Concerns\FromCollection;
class ExportNaksCertificate implements FromCollection
{
/**
* @return \Illuminate\Support\Collection
*/
public function collection()
{
return NaksCertificate::get();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Fields extends Model
{
//
//use SoftDeletes;
protected $table = 'fields';
protected $primaryKey = 'id';
public $incrementing = true;
public $timestamps = true;
}
+3
View File
@@ -0,0 +1,3 @@
<?php function active_projects() {
return db("weld_logs")->whereNotNull("project")->groupBy("project")->get()->pluck("project")->toArray();
} ?>
+448
View File
@@ -0,0 +1,448 @@
<?php
/**
* Register Creator için Excel tablosuna yeni satır ekleme fonksiyonu.
*
* Bu fonksiyon belge bilgilerini alarak Excel tablosuna formatlı bir şekilde yeni satır ekler,
* ilgili PDF dosyasını kopyalar ve log kaydı oluşturur.
*
* @param array $search Aranılan dosya yolları
* @param array $selectDocument Seçilen doküman bilgileri (title, path vs.)
* @param string $fullFolder Hedef klasör yolu
* @param string $lineNumber Hat numarası veya benzeri tanımlayıcı
* @param string $documentDate Doküman tarihi
* @param int $rowNo Satır numarası (sıra)
* @param object $sheet PhpSpreadsheet nesnesi
* @param int $currentRow Mevcut satır pozisyonu
* @param bool $override Mevcut dosyaların üzerine yazılıp yazılmayacağı
*
* @return int Yeni eklenen satırın sonraki pozisyonu veya satır eklenemediyse mevcut pozisyon
*/
function addRowInTable($search, $selectDocument, $fullFolder, $lineNumber, $documentDate, $rowNo, &$sheet, $currentRow, $override = false)
{
try {
// Log başlangıcı
$logPrefix = "[ROW-$rowNo]";
Log::debug("$logPrefix Processing document: " . $selectDocument['title2'] . ', Line: ' . $lineNumber);
// Aynı rapor numarası için zaten satır eklenmiş mi kontrol et
static $addedReports = [];
$reportKey = $selectDocument['title2'] . '_' . $lineNumber;
if (in_array($reportKey, $addedReports)) {
Log::debug("$logPrefix Duplicate report detected, skipping: " . $selectDocument['title2'] . ' - ' . $lineNumber);
// Tekrarlı rapor için basit log
try {
$fullFolder = str_replace("storage/documents/", "", $fullFolder);
$duplicateLog = "🔄 DUPLICATE: " . $selectDocument['title2'] . ' - ' . $lineNumber . "\n";
Storage::append($fullFolder . 'log.txt', $duplicateLog);
} catch (\Throwable $logError) {
echo("⚠️ Log write error\n");
}
return $currentRow;
}
// Bu raporu eklenmiş olarak işaretle
$addedReports[] = $reportKey;
$constructor = Cache::get("rc_contractor");
$firstPage = Cache::get("rc_firstPage");
$lastPage = Cache::get("rc_lastPage");
$rowTitle = $selectDocument['title2'];
if(isset($selectDocument['title3'])) {
$rowTitle = $selectDocument['title3'];
$selectDocument['title2'] = $selectDocument['title3'];
}
if(isset($selectDocument['title4'])) {
$rowTitle = $selectDocument['title4'];
}
$convertTerm = $selectDocument['path'];
if(strpos($convertTerm, "Naks_Consumables") !== false) {
$convertTerm = $selectDocument['path'] . '_' . $selectDocument['type'];
}
if(strpos($convertTerm, "Procedure") !== false) {
$convertTerm = $lineNumber;
}
if(isset($selectDocument['incoming_control_description'])) {
$convertTerm = $selectDocument['incoming_control_description'];
}
// Get row title safely - convertRu might return array
$rowTitleRaw = convertRu($convertTerm);
$rowTitle = is_array($rowTitleRaw) ? json_encode($rowTitleRaw) : (string)$rowTitleRaw;
// Log if conversion returned unexpected type
if (is_array($rowTitleRaw)) {
Log::warning('convertRu returned array', [
'term' => $convertTerm,
'result' => $rowTitleRaw
]);
}
// Basit dosya kontrolü ve log hazırlığı
$foundFiles = [];
$notFoundFiles = [];
foreach ($search as $searchPath) {
if (file_exists($searchPath) || Storage::exists(str_replace("storage/documents/", "", $searchPath))) {
$foundFiles[] = $searchPath;
} else {
$notFoundFiles[] = $searchPath;
}
}
if(!empty($foundFiles)) {
// İlk bulunan dosyayı kullan
$firstFoundFile = $foundFiles[0];
// Dosya türü istatistiği
$fileExtension = strtolower(pathinfo($firstFoundFile, PATHINFO_EXTENSION));
if (!isset($GLOBALS['file_type_stats'])) {
$GLOBALS['file_type_stats'] = [];
}
if (!isset($GLOBALS['file_type_stats'][$fileExtension])) {
$GLOBALS['file_type_stats'][$fileExtension] = 0;
}
$GLOBALS['file_type_stats'][$fileExtension]++;
$order = $selectDocument['order'] + 1;
$addInLineNumber = ['по сварке трубопроводов(ЖСР)'];
if(in_array($selectDocument['title2'], $addInLineNumber)) {
$fileName = "$order - {$selectDocument['title2']} - $lineNumber.pdf";
$fullPath = "$fullFolder"."$fileName";
} else {
if(isset($selectDocument['file_name'])) {
$fileName = "$order - {$selectDocument['file_name']}.pdf";
} else {
if(isset($selectDocument['title2'])) {
$fileName = "$order - {$selectDocument['title2']}.pdf";
} else {
$fileName = "$order - {$selectDocument['title2']}.pdf";
}
}
$fullPath = "$fullFolder"."$fileName";
}
Log::debug("File name: " . $fileName);
$documentDate = df($documentDate);
// Sayfa sayısını al
$allPath = "{$firstFoundFile}";
$command = "pdftk '$allPath' dump_data | grep NumberOfPages";
putenv('LANG=ru_RU.UTF-8');
$output = shell_exec($command);
try {
$pageCount = (int) trim(explode(" ", $output)[1]);
} catch (\Throwable $th) {
$pageCount = 1;
}
if($firstPage == 0) {
$firstPage = 1;
} else {
$firstPage = $lastPage + 1;
}
$lastPage = $firstPage + $pageCount - 1;
Cache::put("rc_lastPage", $lastPage);
Cache::put("rc_firstPage", $firstPage);
// Basit log formatı oluştur
$simpleLog = "";
$simpleLog .= "📋 " . ($rowNo) . ". " . $rowTitle . " - " . $lineNumber . " | " . count($foundFiles) . " file(s)\n";
foreach ($foundFiles as $index => $file) {
$simpleLog .= " -- " . basename($file) . "\n";
}
if (!empty($notFoundFiles)) {
$simpleLog .= " ❓ Not found: " . count($notFoundFiles) . " file(s)\n";
foreach ($notFoundFiles as $index => $file) {
$simpleLog .= " -- " . basename($file) . " (missing)\n";
}
}
$simpleLog .= "\n";
// Sayfa aralığını oluştur
if ($pageCount == 1) {
if($firstPage == 1) {
$pageIndex = $firstPage;
} else {
$prevPage = $firstPage - 1;
$pageIndex = "$prevPage - $firstPage";
}
} else {
$pageIndex = "$firstPage - $lastPage";
}
// Excel'e veri ekle
try {
$templateRowCells = [];
$templateFormulas = [];
$templateRow = Cache::get("rc_template_row", $currentRow);
foreach ($sheet->getRowIterator($templateRow, $templateRow)->current()->getCellIterator() as $cell) {
$colIndex = $cell->getColumn();
$templateRowCells[$colIndex] = $cell->getValue();
if ($cell->isFormula()) {
$templateFormulas[$colIndex] = $cell->getValue();
}
}
$mergedCells = [];
foreach ($sheet->getMergeCells() as $mergeRange) {
if (preg_match('/^\D*'.$templateRow.'$/', explode(':', $mergeRange)[0])) {
$mergedCells[] = $mergeRange;
}
}
$sheet->insertNewRowBefore($currentRow + 1, 1);
$replacements = [];
$replacements['{rowNo}'] = $rowNo;
$replacements['{rowTitle}'] = $rowTitle;
$replacements['{documentReportNumber}'] = $lineNumber;
$replacements['{documentDate}'] = $documentDate;
$replacements['{constructor}'] = $constructor;
$replacements['{pageCount}'] = $pageCount;
$replacements['{pageIndex}'] = $pageIndex;
// Safe get with type checking for all title fields
$replacements['{title1}'] = isset($selectDocument['title1'])
? (is_array($selectDocument['title1']) ? json_encode($selectDocument['title1']) : (string)$selectDocument['title1'])
: '';
$replacements['{ndt_report_no}'] = isset($selectDocument['title2'])
? (is_array($selectDocument['title2']) ? json_encode($selectDocument['title2']) : (string)$selectDocument['title2'])
: '';
$replacements['{pdf_document_title}'] = isset($selectDocument['title3'])
? (is_array($selectDocument['title3']) ? json_encode($selectDocument['title3']) : (string)$selectDocument['title3'])
: '';
$replacements['{ndt_register_title}'] = isset($selectDocument['title4'])
? (is_array($selectDocument['title4']) ? json_encode($selectDocument['title4']) : (string)$selectDocument['title4'])
: '';
foreach ($templateRowCells as $colIndex => $cellValue) {
if (isset($templateFormulas[$colIndex])) {
$formula = $templateFormulas[$colIndex];
$rowOffset = ($currentRow + 1) - $templateRow;
$updatedFormula = preg_replace_callback(
'/([A-Z]+)(\d+)/',
function($matches) use ($rowOffset) {
$col = $matches[1];
$row = intval($matches[2]);
$row += $rowOffset;
return $col . $row;
},
$formula
);
$sheet->setCellValue($colIndex . ($currentRow + 1), $updatedFormula);
} else {
if (is_string($cellValue)) {
foreach ($replacements as $placeholder => $replacement) {
if (strpos($cellValue, $placeholder) !== false) {
$cellValue = str_replace($placeholder, $replacement, $cellValue);
}
}
}
$sheet->setCellValue($colIndex . ($currentRow + 1), $cellValue);
}
}
foreach ($mergedCells as $mergeRange) {
$adjustedMergeRange = preg_replace_callback('/\d+/', function($matches) use ($currentRow, $templateRow) {
return $matches[0] == $templateRow ? $currentRow + 1 : $matches[0];
}, $mergeRange);
try {
$sheet->mergeCells($adjustedMergeRange);
} catch (\Throwable $th) {
// Hata önemsiz
}
}
foreach ($sheet->getRowIterator($templateRow, $templateRow)->current()->getCellIterator() as $cell) {
$colIndex = $cell->getColumn();
try {
$style = $sheet->getStyle($colIndex . $templateRow);
$sheet->duplicateStyle($style, $colIndex . ($currentRow + 1));
} catch (\Throwable $th) {
// Hata önemsiz
}
}
$templateRowHeight = $sheet->getRowDimension($templateRow)->getRowHeight();
$sheet->getRowDimension($currentRow + 1)->setRowHeight($templateRowHeight);
} catch (\Throwable $th) {
$simpleLog .= " ❌ Excel error: " . $th->getMessage() . "\n\n";
throw $th;
}
// Dosya kopyalama
$originalSearchPath = $firstFoundFile;
$search[0] = str_replace("storage/documents/", "", $firstFoundFile);
$fullFolder = str_replace("storage/documents/", "", $fullFolder);
$fullPath = $fullFolder . basename($fileName);
try {
$targetDir = dirname($fullPath);
if (!Storage::exists($targetDir)) {
Storage::makeDirectory($targetDir);
}
$copyStatus = "";
if(Storage::exists($fullPath)) {
if ($override) {
Storage::delete($fullPath);
$copyStatus = "overwritten";
} else {
$currentContent = md5(Storage::get($fullPath));
$newContent = '';
if (Storage::exists($search[0])) {
$newContent = md5(Storage::get($search[0]));
} else if (file_exists($originalSearchPath)) {
$newContent = md5(file_get_contents($originalSearchPath));
}
if ($currentContent !== $newContent) {
Storage::delete($fullPath);
$copyStatus = "updated";
} else {
$copyStatus = "same content, skipped";
}
}
} else {
$copyStatus = "copied";
}
if ($copyStatus !== "same content, skipped") {
if (Storage::exists($search[0])) {
Storage::copy($search[0], $fullPath);
} else if (file_exists($originalSearchPath)) {
$fileContent = file_get_contents($originalSearchPath);
Storage::put($fullPath, $fileContent);
}
}
$simpleLog .= " 📁 File: " . $copyStatus . "\n";
// Template dosyası kontrolü (xlsx)
if(isset($selectDocument['type']) && $selectDocument['type'] === 'template') {
$xlsxSourcePath = str_replace('.pdf', '.xlsx', $search[0]);
$xlsxOriginalSourcePath = str_replace('.pdf', '.xlsx', $originalSearchPath);
$xlsxTargetPath = str_replace('.pdf', '.xlsx', $fullPath);
if(Storage::exists($xlsxSourcePath) || file_exists($xlsxOriginalSourcePath)) {
$xlsxCopyStatus = "";
if(Storage::exists($xlsxTargetPath)) {
if ($override) {
Storage::delete($xlsxTargetPath);
$xlsxCopyStatus = "overwritten";
} else {
$currentXlsxContent = md5(Storage::get($xlsxTargetPath));
$newXlsxContent = '';
if (Storage::exists($xlsxSourcePath)) {
$newXlsxContent = md5(Storage::get($xlsxSourcePath));
} else if (file_exists($xlsxOriginalSourcePath)) {
$newXlsxContent = md5(file_get_contents($xlsxOriginalSourcePath));
}
if ($currentXlsxContent !== $newXlsxContent) {
Storage::delete($xlsxTargetPath);
$xlsxCopyStatus = "updated";
} else {
$xlsxCopyStatus = "same content, skipped";
}
}
} else {
$xlsxCopyStatus = "copied";
}
if ($xlsxCopyStatus !== "same content, skipped") {
if (Storage::exists($xlsxSourcePath)) {
Storage::copy($xlsxSourcePath, $xlsxTargetPath);
} else if (file_exists($xlsxOriginalSourcePath)) {
$xlsxContent = file_get_contents($xlsxOriginalSourcePath);
Storage::put($xlsxTargetPath, $xlsxContent);
}
}
$simpleLog .= " 📊 XLSX: " . $xlsxCopyStatus . "\n";
} else {
$simpleLog .= " ❓ XLSX: not found\n";
}
}
} catch (\Throwable $th) {
$simpleLog .= " ❌ Copy error: " . $th->getMessage() . "\n";
}
$simpleLog .= "\n";
// Basit log'u dosyaya ve ekrana yaz
echo($simpleLog);
Storage::append($fullFolder . 'log.txt', $simpleLog);
// Başarılı işlem sayacı
if (!isset($GLOBALS['successful_operations'])) {
$GLOBALS['successful_operations'] = 0;
}
$GLOBALS['successful_operations']++;
return $currentRow + 1;
} else {
// Dosya bulunamadı - basit log
$notFoundLog = "" . ($rowNo) . ". " . $rowTitle . " - " . $lineNumber . " | NO FILES FOUND\n";
$notFoundLog .= " Search paths:\n";
foreach ($search as $index => $searchPath) {
$notFoundLog .= " -- " . basename($searchPath) . "\n";
}
$notFoundLog .= "\n";
echo($notFoundLog);
try {
$fullFolder = str_replace("storage/documents/", "", $fullFolder);
Storage::append($fullFolder . 'log.txt', $notFoundLog);
} catch (\Throwable $logError) {
echo("❌ Log write error\n");
}
return $currentRow;
}
} catch (\Throwable $th) {
Log::error("Unexpected error in addRowInTable: " . $th->getMessage());
try {
$errorLog = "" . ($rowNo ?? 'UNKNOWN') . ". ERROR: " . $th->getMessage() . "\n\n";
$fullFolder = str_replace("storage/documents/", "", $fullFolder ?? '');
Storage::append($fullFolder . 'log.txt', $errorLog);
echo($errorLog);
} catch (\Throwable $logError) {
echo("❌ Fatal error occurred\n");
}
throw $th;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php function addRowInTableTpCreator($search, $selectDocument, $fullFolder, $lineNumber, $documentDate, $rowNo)
{
Log::debug("➡️ addRowInTableTpCreator called with parameters:");
Log::debug("📄 Has search results: " . (isset($search[0]) ? "Yes" : "No"));
Log::debug("📑 Document title: " . $selectDocument['title2']);
Log::debug("📁 Folder: " . $fullFolder);
Log::debug("🔢 Line number: " . $lineNumber);
Log::debug("📅 Document date: " . $documentDate);
Log::debug("🔢 Row number: " . $rowNo);
$constructor = Cache::get("rc_contractor");
$firstPage = Cache::get("rc_firstPage");
$lastPage = Cache::get("rc_lastPage");
/*
Log::info("firstPage $firstPage");
Log::info("lastPage $lastPage");
*/
$rowTitle = $selectDocument['title2'];
if(isset($selectDocument['title3'])) {
$rowTitle = $selectDocument['title3'];
$selectDocument['title2'] = $selectDocument['title3'];
}
if(isset($selectDocument['title4'])) {
$rowTitle = $selectDocument['title4'];
}
if(isset($search[0])) {
$order = $selectDocument['order'] + 1;
@mkdir($fullFolder, 0777, true);
Log::info("creating folder $fullFolder");
$addInLineNumber = ['по сварке трубопроводов(ЖСР)'];
if(in_array($selectDocument['title2'], $addInLineNumber)) {
$fileName = "$order - {$selectDocument['title2']} - $lineNumber.pdf";
$fullPath = "$fullFolder"."$fileName";
Log::debug("🔖 Special case file name with line number: " . $fileName);
} else {
$fileName = "$order - {$selectDocument['title2']}.pdf";
$fullPath = "$fullFolder"."$fileName";
Log::debug("🔖 Standard file name: " . $fileName);
}
$documentDate = df($documentDate);
$output = null;
$returnValue = null;
$allPath = "{$search[0]}";
/*
$command = "pdftk '$allPath' dump_data | grep NumberOfPages";
// dump($command);
// dump($allPath);
putenv('LANG=ru_RU.UTF-8');
$output = shell_exec($command);
try {
$pageCount = (int) trim(explode(" ", $output)[1]);
} catch (\Throwable $th) {
$pageCount = 1;
}
if($firstPage == 0) {
$firstPage = 1;
} else {
$firstPage = $lastPage + 1;
}
// Son sayfa numarasını hesapla
$lastPage = $firstPage + $pageCount - 1;
Cache::put("rc_lastPage", $lastPage);
Cache::put("rc_firstPage", $firstPage);
// Sayfa aralığını oluştur
$pageIndex = ($firstPage == $lastPage) ? $firstPage : "$firstPage - $lastPage";
$tableRow = "
<tr class='bordered'>
<td >$rowNo</td>
<td width='50%' colspan='2'>$rowTitle</td>
<td>$lineNumber</td>
<td>$documentDate</td>
<td>$constructor</td>
<td>$pageCount</td>
<td>$pageIndex</td>
</tr>
";
*/
$search[0] = str_replace("storage/documents/", "", $search[0]);
$fullFolder = str_replace("storage/documents/", "", $fullFolder);
$fullPath = $fullFolder . basename($fileName);
Log::debug("🛠️ File path processing:");
Log::debug("📄 Original source path: storage/documents/" . $search[0]);
Log::debug("📁 Original folder path: storage/documents/" . $fullFolder);
Log::debug("📄 Target filename: " . basename($fileName));
Log::debug("📄 Final target path: " . $fullPath);
/*
Log::info("fullPath: " . $fullPath);
Log::info("search: " . $search[0]);
Log::info("fullFolder: " . $fullFolder);
*/
try {
if(Storage::exists($fullPath)) {
$currentContent = md5(Storage::get($fullPath));
$newContent = md5(Storage::get($search[0]));
Log::debug("📁 Checking file: " . $fullPath);
Log::debug("🔑 Current content MD5: " . $currentContent);
Log::debug("🔑 New content MD5: " . $newContent);
if ($currentContent !== $newContent) {
$log = "🔃✅".$search[0] . " old file deleted and new file copied" . "\n";
Log::debug("📝 " . $log);
Storage::delete($fullPath);
Storage::copy($search[0], $fullPath);
} else {
$log = "🟰" . $search[0] . " already same file, not copied" . "\n";
Log::debug("📝 " . $log);
}
} else {
$log = "".$search[0] . " file copied" . "\n";
Log::debug("📝 " . $log);
Storage::copy($search[0], $fullPath);
}
echo($log);
} catch (\Throwable $th) {
$log = "" . $search[0] . " error ---> \n " . $th->getMessage() . "\n";
echo($log);
Log::debug("❌ Error copying file: " . $search[0]);
Log::debug("❌ Error message: " . $th->getMessage());
Log::debug("❌ Error trace: " . $th->getTraceAsString());
}
Storage::append($fullFolder . 'log.txt', $log);
Log::debug("📜 Appended to log file: " . $fullFolder . 'log.txt');
Log::debug("📜 Log entry: " . $log);
$return = null; //= $tableRow;
} else {
$log = "$rowTitle - $lineNumber not found " . "\n";
echo($log);
Log::debug("❓ Document not found: " . $rowTitle . " - " . $lineNumber);
Log::debug("❓ Search path likely was: storage/documents/{$selectDocument['path']}/*{$lineNumber}*.pdf");
$fullFolder = str_replace("storage/documents/", "", $fullFolder);
Storage::append($fullFolder . 'log.txt', $log);
Log::debug("📜 Appended to log file: " . $fullFolder . 'log.txt');
$return = null;
}
return $return;
} ?>
+130
View File
@@ -0,0 +1,130 @@
<?php
/**
* Excel dosyasını analiz eder ve bellek kullanımı, sorunlu formüller hakkında bilgi verir
*
* @param string $filePath Excel dosyasının geçici yolu
* @return array Analiz sonuçları
*/
function analyzeExcelFile($filePath) {
// Varsayılan sonuç
$result = [
'file_size' => filesize($filePath),
'file_size_formatted' => formatBytes(filesize($filePath)),
'memory_warning' => false,
'problematic_formulas' => []
];
// 5MB üzerindeki dosyalar için bellek uyarısı
if ($result['file_size'] > 5 * 1024 * 1024) {
$result['memory_warning'] = true;
}
// PhpSpreadsheet kütüphanesini kullanarak Excel'i analiz et
try {
require_once base_path('vendor/autoload.php');
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile($filePath);
$reader->setReadDataOnly(false);
$spreadsheet = $reader->load($filePath);
// Tüm çalışma sayfalarını kontrol et
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
$sheetTitle = $worksheet->getTitle();
// Formül içeren hücreleri bul
foreach ($worksheet->getCoordinates() as $coordinate) {
$cell = $worksheet->getCell($coordinate);
if ($cell->isFormula()) {
$formula = $cell->getValue();
// Sorunlu formülleri kontrol et
// 1. Çok geniş aralıkları kontrol et (örn. A1:Z1000000)
if (preg_match('/[A-Z]+\d+:[A-Z]+\d+/', $formula, $matches)) {
foreach ($matches as $range) {
list($start, $end) = explode(':', $range);
// Sayısal kısmı al
preg_match('/\d+/', $end, $endRow);
$endRowNum = intval($endRow[0]);
// Çok büyük aralıklar için uyarı (örneğin 10000 satırdan fazla)
if ($endRowNum > 10000) {
$result['problematic_formulas'][] = [
'sheet' => $sheetTitle,
'cell' => $coordinate,
'formula' => $formula,
'issue' => "Çok geniş aralık kullanımı: $range ($endRowNum satır)"
];
}
}
}
// 2. MAX, MIN, SUM gibi fonksiyonlarda büyük aralıklar
if (preg_match('/(SUM|MIN|MAX|AVERAGE|COUNT)\s*\([A-Z]+\d+:[A-Z]+\d+\)/i', $formula, $matches)) {
foreach ($matches as $func) {
if (preg_match('/\(([A-Z]+\d+:[A-Z]+\d+)\)/', $func, $rangeMatches)) {
$range = $rangeMatches[1];
list($start, $end) = explode(':', $range);
preg_match('/\d+/', $end, $endRow);
$endRowNum = intval($endRow[0]);
if ($endRowNum > 10000) {
$result['problematic_formulas'][] = [
'sheet' => $sheetTitle,
'cell' => $coordinate,
'formula' => $formula,
'issue' => "Fonksiyonda büyük aralık: $func ($endRowNum satır)"
];
}
}
}
}
// 3. OFFSET fonksiyonunun kontrolü
if (stripos($formula, 'OFFSET') !== false) {
$result['problematic_formulas'][] = [
'sheet' => $sheetTitle,
'cell' => $coordinate,
'formula' => $formula,
'issue' => "OFFSET fonksiyonu kullanımı bellek problemlerine neden olabilir"
];
}
// 4. Çok karmaşık iç içe formüller
$openParenCount = substr_count($formula, '(');
if ($openParenCount > 10) {
$result['problematic_formulas'][] = [
'sheet' => $sheetTitle,
'cell' => $coordinate,
'formula' => $formula,
'issue' => "Çok karmaşık iç içe formül ($openParenCount seviyesinde iç içe geçmiş)"
];
}
}
}
}
} catch (\Exception $e) {
// Hata durumunda
$result['error'] = $e->getMessage();
}
return $result;
}
/**
* Bayt cinsinden boyutu okunabilir formata çevirir
*/
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
?>
+11
View File
@@ -0,0 +1,11 @@
<?php
function autocomplete_url($tableName, $column) {
return url("admin/autocomplete/$tableName/$column?with-column");
}
function autocomplete_url2($tableName, $column) {
return url("admin/autocomplete/$tableName/$column");
}
function autocomplete_type($type) {
return url("admin/autocomplete_type/$type");
}
?>
+19
View File
@@ -0,0 +1,19 @@
<?php
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
function cacheBladeLoad($cacheName) {
try {
// Fetch the cached content
$cachedContent = Storage::get('cache/'. $cacheName .'.blade.php');
// Display the cached content
echo $cachedContent;
} catch (\Throwable $th) {
//throw $th;
Log::error('Cache blade load error: ' . $th->getMessage());
}
}
?>
+96
View File
@@ -0,0 +1,96 @@
<?php
use App\Jobs\CacheBladeViewJob;
use Illuminate\Support\Facades\Log;
/**
* Dispatch cache blade view jobs
*
* This helper function dispatches CacheBladeViewJob for specified views.
* It can be used to refresh cached blade views after data updates.
*
* @param array $cacheViews Optional array of cache views to dispatch.
* If provided, only these views will be dispatched (default views will be ignored).
* If empty, default cache views will be dispatched.
* Format: [['view' => 'view.path', 'cache' => 'cache-name'], ...]
* @return void
*/
function dispatchCacheBladeViews(array $cacheViews = [])
{
// Default cache views that should be refreshed after spool-related operations
$defaultCacheViews = [
[
'view' => 'admin-ajax.spool-area-release-no-cache',
'cache' => 'spool-area-release'
],
[
'view' => 'admin-ajax.spool-list-no-cache',
'cache' => 'spool-list'
],
[
'view' => 'admin.type.spool-release.spool-list-chart-no-cache',
'cache' => 'spool-list-chart'
],
];
// If cacheViews is empty, use defaultCacheViews; otherwise use only cacheViews
if (empty($cacheViews)) {
$uniqueCacheViews = $defaultCacheViews;
} else {
// Remove duplicates based on cache name (keep the last one)
$seen = [];
$uniqueCacheViews = [];
// Reverse to process from end, so we keep the last occurrence
foreach (array_reverse($cacheViews) as $cacheView) {
// Validate array structure
if (!is_array($cacheView) || !isset($cacheView['cache']) || !isset($cacheView['view'])) {
continue;
}
$cacheName = $cacheView['cache'];
if (!in_array($cacheName, $seen)) {
$seen[] = $cacheName;
$uniqueCacheViews[] = $cacheView;
}
}
// Reverse back to original order
$uniqueCacheViews = array_reverse($uniqueCacheViews);
}
// Dispatch job for each cache view
foreach ($uniqueCacheViews as $cacheView) {
if (isset($cacheView['view']) && isset($cacheView['cache'])) {
try {
// Rate Limiting (Debounce): Aynı cache view için 60 saniyede bir kez job oluştur.
// Batch excel gibi aynı anda binlerce satırın import edildiği senaryolarda kuyruğun dolmasını engeller.
$rateLimitKey = "rate_limit_dispatch_{$cacheView['cache']}";
if (!\Illuminate\Support\Facades\Cache::has($rateLimitKey)) {
// 60 saniyeliğine kilitle
\Illuminate\Support\Facades\Cache::put($rateLimitKey, true, 60);
// Job'u 15 saniye gecikmeli gönder (arka plandaki güncellemelerin bitmesi için zaman tanı)
CacheBladeViewJob::dispatch($cacheView['view'], $cacheView['cache'])->delay(now()->addSeconds(15));
Log::info('CacheBladeViewJob dispatched to queue with 15s delay (Rate limit started)', [
'view' => $cacheView['view'],
'cache' => $cacheView['cache']
]);
} else {
// Log::debug('CacheBladeViewJob skipped due to rate limit (debounce)', ['cache' => $cacheView['cache']]);
}
} catch (\Exception $e) {
// Silently fail to prevent breaking the main operation
Log::error('Failed to dispatch CacheBladeViewJob', [
'view' => $cacheView['view'],
'cache' => $cacheView['cache'],
'error' => $e->getMessage()
]);
}
}
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
use App\Models\CalibrationLog;
use App\Models\WeldingEquipment;
use Illuminate\Support\Carbon;
if (!function_exists('syncCalibrationFromWeldingEquipment')) {
/**
* Sync a single WeldingEquipment record into CalibrationLog table.
*
* This allows NAKS Welding Equipments to automatically appear in the
* QA -> Calibration Log module while still allowing manual entries.
*
* @param \App\Models\WeldingEquipment|array $equipment
* @return \App\Models\CalibrationLog
*/
function syncCalibrationFromWeldingEquipment($equipment): CalibrationLog
{
if (is_array($equipment)) {
$data = $equipment;
} else {
$data = $equipment->toArray();
}
$sourceId = $data['id'] ?? null;
\Log::debug("Sync attempt for equipment ID: " . $sourceId);
$log = CalibrationLog::firstOrNew([
'source_module' => 'naks_welding_equipment',
'source_id' => $sourceId,
]);
\Log::debug("CalibrationLog exists: " . ($log->exists ? "yes (id: {$log->id})" : "no, will create new"));
$log->instrument = 'Welding Equipment';
$log->description = $data['type_of_welding_machine'] ?? null;
$log->item_no = $log->item_no ?? null;
$log->equipment_name = $data['brand'] ?? null; // brand -> equipment_name
$log->manufacturer = $data['producer'] ?? null; // producer -> manufacturer
$log->serial_no = $data['manufacturer_code'] ?? null; // manufacturer_code -> serial_no
$log->passport_certificate_no = $data['attestation'] ?? null;
$log->quantity = 1; // Always set quantity to 1
// Use date_of_issue as calibration_date and valid_until as calibration_due_date
if (!empty($data['date_of_issue'])) {
$log->calibration_date = $data['date_of_issue'];
}
if (!empty($data['valid_until'])) {
$log->calibration_due_date = $data['valid_until'];
}
// Status calculation based on due date
if (!empty($log->calibration_due_date)) {
$today = Carbon::today();
$dueDate = Carbon::parse($log->calibration_due_date)->startOfDay();
if ($dueDate->lt($today)) {
$log->status = 'Out Of service'; // Expired -> Out Of service
} elseif ($dueDate->lte($today->copy()->addDays(20))) {
$log->status = 'Calibration-Verification'; // Due Soon -> needs calibration
} else {
$log->status = 'In use'; // Valid -> In use
}
}
try {
$result = $log->save();
\Log::debug("Save result: " . ($result ? "success, new id: {$log->id}" : "failed"));
} catch (\Exception $e) {
\Log::error("Save exception: " . $e->getMessage());
throw $e;
}
return $log;
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
function col($size, $title="", $color="0", $options=[], $topButtons=[], $columns=[]) {
$id = str_slug($title);
$colors = colors();
$u = u();
?>
<div class="<?php echo $size ?>">
<div class="block block-themed block-rounded <?php echo isset($options['border']) ? "border" : "" ?>" id="<?php echo $id; ?>">
<?php if($title!="") {
?>
<div class="block-header
<?php if($color!=-1) {
?>
bg-<?php echo $colors[$color]; ?>
<?php
} ?>
">
<div class="block-title"><?php echo e2($title) ?></div>
<?php if(!isset($options['no-options'])) {
?>
<div class="block-options">
<div type="button" class="btn-block-option select-columns d-none" data-toggle="modal" data-target="#select-columns" >
<i class="fa fa-table-columns"></i>
</div>
<?php foreach($options AS $icon => $href) {
?>
<a href="<?php echo $href ?>" class="btn-block-option"><i class="fa fa-<?php echo $icon ?>"></i></a>
<?php
} ?>
<?php if(isAuth($id, "write")) { ?>
<div class="btn btn-outline-success add-btn d-none" onclick="$('#new-modal').modal()"><i class="fa fa-plus"></i></div>
<?php } ?>
<!-- To toggle fullscreen a block, just add the following properties to your button: data-toggle="block-option" data-action="fullscreen_toggle" -->
<button type="button" class="btn-block-option d-none" data-toggle="block-option"
<?php if(oturumesit("full-screen-block", $id)) {
?>
onclick="$.get('?ajax=full-screen-block-remove')"
<?php
} else {
?>
onclick="$.get('?ajax=full-screen-block&id=<?php echo($id) ?>')"
<?php } ?>
data-action="fullscreen_toggle"><i class="si si-size-fullscreen"></i></button>
<?php if(isset($options['export'])) { ?>
<?php if($u->level == "Admin") { ?>
<a href="<?php echo url("admin/truncate/". $options['export']) ?>" <?php echo delete_teyit() ?> class="btn-block-option " title="<?php echo e2("Delete All") ?>" ><i class="fa fa-trash"></i></a>
<?php } ?>
<?php //if(is_stellar()) {
?>
<a target="_blank" href="<?php echo url("admin-ajax/export-query?table_name=". $options['export']) ?>&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option" title="<?php echo e2("Export to CSV") ?>" ><i class="fa fa-download"></i> CSV</a>
<a target="_blank" href="<?php echo url("admin-ajax/export-query?file_type=xlsx&table_name=". $options['export']) ?>&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option" title="<?php echo e2("Export to XLSX") ?>" ><i class="fa fa-download"></i> XLSX</a>
<a href="<?php echo url("admin/export/". $options['export']) ?>?&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option d-none" title="<?php echo e2("Export to Excel") ?>" ><i class="fa fa-download"></i></a>
<?php // } ?>
<?php if(isAuth($id, "write")) {
?>
<label
for="excel-file"
class="btn-block-option d-none" click="" title="<?php echo e2("Import to Excel") ?>" ><i class="fa fa-upload"></i></label>
<?php } ?>
<?php } ?>
<?php if(isset($topButtons)) {
foreach($topButtons AS $topButton) {
?>
<a href="<?php echo $topButton['href'] ?>" class="<?php echo @$topButton['class'] ?>"><?php echo $topButton['html'] ?></a>
<?php
}
} ?>
</div>
<?php } ?>
</div>
<?php
} ?>
<div class="block-content <?php echo isset($options['content-class']) ? $options['content-class'] : "" ?>">
<?php
}
function _col() {
?>
</div>
</div>
</div>
<?php
}
+64
View File
@@ -0,0 +1,64 @@
<?php
function col2($size, $title="", $color="0", $options=[], $topButtons=[], $columns=[]) {
$id = str_slug($title);
$colors = colors();
$u = u();
?>
<div class="<?php echo $size ?>">
<div class="block block-themed block-rounded <?php echo isset($options['border']) ? "border" : "" ?>" id="<?php echo $id; ?>">
<?php if(!isset($options['no-options']) && $title!="") { ?>
<!-- Fixed toolbar for buttons, positioned at the top-right -->
<div class="fixed-toolbar" style="position: fixed; top: 70px; right: 0px; width: auto; z-index: 1000; background-color: rgba(255,255,255,0.8); padding: 5px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<div type="button" class="btn-block-option select-columns d-none" data-toggle="modal" data-target="#select-columns" >
<i class="fa fa-table-columns"></i>
</div>
<?php foreach($options AS $icon => $href) { ?>
<a href="<?php echo $href ?>" class="btn-block-option"><i class="fa fa-<?php echo $icon ?>"></i></a>
<?php } ?>
<?php if(isAuth($id, "write")) { ?>
<div class="btn btn-outline-success add-btn d-none" onclick="$('#new-modal').modal()"><i class="fa fa-plus"></i></div>
<?php } ?>
<!-- To toggle fullscreen a block, just add the following properties to your button: data-toggle="block-option" data-action="fullscreen_toggle" -->
<button type="button" class="btn-block-option d-none" data-toggle="block-option"
<?php if(oturumesit("full-screen-block", $id)) { ?>
onclick="$.get('?ajax=full-screen-block-remove')"
<?php } else { ?>
onclick="$.get('?ajax=full-screen-block&id=<?php echo($id) ?>')"
<?php } ?>
data-action="fullscreen_toggle"><i class="si si-size-fullscreen"></i></button>
<?php if(isset($options['export'])) { ?>
<?php if($u->level == "Admin") { ?>
<a href="<?php echo url("admin/truncate/". $options['export']) ?>" <?php echo delete_teyit() ?> class="btn-block-option " title="<?php echo e2("Delete All") ?>" ><i class="fa fa-trash"></i></a>
<?php } ?>
<a target="_blank" href="<?php echo url("admin-ajax/export-query?table_name=". $options['export']) ?>&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option" title="<?php echo e2("Export to CSV") ?>" ><i class="fa fa-download"></i> CSV (Semicolon ;;;)</a>
<a target="_blank" href="<?php echo url("admin-ajax/export-query?table_name=". $options['export']) ?>&module=<?php echo $id ?>&delimiter=,&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option" title="<?php echo e2("Export to CSV") ?>" ><i class="fa fa-download"></i> CSV (Comma ,,,)</a>
<a target="_blank" href="<?php echo url("admin-ajax/export-query?file_type=xlsx&table_name=". $options['export']) ?>&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option" title="<?php echo e2("Export to XLSX") ?>" ><i class="fa fa-download"></i> XLSX</a>
<a href="<?php echo url("admin/export/". $options['export']) ?>?&module=<?php echo $id ?>&columns=<?php echo base64_encode(implode(",", $columns)) ?>" class="btn-block-option d-none" title="<?php echo e2("Export to Excel") ?>" ><i class="fa fa-download"></i></a>
<?php if(isAuth($id, "write")) { ?>
<label for="excel-file" class="btn-block-option d-none" click="" title="<?php echo e2("Import to Excel") ?>" ><i class="fa fa-upload"></i></label>
<?php } ?>
<?php } ?>
<?php if(isset($topButtons)) {
foreach($topButtons AS $topButton) { ?>
<a href="<?php echo $topButton['href'] ?>" class="<?php echo @$topButton['class'] ?>"><?php echo $topButton['html'] ?></a>
<?php }
} ?>
</div>
<?php } ?>
<div class="block-content <?php echo isset($options['content-class']) ? $options['content-class'] : "" ?>">
<?php
}
function _col2() {
?>
</div>
</div>
</div>
<?php
}
+112
View File
@@ -0,0 +1,112 @@
<?php
function convertMapCache() {
// Converter-map verilerini al
$converterMap = j(setting("converter-map"));
// Mapping arrays oluştur ve cache'le
$cacheKey = "converter_map_data";
$mappings = Cache::remember($cacheKey, 3600, function() use ($converterMap) {
$termToRu = $termToEn = $enToRu = $ruToEn = $enToTerm = $ruToTerm = [];
if (is_array($converterMap)) {
foreach ($converterMap as $row) {
if (!empty($row['term']) && !empty($row['term_ru'])) {
$termToRu[$row['term']] = $row['term_ru'];
}
if (!empty($row['term']) && !empty($row['term_eng'])) {
$termToEn[$row['term']] = $row['term_eng'];
}
if (!empty($row['term_eng']) && !empty($row['term_ru'])) {
$enToRu[$row['term_eng']] = $row['term_ru'];
}
if (!empty($row['term_ru']) && !empty($row['term_eng'])) {
$ruToEn[$row['term_ru']] = $row['term_eng'];
}
if (!empty($row['term_eng']) && !empty($row['term'])) {
$enToTerm[$row['term_eng']] = $row['term'];
}
if (!empty($row['term_ru']) && !empty($row['term'])) {
$ruToTerm[$row['term_ru']] = $row['term'];
}
}
}
return [
'termToRu' => $termToRu,
'termToEn' => $termToEn,
'enToRu' => $enToRu,
'ruToEn' => $ruToEn,
'enToTerm' => $enToTerm,
'ruToTerm' => $ruToTerm
];
});
// Her bir mapping'i ayrı cache key'e kaydet
Cache::put('termToRu', $mappings['termToRu'], 3600);
Cache::put('termToEn', $mappings['termToEn'], 3600);
Cache::put('enToRu', $mappings['enToRu'], 3600);
Cache::put('ruToEn', $mappings['ruToEn'], 3600);
Cache::put('enToTerm', $mappings['enToTerm'], 3600);
Cache::put('ruToTerm', $mappings['ruToTerm'], 3600);
return $mappings;
}
function convertMap() {
$cacheKey = "converter_map_data";
// Önce cache'den okumayı dene
if (Cache::has($cacheKey)) {
$mappings = Cache::get($cacheKey);
// Cache'den okunan veri geçerli mi kontrol et
if (!empty($mappings) && is_array($mappings)) {
return $mappings;
}
}
// Cache yoksa veya geçersizse yeniden oluştur
$mappings = j(setting("converter-map"));
// Yeni veriyi cache'e kaydet
if (!empty($mappings) && is_array($mappings)) {
Cache::put($cacheKey, $mappings, 60);
}
return $mappings;
}
function convertRu($term) {
// Önce cache'de mapping var mı kontrol et
$converterMap = Cache::get('termToRu');
// Eğer cache'de yoksa, convertMapCache'i çağır
if (empty($converterMap)) {
convertMapCache();
$converterMap = Cache::get('termToRu');
}
if(isset($converterMap[$term])) {
return $converterMap[$term];
} else {
Log::info("convertRu not found", ['term' => $term]);
}
return $term;
}
function convertEn($term) {
// Önce cache'de mapping var mı kontrol et
$converterMap = Cache::get('termToEn');
// Eğer cache'de yoksa, convertMapCache'i çağır
if (empty($converterMap)) {
convertMapCache();
$converterMap = Cache::get('termToEn');
}
if(isset($converterMap[$term])) {
return $converterMap[$term];
}
return $term;
}
?>
+229
View File
@@ -0,0 +1,229 @@
<?php
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Illuminate\Support\Facades\Cache;
function converterMapReplacer(?Spreadsheet $spreadsheet, $type="replacer") {
// Converter-map verilerini al
$converterMap = j(setting("converter-map"));
// Mapping arrays oluştur ve cache'le
$cacheKey = "converter_map_data";
$mappings = Cache::remember($cacheKey, 3600, function() use ($converterMap) {
$termToRu = $termToEn = $enToRu = $ruToEn = $enToTerm = $ruToTerm = [];
if (is_array($converterMap)) {
foreach ($converterMap as $row) {
if (!empty($row['term']) && !empty($row['term_ru'])) {
$termToRu[$row['term']] = $row['term_ru'];
}
if (!empty($row['term']) && !empty($row['term_eng'])) {
$termToEn[$row['term']] = $row['term_eng'];
}
if (!empty($row['term_eng']) && !empty($row['term_ru'])) {
$enToRu[$row['term_eng']] = $row['term_ru'];
}
if (!empty($row['term_ru']) && !empty($row['term_eng'])) {
$ruToEn[$row['term_ru']] = $row['term_eng'];
}
if (!empty($row['term_eng']) && !empty($row['term'])) {
$enToTerm[$row['term_eng']] = $row['term'];
}
if (!empty($row['term_ru']) && !empty($row['term'])) {
$ruToTerm[$row['term_ru']] = $row['term'];
}
}
}
return [
'termToRu' => $termToRu,
'termToEn' => $termToEn,
'enToRu' => $enToRu,
'ruToEn' => $ruToEn,
'enToTerm' => $enToTerm,
'ruToTerm' => $ruToTerm
];
});
if ($type == "replacer") {
// Sadece spreadsheet null değilse işle
if ($spreadsheet !== null) {
try {
// Spreadsheet'teki her sheet'i dolaş
foreach ($spreadsheet->getAllSheets() as $sheet) {
// Her satır ve hücreyi dolaş
foreach ($sheet->getRowIterator() as $row) {
foreach ($row->getCellIterator() as $cell) {
$cellValue = $cell->getValue();
if (is_string($cellValue)) {
$originalValue = $cellValue;
// _en pattern'lerini bul ve değiştir (TERM_en -> İngilizce karşılığı)
$cellValue = preg_replace_callback('/(\w+)_en\b/', function($matches) use ($mappings) {
$term = $matches[1];
if (isset($mappings['termToEn'][$term])) {
return $mappings['termToEn'][$term];
}
return $matches[0]; // Bulunamazsa orijinal değeri döndür
}, $cellValue);
// _ru pattern'lerini bul ve değiştir (TERM_ru -> Rusça karşılığı)
$cellValue = preg_replace_callback('/(\w+)_ru\b/', function($matches) use ($mappings) {
$term = $matches[1];
if (isset($mappings['termToRu'][$term])) {
return $mappings['termToRu'][$term];
}
return $matches[0]; // Bulunamazsa orijinal değeri döndür
}, $cellValue);
// _tr pattern'lerini bul ve değiştir (TERM_tr -> Türkçe karşılığı)
$cellValue = preg_replace_callback('/(\w+)_tr\b/', function($matches) use ($mappings) {
$term = $matches[1];
// Türkçe için term alanını kullan (orijinal)
return $term; // Terim kendisi zaten Türkçe
}, $cellValue);
// Placeholder formatında da çeviri yap: {TERM_en}, {TERM_ru}
$cellValue = preg_replace_callback('/\{(\w+)_en\}/', function($matches) use ($mappings) {
$term = $matches[1];
if (isset($mappings['termToEn'][$term])) {
return $mappings['termToEn'][$term];
}
return $matches[0]; // Bulunamazsa orijinal değeri döndür
}, $cellValue);
$cellValue = preg_replace_callback('/\{(\w+)_ru\}/', function($matches) use ($mappings) {
$term = $matches[1];
if (isset($mappings['termToRu'][$term])) {
return $mappings['termToRu'][$term];
}
return $matches[0]; // Bulunamazsa orijinal değeri döndür
}, $cellValue);
$cellValue = preg_replace_callback('/\{(\w+)_tr\}/', function($matches) use ($mappings) {
$term = $matches[1];
// Türkçe için term alanını kullan (orijinal)
return $term; // Terim kendisi zaten Türkçe
}, $cellValue);
// Eğer değişiklik olduysa hücreyi güncelle
if ($cellValue !== $originalValue) {
$cell->setValue($cellValue);
}
}
}
}
}
// Değiştirilmiş spreadsheet'i döndür
return $spreadsheet;
} catch (\Throwable $th) {
// Hata durumunda log ama devam et
logProcessInfo('Error in converterMapReplacer', [
'error' => $th->getMessage(),
'trace' => $th->getTraceAsString()
], 'error');
}
}
// Spreadsheet null ise veya hata oluştuysa boş array döndür
return [];
} else {
// "data" tipi için mevcut terimleri placeholder listesi olarak döndür
$placeholders = [];
if (is_array($converterMap)) {
foreach ($converterMap as $row) {
if (!empty($row['term'])) {
$term = $row['term'];
// Farklı dil pattern'leri ekle
$placeholders[] = $term . "_en";
$placeholders[] = $term . "_ru";
$placeholders[] = $term . "_tr";
$placeholders[] = "{" . $term . "_en}";
$placeholders[] = "{" . $term . "_ru}";
$placeholders[] = "{" . $term . "_tr}";
}
}
}
return array_unique($placeholders);
}
}
// String replacement için yardımcı fonksiyon
function converterMapStringReplacer($string) {
// Cache kullanarak converter-map verilerini al (1 saat süreyle)
$mappings = Cache::remember('converter_map_string_data', 3600, function() {
$converterMap = j(setting("converter-map"));
$termToRu = $termToEn = [];
if (is_array($converterMap)) {
foreach ($converterMap as $row) {
if (!empty($row['term']) && !empty($row['term_ru'])) {
$termToRu[$row['term']] = $row['term_ru'];
}
if (!empty($row['term']) && !empty($row['term_eng'])) {
$termToEn[$row['term']] = $row['term_eng'];
}
}
}
return [
'termToRu' => $termToRu,
'termToEn' => $termToEn
];
});
$termToRu = $mappings['termToRu'];
$termToEn = $mappings['termToEn'];
// _en pattern'lerini değiştir
$string = preg_replace_callback('/(\w+)_en\b/', function($matches) use ($termToEn) {
$term = $matches[1];
if (isset($termToEn[$term])) {
return $termToEn[$term];
}
return $matches[0];
}, $string);
// _ru pattern'lerini değiştir
$string = preg_replace_callback('/(\w+)_ru\b/', function($matches) use ($termToRu) {
$term = $matches[1];
if (isset($termToRu[$term])) {
return $termToRu[$term];
}
return $matches[0];
}, $string);
// _tr pattern'lerini değiştir (Türkçe - orijinal terim)
$string = preg_replace_callback('/(\w+)_tr\b/', function($matches) {
$term = $matches[1];
return $term; // Terim kendisi zaten Türkçe
}, $string);
// Placeholder formatında da çeviri yap
$string = preg_replace_callback('/\{(\w+)_en\}/', function($matches) use ($termToEn) {
$term = $matches[1];
if (isset($termToEn[$term])) {
return $termToEn[$term];
}
return $matches[0];
}, $string);
$string = preg_replace_callback('/\{(\w+)_ru\}/', function($matches) use ($termToRu) {
$term = $matches[1];
if (isset($termToRu[$term])) {
return $termToRu[$term];
}
return $matches[0];
}, $string);
$string = preg_replace_callback('/\{(\w+)_tr\}/', function($matches) {
$term = $matches[1];
return $term; // Terim kendisi zaten Türkçe
}, $string);
return $string;
}
?>
+38
View File
@@ -0,0 +1,38 @@
<?php
use App\Models\Counter;
use Carbon\Carbon;
function get_counter($prefix, $type="1") {
//eğer type boşsa o gün üretilen başka bir counter varsa onu al
$run = true;
if($type == "") {
$todayCounter = Counter::whereDate("updated_at", Carbon::today())->where("prefix", $prefix)->first();
if($todayCounter) {
$get = $todayCounter->value;
$run = false;
} else {
$run = true;
}
}
if($run) {
$set = Counter::updateOrCreate(
[
'prefix' => $prefix
])
->increment('value');
$get = Counter::where("prefix", $prefix)->first()->value;
}
$totalZero = 6;
$totalGet = strlen($get);
$resultZero = $totalZero - $totalGet;
$result = "";
for($k=1;$k<=$resultZero; $k++) {
$result .= "0";
}
$result .= $get;
return $result;
} ?>
+51
View File
@@ -0,0 +1,51 @@
<?php
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
function csv_to_xlsx($csvFilePath, $outputFilePath, $delimiter= "\t")
{
try {
// Create a new Spreadsheet object
$spreadsheet = new Spreadsheet();
// Get the active sheet
$sheet = $spreadsheet->getActiveSheet();
// Open the CSV file
$file = fopen($csvFilePath, 'r');
if (!$file) {
throw new \Exception("Cannot open CSV file.");
}
$rowIndex = 1;
// Read each row and add it to the spreadsheet
while (($row = fgetcsv($file, 0, $delimiter)) !== false) {
$colIndex = 'A';
foreach ($row as $cell) {
// Veriyi hücreye yaz
// if($cell == "NULL") $cell = "";
$sheet->setCellValue($colIndex . $rowIndex, $cell);
$colIndex++;
}
$rowIndex++;
}
fclose($file);
// Write to XLSX file
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save($outputFilePath);
return $outputFilePath;
} catch (\Exception $e) {
$result = [];
$result['error'] = "Error: " . $e->getMessage();
return $result;
}
}
?>
+278
View File
@@ -0,0 +1,278 @@
<?php function datagrid_scripts() {
?>
<!-- DevExtreme CSS -->
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/23.1.5/css/dx-diagram.css" />
<!-- DevExtreme Diagram JavaScript -->
<script src="https://cdn3.devexpress.com/jslib/23.1.5/js/dx-diagram.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/7.4.0/polyfill.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.1.1/exceljs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.2/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.0.0/jspdf.umd.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/devextreme-dist/23.1.5/css/<?php echo setting("DevExpress_Theme", false,"dx.light.compact") ?>">
<!-- Quill CSS -->
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.7/quill.snow.css">
<!-- Quill JS -->
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script type="text/javascript" src="https://cdn3.devexpress.com/jslib/23.1.5/js/dx.all.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.0.0/jspdf.umd.min.js"></script>
<style>
.dx-datagrid tr:nth-child(odd) {
background-color:#e8e8e8;
}
#dataGrid {
height: calc(100vh - 220px);
}
.dx-datagrid .dx-row > td, .dx-datagrid .dx-row > tr > td {
padding: 5px !important;
}
.dx-editor-cell .dx-texteditor .dx-texteditor-input {
height: 16px;
}
.dx-datagrid .dx-row-lines > td {
border-bottom: 1px solid #d2d2d2;
}
</style>
<?php
} ?>
<?php function datagrid_configurations() {
?>
toolbar: {
items: [
"addRowButton",
"applyFilterButton",
"columnChooserButton",
"revertButton",
"saveButton",
"searchPanel",
"exportButton",
"groupPanel",
{
location: 'before',
widget: 'dxButton',
options: {
icon: 'filter',
text: '{{e2("Clear Filter")}}',
onClick: function(e) {
dataGrid.clearFilter();
}
}
},
]
},
paging: {
pageSize: <?php echo setting('row_count') ?>,
},
allowColumnResizing: true,
columnAutoWidth: true,
allowColumnReordering: true,
columnMinWidth: 150,
columnMaxWidth: 250,
columnResizingMode: "widget",
showColumnLines: true,
filterRow: { visible: true },
searchPanel: { visible: true },
columnFixing: {
enabled: true
},
headerFilter: {
visible: true,
allowSearch: true,
},
loadPanel: {
enabled: true,
},
scrolling: {
mode: 'standart',
rowRenderingMode: 'infinite',
columnRenderingMode: 'infinite',
},
hoverStateEnabled: true,
showBorders: true,
remoteOperations: {
filtering: true,
paging: true,
sorting: true,
groupPaging: true,
grouping: true,
summary: true
},
onContextMenuPreparing: function(e) {
console.log("onContextMenuPreparing");
console.log(e);
if (e.target == "content") {
if (!e.items) e.items = [];
@if(isAuth($id, "modify"))
e.items.push({
text: "<?php echo e2("Clear Value") ?>",
onItemClick: function(args) {
e.component.cellValue(e.rowIndex, e.columnIndex, null);
}
});
@endif
@if(isAuth($id, "write"))
e.items.push({
text: "<?php echo e2("Clone Row") ?>",
onItemClick: function(args) {
$(".dx-icon-edit-button-addrow").trigger("click");
selectedData = e.row.data;
}
});
@endif
@if(isAuth($id, "modify"))
e.items.push({
text: "<?php echo e2("Copy Start") ?>",
onItemClick: function(args) {
copyData = e.component.cellValue(e.rowIndex, e.columnIndex);
selectedColIndex = e.columnIndex;
selectedRowIndex = e.rowIndex;
console.log(e);
console.log(selectedColIndex);
console.log(selectedRowIndex);
Swal.fire(copyData, "<?php echo e2("Data copied start") ?>", "success");
}
});
e.items.push({
text: "<?php echo e2("Until Paste") ?>",
onItemClick: function(args) {
if(e.columnIndex != selectedColIndex) {
Swal.fire("<?php echo e2("Wrong") ?>", "<?php echo e2("Please copy using the same columns.") ?>", "error");
} else {
for(var rowIndex = selectedRowIndex + 1; rowIndex<=e.rowIndex; rowIndex++) {
e.component.cellValue(rowIndex, e.columnIndex, copyData);
}
}
}
});
@endif
}
},
onEditingStart(e) {
var focusedSelector = $("td.dx-focused");
var rowIndex = dataGrid.option("focusedRowIndex");
var colIndex = dataGrid.option("focusedColumnIndex");
try {
var dataField = e.column.dataField;
var allEntries = JSON.parse(localStorage.getItem("originalData" + rowIndex + colIndex)) || [];
allEntries.push(e.data[dataField]);
localStorage.setItem("originalData" + rowIndex + colIndex, JSON.stringify(allEntries));
} catch (error) {
}
},
onInitNewRow(e) {
if(selectedData) {
e.data = selectedData;
selectedData = null;
}
},
onRowInserting(e) {
logEvent('RowInserting');
console.log(e);
},
onRowInserted() {
// logEvent('RowInserted');
},
onRowUpdating() {
},
onRowUpdated() {
// logEvent('RowUpdated');
},
onRowRemoving() {
// logEvent('RowRemoving');
},
onRowRemoved() {
// logEvent('RowRemoved');
},
onSaving() {
},
onInitialized() {
$("#dataGrid td").on("click", function() {
logEvent("#dataGrid td");
});
},
onRowClick: function(e) {
/*
console.log("onRowClick");
console.log(e);
selectedData = e.data;
selectedData.id = null;
if(e.rowType === "data") {
e.component.editRow(e.rowIndex);
}
*/
},
onSaved() {
logEvent('Saving Success');
},
onEditCanceling() {
// logEvent('EditCanceling');
},
onEditCanceled() {
// logEvent('EditCanceled');
},
onFocusedRowChanging(e) {
// console.log(e);
},
sorting: {
mode: "multiple" // or "multiple" | "none"
},
<?php
} ?>
+48
View File
@@ -0,0 +1,48 @@
<?php
function defect_names() {
return array_map("trim", explode("\n", "Single Porosity
Chain Porosity
Cluster Porosity
Single Slag
Chain Slag
Cluster Slag
Single Tungsten
Chain Tungsten
Cluster Tungsten
Incomplete Root Penetration
Incomplete Inter-pass Fusion
Incomplet Side Wall Fusion
Longitudinal Crack
Transverse Crack
Branched Crack
Root Concavity / Suck Back
Root Convexity / Extensive Root
Undercut
High-Low
Other"));
}
function defects() {
return [
'aa',
'ab',
'ac',
'ba',
'bb',
'bc',
'ca',
'cb',
'cc',
'da',
'db',
'dc',
'ea',
'eb',
'ec',
'fa',
'fb',
'fc',
'fd',
'other',
];
} ?>
+13
View File
@@ -0,0 +1,13 @@
<?php
function deleteAfterR($input) {
$pos = strpos($input, 'R'); // R karakterinin pozisyonunu bul
if ($pos !== false) {
// Eğer R karakteri bulunduysa, R karakterinden sonrasını sil
$output = substr($input, 0, $pos);
} else {
// Eğer R karakteri bulunamazsa, input'u olduğu gibi döndür
$output = $input;
}
return $output;
}
?>
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Helper function to detect changed fields
*
* Compares current data with previous data to identify which fields changed
*
* @param mixed $data Current data
* @param mixed $beforeData Previous data (null for new records)
* @return array Array of changed field names
*/
function detectChangedFields($data, $beforeData): array
{
if (is_null($beforeData)) {
// New record - all fields are "changed"
return array_keys((array) $data);
}
$changedFields = [];
$dataArray = (array) $data;
$beforeDataArray = (array) $beforeData;
foreach ($dataArray as $key => $value) {
$oldValue = $beforeDataArray[$key] ?? null;
// Compare values - consider both strict and loose equality for type changes
if ($oldValue !== $value) {
$changedFields[] = $key;
}
}
return $changedFields;
}
?>
@@ -0,0 +1,198 @@
<?php
function onPreparingVariables() {
?>
var options = e.editorOptions;
var rowIndex = e.row.rowIndex;
var rowData = e.row.data;
var dataGrid = $("#dataGrid").dxDataGrid("instance");
var exceptValue = ['undefined', 'null'];
var column = e.dataField;
var urlRowData = encodeURIComponent(JSON.stringify(rowData));
<?php
}
function dxAutocomplete($columnName, $tableName, $targetColumn, $filter = "", $affected = "") {
if ($filter != "") {
$filter2 = [];
foreach ($filter as $filterColumn => $filterValue) {
if (is_array($filterValue)) {
$filter2[$filterColumn] = implode(",", $filterValue);
} elseif (strpos($filterValue, '"') !== false) {
$filterValue = str_replace('"', "", $filterValue);
$filter2[$filterColumn] = "{$filterValue}";
} else {
$filter2[$filterColumn] = "'+ dataRow.{$filterValue} +'";
}
}
$filter = "&filter=" . json_encode_tr($filter2);
}
?>
<?php if ($affected == "") { ?>
if (e.dataField == "<?php echo $columnName ?>") {
e.editorName = "dxAutocomplete";
e.editorOptions.searchEnabled = true;
e.editorOptions.acceptCustomValue = true;
e.editorOptions.dataSource = new DevExpress.data.ODataStore({
url: '<?php echo autocomplete_url($tableName, $targetColumn) ?><?php echo $filter ?>',
key: 'id',
});
e.editorOptions.valueExpr = '<?php echo $targetColumn ?>';
e.editorOptions.displayExpr = '<?php echo $targetColumn ?>';
}
<?php } else { ?>
var options = e.editorOptions;
var rowIndex = e.row.rowIndex;
var dataGrid = $("#dataGrid").dxDataGrid("instance");
var exceptValue = ['undefined', 'null'];
if (e.dataField == "<?php echo $columnName ?>") {
e.editorName = "dxAutocomplete";
e.editorOptions.searchEnabled = true;
e.editorOptions.acceptCustomValue = true;
e.editorOptions.showClearButton = true;
e.editorOptions.dataSource = new DevExpress.data.ODataStore({
url: '<?php echo autocomplete_url($tableName, $targetColumn) ?><?php echo $filter ?>',
key: 'id',
});
e.editorOptions.valueExpr = '<?php echo $targetColumn ?>';
e.editorOptions.displayExpr = '<?php echo $targetColumn ?>';
var fetchRelatedData = function(typedValue) {
if (typedValue === null || typedValue === '') {
return;
}
$.getJSON("<?php echo row_detail_url($tableName, $targetColumn) ?>?value=" + typedValue, function(responseJSON) {
console.log(responseJSON);
if (responseJSON && typeof responseJSON.<?php echo $targetColumn ?> !== 'undefined') {
<?php
$affectedCols = $affected;
foreach ($affectedCols as $affectedCol => $valueCol) {
$valueCol = str_replace('{', '${responseJSON.', $valueCol); ?>
if (!exceptValue.includes(`<?php echo $valueCol ?>`)) {
dataGrid.cellValue(rowIndex, "<?php echo $affectedCol ?>", `<?php echo $valueCol ?>`);
} else {
console.log("except value <?php echo $valueCol ?>");
}
<?php } ?>
} else {
console.log("undefined <?php echo $targetColumn ?>");
}
});
};
options.onValueChanged = function(selectData) {
console.log("on value changed");
e.setValue(selectData.value);
if (options.__stellarExactMatchTimer) {
clearTimeout(options.__stellarExactMatchTimer);
}
options.__stellarExactMatchTimer = setTimeout(function() {
var typedValue = (selectData && typeof selectData.value !== 'undefined') ? selectData.value : null;
fetchRelatedData(typedValue);
}, 250);
}
options.onSelectionChanged = function(eSelection) {
if (eSelection.selectedItem) {
if (options.__stellarExactMatchTimer) {
clearTimeout(options.__stellarExactMatchTimer);
}
// Use the value directly from the selected item
var selectedValue = eSelection.selectedItem['<?php echo $targetColumn ?>'];
fetchRelatedData(selectedValue);
}
}
}
<?php } ?>
<?php
}
function dxSelectBox($columnName, $tableName, $targetColumn, $filter = "", $affected = "", $order = "") {
if ($filter != "") {
$filter2 = [];
foreach ($filter as $filterColumn => $filterValue) {
if (is_array($filterValue)) {
$filter2[$filterColumn] = implode(",", $filterValue);
} elseif (strpos($filterValue, '"') !== false) {
$filterValue = str_replace('"', "", $filterValue);
$filter2[$filterColumn] = "{$filterValue}";
} else {
$filter2[$filterColumn] = "'+ dataRow.{$filterValue} +'";
}
}
$filter = "&filter=" . json_encode_tr($filter2);
}
?>
<?php if ($affected == "") { ?>
if (e.dataField == "<?php echo $columnName ?>") {
e.editorName = "dxSelectBox";
e.editorOptions.searchEnabled = true;
e.editorOptions.acceptCustomValue = true;
e.editorOptions.allowCustomValues = true;
e.editorOptions.dataSource = new DevExpress.data.ODataStore({
url: '<?php echo autocomplete_url($tableName, $targetColumn) ?><?php echo $filter ?>',
key: 'id',
});
e.editorOptions.valueExpr = '<?php echo $targetColumn ?>';
e.editorOptions.displayExpr = '<?php echo $targetColumn ?>';
}
<?php } else { ?>
var options = e.editorOptions;
var rowIndex = e.row.rowIndex;
var dataGrid = $("#dataGrid").dxDataGrid("instance");
var exceptValue = ['undefined', 'null'];
if (e.dataField == "<?php echo $columnName ?>") {
e.editorName = "dxSelectBox";
e.editorOptions.searchEnabled = true;
e.editorOptions.acceptCustomValue = true;
e.editorOptions.allowCustomValues = true;
e.editorOptions.showClearButton = true;
e.editorOptions.dataSource = new DevExpress.data.ODataStore({
url: '<?php echo autocomplete_url($tableName, $targetColumn) ?><?php echo $filter ?>',
key: 'id',
});
e.editorOptions.valueExpr = '<?php echo $targetColumn ?>';
e.editorOptions.displayExpr = '<?php echo $targetColumn ?>';
options.onValueChanged = function(selectData) {
e.setValue(selectData.value);
if (options.__stellarExactMatchTimer) {
clearTimeout(options.__stellarExactMatchTimer);
}
options.__stellarExactMatchTimer = setTimeout(function() {
var typedValue = (selectData && typeof selectData.value !== 'undefined') ? selectData.value : null;
if (typedValue === null || typedValue === '') {
return;
}
$.getJSON("<?php echo row_detail_url($tableName, $targetColumn) ?>?order=<?php echo $order ?>&nolike=1&value=" + encodeURIComponent(typedValue), function(responseJSON) {
console.log(responseJSON);
if (responseJSON && typeof responseJSON.<?php echo $targetColumn ?> !== 'undefined') {
<?php
$affectedCols = $affected;
foreach ($affectedCols as $affectedCol => $valueCol) {
$valueCol = str_replace('{', '${responseJSON.', $valueCol); ?>
if (!exceptValue.includes(`<?php echo $valueCol ?>`)) {
dataGrid.cellValue(rowIndex, "<?php echo $affectedCol ?>", `<?php echo $valueCol ?>`);
} else {
console.log("except value <?php echo $valueCol ?>");
}
<?php } ?>
} else {
console.log("undefined <?php echo $targetColumn ?>");
}
});
}, 250);
}
}
<?php } ?>
<?php
}
?>
+8
View File
@@ -0,0 +1,8 @@
<?php function dxDataStore($tableName, $columnName, $filter = "") {
?>
new DevExpress.data.ODataStore({
                url: '<?php echo autocomplete_url2($tableName, $columnName) ?><?php echo $filter ?>,
                key: 'id',
            });
<?php
} ?>
+4
View File
@@ -0,0 +1,4 @@
<?php function document_template($id) {
$document = db("document_templates")->orWhere("slug", $id)->orWhere("id", $id)->first();
return $document;
} ?>
+118
View File
@@ -0,0 +1,118 @@
<?php
use PhpOffice\PhpWord\TemplateProcessor;
function replacePlaceholdersInDocx($templateFilePath, $outputFilePath, $replacements) {
// Load the DOCX template
//$storagePath = "storage/documents/$outputFilePath";
$storagePath = $outputFilePath;
$directoryPath = dirname($storagePath);
if (!is_dir($directoryPath)) {
mkdir($directoryPath, 0777, true);
}
$templateProcessor = new TemplateProcessor($templateFilePath);
$templateProcessor->setValue("text.ncr_no", "test");
// $templateProcessor->setValue("corrective_action_ru", "teasdasdast");
/*
$reflectionClass = new \ReflectionClass($templateProcessor);
// Accessing the protected/private property
$property = $reflectionClass->getProperty('tempDocumentMainPart');
$property->setAccessible(true);
// Get the value of the property
$xmlContent = $property->getValue($templateProcessor);
$xmlContent = str_replace("corrective_action_ru", "test", $xmlContent);
$property->setValue($templateProcessor, $xmlContent);
*/
/*
$formTypes = ['date', 'time', 'text', 'textarea', 'radio', 'checkbox', 'select'];
// Replace placeholders
foreach ($replacements as $search => $replace) {
foreach($formTypes AS $formType)
{
$isValidDate = false;
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $replace)) {
// YYYY-MM-DD formatında tarih
$isValidDate = true;
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $replace)) {
// YYYY-MM-DD HH:MM:SS formatında tarih ve saat
$isValidDate = true;
} elseif (preg_match('/^\d{2}.\d{2}.\d{4}$/', $replace)) {
// DD.MM.YYYY formatında tarih
$isValidDate = true;
} elseif (preg_match('/^\d{2}.\d{2}.\d{4} \d{2}:\d{2}:\d{2}$/', $replace)) {
// DD.MM.YYYY HH:MM:SS formatında tarih ve saat
$isValidDate = true;
}
if($isValidDate)
{
if (strtotime($replace)) {
$dateTime = new DateTime($replace);
// Eğer $replace saati de içeriyorsa
if ($dateTime->format('H:i:s') !== '00:00:00') {
$replace = $dateTime->format('H:i');
} else {
$replace = $dateTime->format('d.m.Y');
}
}
}
if (in_array($formType, ['radio', 'checkbox'])) {
$replacePlaceHolder = $formType . '.' . $search . '.' . $replace;
$okValue = '✓';
$cellValue = $okValue;
$otherPattern = "/<w:t>\{$formType\.$search\.[^}]*\}<\/w:t>/";
//$xmlContent = preg_replace($otherPattern, "", /$xmlContent);
} else {
$replacePlaceHolder = $formType . '.' . $search; // Note the curly braces
$cellValue = $replace;
$pattern = '/\{' . preg_quote($formType, '/') . '\.<\/w:t><\/w:r><w:r[^>]*><w:rPr>.*?<\/w:rPr><w:t>' . preg_quote($search, '/') . '<\/w:t>/';
dump($pattern);
// $xmlContent = preg_replace($pattern, $cellValue, $xmlContent);
$property->setValue($templateProcessor, $xmlContent);
}
// dump($pattern);
// dump($cellValue);
// preg_replace ile değişim
}
}
*/
$templateProcessor->saveAs($storagePath);
}
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
function docx_to_html($docx, $html_dir) {
// Ensure the LANG environment variable is set
putenv('LANG=ru_RU.UTF-8');
// Ensure the output directory exists
if (!is_dir($html_dir)) {
mkdir($html_dir, 0777, true);
}
// Construct the command
$command = "libreoffice --headless --convert-to html --outdir " . escapeshellarg($html_dir) . " " . escapeshellarg($docx);
// Execute the command
$output = shell_exec($command);
$output = extract_between_markers_docx($output);
update_src_paths($output, $html_dir);
return $output;
}
function extract_between_markers_docx($input_str) {
// Define the regular expression pattern to match the content between the markers
$pattern = '/->\s*(.*?)\s*using filter : HTML \(StarWriter\)/';
// Perform the regex match
if (preg_match($pattern, $input_str, $matches)) {
// Return the matched content
return $matches[1];
}
// Return null if no match is found
return null;
}
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
function export_excel($dizi, $dosya_adi) {
$dizi = $dizi->toArray();
$excepts = ['created_at', 'updated_at'];
$filtered = [];
foreach($dizi AS $key => $value) {
foreach($excepts AS $except) {
$value = (Array) $value;
unset($value[$except]);
}
$filtered[] = $value;
}
$dizi = $filtered;
$dosya_adi = (String) $dosya_adi;
$icerik = "";
foreach($dizi[0] AS $column => $value) {
$icerik .= "$column\t";
}
$icerik .= "\r\n";
foreach($dizi AS $column) {
foreach($column AS $col => $value) {
$icerik .= "$value\t";
}
$icerik .= "\r\n";
}
$icerik = trim($icerik);
return response($icerik)
->header('Content-type','application/ms-excel')
->header('Content-Disposition','attachment; filename="'.$dosya_adi.'.xls"')
->send();
} ?>
+38
View File
@@ -0,0 +1,38 @@
<?php
use Barryvdh\DomPDF\Facade\Pdf;
function file_force_contents( $fullPath, $contents, $flags = 0 ){
$parts = explode( '/', $fullPath );
array_pop( $parts );
$dir = implode( '/', $parts );
if( !is_dir( $dir ) )
mkdir( $dir, 0777, true );
file_put_contents( $fullPath, $contents, $flags );
}
function pdf_create($path, $html)
{
$pdf = App::make('dompdf.wrapper');
$pdf->setPaper('A4',$j['paper']);
$pdf->setOption(['dpi' => $j['dpi'], ]);
$pdf->loadHTML($html);
$path = "$path.pdf";
Storage::delete($path);
Storage::put($path, $pdf->output());
}
function html_create($path, $html, $paper="portrait")
{
$path = "$path.html";
Storage::delete($path);
Storage::put($path, $html);
$htmlPath = "storage/documents/" . $path;
$pdfPath = str_replace(".html", ".pdf", $htmlPath);
html_to_pdf($htmlPath, $pdfPath, $paper);
unlink($htmlPath);
} ?>
+10
View File
@@ -0,0 +1,10 @@
<?php function findItemObject($text, $column, $object) {
$array = @json_decode(json_encode($object));
$find = array_search($text, array_column($array, $column));
if(!$find) {
return false;
} else {
return $object[$find];
}
} ?>
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* Try catch kullanarak index değerine göre yoksa ekler
*/
function firstOrCreate($data, $table, $debug=false) {
try {
$data['created_at'] = simdi();
$id = db($table)->insertGetId($data);
return $id;
} catch (\Throwable $th) {
if($debug) {
dump($th);
}
}
} ?>
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* Try catch kullanarak ekler veya günceller
* Transaction management added to prevent lock issues
*/
function firstOrUpdate($data, $table, $where, $debug = false) {
if(isset($data['id'])) {
if($data['id'] == "") {
unset($data['id']);
}
}
return \DB::transaction(function() use ($data, $table, $where, $debug) {
try {
$data['created_at'] = simdi();
$id = db($table)->insertGetId($data);
return $id;
} catch (\Throwable $th) {
if($debug) {
dump($th);
}
// Use lockForUpdate to prevent race conditions
$affectedRows = db($table)
->where($where)
->lockForUpdate()
->get();
$affectedRows = db($table)->where($where)->update($data);
return "update $affectedRows";
}
}, 3); // 3 retry attempts
} ?>
+16
View File
@@ -0,0 +1,16 @@
<?php
/**
* Generate a unique report number with prefix and identifier
*
* @param string $prefix The prefix for the report number
* @param string $identifier The identifier (line+spool) for the report
* @return string The formatted report number
*/
if (!function_exists('generateReportNumber')) {
function generateReportNumber($prefix, $identifier) {
$date = date('Ymd');
$randomNumber = mt_rand(100, 999);
return $prefix . '-' . $identifier . '-' . $date . '-' . $randomNumber;
}
}
?>
+5
View File
@@ -0,0 +1,5 @@
<?php function generate_hash_link($type, $title="") {
$hash = Hash::make($type);
$showLink = url("mail-link?title=$title&type=$type&hash=$hash");
return $showLink;
} ?>
+7
View File
@@ -0,0 +1,7 @@
<?php
function getBeforeSlash($input) {
$input = str_replace("/141", "", $input);
$input = str_replace("/111", "", $input);
return $input;
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
function getKeyColumns($table) {
$keyColumnsMap = [
"line_lists" => ["line", "fluid_code"],
"supports" => ["id"],
"paint_matrices" => ["line", "fluid_code"],
"nde_matrices" => ["line", "fluid", "type_of_joint"],
"document_revisions" => ["drawing_no", "zone"],
"m_t_o_s" => ["line", "component_code_id"],
"test_packages" => ["test_package_number"],
"test_pack_base_statuses" => ["test_package_no"],
"punch_lists" => ["test_package", "punch_list_no"],
"weld_logs" => ["iso", "joint"]
];
// log_test_types() tabloları için özel kontrol
if (in_array($table, log_test_types())) {
return ["iso", "joint", "welding_date"];
}
// Tablo için tanımlı sütunları döndür
return $keyColumnsMap[$table] ?? [];
}
?>
@@ -0,0 +1,6 @@
<?php function get_revision_from_string($string) {
preg_match('/(?i)(?<=rev)\w+/', $string, $matches);
$number = $matches[0] ?? "";
return $number;
} ?>
@@ -0,0 +1,4 @@
<?php function get_variables_from_pattern($patternString) {
preg_match_all('/{(.*?)}/',$patternString, $matches);
return $matches[1];
} ?>
+32
View File
@@ -0,0 +1,32 @@
<?php
function getLatestSpoolStatus($spoolStatuses) {
// Eğer tüm spool_status değerleri "Waiting" ise
if (count(array_filter($spoolStatuses, fn($status) => $status !== 'Waiting')) === 0) {
return 'Waiting';
}
// Spool durumlarını sayısal değerlere göre sıralayıp en yüksek olanı döndür
$statusValues = [
'Waiting',
'On Going',
'Spool Release',
'NDT Release',
'Paint',
'Completed'
];
// Geçerli spool durumlarını kontrol et ve en yüksek durumu bul
$latestStatus = null; // Başlangıçta en yüksek durum yok
foreach ($statusValues as $status) {
if (in_array($status, $spoolStatuses)) {
if ($latestStatus === null || array_search($status, $statusValues) > array_search($latestStatus, $statusValues)) {
$latestStatus = $status;
}
}
}
return $latestStatus;
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
function getLowestSpoolStatus($spoolStatuses) {
// Eğer tüm spool_status değerleri "Completed" ise
if (count(array_filter($spoolStatuses, fn($status) => $status !== 'Completed')) === 0) {
return 'Completed';
}
// Spool durumlarını sayısal değerlere göre sıralayıp en düşük olanı döndür
$statusValues = [
'Waiting',
'On Going',
'Spool Release',
'NDT Release',
'Paint',
'Completed'
];
// Geçerli spool durumlarını kontrol et ve en düşük durumu bul
$lowestStatus = null; // Başlangıçta en düşük durum yok
foreach ($statusValues as $status) {
if (in_array($status, $spoolStatuses)) {
if ($lowestStatus === null || array_search($status, $statusValues) < array_search($lowestStatus, $statusValues)) {
$lowestStatus = $status;
}
}
}
return $lowestStatus;
}
?>
+3
View File
@@ -0,0 +1,3 @@
<?php function get_number() {
return date("ynjg") . rand(111,999);
} ?>
+3
View File
@@ -0,0 +1,3 @@
<?php function get_url_json($tableName, $columnName, $value="") {
return url("admin/get/$tableName/$columnName/$value") . "/";
} ?>
+11
View File
@@ -0,0 +1,11 @@
<?php function get_welder_id($metin) {
$pattern = "/\[([A-Z0-9]+)\]/"; // "[BZ7X]" gibi örüntüyü yakalar
if (preg_match($pattern, $metin, $matches)) {
$deger = $matches[1];
$deger = trim($deger);
return $deger;
} else {
return $metin;
}
} ?>
+20
View File
@@ -0,0 +1,20 @@
<?php
function get_welder_name($welder_id, $type = "en") {
$welderName = db("naks_welders")->where("welder_id", $welder_id)->first();
if($welderName) {
if($type == "en") {
return $welderName->welder_name_en;
} else {
return $welderName->welder_name_ru;
}
} else {
return null;
}
}
function get_welder_info($welder_id) {
return db("naks_welders")->where("welder_id", $welder_id)->first();
}
?>
+11
View File
@@ -0,0 +1,11 @@
<?php function html_to_pdf($html, $pdf, $paper="portrait")
{
$params = "";
if($paper == "landscape") {
$params = "-O landscape";
}
putenv('LANG=ru_RU.UTF-8');
$wk_path = env("wk_path");
$command = "$wk_path $params '$html' '$pdf'";
return shell_exec($command);
} ?>
+5
View File
@@ -0,0 +1,5 @@
<?php function icon($type) {
echo "<span class=\"material-symbols-outlined\">
$type
</span>";
} ?>
+14
View File
@@ -0,0 +1,14 @@
<?php function get_cache_id($prefix) {
$lastId = 0;
if(Cache::has($prefix)) {
$lastId = Cache::get($prefix);
}
return $lastId;
}
function set_cache_id($prefix, $lastId) {
Cache::put($prefix, $lastId);
}
?>
@@ -0,0 +1,75 @@
<?php
/**
* Get inspection history configuration for a module from settings
*
* @param string $moduleSlug Module slug to get configuration for
* @param string|null $moduleTitle Optional module title for path generation
* @return array|null Returns array with 'path', 'pattern', and 'active' keys, or null if not found
*/
function getInspectionHistoryConfig($moduleSlug, $moduleTitle = null)
{
if (empty($moduleSlug)) {
return null;
}
// Get the module settings value
$settingsJson = setting('inspection_history_management', false, '{}');
$settings = json_decode($settingsJson, true);
$moduleConfig = null;
if (isset($settings[$moduleSlug])) {
$moduleConfig = $settings[$moduleSlug];
} else {
// Try kebab-case version if underscore version not found
$kebabSlug = str_replace('_', '-', $moduleSlug);
if (isset($settings[$kebabSlug])) {
$moduleConfig = $settings[$kebabSlug];
}
}
if (!$moduleConfig) {
if ($moduleSlug === 'Other') {
$moduleConfig = [
'active' => true,
'path_pattern' => '',
'file_pattern' => '{timestamp}_{file_name}'
];
} else {
return null;
}
}
// Get the main path setting
$mainPath = setting('inspection_history_main_path', false, '');
// If no main path is set, return null (feature not configured)
if (empty($mainPath)) {
return null;
}
// Get module title if not provided
if (empty($moduleTitle)) {
$module = \App\Types::where('slug', $moduleSlug)->first(['title']);
$moduleTitle = $module ? $module->title : $moduleSlug;
}
// Clean module title for use as directory name
$cleanTitle = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $moduleTitle);
$cleanTitle = preg_replace('/_+/', '_', $cleanTitle);
$cleanTitle = trim($cleanTitle, '_');
// Build the full path: main_path/module_title
$fullPath = $mainPath . '/' . $cleanTitle;
return [
'path' => $fullPath,
'path' => $fullPath,
'path_pattern' => $moduleConfig['path_pattern'] ?? null,
'file_pattern' => $moduleConfig['file_pattern'] ?? $moduleConfig['pattern'] ?? null,
'pattern' => $moduleConfig['pattern'] ?? null, // Keep for backward compatibility elsewhere if needed
'active' => $moduleConfig['active'] ?? false
];
}
+10
View File
@@ -0,0 +1,10 @@
<?php function isAdmin($u = null) {
if(is_null($u)) {
$u = u();
}
if($u->level=="Admin") {
return true;
} else {
return false;
}
} ?>
+9
View File
@@ -0,0 +1,9 @@
<?php function is_stellar() {
$stellar = db("subcontractors")->where("company_name_en", "like", "%stellar%")->first()->company_name_en;
$u = u();
if($u->subcontructer == $stellar) {
return true;
} else {
return false;
}
} ?>
+12
View File
@@ -0,0 +1,12 @@
<?php
function isoToLine()
{
// ISO numaralarına karşılık gelen line_number'ları almak
$isoToLine = db("weld_logs")
->select('line_number', 'iso_number')
->pluck('line_number', 'iso_number')
->toArray();
return $isoToLine;
}
?>
+6
View File
@@ -0,0 +1,6 @@
<?php function job_descriptions() {
$jobDesc = db("job_descriptions")->get()->pluck("title")->toArray();
$jobDesc[] = null;
$jobDesc[] = "";
return $jobDesc;
} ?>
+126
View File
@@ -0,0 +1,126 @@
<?php
use App\Services\JointTypeService;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
if (!function_exists('joint_type_service')) {
function joint_type_service(): JointTypeService
{
return app(JointTypeService::class);
}
}
if (!function_exists('is_welded_joint')) {
/**
* Check if a joint type is a welded joint
*
* @param string $jointType - Joint type short_name_en or naks_name
* @return bool
*/
function is_welded_joint($jointType) {
return joint_type_service()->isWelded($jointType);
}
}
if (!function_exists('requires_ndt')) {
/**
* Check if a joint type requires NDT
*
* @param string $jointType - Joint type short_name_en or naks_name
* @return bool
*/
function requires_ndt($jointType) {
return joint_type_service()->requiresNdt($jointType);
}
}
if (!function_exists('is_mechanical_joint')) {
/**
* Check if a joint type is mechanical
*
* @param string $jointType - Joint type short_name_en or naks_name
* @return bool
*/
function is_mechanical_joint($jointType) {
return joint_type_service()->isMechanical($jointType);
}
}
if (!function_exists('get_welded_joint_types')) {
/**
* Get array of welded joint types for query filtering
*
* @return array
*/
function get_welded_joint_types() {
return joint_type_service()->weldedTypes();
}
}
if (!function_exists('get_mechanical_joint_types')) {
/**
* Get array of mechanical joint types for query filtering
*
* @return array
*/
function get_mechanical_joint_types() {
return joint_type_service()->mechanicalTypes();
}
}
if (!function_exists('clear_joint_types_cache')) {
/**
* Clear joint types cache
* Call this after updating joint_types table
*
* @return void
*/
function clear_joint_types_cache() {
joint_type_service()->clearCache();
}
}
if (!function_exists('apply_welded_filter')) {
/**
* Apply welded joint filtering to a query builder or table name
*
* @param Builder|EloquentBuilder|string $queryOrTable
* @param string $column
* @return Builder|EloquentBuilder
*/
function apply_welded_filter($queryOrTable, string $column = 'type_of_welds') {
$builder = resolve_builder_from_argument($queryOrTable);
return joint_type_service()->filterWelded($builder, $column);
}
}
if (!function_exists('apply_mechanical_filter')) {
/**
* Apply mechanical joint filtering to a query builder or table name
*
* @param Builder|EloquentBuilder|string $queryOrTable
* @param string $column
* @return Builder|EloquentBuilder
*/
function apply_mechanical_filter($queryOrTable, string $column = 'type_of_welds') {
$builder = resolve_builder_from_argument($queryOrTable);
return joint_type_service()->filterMechanical($builder, $column);
}
}
if (!function_exists('resolve_builder_from_argument')) {
/**
* @param Builder|EloquentBuilder|string $queryOrTable
* @return Builder|EloquentBuilder
*/
function resolve_builder_from_argument($queryOrTable) {
if ($queryOrTable instanceof Builder || $queryOrTable instanceof EloquentBuilder) {
return $queryOrTable;
}
return DB::table($queryOrTable);
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
function languages() {
$diller = explode(",","en,tr,ru");
return $diller;
}
+13
View File
@@ -0,0 +1,13 @@
<?php
if (!function_exists('levelColor')) {
function levelColor($level) {
$colors = [
1 => 'success',
2 => 'primary',
3 => 'warning',
4 => 'info'
];
return $colors[$level] ?? 'secondary';
}
}
?>
+69
View File
@@ -0,0 +1,69 @@
<?php
function permission_type_index($type) {
$permissionTypeToIndex = [
'full_control' => 1,
'write' => 2,
'read' => 3,
'modify' => 4,
];
return $permissionTypeToIndex[$type];
}
function levels_old() {
// user no, full control, write, read, modify
return [
'Admin' => [1,1,1,1,1],
'Manager (Center Office)' => [2,0,1,1,1],
'Manager (QC)' => [2,0,1,1,0],
'Manager (PTO)' => [2,0,1,1,0],
'Manager (Lead)' => [3,0,1,1,0],
'Welder (Subcontractor)' => [4,0,1,1,0],
'Painter (Subcontractor)' => [5,0,1,1,0],
'Insulator (Subcontractor)' => [6,0,1,1,0],
'Welder, Painter, Insulator (Subcontractor / Payrollless)' => [7,0,1,1,0],
'Quality Staff' => [8,0,1,1,1],
'Document Staff' => [9,0,1,1,1],
'Field Staff' => [10,0,0,1,0],
];
}
function levels() {
// user no, full control, write, read, modify
$userLevels = db("user_levels")->get();
$levels = [];
foreach($userLevels AS $userLevel) {
$levels[$userLevel->title] = [
$userLevel->level_index,
$userLevel->full_control,
$userLevel->write,
$userLevel->read,
$userLevel->modify,
];
}
return $levels;
}
function getLevelIndex($level) {
$levels = levels();
$k = 1;
$index = null;
foreach($levels AS $thisLevel => $permissions) {
if($thisLevel == $level) {
$index = $permissions[0];
}
$k++;
}
return $index;
}
function levels2() {
return [
'Welder',
'Engineer',
];
}
function level_keys() {
return array_keys(levels());
}
+36
View File
@@ -0,0 +1,36 @@
<?php
function lineToIso()
{
// ISO numaralarına karşılık gelen line_number'ları almak
$isoToLine = db("weld_logs")
->select('line_number', 'iso_number')
->pluck('iso_number', 'line_number')
->toArray();
return $isoToLine;
}
function lineTypeWelderToIso()
{
// (line_number, type_of_welds, welder) kombinasyonuna karşılık gelen iso_number'ları almak
$rows = db("weld_logs")
->select('line_number', 'type_of_welds', 'welder_1', 'welder_2', 'iso_number')
->get();
$map = [];
foreach ($rows as $row) {
// welder_1 için map oluştur
if (!empty($row->welder_1)) {
$key = $row->line_number . '|' . $row->type_of_welds . '|' . $row->welder_1;
$map[$key] = $row->iso_number;
}
// welder_2 için de map oluştur (eğer welder_1'den farklıysa)
if (!empty($row->welder_2) && $row->welder_2 !== $row->welder_1) {
$key = $row->line_number . '|' . $row->type_of_welds . '|' . $row->welder_2;
$map[$key] = $row->iso_number;
}
}
return $map;
}
?>
+3
View File
@@ -0,0 +1,3 @@
<?php function locations() {
return json_encode_tr(db("locations")->get()->pluck("title")->toArray());
} ?>
+35
View File
@@ -0,0 +1,35 @@
<?php function log_test_types() {
return [
'ht' => 'hardness_tests',
'rt' => 'radiographic_tests',
'ut' => 'ultrasonic_tests',
'mt' => 'magnetic_tests',
'pmi' => 'p_m_i_tests',
'vt' => 'v_t_logs',
'pt' => 'p_t_logs',
'ferrite' => 'ferrits',
'pwht' => 'p_w_h_t_s',
];
}
function get_key_by_value($value) {
$array = log_test_types();
$key = array_search($value, $array);
return $key !== false ? $key : null; // Eğer değer bulunamazsa null döner
}
function log_paths() {
$mainPath = "004_QA/";
return [
'ht' => $mainPath . '0008_HT',
'rt' => $mainPath . '0001_RT',
'ut' => $mainPath . '0002_UT',
'mt' => $mainPath . '0003_MT',
'pmi' => $mainPath . '0005_PMI',
'vt' => $mainPath . '0000_VT',
'pt' => $mainPath . '0004_PT',
'ferrite' => $mainPath . '0006_Ferrite',
'pwht' => $mainPath . '0007_PWHT',
];
}
?>
+16
View File
@@ -0,0 +1,16 @@
<?php
function mailtemp($mail,$name,$data="") {
$temp = db("mail_templates")->where("title",$name)->first();
$html = $temp->html;
$subject = $temp->title2;
if(is_array($data)) {
foreach($data AS $a => $d) {
$html = str_replace("{".$a."}",$d,$html);
$subject = str_replace("{".$a."}",$d,$subject);
}
}
@mailsend($mail,$subject,$html);
}
?>

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