#6 added csv and persitence

Birleştirildi
dcovington 2 hafta önce Export_Persist içindeki 1 işleme main ile birleştirdi
  1. +145
    -0
      app/Controllers/JobController.php
  2. +6
    -1
      app/Views/jobs/campaign.php
  3. +6
    -1
      app/Views/jobs/index.php
  4. +136
    -0
      public/js/app.js
  5. +3
    -0
      routes/web.php

+ 145
- 0
app/Controllers/JobController.php Dosyayı Görüntüle

@@ -16,6 +16,7 @@ use Core\Controller;
use Core\Request;
use Core\Response;
use Core\Validator;
use Throwable;

class JobController extends Controller
{
@@ -44,6 +45,28 @@ class JobController extends Controller
));
}

public function export(): Response
{
return $this->csvResponse(
$this->exportRowsFromRequest(),
'jobs-' . date('Y-m-d-His') . '.csv'
);
}

public function exportForCampaign(string $campaignId): Response
{
$campaign = $this->campaignRepo()->findWithType((int) $campaignId);

if ($campaign === null) {
return $this->redirect('/campaigns');
}

return $this->csvResponse(
$this->exportRowsFromRequest(),
'campaign-' . (int) $campaignId . '-jobs-' . date('Y-m-d-His') . '.csv'
);
}

public function campaign(string $campaignId): Response
{
$campaign = $this->campaignRepo()->findWithType((int) $campaignId);
@@ -365,6 +388,128 @@ class JobController extends Controller
return trim(preg_replace('/\s+/', ' ', $value) ?? '');
}

/**
* @return list<array<string, mixed>>
*/
private function exportRowsFromRequest(): array
{
$token = (string) ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');

if (!verify_csrf_token($token)) {
return [];
}

$rawBody = file_get_contents('php://input');
if (!is_string($rawBody) || trim($rawBody) === '') {
return [];
}

try {
$payload = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
} catch (Throwable) {
return [];
}

$rows = $payload['rows'] ?? [];
$columns = $payload['columns'] ?? [];

if (!is_array($rows) || !is_array($columns)) {
return [];
}

$visibleColumns = [];
foreach ($columns as $column) {
if (!is_array($column)) {
continue;
}

$field = trim((string) ($column['field'] ?? ''));
if ($field === '' || $field === 'edit_url') {
continue;
}

$visibleColumns[] = [
'field' => $field,
'title' => trim((string) ($column['title'] ?? $field)),
];
}

if ($visibleColumns === []) {
return [];
}

$exportRows = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}

$exportRow = [];
foreach ($visibleColumns as $column) {
$exportRow[$column['title']] = $this->csvCellValue($row[$column['field']] ?? '');
}

$exportRows[] = $exportRow;
}

return $exportRows;
}

/**
* @param list<array<string, mixed>> $rows
*/
private function csvResponse(array $rows, string $filename): Response
{
$handle = fopen('php://temp', 'r+');
if ($handle === false) {
return Response::make('Unable to create CSV export.', 500);
}

try {
fwrite($handle, "\xEF\xBB\xBF");

if ($rows !== []) {
fputcsv($handle, array_keys($rows[0]));

foreach ($rows as $row) {
fputcsv($handle, array_values($row));
}
}

rewind($handle);
$csv = stream_get_contents($handle);
} finally {
fclose($handle);
}

if ($csv === false) {
return Response::make('Unable to create CSV export.', 500);
}

return new Response($csv, 200, [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0',
]);
}

private function csvCellValue(mixed $value): string
{
if ($value === null) {
return '';
}

if (is_array($value) || is_object($value)) {
try {
return json_encode($value, JSON_THROW_ON_ERROR) ?: '';
} catch (Throwable) {
return '';
}
}

return (string) $value;
}

private function googleSheets(): GoogleSheetImportService
{
return new GoogleSheetImportService();


+ 6
- 1
app/Views/jobs/campaign.php Dosyayı Görüntüle

@@ -14,6 +14,8 @@ window.__campaignJobTypes = <?= json_encode($jobTypes ?? [], JSON_HEX_TAG | JSON
<p><?= e($campaignTypeName) ?> #<?= e((string) $campaignId) ?></p>
</div>
<div class="panel-actions">
<button class="button button-secondary" type="button" x-on:click="showConfiguration()">Configuration</button>
<button class="button button-secondary" type="button" x-on:click="exportCsv()">Export CSV</button>
<a class="button button-secondary" href="/campaigns">&larr; Back to campaigns</a>
<a class="button button-primary" href="/jobs/create">+ New Job</a>
</div>
@@ -141,11 +143,14 @@ window.__campaignJobTypes = <?= json_encode($jobTypes ?? [], JSON_HEX_TAG | JSON
<h2>Job Directory</h2>
<p>All jobs in this campaign with job fields and attribute fields.</p>
</div>
<button class="button button-secondary" type="button" x-on:click="reloadTable()">Refresh</button>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="button button-secondary" type="button" x-on:click="reloadTable()">Refresh</button>
</div>
</div>

<div class="inline-indicator" x-cloak x-show="isLoading">Loading jobs...</div>
<div class="alert alert-error" x-cloak x-show="errorMessage" x-text="errorMessage"></div>
<div class="alert alert-success" x-cloak x-show="configMessage" x-text="configMessage"></div>
<div id="campaign-jobs-page-table" class="tabulator-host"></div>
</section>



+ 6
- 1
app/Views/jobs/index.php Dosyayı Görüntüle

@@ -26,7 +26,11 @@
<h2>Job Directory</h2>
<p>All jobs with their campaign and job type.</p>
</div>
<button class="button button-secondary" type="button" x-on:click="reloadTable()">Refresh</button>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="button button-secondary" type="button" x-on:click="showConfiguration()">Configuration</button>
<button class="button button-secondary" type="button" x-on:click="exportCsv()">Export CSV</button>
<button class="button button-secondary" type="button" x-on:click="reloadTable()">Refresh</button>
</div>
</div>
<div class="skeleton-rows" x-cloak x-show="isLoading">
<div class="skeleton-row"></div>
@@ -36,6 +40,7 @@
<div class="skeleton-row"></div>
</div>
<div class="alert alert-error" x-cloak x-show="errorMessage" x-text="errorMessage"></div>
<div class="alert alert-success" x-cloak x-show="configMessage" x-text="configMessage"></div>
<div id="job-table" class="tabulator-host"></div>
</section>



+ 136
- 0
public/js/app.js Dosyayı Görüntüle

@@ -51,6 +51,102 @@ function _escapeHtml(value) {
});
}

function _tabulatorPersistenceKey(key) {
return 'ct.tabulator.' + key;
}

function _tabulatorBaseOptions(persistenceKey) {
return {
persistence: {
columns: true,
sort: true,
filter: true,
page: {
size: true,
page: true,
},
},
persistenceID: persistenceKey,
};
}

function _clearTabulatorPersistence(persistenceKey) {
try {
window.localStorage.removeItem(_tabulatorPersistenceKey(persistenceKey + '-columns'));
window.localStorage.removeItem(_tabulatorPersistenceKey(persistenceKey + '-sort'));
window.localStorage.removeItem(_tabulatorPersistenceKey(persistenceKey + '-filter'));
window.localStorage.removeItem(_tabulatorPersistenceKey(persistenceKey + '-page'));
} catch (error) {
}
}

function _tabulatorVisibleColumns(table) {
if (!table || typeof table.getColumns !== 'function') {
return [];
}

return table.getColumns()
.map((column) => {
const definition = typeof column.getDefinition === 'function' ? column.getDefinition() : null;
if (!definition || !definition.field || definition.field === 'edit_url') {
return null;
}

const element = typeof column.getElement === 'function' ? column.getElement() : null;
const isVisible = !element || element.offsetParent !== null;
if (!isVisible) {
return null;
}

return {
field: definition.field,
title: definition.title || definition.field,
};
})
.filter((column) => column !== null);
}

function _downloadBlob(blob, filename) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => window.URL.revokeObjectURL(url), 0);
}

async function _exportTabulatorCsv(url, table, filename) {
if (!table) {
throw new Error('The table is not ready yet.');
}

const payload = {
columns: _tabulatorVisibleColumns(table),
rows: typeof table.getData === 'function' ? table.getData('active') : [],
};

const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'text/csv',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': window.__csrf || '',
},
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error('Unable to export CSV.');
}

const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
const match = disposition.match(/filename="?([^";]+)"?/i);
_downloadBlob(blob, match ? match[1] : filename);
}

// ── Campaign Type ─────────────────────────────────────────────────────────────

window.campaignTypeTable = function () {
@@ -418,6 +514,7 @@ window.campaignTable = function () {
placeholder: 'No jobs found for this campaign.',
initialSort: [{ column: 'job_type_name', dir: 'asc' }],
columns: columns,
..._tabulatorBaseOptions(this.persistenceKey),
});
} else {
this.jobsTable.setColumns(columns);
@@ -524,6 +621,7 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) {
table: null,
jobTypes: Array.isArray(jobTypes) ? jobTypes : [],
isLoading: false,
configMessage: '',
isConnecting: false,
isImporting: false,
errorMessage: '',
@@ -543,6 +641,7 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) {
selectedFileJobTypeId: '0',
isLoadingFile: false,
isImportingFile: false,
persistenceKey: 'campaign-jobs-page-' + campaignId,

init() {
this.loadTable();
@@ -714,6 +813,14 @@ window.campaignJobsPageTable = function (campaignId, jobTypes) {
this.loadTable();
},

async exportCsv() {
try {
await _exportTabulatorCsv('/campaigns/' + encodeURIComponent(campaignId) + '/jobs/export', this.table, 'campaign-jobs-export.csv');
} catch (error) {
this.errorMessage = error.message || 'Unable to export CSV.';
}
},

async connectGoogleSheet() {
this.isConnecting = true;
this.importMessage = '';
@@ -1276,6 +1383,8 @@ window.jobTable = function () {
table: null,
isLoading: false,
errorMessage: '',
configMessage: '',
persistenceKey: 'jobs-directory',

init() {
this.loadTable();
@@ -1321,6 +1430,7 @@ window.jobTable = function () {
placeholder: 'No jobs found.',
initialSort: [{ column: 'job_type_name', dir: 'asc' }],
columns: columns,
..._tabulatorBaseOptions(this.persistenceKey),
});
} catch (error) {
this.errorMessage = error.message || 'Unable to load jobs.';
@@ -1434,6 +1544,32 @@ window.jobTable = function () {
reloadTable() {
this.loadTable();
},

async exportCsv() {
try {
await _exportTabulatorCsv('/jobs/export', this.table, 'jobs-export.csv');
} catch (error) {
this.errorMessage = error.message || 'Unable to export CSV.';
}
},

showConfiguration() {
if (!this.table) {
return;
}

const shouldReset = window.confirm('Reset this table configuration? This clears saved columns, filters, sorting, and page size.');
if (!shouldReset) {
return;
}

_clearTabulatorPersistence(this.persistenceKey);
this.configMessage = 'Table configuration reset.';
this.loadTable();
window.setTimeout(() => {
this.configMessage = '';
}, 3000);
},
};
};



+ 3
- 0
routes/web.php Dosyayı Görüntüle

@@ -41,6 +41,8 @@ $router->get('/campaigns/{id}/jobs', [JobController::class, 'campaign'])
->middleware('auth');
$router->get('/campaigns/{id}/jobs/data', [JobController::class, 'dataForCampaign'])
->middleware('auth');
$router->post('/campaigns/{id}/jobs/export', [JobController::class, 'exportForCampaign'])
->middleware('auth');
$router->post('/campaigns/{id}/jobs/import/sheets', [JobController::class, 'googleSheetsList'])->middleware('auth');
$router->post('/campaigns/{id}/jobs/import', [JobController::class, 'importGoogleSheet'])->middleware('auth');
$router->post('/campaigns/{id}/jobs/import/file/sheets', [JobController::class, 'fileSheetsList']) ->middleware('auth');
@@ -61,6 +63,7 @@ $router->post('/campaign-types/{id}/delete', [CampaignTypeController::class, 'de
// ── Jobs ──────────────────────────────────────────────────────────────────────
$router->get('/jobs', [JobController::class, 'index']) ->middleware('auth');
$router->get('/jobs/data', [JobController::class, 'data']) ->middleware('auth');
$router->post('/jobs/export', [JobController::class, 'export']) ->middleware('auth');
$router->get('/jobs/create', [JobController::class, 'create']) ->middleware('auth');
$router->post('/jobs', [JobController::class, 'store']) ->middleware('auth');
$router->get('/jobs/{id}/edit', [JobController::class, 'edit']) ->middleware('auth');


Yükleniyor…
İptal
Kaydet

Powered by TurnKey Linux.