Skip to content

Commit b4a3707

Browse files
authored
Merge pull request #3135 from codeeu/dev
export script update
2 parents bdc60bc + 6426aaf commit b4a3707

3 files changed

Lines changed: 210 additions & 70 deletions

File tree

app/Console/Commands/ExportCertificatesProof.php

Lines changed: 208 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,115 +9,253 @@
99

1010
class ExportCertificatesProof extends Command
1111
{
12-
protected $signature = 'cw:export-certificates-proof
13-
{--start= : Start datetime (YYYY-MM-DD or full Y-m-d H:i:s)}
14-
{--end= : End datetime (YYYY-MM-DD or full Y-m-d H:i:s)}
15-
{--path= : Output relative path under storage/app (default: exports/certificates_manifest_[range].csv)}';
12+
protected $signature = 'cw:export-certificates-proof
13+
{--start= : Start datetime (YYYY-MM-DD or full Y-m-d H:i:s)}
14+
{--end= : End datetime (YYYY-MM-DD or full Y-m-d H:i:s)}
15+
{--path= : Output path under storage/app (default: exports/certificates_manifest_[range].csv)}
16+
{--family=both : Which family to export: participations|excellence|both}
17+
{--inclusive=0 : If 1, do not require URL and do not force status=DONE}
18+
{--date-field=created_at : Date field to use (created_at|event_date|issued_at if present)}';
1619

17-
protected $description = 'Export a CSV manifest of issued certificates (with PDF links) for an interval';
20+
protected $description = 'Export a CSV manifest of issued certificates (links + metadata) for the requested interval';
1821

1922
public function handle()
2023
{
24+
// ---- Window normalize
2125
$start = $this->option('start') ?: now()->subYear()->startOfDay()->toDateTimeString();
2226
$end = $this->option('end') ?: now()->endOfDay()->toDateTimeString();
23-
24-
// Normalize date-only inputs
2527
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start)) $start .= ' 00:00:00';
2628
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) $end .= ' 23:59:59';
2729

28-
// --- Schema detection -------------------------------------------------
29-
$hasEventId = Schema::hasColumn('participations', 'event_id');
30-
$hasActivityId = Schema::hasColumn('participations', 'activity_id'); // common alternative
31-
$hasEventTitle = Schema::hasColumn('participations', 'event_title'); // sometimes stored directly
32-
$hasTitle = Schema::hasColumn('participations', 'title'); // generic fallback
30+
$family = strtolower($this->option('family') ?: 'both'); // participations|excellence|both
31+
$inclusive = (int)($this->option('inclusive') ?: 0) === 1;
32+
$datePref = strtolower($this->option('date-field') ?: 'created_at'); // created_at|event_date|issued_at
33+
34+
$defaultPath = 'exports/certificates_manifest_'
35+
. str_replace([':', ' '], ['_', '_'], $start)
36+
. '_to_'
37+
. str_replace([':', ' '], ['_', '_'], $end)
38+
. ($inclusive ? '_inclusive' : '')
39+
. ($family !== 'both' ? "_{$family}" : '')
40+
. '.csv';
41+
42+
$path = $this->option('path') ?: $defaultPath;
43+
44+
$rows = collect();
45+
46+
if ($family === 'participations' || $family === 'both') {
47+
$rows = $rows->merge($this->exportParticipations($start, $end, $inclusive, $datePref));
48+
}
49+
50+
if ($family === 'excellence' || $family === 'both') {
51+
$rows = $rows->merge($this->exportExcellence($start, $end, $inclusive, $datePref));
52+
}
53+
54+
// Write merged CSV
55+
$stream = fopen('php://temp', 'w+');
56+
fputcsv($stream, [
57+
'family', 'record_id', 'issued_at', 'event_date',
58+
'status', 'owner_email', 'event_id', 'title',
59+
'certificate_url', 'missing_url'
60+
]);
61+
62+
foreach ($rows as $r) {
63+
fputcsv($stream, [
64+
$r['family'] ?? null,
65+
$r['record_id'] ?? null,
66+
$r['issued_at'] ?? null,
67+
$r['event_date'] ?? null,
68+
$r['status'] ?? null,
69+
$r['owner_email'] ?? null,
70+
$r['event_id'] ?? null,
71+
$r['title'] ?? null,
72+
$r['certificate_url'] ?? null,
73+
!empty($r['certificate_url']) ? 0 : 1,
74+
]);
75+
}
76+
77+
rewind($stream);
78+
$csv = stream_get_contents($stream);
79+
fclose($stream);
80+
Storage::disk('local')->put($path, $csv);
81+
82+
$this->info("Wrote {$rows->count()} rows to storage/app/{$path}");
83+
$this->line('Breakdown:');
84+
85+
// Print per-family monthly breakdowns for the memo
86+
$this->printMonthly('participations', $start, $end, $inclusive, $datePref);
87+
$this->printMonthly('excellence', $start, $end, $inclusive, $datePref);
88+
89+
return self::SUCCESS;
90+
}
91+
92+
// ---------- Helpers ----------
93+
94+
protected function pickDateColumn(string $table, string $preferred): ?string
95+
{
96+
// Respect requested preference first
97+
if ($preferred === 'event_date' && Schema::hasColumn($table, 'event_date')) {
98+
return 'event_date';
99+
}
100+
if ($preferred === 'issued_at' && Schema::hasColumn($table, 'issued_at')) {
101+
return 'issued_at';
102+
}
103+
if ($preferred === 'created_at' && Schema::hasColumn($table, 'created_at')) {
104+
return 'created_at';
105+
}
106+
// Fallbacks
107+
foreach (['created_at','issued_at','event_date','date'] as $c) {
108+
if (Schema::hasColumn($table, $c)) return $c;
109+
}
110+
return null;
111+
}
112+
113+
protected function exportParticipations(string $start, string $end, bool $inclusive, string $datePref)
114+
{
115+
$table = 'participations';
116+
$dateCol = $this->pickDateColumn($table, $datePref) ?? 'created_at';
117+
$dateExpr = "p.$dateCol";
33118

34119
$q = DB::table('participations as p')
35120
->leftJoin('users as u', 'u.id', '=', 'p.user_id')
36-
->where('p.status', 'DONE')
37-
->whereNotNull('p.participation_url')
38-
->whereBetween('p.created_at', [$start, $end])
121+
->whereBetween($dateExpr, [$start, $end])
39122
->orderBy('p.id');
40123

41-
// Join events table only if we have a FK on participations
124+
$hasEventId = Schema::hasColumn($table, 'event_id');
125+
$hasActivityId = Schema::hasColumn($table, 'activity_id');
126+
42127
if ($hasEventId) {
43128
$q->leftJoin('events as e', 'e.id', '=', 'p.event_id');
44129
} elseif ($hasActivityId) {
45130
$q->leftJoin('events as e', 'e.id', '=', 'p.activity_id');
46131
}
47132

133+
if (!$inclusive) {
134+
if (Schema::hasColumn($table, 'status')) {
135+
$q->where('p.status', 'DONE');
136+
}
137+
$q->whereNotNull('p.participation_url');
138+
}
139+
48140
$select = [
49-
'p.id as participation_id',
50-
'p.created_at as issued_at',
51-
'p.event_date',
141+
'p.id as record_id',
142+
DB::raw("$dateExpr as issued_at"),
143+
(Schema::hasColumn($table, 'event_date') ? 'p.event_date' : DB::raw('NULL as event_date')),
144+
(Schema::hasColumn($table, 'status') ? 'p.status' : DB::raw('NULL as status')),
52145
'u.email as owner_email',
53-
'p.participation_url as certificate_url',
146+
(Schema::hasColumn($table, 'participation_url') ? 'p.participation_url as certificate_url' : DB::raw('NULL as certificate_url')),
54147
];
55148

56149
if ($hasEventId || $hasActivityId) {
57-
// We can read from events
58150
$select[] = 'e.id as event_id';
59-
$select[] = 'e.title as event_title';
151+
$select[] = 'e.title as title';
60152
} else {
61-
// No join available; fall back to a title present on participations (or NULL)
62153
$select[] = DB::raw('NULL as event_id');
63-
if ($hasEventTitle) {
64-
$select[] = 'p.event_title as event_title';
65-
} elseif ($hasTitle) {
66-
$select[] = 'p.title as event_title';
67-
} else {
68-
$select[] = DB::raw('NULL as event_title');
69-
}
154+
$select[] = DB::raw('NULL as title'); // `participations` has no native title in your DB
70155
}
71156

72-
$rows = $q->get($select);
157+
return collect($q->get($select))->map(function ($r) {
158+
return [
159+
'family' => 'participations',
160+
'record_id' => $r->record_id,
161+
'issued_at' => $r->issued_at,
162+
'event_date' => $r->event_date,
163+
'status' => $r->status,
164+
'owner_email' => $r->owner_email,
165+
'event_id' => property_exists($r, 'event_id') ? $r->event_id : null,
166+
'title' => $r->title ?? null,
167+
'certificate_url' => $r->certificate_url ?? null,
168+
];
169+
});
170+
}
73171

74-
$defaultPath = 'exports/certificates_manifest_'
75-
. str_replace([':', ' '], ['_', '_'], $start)
76-
. '_to_'
77-
. str_replace([':', ' '], ['_', '_'], $end)
78-
. '.csv';
172+
protected function exportExcellence(string $start, string $end, bool $inclusive, string $datePref)
173+
{
174+
$table = 'CertificatesOfExcellence';
175+
if (!Schema::hasTable($table)) return collect();
79176

80-
$path = $this->option('path') ?: $defaultPath;
177+
$dateCol = $this->pickDateColumn($table, $datePref) ?? 'created_at';
178+
$alias = 'x';
179+
$dateExpr = "$alias.$dateCol";
81180

82-
// Write CSV
83-
$stream = fopen('php://temp', 'w+');
84-
fputcsv($stream, ['participation_id','issued_at','event_date','owner_email','event_id','event_title','certificate_url']);
85-
foreach ($rows as $r) {
86-
// event_id may be missing if we couldn’t join events
87-
$eventId = property_exists($r, 'event_id') ? $r->event_id : null;
88-
fputcsv($stream, [
89-
$r->participation_id,
90-
$r->issued_at,
91-
$r->event_date,
92-
$r->owner_email,
93-
$eventId,
94-
$r->event_title,
95-
$r->certificate_url,
96-
]);
181+
$q = DB::table("$table as $alias")
182+
->whereBetween($dateExpr, [$start, $end])
183+
->orderBy("$alias.id");
184+
185+
if (!$inclusive && Schema::hasColumn($table, 'status')) {
186+
$q->where("$alias.status", 'DONE');
187+
}
188+
if (!$inclusive) {
189+
$urlCol = Schema::hasColumn($table, 'certificate_url') ? 'certificate_url'
190+
: (Schema::hasColumn($table, 'url') ? 'url' : null);
191+
if ($urlCol) $q->whereNotNull("$alias.$urlCol");
97192
}
98-
rewind($stream);
99-
$csv = stream_get_contents($stream);
100-
fclose($stream);
101193

102-
Storage::disk('local')->put($path, $csv);
194+
// Build select list defensively
195+
$select = ["$alias.id as record_id", DB::raw("$dateExpr as issued_at")];
196+
$select[] = Schema::hasColumn($table,'event_date') ? "$alias.event_date" : DB::raw('NULL as event_date');
197+
$select[] = Schema::hasColumn($table,'status') ? "$alias.status" : DB::raw('NULL as status');
103198

104-
$this->info("Wrote ".count($rows)." rows to storage/app/{$path}");
199+
if (Schema::hasColumn($table,'email')) $select[] = "$alias.email as owner_email";
200+
elseif (Schema::hasColumn($table,'user_email')) $select[] = "$alias.user_email as owner_email";
201+
else $select[] = DB::raw('NULL as owner_email');
105202

106-
// Monthly breakdown for the audit note
107-
$monthly = DB::table('participations')
108-
->selectRaw('DATE_FORMAT(created_at, "%Y-%m") as yyyymm, COUNT(*) as cnt')
109-
->where('status','DONE')
110-
->whereNotNull('participation_url')
111-
->whereBetween('created_at', [$start,$end])
112-
->groupBy('yyyymm')
113-
->orderBy('yyyymm')
114-
->get();
203+
$select[] = Schema::hasColumn($table,'event_id') ? "$alias.event_id" : DB::raw('NULL as event_id');
204+
$select[] = Schema::hasColumn($table,'title') ? "$alias.title" : DB::raw('NULL as title');
115205

116-
$this->line('Breakdown:');
117-
foreach ($monthly as $m) {
118-
$this->line(" {$m->yyyymm}: {$m->cnt}");
206+
if (Schema::hasColumn($table,'certificate_url')) $select[] = "$alias.certificate_url as certificate_url";
207+
elseif (Schema::hasColumn($table,'url')) $select[] = "$alias.url as certificate_url";
208+
else $select[] = DB::raw('NULL as certificate_url');
209+
210+
return collect($q->get($select))->map(function ($r) {
211+
return [
212+
'family' => 'excellence',
213+
'record_id' => $r->record_id,
214+
'issued_at' => $r->issued_at,
215+
'event_date' => $r->event_date ?? null,
216+
'status' => $r->status ?? null,
217+
'owner_email' => $r->owner_email ?? null,
218+
'event_id' => $r->event_id ?? null,
219+
'title' => $r->title ?? null,
220+
'certificate_url' => $r->certificate_url ?? null,
221+
];
222+
});
223+
}
224+
225+
protected function printMonthly(string $family, string $start, string $end, bool $inclusive, string $datePref): void
226+
{
227+
if ($family === 'participations') {
228+
$table = 'participations';
229+
$alias = 'p';
230+
} elseif ($family === 'excellence') {
231+
$table = 'CertificatesOfExcellence';
232+
if (!Schema::hasTable($table)) { $this->line(" {$family}: table missing"); return; }
233+
$alias = 'x';
234+
} else {
235+
return;
119236
}
120237

121-
return self::SUCCESS;
238+
$dateCol = $this->pickDateColumn($table, $datePref) ?? 'created_at';
239+
$dateExpr = "$alias.$dateCol";
240+
241+
$q = DB::table("$table as $alias")->whereBetween($dateExpr, [$start, $end]);
242+
243+
if (!$inclusive && Schema::hasColumn($table, 'status')) {
244+
$q->where("$alias.status", 'DONE');
245+
}
246+
if (!$inclusive) {
247+
$urlCol = Schema::hasColumn($table,'participation_url') ? 'participation_url'
248+
: (Schema::hasColumn($table,'certificate_url') ? 'certificate_url'
249+
: (Schema::hasColumn($table,'url') ? 'url' : null));
250+
if ($urlCol) $q->whereNotNull("$alias.$urlCol");
251+
}
252+
253+
$monthly = $q->selectRaw('DATE_FORMAT('.$dateExpr.', "%Y-%m") as yyyymm, COUNT(*) as cnt')
254+
->groupBy('yyyymm')->orderBy('yyyymm')->get();
255+
256+
$this->line(" {$family}:");
257+
foreach ($monthly as $m) {
258+
$this->line(" {$m->yyyymm}: {$m->cnt}");
259+
}
122260
}
123261
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
family,record_id,issued_at,event_date,status,owner_email,event_id,title,certificate_url,missing_url
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
family,record_id,issued_at,event_date,status,owner_email,event_id,title,certificate_url,missing_url

0 commit comments

Comments
 (0)