| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__).'/includes/functions.php';
$user=require_tips_manager(); ensure_schedule_schema(); ensure_tips_schema();
$date=trim((string)($_POST['tip_date']??date('Y-m-d')));$shift='all_day';
if($_SERVER['REQUEST_METHOD']==='POST'){
verify_csrf();
try{
$cash=tips_parse_amount((string)($_POST['cash_amount']??'0'));$card=tips_parse_amount((string)($_POST['card_amount']??'0'));$other=tips_parse_amount((string)($_POST['other_amount']??'0'));$note=trim((string)($_POST['note']??''));
$upload=$_FILES['photo']??null;if(!$upload||(int)($upload['error']??UPLOAD_ERR_NO_FILE)!==UPLOAD_ERR_OK)throw new RuntimeException('Zrób lub wybierz zdjęcie rozliczenia.');
$tmp=(string)$upload['tmp_name'];$size=(int)($upload['size']??0);if(!is_uploaded_file($tmp))throw new RuntimeException('Nieprawidłowy upload.');if($size>12*1024*1024)throw new RuntimeException('Zdjęcie jest za duże (maks. 12 MB).');
$finfo=new finfo(FILEINFO_MIME_TYPE);$mime=(string)$finfo->file($tmp);$allowed=['image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp'];if(!isset($allowed[$mime]))throw new RuntimeException('Obsługiwane zdjęcia: JPG, PNG, WEBP.');
$dir=dirname(__DIR__).'/storage/tips-photos/'.date('Y/m');if(!is_dir($dir)&&!mkdir($dir,0700,true)&&!is_dir($dir))throw new RuntimeException('Nie udało się utworzyć katalogu zdjęć.');
$filename=bin2hex(random_bytes(12)).'.'.$allowed[$mime];$target=$dir.'/'.$filename;if(!move_uploaded_file($tmp,$target))throw new RuntimeException('Nie udało się zapisać zdjęcia.');@chmod($target,0600);
$relative=str_replace(dirname(__DIR__).'/','',$target);$ids=schedule_user_ids_for_date($date);if(!$ids){@unlink($target);throw new RuntimeException('Najpierw ustaw obsadę tego dnia w grafiku.');}
$existing=tip_pool_by_date($date,$shift);$reason=$existing?'Aktualizacja na podstawie zdjęcia rozliczenia':'';
$pool=tip_pool_save($date,$cash,$card,$other,$note,$ids,(int)$user['id'],$reason,$shift,['type'=>'photo','file'=>$relative,'original_name'=>(string)($upload['name']??'zdjecie')]);
flash('success','Rozliczenie ze zdjęcia zapisane.');redirect('/admin/tips.php?month='.substr($date,0,7).'&edit='.(int)$pool['id']);
}catch(Throwable $e){flash('error',$e->getMessage());}
}
render_admin_header('Napiwki ze zdjęcia'); ?>
<div class="admin-title-row"><div><h1>Rozliczenie ze zdjęcia</h1><p>Zrób zdjęcie raportu. OCR może podpowiedzieć kwoty, ale przed zapisem zawsze je sprawdź.</p></div><a class="btn btn-light" href="/admin/tips.php">← Napiwki</a></div>
<form class="form-card form-grid tips-photo-form" method="post" enctype="multipart/form-data"><?= csrf_field() ?>
<label>Data<input type="date" name="tip_date" value="<?= e($date) ?>" required></label>
<label class="full tips-photo-capture">Zdjęcie<input id="tips-photo-input" type="file" name="photo" accept="image/jpeg,image/png,image/webp" capture="environment" required><img id="tips-photo-preview" alt="Podgląd zdjęcia" hidden></label><div class="full actions"><button class="btn btn-light" type="button" id="tips-ocr-btn">Odczytaj kwoty ze zdjęcia (OCR)</button><span id="tips-ocr-status" class="muted"></span></div><label class="full" id="tips-ocr-wrap" hidden>Tekst rozpoznany<textarea id="tips-ocr-text" rows="6" readonly></textarea></label>
<label>Gotówka<input name="cash_amount" inputmode="decimal" placeholder="0,00"></label><label>Karta / terminal<input name="card_amount" inputmode="decimal" placeholder="0,00"></label><label>Inne<input name="other_amount" inputmode="decimal" placeholder="0,00"></label><label class="full">Notatka<textarea name="note" rows="2"></textarea></label><div class="full"><button class="btn btn-primary">Zapisz rozliczenie</button></div></form>
<script>
const photoInput=document.getElementById('tips-photo-input');
photoInput?.addEventListener('change',e=>{const f=e.target.files?.[0],img=document.getElementById('tips-photo-preview');if(!f||!img)return;img.src=URL.createObjectURL(f);img.hidden=false});
const parseAmount=line=>{const matches=[...String(line).matchAll(/(\d{1,6}(?:[\s.]\d{3})*[,.]\d{2}|\d{1,6})/g)];if(!matches.length)return null;const raw=matches[matches.length-1][1].replace(/\s/g,'').replace(/\.(?=\d{3}(?:\D|$))/g,'').replace(',','.');const n=parseFloat(raw);return Number.isFinite(n)?n:null};
const money=v=>v==null?'':v.toFixed(2).replace('.',',');
const fillFromText=text=>{const lines=text.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);let cash=null,card=null,other=null,tip=null;for(const line of lines){const low=line.toLowerCase();const val=parseAmount(line);if(val==null)continue;if(/got[oó]wk|cash/.test(low))cash=val;else if(/karta|terminal|card/.test(low))card=val;else if(/inne|other/.test(low))other=val;else if(/napiw|tip/.test(low))tip=val;}const form=document.querySelector('.tips-photo-form');if(cash!=null)form.cash_amount.value=money(cash);if(card!=null)form.card_amount.value=money(card);else if(tip!=null)form.card_amount.value=money(tip);if(other!=null)form.other_amount.value=money(other);};
const loadTesseract=()=>new Promise((resolve,reject)=>{if(window.Tesseract)return resolve();const s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js';s.onload=resolve;s.onerror=()=>reject(new Error('Nie udało się pobrać modułu OCR.'));document.head.appendChild(s)});
document.getElementById('tips-ocr-btn')?.addEventListener('click',async()=>{const f=photoInput?.files?.[0],status=document.getElementById('tips-ocr-status'),wrap=document.getElementById('tips-ocr-wrap'),out=document.getElementById('tips-ocr-text');if(!f){status.textContent='Najpierw zrób lub wybierz zdjęcie.';return;}try{status.textContent='Ładowanie OCR…';await loadTesseract();status.textContent='Czytam zdjęcie…';const result=await Tesseract.recognize(f,'pol+eng',{logger:m=>{if(m.status==='recognizing text')status.textContent='OCR '+Math.round((m.progress||0)*100)+'%';}});const text=result?.data?.text||'';out.value=text;wrap.hidden=false;fillFromText(text);status.textContent='Gotowe. Sprawdź podpowiedziane kwoty przed zapisem.';}catch(e){status.textContent='OCR niedostępny. Wpisz kwoty ręcznie.';}});
</script>
<?php render_admin_footer(); ?>