Zaktualizuj funkcję wysyłania e-maili, aby obsługiwała wiele załączników oraz dodaj walidację plików

This commit is contained in:
2026-02-08 22:07:22 +01:00
parent cf05060d01
commit 5f1dbd9e19
2 changed files with 381 additions and 52 deletions

View File

@@ -3,7 +3,7 @@ session_start();
require_once 'phpmailer/class.phpmailer.php';
require_once 'phpmailer/class.smtp.php';
function send_email( $email, $reply, $subject, $text, $attachment = null )
function send_email( $email, $reply, $subject, $text, $attachments = array() )
{
$mail = new PHPMailer;
$mail -> IsSMTP();
@@ -28,16 +28,87 @@ function send_email( $email, $reply, $subject, $text, $attachment = null )
$mail -> Subject = $subject;
$mail -> Body = $text;
if ($attachment && isset($attachment['tmp_name']) && file_exists($attachment['tmp_name'])) {
$mail->addAttachment($attachment['tmp_name'], $attachment['name']);
// Obsługa wielu załączników
if (is_array($attachments) && count($attachments) > 0) {
foreach ($attachments as $attachment) {
if (isset($attachment['tmp_name']) && file_exists($attachment['tmp_name'])) {
$mail->addAttachment($attachment['tmp_name'], $attachment['name']);
}
}
}
return $mail -> send();
}
function validate_file($file) {
$maxSize = 10 * 1024 * 1024; // 10MB
$allowedExtensions = array('jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx', 'xls', 'xlsx');
$allowedMimeTypes = array(
'image/jpeg',
'image/jpg',
'image/png',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
// Sprawdź czy plik istnieje
if (!isset($file['tmp_name']) || !file_exists($file['tmp_name'])) {
return array('valid' => false, 'error' => 'Plik nie istnieje');
}
// Sprawdź rozmiar
if ($file['size'] > $maxSize) {
return array('valid' => false, 'error' => 'Plik jest za duży (max 10MB)');
}
// Sprawdź rozszerzenie
$fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($fileExtension, $allowedExtensions)) {
return array('valid' => false, 'error' => 'Niedozwolone rozszerzenie pliku');
}
// Sprawdź MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedMimeTypes)) {
return array('valid' => false, 'error' => 'Niedozwolony typ pliku');
}
return array('valid' => true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$configData = json_decode($_POST['configData'], true);
$file = isset($_FILES['attachment']) ? $_FILES['attachment'] : null;
// Pobierz wszystkie załączniki
$attachments = array();
$attachments_count = isset($_POST['attachments_count']) ? intval($_POST['attachments_count']) : 0;
// Walidacja liczby załączników (max 10)
if ($attachments_count > 10) {
echo json_encode(['status' => 'error', 'message' => 'Maksymalnie 10 załączników']);
exit();
}
for ($i = 0; $i < $attachments_count; $i++) {
$fileKey = 'attachment_' . $i;
if (isset($_FILES[$fileKey])) {
$file = $_FILES[$fileKey];
// Walidacja pliku
$validation = validate_file($file);
if (!$validation['valid']) {
echo json_encode(['status' => 'error', 'message' => 'Błąd walidacji pliku: ' . $validation['error']]);
exit();
}
$attachments[] = $file;
}
}
$to = 'kontakt@ostal.pl';
$subject = 'ostal.pl - Konfigurator';
@@ -94,7 +165,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message .= '</ul>';
}
if (send_email($to, $configData['step_4']['email'], $subject, $message, $file)) {
// Dodaj informację o załącznikach
if (count($attachments) > 0) {
$message .= '<p>Załączniki (' . count($attachments) . '):</p>';
$message .= '<ul>';
foreach ($attachments as $attachment) {
$fileSize = round($attachment['size'] / 1024, 2); // KB
$message .= '<li>' . htmlspecialchars($attachment['name']) . ' (' . $fileSize . ' KB)</li>';
}
$message .= '</ul>';
}
if (send_email($to, $configData['step_4']['email'], $subject, $message, $attachments)) {
$_SESSION["configurator_sended"] = true;
echo json_encode(['status' => 'ok', 'message' => 'Wiadmość wysłana']);
} else {

View File

@@ -112,47 +112,6 @@
<p v-if="selected_option_1 === 0" class="budget-info" style="margin-top: 20px; font-size: 14px; color: #666; font-style: italic;">
Orientacyjny budżet całorocznego ogrodu zimowego zwykle mieści się w przedziale 120 000 180 000 zł netto
</p>
<div class="tabs_examples" v-if="selected_option_1 === 0">
<h3>Przykłady cen ogrodu zimowego 4x5m</h3>
<div class="tabs_examples-nav">
<button :class="{ active: activeExampleTab === 'standard' }" @click="activeExampleTab = 'standard'">Standard</button>
<button :class="{ active: activeExampleTab === 'comfort' }" @click="activeExampleTab = 'comfort'">Comfort</button>
<button :class="{ active: activeExampleTab === 'premium' }" @click="activeExampleTab = 'premium'">Premium</button>
</div>
<div class="tabs_examples-content">
<!-- Standard -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'standard'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-1.png" alt="Ogród zimowy Standard 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Standard</h4>
<p>Ogród zimowy standard z szybami Ug=1,1 na dachu i ścianach i z jednymi drzwiami tarasowymi uchylno-przesuwnymi PSK na frontowej ścianie 2,5x2,2m. Bez osłon przeciwsłonecznych (nadaje się w zacienione miejsca).</p>
</div>
</div>
<!-- Comfort -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'comfort'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-2.png" alt="Ogród zimowy Comfort 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Comfort</h4>
<p>Ogród zimowy Comfort z szybami Ug=1,1 na dachu i ścianach i z dwoma drzwiami tarasowymi uchylno-przesuwnymi PSK na frontowej ścianie 2,5x2,2m + boczna ściana 3 kwatery 1 skrzydło PSK + osłona przeciwsłoneczna dachu, 2 osłony Veranda ster. elektr.</p>
</div>
</div>
<!-- Premium -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'premium'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-3.png" alt="Ogród zimowy Premium 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Premium</h4>
<p>Ogród zimowy premium z szybami energooszczędnymi Ug=0,5 na dachu i ścianach i z dwoma drzwiami tarasowymi. 1 szt. uchylno-przesuwnymi PSK na frontowej ścianie 2,5x2,2m + 1 szt. drzwi HS na bocznej ścianie 1 skrzydło + komplet osłon przeciwsłonecznych na dachu, 2 osłony Veranda ster. elektr. z czujnikiem słońce wiatr + 1 szt. osłony przeciwsłoneczne żaluzje C80 na bocznych ścianach ster. el. + okno HS w dachu przesuwane, sterowane el. + oświetlenie punktowe LED z pilotem. Montaż na co drugiej krokwi.</p>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -227,6 +186,56 @@
</select>
</li>
</ul>
<div class="tabs_examples" v-if="selected_option_1 === 0" style="margin-top: 50px;">
<h3>Przykłady cen ogrodu zimowego 4x5m</h3>
<div class="tabs_examples-nav">
<button :class="{ active: activeExampleTab === 'standard' }" @click="activeExampleTab = 'standard'">Standard</button>
<button :class="{ active: activeExampleTab === 'comfort' }" @click="activeExampleTab = 'comfort'">Comfort</button>
<button :class="{ active: activeExampleTab === 'premium' }" @click="activeExampleTab = 'premium'">Premium</button>
</div>
<div class="tabs_examples-content">
<!-- Standard -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'standard'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-1.png" alt="Ogród zimowy Standard 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Classic 1 Standard.<br>Cena brutto 114.390,00 zł (z VAT 8%)</h4>
<p>- Szyby izolacyjne Ug=1,1 na dachu i ścianach.<br>
- 1 szt. drzwi tarasowe uchylno-przesuwnymi PSK na frontowej ścianie.<br>
- Bez osłon przeciwsłonecznych. (w zacienione miejsca)</p>
</div>
</div>
<!-- Comfort -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'comfort'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-2.png" alt="Ogród zimowy Comfort 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Classic 2 Comfort.<br>Cena brutto 142.819,00 zł (z VAT 8%)</h4>
<p>- Szyby izolacyjne Ug=1,1 na dachu i ścianach.<br>
- 2 szt. drzwi tarasowych uchylno-przesuwnymi PSK na ścianie frontowej i ścianie bocznej.<br>
- Komplet osłon przeciwsłonecznych na dachu. 2 osłony Veranda ster. Elektr + pilot.</p>
</div>
</div>
<!-- Premium -->
<div class="tabs_examples-panel" v-if="activeExampleTab === 'premium'">
<div class="tabs_examples-img">
<img src="/wp-content/uploads/2026/02/image-3.png" alt="Ogród zimowy Premium 4x5m">
</div>
<div class="tabs_examples-desc">
<h4>Ogród zimowy Classic 3 Premium.<br>Cena brutto 203.550,00 zł (z VAT 8%)</h4>
<p>- Szyby Energooszczędne Ug=0,5 na dachu i ścianach.<br>
- 1 szt. drzwi tarasowych uchylno-przesuwnymi PSK na frontowej ścianie + 1 szt. drzwi unoszono przesuwne HS na bocznej ścianie.<br>
- Komplet osłon przeciwsłonecznych na dachu. 2 osłony Veranda ster. Elektr. + pilot + czujnik słoneczno-wiatrowy.<br>
- Osłony przeciwsłoneczne ścian żaluzje C80 sterowane ele. + pilot.<br>
- Okno HS w dachu przesuwane, sterowane ele.<br>
- oświetlenie punktowe LED z pilotem.</p>
</div>
</div>
</div>
</div>
</div>
<div v-if="windowWidth > 1000">
@@ -292,7 +301,46 @@
</li>
<li>
<p>{{acfData.acf.step_4.attachment}}</p>
<input type="file" ref="fileInput" name="attachment" id="calc_attachment" class="input">
<div class="file-upload-area"
:class="{ 'dragover': isDragOver }"
@dragover.prevent="isDragOver = true"
@dragleave.prevent="isDragOver = false"
@drop.prevent="handleFileDrop">
<input type="file"
ref="fileInput"
name="attachment"
id="calc_attachment"
class="file-input-hidden"
multiple
accept=".jpg,.jpeg,.png,.pdf,.doc,.docx,.xls,.xlsx"
@change="handleFileSelect">
<div class="file-upload-content" @click="$refs.fileInput.click()">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#88b14b" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<p class="upload-text">Przeciągnij pliki tutaj lub kliknij aby wybrać</p>
<p class="upload-hint">Dozwolone: JPG, PNG, PDF, DOC, DOCX, XLS, XLSX (max 10MB każdy)</p>
</div>
</div>
<div v-if="uploadedFiles.length > 0" class="uploaded-files-list">
<div v-for="(file, index) in uploadedFiles" :key="index" class="uploaded-file-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#88b14b" stroke-width="2">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
<polyline points="13 2 13 9 20 9"></polyline>
</svg>
<span class="file-name">{{ file.name }}</span>
<span class="file-size">({{ formatFileSize(file.size) }})</span>
<button type="button" class="remove-file-btn" @click="removeFile(index)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#dc3545" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<p v-if="fileError" class="file-error">{{ fileError }}</p>
</li>
</ul>
@@ -451,6 +499,9 @@
showDatepicker: false,
dpMonth: new Date().getMonth(),
dpYear: new Date().getFullYear(),
uploadedFiles: [],
isDragOver: false,
fileError: '',
acfData: {
acf: {}
@@ -557,10 +608,11 @@
formData.append('configData', JSON.stringify(this.configData));
const file = this.$refs.fileInput?.files[0];
if (file) {
formData.append('attachment', file);
}
// Dodaj wszystkie załączone pliki
this.uploadedFiles.forEach((file, index) => {
formData.append(`attachment_${index}`, file);
});
formData.append('attachments_count', this.uploadedFiles.length);
// const data = {
// configData: this.configData,
// };
@@ -641,6 +693,8 @@
this.selected_option_1 = null
this.selected_option_3 = null
this.uploadedFiles = []
this.fileError = ''
while (stack.length > 0) {
const obj = stack.pop();
@@ -696,6 +750,82 @@
const now = new Date();
return day === now.getDate() && this.dpMonth === now.getMonth() && this.dpYear === now.getFullYear();
},
handleFileDrop(e) {
this.isDragOver = false;
const files = Array.from(e.dataTransfer.files);
this.addFiles(files);
},
handleFileSelect(e) {
const files = Array.from(e.target.files);
this.addFiles(files);
},
addFiles(files) {
this.fileError = '';
for (const file of files) {
const validation = this.validateFile(file);
if (!validation.valid) {
this.fileError = validation.error;
continue;
}
// Sprawdź czy plik już nie został dodany
const exists = this.uploadedFiles.some(f => f.name === file.name && f.size === file.size);
if (!exists) {
this.uploadedFiles.push(file);
}
}
// Wyczyść input aby móc dodać ten sam plik ponownie
if (this.$refs.fileInput) {
this.$refs.fileInput.value = '';
}
},
validateFile(file) {
const maxSize = 10 * 1024 * 1024; // 10MB
const allowedTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
];
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.pdf', '.doc', '.docx', '.xls', '.xlsx'];
// Sprawdź rozszerzenie pliku
const fileName = file.name.toLowerCase();
const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension && !allowedTypes.includes(file.type)) {
return {
valid: false,
error: `Nieprawidłowy typ pliku: ${file.name}. Dozwolone: JPG, PNG, PDF, DOC, DOCX, XLS, XLSX`
};
}
if (file.size > maxSize) {
return {
valid: false,
error: `Plik ${file.name} jest za duży. Maksymalny rozmiar: 10MB`
};
}
return { valid: true };
},
removeFile(index) {
this.uploadedFiles.splice(index, 1);
this.fileError = '';
},
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
},
getWindowWidth() {
this.windowWidth = window.innerWidth;
},
@@ -742,4 +872,121 @@
app.mount('#app')
</script>
<style>
.file-upload-area {
border: 2px dashed #88b14b;
border-radius: 8px;
padding: 30px;
text-align: center;
background-color: #f8f9fa;
transition: all 0.3s ease;
cursor: pointer;
margin-top: 10px;
}
.file-upload-area:hover {
background-color: #e8f5e9;
border-color: #6a8f3a;
}
.file-upload-area.dragover {
background-color: #e8f5e9;
border-color: #6a8f3a;
transform: scale(1.02);
}
.file-input-hidden {
display: none;
}
.file-upload-content {
cursor: pointer;
}
.file-upload-content svg {
margin: 0 auto 15px;
display: block;
}
.upload-text {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 10px 0 5px;
}
.upload-hint {
font-size: 12px;
color: #666;
margin: 0;
}
.uploaded-files-list {
margin-top: 20px;
}
.uploaded-file-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 8px;
transition: all 0.2s ease;
}
.uploaded-file-item:hover {
background-color: #f8f9fa;
border-color: #88b14b;
}
.uploaded-file-item svg:first-child {
flex-shrink: 0;
}
.file-name {
flex: 1;
font-size: 14px;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
font-size: 12px;
color: #666;
flex-shrink: 0;
}
.remove-file-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s ease;
flex-shrink: 0;
}
.remove-file-btn:hover {
background-color: #fee;
}
.file-error {
color: #dc3545;
font-size: 13px;
margin-top: 10px;
padding: 8px 12px;
background-color: #fee;
border-radius: 4px;
border-left: 3px solid #dc3545;
}
</style>
<?php get_footer(); ?>