Commit eec03363 authored by Mahmoud Aglan's avatar Mahmoud Aglan

go

parent 6fb137f8
...@@ -75,6 +75,7 @@ class RetroactiveWizardController extends Controller ...@@ -75,6 +75,7 @@ class RetroactiveWizardController extends Controller
'form_fee' => $data['form_fee_amount'] ?? '0.00', 'form_fee' => $data['form_fee_amount'] ?? '0.00',
'spouses_count' => count($data['spouses'] ?? []), 'spouses_count' => count($data['spouses'] ?? []),
'children_count' => count($data['children'] ?? []), 'children_count' => count($data['children'] ?? []),
'temp_members_count' => count($data['temporary_members'] ?? []),
'subscriptions' => $data['subscriptions'] ?? [], 'subscriptions' => $data['subscriptions'] ?? [],
'violations' => $data['violations'] ?? [], 'violations' => $data['violations'] ?? [],
'has_installment' => ($data['membership_payment_method'] ?? 'cash') === 'installment', 'has_installment' => ($data['membership_payment_method'] ?? 'cash') === 'installment',
...@@ -222,6 +223,26 @@ class RetroactiveWizardController extends Controller ...@@ -222,6 +223,26 @@ class RetroactiveWizardController extends Controller
} }
} }
$data['temporary_members'] = [];
$tempCount = (int) ($post['temp_member_count'] ?? 0);
for ($i = 1; $i <= $tempCount; $i++) {
if (!empty($post["temp_name_{$i}"])) {
$data['temporary_members'][] = [
'full_name_ar' => $post["temp_name_{$i}"],
'national_id' => $post["temp_nid_{$i}"] ?? null,
'date_of_birth' => $post["temp_dob_{$i}"] ?? null,
'gender' => $post["temp_gender_{$i}"] ?? 'male',
'category' => $post["temp_category_{$i}"] ?? 'parent',
'relationship_to_member' => $post["temp_relationship_{$i}"] ?? null,
'join_date' => $post["temp_join_date_{$i}"] ?? ($post['join_date'] ?? date('Y-m-d')),
'addition_fee' => $post["temp_fee_{$i}"] ?? '0.00',
'has_separate_fee' => !empty($post["temp_separate_fee_{$i}"]),
'payment_method' => $post["temp_pay_method_{$i}"] ?? 'cash',
'payment_date' => $post["temp_pay_date_{$i}"] ?? null,
];
}
}
// Step 6: Subscriptions // Step 6: Subscriptions
$data['subscriptions'] = []; $data['subscriptions'] = [];
$subCount = (int) ($post['subscription_count'] ?? 0); $subCount = (int) ($post['subscription_count'] ?? 0);
...@@ -229,8 +250,9 @@ class RetroactiveWizardController extends Controller ...@@ -229,8 +250,9 @@ class RetroactiveWizardController extends Controller
if (!empty($post["sub_year_{$i}"])) { if (!empty($post["sub_year_{$i}"])) {
$data['subscriptions'][] = [ $data['subscriptions'][] = [
'financial_year' => $post["sub_year_{$i}"], 'financial_year' => $post["sub_year_{$i}"],
'person_type' => 'member', 'person_type' => $post["sub_person_type_{$i}"] ?? 'member',
'person_name' => $data['full_name_ar'], 'person_index' => (int) ($post["sub_person_index_{$i}"] ?? 0),
'person_name' => $post["sub_person_name_{$i}"] ?? $data['full_name_ar'],
'base_amount' => $post["sub_base_{$i}"] ?? '492.00', 'base_amount' => $post["sub_base_{$i}"] ?? '492.00',
'development_fee' => $post["sub_dev_fee_{$i}"] ?? '35.00', 'development_fee' => $post["sub_dev_fee_{$i}"] ?? '35.00',
'total_amount' => $post["sub_total_{$i}"] ?? '527.00', 'total_amount' => $post["sub_total_{$i}"] ?? '527.00',
......
...@@ -223,9 +223,65 @@ final class RetroactiveMembershipService ...@@ -223,9 +223,65 @@ final class RetroactiveMembershipService
} }
} }
if (!empty($data['temporary_members'])) {
foreach ($data['temporary_members'] as $temp) {
$tempJoinDate = self::emptyToNull($temp['join_date'] ?? null) ?? $joinDate;
$tempId = $db->insert('temporary_members', [
'member_id' => $memberId,
'full_name_ar' => $temp['full_name_ar'],
'national_id' => self::emptyToNull($temp['national_id'] ?? null),
'date_of_birth' => self::emptyToNull($temp['date_of_birth'] ?? null),
'gender' => $temp['gender'] ?? 'male',
'category' => $temp['category'] ?? 'parent',
'relationship_to_member' => self::emptyToNull($temp['relationship_to_member'] ?? null),
'status' => 'active',
'join_date' => $tempJoinDate,
'addition_fee' => $temp['addition_fee'] ?? '0.00',
'is_archived' => 0,
'created_at' => $tempJoinDate . ' 09:00:00',
'updated_at' => $ts,
]);
if (!empty($temp['addition_fee']) && bccomp($temp['addition_fee'], '0', 2) > 0 && !empty($temp['has_separate_fee'])) {
$tPayId = self::createRetroactivePayment($memberId, [
'payment_type' => 'addition_fee',
'amount' => $temp['addition_fee'],
'payment_method' => $temp['payment_method'] ?? 'cash',
'payment_date' => $temp['payment_date'] ?? $joinDate,
'related_entity_type' => 'temporary_members',
'related_entity_id' => $tempId,
'description' => 'رسوم إضافة عضو مؤقت — إدخال بأثر رجعي',
]);
}
$result['dependents'][] = ['type' => 'temporary', 'id' => $tempId, 'name' => $temp['full_name_ar']];
}
}
// Step 5: Create retroactive subscriptions // Step 5: Create retroactive subscriptions
// Build person_id map for dependents
$personIdMap = [
'member' => [$memberId],
'spouse' => [],
'child' => [],
'temporary' => [],
];
foreach ($result['dependents'] as $dep) {
$personIdMap[$dep['type']][] = $dep['id'];
}
if (!empty($data['subscriptions'])) { if (!empty($data['subscriptions'])) {
foreach ($data['subscriptions'] as $sub) { foreach ($data['subscriptions'] as $sub) {
$personType = $sub['person_type'] ?? 'member';
$personIndex = (int) ($sub['person_index'] ?? 0);
$personId = $memberId;
if ($personType !== 'member' && isset($personIdMap[$personType][$personIndex])) {
$personId = $personIdMap[$personType][$personIndex];
}
$sub['person_id'] = $personId;
$subId = self::createRetroactiveSubscription($memberId, $sub); $subId = self::createRetroactiveSubscription($memberId, $sub);
$result['subscriptions'][] = $subId; $result['subscriptions'][] = $subId;
} }
...@@ -485,7 +541,7 @@ final class RetroactiveMembershipService ...@@ -485,7 +541,7 @@ final class RetroactiveMembershipService
'member_id' => $memberId, 'member_id' => $memberId,
'financial_year' => $sub['financial_year'], 'financial_year' => $sub['financial_year'],
'person_type' => $sub['person_type'] ?? 'member', 'person_type' => $sub['person_type'] ?? 'member',
'person_id' => $memberId, 'person_id' => $sub['person_id'] ?? $memberId,
'person_name' => $sub['person_name'] ?? '', 'person_name' => $sub['person_name'] ?? '',
'base_amount' => $sub['base_amount'] ?? '0.00', 'base_amount' => $sub['base_amount'] ?? '0.00',
'development_fee' => $sub['development_fee'] ?? '35.00', 'development_fee' => $sub['development_fee'] ?? '35.00',
......
...@@ -368,7 +368,7 @@ ...@@ -368,7 +368,7 @@
<div class="wizard-panel" id="panel-5" style="display:none;"> <div class="wizard-panel" id="panel-5" style="display:none;">
<div class="card" style="margin-bottom:16px;"> <div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;"> <div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:700;"><i data-lucide="users" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الملحقون (الزوجات والأبناء)</h3> <h3 style="margin:0;font-size:16px;font-weight:700;"><i data-lucide="users" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الملحقون (الزوجات والأبناء والأعضاء المؤقتون)</h3>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;">
<!-- Spouses --> <!-- Spouses -->
...@@ -382,7 +382,7 @@ ...@@ -382,7 +382,7 @@
</div> </div>
<!-- Children --> <!-- Children -->
<div> <div style="margin-bottom:24px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="font-size:14px;font-weight:700;color:#374151;">الأبناء</div> <div style="font-size:14px;font-weight:700;color:#374151;">الأبناء</div>
<button type="button" onclick="addChild()" style="padding:6px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600;">+ إضافة ابن/ابنة</button> <button type="button" onclick="addChild()" style="padding:6px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600;">+ إضافة ابن/ابنة</button>
...@@ -390,6 +390,16 @@ ...@@ -390,6 +390,16 @@
<div id="childrenList"></div> <div id="childrenList"></div>
<input type="hidden" name="child_count" id="childCount" value="0"> <input type="hidden" name="child_count" id="childCount" value="0">
</div> </div>
<!-- Temporary Members -->
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="font-size:14px;font-weight:700;color:#374151;">أعضاء مؤقتون</div>
<button type="button" onclick="addTempMember()" style="padding:6px 14px;background:#2563EB;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600;">+ إضافة عضو مؤقت</button>
</div>
<div id="tempMembersList"></div>
<input type="hidden" name="temp_member_count" id="tempMemberCount" value="0">
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -411,6 +421,7 @@ ...@@ -411,6 +421,7 @@
<table style="width:100%;border-collapse:collapse;font-size:12px;" id="subsTable"> <table style="width:100%;border-collapse:collapse;font-size:12px;" id="subsTable">
<thead> <thead>
<tr style="background:#F3F4F6;"> <tr style="background:#F3F4F6;">
<th style="padding:8px;text-align:right;">الشخص</th>
<th style="padding:8px;text-align:right;">السنة المالية</th> <th style="padding:8px;text-align:right;">السنة المالية</th>
<th style="padding:8px;text-align:right;">المبلغ الأساسي</th> <th style="padding:8px;text-align:right;">المبلغ الأساسي</th>
<th style="padding:8px;text-align:right;">رسم تنمية</th> <th style="padding:8px;text-align:right;">رسم تنمية</th>
...@@ -764,19 +775,77 @@ function addChild() { ...@@ -764,19 +775,77 @@ function addChild() {
</div>`; </div>`;
} }
// Temporary Members
let tempIdx = 0;
function addTempMember() {
tempIdx++;
document.getElementById('tempMemberCount').value = tempIdx;
const joinDate = document.querySelector('[name=join_date]').value || '';
const idx = tempIdx;
document.getElementById('tempMembersList').innerHTML += `
<div style="padding:12px;background:#FFF7ED;border-radius:8px;border:1px solid #FDBA74;margin-bottom:10px;" id="tempRow${idx}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:13px;font-weight:600;color:#9A3412;">عضو مؤقت #${idx}</span>
<button type="button" onclick="document.getElementById('tempRow${idx}').remove()" style="padding:2px 8px;background:#FEE2E2;border:1px solid #FECACA;border-radius:4px;color:#DC2626;cursor:pointer;font-size:12px;">حذف</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;">
<div><label style="font-size:11px;color:#6B7280;">الاسم *</label><input type="text" name="temp_name_${idx}" required class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></div>
<div><label style="font-size:11px;color:#6B7280;">الرقم القومي</label><input type="text" name="temp_nid_${idx}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;direction:ltr;"></div>
<div><label style="font-size:11px;color:#6B7280;">تاريخ الميلاد</label><input type="date" name="temp_dob_${idx}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></div>
<div><label style="font-size:11px;color:#6B7280;">النوع</label><select name="temp_gender_${idx}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"><option value="male">ذكر</option><option value="female">أنثى</option></select></div>
</div>
<div style="margin-top:8px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;">
<div>
<label style="font-size:11px;color:#6B7280;">الفئة *</label>
<select name="temp_category_${idx}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;">
<option value="parent">والدين العضو العامل</option>
<option value="special_needs">أبناء ذوي الاحتياجات الخاصة</option>
<option value="unmarried_daughter">بنات العضو غير المتزوجات</option>
<option value="sister">شقيقة العضو العامل</option>
<option value="stepchild">أبناء الزوج/الزوجة</option>
<option value="orphan">الطفل اليتيم</option>
<option value="disabled_sibling">شقيق العضو المعاق</option>
<option value="nanny">المربية</option>
</select>
</div>
<div><label style="font-size:11px;color:#6B7280;">صلة القرابة</label><input type="text" name="temp_relationship_${idx}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></div>
<div><label style="font-size:11px;color:#6B7280;">تاريخ الانضمام</label><input type="date" name="temp_join_date_${idx}" value="${joinDate}" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></div>
</div>
<div style="margin-top:8px;display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;">
<div><label style="font-size:11px;color:#6B7280;">رسوم الإضافة</label><input type="number" name="temp_fee_${idx}" value="0" step="0.01" class="form-input" style="width:100%;padding:6px 8px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></div>
<div style="display:flex;align-items:end;">
<label style="font-size:11px;color:#6B7280;display:flex;align-items:center;gap:4px;"><input type="checkbox" name="temp_separate_fee_${idx}" value="1"> رسوم منفصلة</label>
</div>
<div>
<label style="font-size:11px;color:#6B7280;">طريقة الدفع</label>
<select name="temp_pay_method_${idx}" style="font-size:11px;padding:4px 8px;border:1px solid #D1D5DB;border-radius:4px;width:100%;"><option value="cash">نقدي</option><option value="check">شيك</option></select>
</div>
</div>
</div>`;
}
// Subscriptions // Subscriptions
let subIdx = 0; let subIdx = 0;
function addSubscriptionRow(year, status, baseAmt, devFee) { function addSubscriptionRow(year, status, baseAmt, devFee, personType, personIndex, personName) {
subIdx++; subIdx++;
document.getElementById('subscriptionCount').value = subIdx; document.getElementById('subscriptionCount').value = subIdx;
year = year || ''; year = year || '';
status = status || 'pending'; status = status || 'pending';
baseAmt = baseAmt || '492.00'; baseAmt = baseAmt || '492.00';
devFee = devFee || '35.00'; devFee = devFee || '35.00';
personType = personType || 'member';
personIndex = personIndex || 0;
personName = personName || 'العضو';
const total = (parseFloat(baseAmt) + parseFloat(devFee)).toFixed(2); const total = (parseFloat(baseAmt) + parseFloat(devFee)).toFixed(2);
document.getElementById('subscriptionRows').innerHTML += ` document.getElementById('subscriptionRows').innerHTML += `
<tr style="border-bottom:1px solid #F3F4F6;" id="subRow${subIdx}"> <tr style="border-bottom:1px solid #F3F4F6;" id="subRow${subIdx}">
<td style="padding:6px;">
<span style="font-size:11px;color:#374151;white-space:nowrap;">${personName}</span>
<input type="hidden" name="sub_person_type_${subIdx}" value="${personType}">
<input type="hidden" name="sub_person_index_${subIdx}" value="${personIndex}">
<input type="hidden" name="sub_person_name_${subIdx}" value="${personName}">
</td>
<td style="padding:6px;"><input type="text" name="sub_year_${subIdx}" value="${year}" placeholder="2023/2024" style="width:90px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;direction:ltr;"></td> <td style="padding:6px;"><input type="text" name="sub_year_${subIdx}" value="${year}" placeholder="2023/2024" style="width:90px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;direction:ltr;"></td>
<td style="padding:6px;"><input type="number" name="sub_base_${subIdx}" value="${baseAmt}" step="0.01" style="width:70px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></td> <td style="padding:6px;"><input type="number" name="sub_base_${subIdx}" value="${baseAmt}" step="0.01" style="width:70px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></td>
<td style="padding:6px;"><input type="number" name="sub_dev_fee_${subIdx}" value="${devFee}" step="0.01" style="width:60px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></td> <td style="padding:6px;"><input type="number" name="sub_dev_fee_${subIdx}" value="${devFee}" step="0.01" style="width:60px;padding:4px 6px;border:1px solid #D1D5DB;border-radius:4px;font-size:12px;"></td>
...@@ -805,12 +874,62 @@ function generateSubscriptionYears() { ...@@ -805,12 +874,62 @@ function generateSubscriptionYears() {
const joinYear = parseInt(joinDate.split('-')[0]); const joinYear = parseInt(joinDate.split('-')[0]);
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const memberName = document.querySelector('[name=full_name_ar]').value || 'العضو';
// Member subscriptions
for (let y = joinYear; y <= currentYear; y++) { for (let y = joinYear; y <= currentYear; y++) {
const fy = y + '/' + (y + 1); const fy = y + '/' + (y + 1);
const isPast = y < currentYear - 1; const isPast = y < currentYear - 1;
const status = isPast ? 'paid' : (y === currentYear - 1 ? 'overdue' : 'pending'); const status = isPast ? 'paid' : (y === currentYear - 1 ? 'overdue' : 'pending');
addSubscriptionRow(fy, status, '492.00', '35.00'); addSubscriptionRow(fy, status, '492.00', '35.00', 'member', 0, memberName);
}
// Spouse subscriptions
const spouseCount = parseInt(document.getElementById('spouseCount').value) || 0;
for (let s = 1; s <= spouseCount; s++) {
const spouseNameEl = document.querySelector(`[name=spouse_name_${s}]`);
if (!spouseNameEl || !spouseNameEl.value) continue;
const spouseName = spouseNameEl.value || ('زوجة #' + s);
const spouseJoinEl = document.querySelector(`[name=spouse_join_date_${s}]`);
const spouseJoinYear = spouseJoinEl && spouseJoinEl.value ? parseInt(spouseJoinEl.value.split('-')[0]) : joinYear;
for (let y = spouseJoinYear; y <= currentYear; y++) {
const fy = y + '/' + (y + 1);
const isPast = y < currentYear - 1;
const status = isPast ? 'paid' : (y === currentYear - 1 ? 'overdue' : 'pending');
addSubscriptionRow(fy, status, '492.00', '35.00', 'spouse', s, spouseName);
}
}
// Child subscriptions
const childCount = parseInt(document.getElementById('childCount').value) || 0;
for (let c = 1; c <= childCount; c++) {
const childNameEl = document.querySelector(`[name=child_name_${c}]`);
if (!childNameEl || !childNameEl.value) continue;
const childName = childNameEl.value || ('ابن #' + c);
const childJoinEl = document.querySelector(`[name=child_join_date_${c}]`);
const childJoinYear = childJoinEl && childJoinEl.value ? parseInt(childJoinEl.value.split('-')[0]) : joinYear;
for (let y = childJoinYear; y <= currentYear; y++) {
const fy = y + '/' + (y + 1);
const isPast = y < currentYear - 1;
const status = isPast ? 'paid' : (y === currentYear - 1 ? 'overdue' : 'pending');
addSubscriptionRow(fy, status, '222.00', '35.00', 'child', c, childName);
}
}
// Temporary member subscriptions
const tempCount = parseInt(document.getElementById('tempMemberCount').value) || 0;
for (let t = 1; t <= tempCount; t++) {
const tempNameEl = document.querySelector(`[name=temp_name_${t}]`);
if (!tempNameEl || !tempNameEl.value) continue;
const tempName = tempNameEl.value || ('مؤقت #' + t);
const tempJoinEl = document.querySelector(`[name=temp_join_date_${t}]`);
const tempJoinYear = tempJoinEl && tempJoinEl.value ? parseInt(tempJoinEl.value.split('-')[0]) : joinYear;
for (let y = tempJoinYear; y <= currentYear; y++) {
const fy = y + '/' + (y + 1);
const isPast = y < currentYear - 1;
const status = isPast ? 'paid' : (y === currentYear - 1 ? 'overdue' : 'pending');
addSubscriptionRow(fy, status, '222.00', '35.00', 'temporary', t, tempName);
}
} }
} }
...@@ -873,6 +992,7 @@ function updateFinalSummary() { ...@@ -873,6 +992,7 @@ function updateFinalSummary() {
const methodLabels = {cash:'نقدي كامل',check:'شيك',visa:'فيزا',bank_transfer:'تحويل بنكي',installment:'تقسيط'}; const methodLabels = {cash:'نقدي كامل',check:'شيك',visa:'فيزا',bank_transfer:'تحويل بنكي',installment:'تقسيط'};
const spouses = parseInt(document.getElementById('spouseCount').value) || 0; const spouses = parseInt(document.getElementById('spouseCount').value) || 0;
const children = parseInt(document.getElementById('childCount').value) || 0; const children = parseInt(document.getElementById('childCount').value) || 0;
const temps = parseInt(document.getElementById('tempMemberCount').value) || 0;
const subs = parseInt(document.getElementById('subscriptionCount').value) || 0; const subs = parseInt(document.getElementById('subscriptionCount').value) || 0;
const violations = parseInt(document.getElementById('violationCount').value) || 0; const violations = parseInt(document.getElementById('violationCount').value) || 0;
...@@ -884,6 +1004,7 @@ function updateFinalSummary() { ...@@ -884,6 +1004,7 @@ function updateFinalSummary() {
<div><strong>طريقة السداد:</strong> ${methodLabels[method] || method}</div> <div><strong>طريقة السداد:</strong> ${methodLabels[method] || method}</div>
<div><strong>الزوجات:</strong> ${spouses}</div> <div><strong>الزوجات:</strong> ${spouses}</div>
<div><strong>الأبناء:</strong> ${children}</div> <div><strong>الأبناء:</strong> ${children}</div>
<div><strong>أعضاء مؤقتون:</strong> ${temps}</div>
<div><strong>اشتراكات سنوية:</strong> ${subs} سنة</div> <div><strong>اشتراكات سنوية:</strong> ${subs} سنة</div>
<div><strong>مخالفات:</strong> ${violations}</div> <div><strong>مخالفات:</strong> ${violations}</div>
</div>`; </div>`;
......
...@@ -590,12 +590,13 @@ class TutorialController extends Controller ...@@ -590,12 +590,13 @@ class TutorialController extends Controller
$tmpOutput = tempnam(sys_get_temp_dir(), 'pdf_out_') . '.pdf'; $tmpOutput = tempnam(sys_get_temp_dir(), 'pdf_out_') . '.pdf';
file_put_contents($tmpInput, $html); file_put_contents($tmpInput, $html);
$cmd = escapeshellarg($wkhtmltopdf) $cmd = 'xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" '
. escapeshellarg($wkhtmltopdf)
. ' --encoding utf-8 --page-size A4' . ' --encoding utf-8 --page-size A4'
. ' --margin-top 15 --margin-bottom 15 --margin-left 12 --margin-right 12' . ' --margin-top 15 --margin-bottom 15 --margin-left 12 --margin-right 12'
. ' --disable-local-file-access' . ' --enable-local-file-access'
. ' --disable-javascript'
. ' --no-stop-slow-scripts' . ' --no-stop-slow-scripts'
. ' --javascript-delay 100'
. ' --image-quality 70' . ' --image-quality 70'
. ' --image-dpi 150' . ' --image-dpi 150'
. ' --print-media-type' . ' --print-media-type'
...@@ -604,6 +605,11 @@ class TutorialController extends Controller ...@@ -604,6 +605,11 @@ class TutorialController extends Controller
. ' 2>&1'; . ' 2>&1';
exec($cmd, $output, $returnCode); exec($cmd, $output, $returnCode);
if ($returnCode > 1) {
error_log('wkhtmltopdf failed (code ' . $returnCode . '): ' . implode("\n", $output));
}
@unlink($tmpInput); @unlink($tmpInput);
if (($returnCode === 0 || $returnCode === 1) && file_exists($tmpOutput) && filesize($tmpOutput) > 0) { if (($returnCode === 0 || $returnCode === 1) && file_exists($tmpOutput) && filesize($tmpOutput) > 0) {
...@@ -722,70 +728,22 @@ class TutorialController extends Controller ...@@ -722,70 +728,22 @@ class TutorialController extends Controller
if (!file_exists($path)) { if (!file_exists($path)) {
return $m[0]; return $m[0];
} }
$resized = $this->resizeForPdf($path); $raw = file_get_contents($path);
if ($raw === false) {
return $m[0];
}
$mime = match (strtolower($m[2])) { $mime = match (strtolower($m[2])) {
'jpg', 'jpeg' => 'image/jpeg', 'jpg', 'jpeg' => 'image/jpeg',
'gif' => 'image/gif', 'gif' => 'image/gif',
'webp' => 'image/webp', 'webp' => 'image/webp',
default => 'image/png', default => 'image/png',
}; };
return 'src="data:' . $mime . ';base64,' . base64_encode($resized) . '"'; return 'src="data:' . $mime . ';base64,' . base64_encode($raw) . '"';
}, },
$html $html
); );
} }
private function resizeForPdf(string $path): string
{
$info = @getimagesize($path);
if (!$info) {
return (string) file_get_contents($path);
}
[$origW, $origH, $type] = $info;
$maxW = 1200;
if ($origW <= $maxW) {
$img = match ($type) {
IMAGETYPE_PNG => @imagecreatefrompng($path),
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
IMAGETYPE_GIF => @imagecreatefromgif($path),
IMAGETYPE_WEBP => @imagecreatefromwebp($path),
default => null,
};
if (!$img) {
return (string) file_get_contents($path);
}
ob_start();
imagejpeg($img, null, 65);
imagedestroy($img);
return (string) ob_get_clean();
}
$newW = $maxW;
$newH = (int) round($origH * ($maxW / $origW));
$src = match ($type) {
IMAGETYPE_PNG => @imagecreatefrompng($path),
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
IMAGETYPE_GIF => @imagecreatefromgif($path),
IMAGETYPE_WEBP => @imagecreatefromwebp($path),
default => null,
};
if (!$src) {
return (string) file_get_contents($path);
}
$dst = imagecreatetruecolor($newW, $newH);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $origW, $origH);
imagedestroy($src);
ob_start();
imagejpeg($dst, null, 65);
imagedestroy($dst);
return (string) ob_get_clean();
}
private function extractTutorialContent(string $raw): string private function extractTutorialContent(string $raw): string
{ {
$start = strpos($raw, '<div class="tut-page">'); $start = strpos($raw, '<div class="tut-page">');
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
upload_max_filesize = 20M upload_max_filesize = 20M
post_max_size = 25M post_max_size = 25M
memory_limit = 1024M memory_limit = 2048M
max_execution_time = 1800 max_execution_time = 1800
max_input_time = 240 max_input_time = 240
max_input_vars = 5000 max_input_vars = 5000
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment