| 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);
/** POLANA V17.3 — Google Calendar: one connected account, admin-only. */
require_once dirname(__DIR__) . '/includes/functions.php';
require_once dirname(__DIR__) . '/includes/polana_gcal.php';
require_admin_role();
header('Cache-Control: private, no-store, max-age=0');
header('X-Robots-Tag: noindex,nofollow');
header('Referrer-Policy: no-referrer');
function pc173_h($s): string { return htmlspecialchars((string)$s,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'); }
try { pg173_ensure_schema(); }
catch (Throwable $e) { render_admin_header('Kalendarz Google'); echo '<section class="card"><h1>Błąd bazy kalendarza</h1><p>'.pc173_h($e->getMessage()).'</p><a href="/admin/polana-v173-instalator.php">Otwórz instalator</a></section>';render_admin_footer();exit; }
if (isset($_GET['code']) || isset($_GET['error'])) {
start_session();
$state=(string)($_GET['state']??'');
$expected=(string)($_SESSION['pg173_oauth_state']??'');
$age=time()-(int)($_SESSION['pg173_oauth_started']??0);
unset($_SESSION['pg173_oauth_state'],$_SESSION['pg173_oauth_started']);
try {
if (!$expected || !$state || !hash_equals($expected,$state) || $age<0 || $age>600)throw new RuntimeException('Sesja połączenia z Google wygasła. Spróbuj ponownie.');
if (isset($_GET['error'])) throw new RuntimeException('Google nie przyznał dostępu: '.substr((string)$_GET['error'],0,100));
$code=(string)($_GET['code']??'');
if ($code==='') throw new RuntimeException('Google nie zwrócił kodu połączenia.');
$secret=pg173_read_secrets();
if (empty($secret['client_id']) || empty($secret['client_secret']))throw new RuntimeException('Brak konfiguracji Google OAuth.');
$token=pg173_oauth_token_request([
'code'=>$code,'client_id'=>$secret['client_id'],'client_secret'=>$secret['client_secret'],
'redirect_uri'=>pg173_callback_url(),'grant_type'=>'authorization_code'
]);
$secret['access_token']=(string)$token['access_token'];
$secret['expires_at']=time()+max(60,(int)($token['expires_in']??3600));
if (!empty($token['refresh_token']))$secret['refresh_token']=(string)$token['refresh_token'];
if (empty($secret['refresh_token']))throw new RuntimeException('Brak refresh_token — w Google Cloud dodaj konto testowe i ponów zgodę.');
pg173_write_secrets($secret);
admin_log('calendar_oauth','Połączono Kalendarz Google, bez zapisu tokenu w logach');
flash('success','Konto Google połączone. Teraz zsynchronizuj zatwierdzone rezerwacje.');
} catch(Throwable $e) {flash('error','Połączenie Google nie powiodło się: '.$e->getMessage());}
redirect('/admin/polana-kalendarz.php');
}
if (($_SERVER['REQUEST_METHOD']??'GET')==='POST') {
verify_csrf();
$action=(string)($_POST['action']??'');
try {
if ($action==='save_oauth') {
$id=trim((string)($_POST['client_id']??''));$secret=trim((string)($_POST['client_secret']??''));
if (strlen($id)>260 || strlen($secret)>500 || !str_ends_with($id,'.apps.googleusercontent.com') || strlen($secret)<8)throw new RuntimeException('Podaj poprawny Client ID i Client Secret z Google Cloud.');
$data=pg173_read_secrets();
// Changing OAuth credentials invalidates the previous account's refresh token.
if (($data['client_id']??'')!==$id || ($data['client_secret']??'')!==$secret) {
unset($data['refresh_token'],$data['access_token'],$data['expires_at']);
}
$data['client_id']=$id;$data['client_secret']=$secret;
pg173_write_secrets($data);
flash('success','Zapisano klienta OAuth w zaszyfrowanym pliku poza WWW. Kliknij „Połącz konto Google”.');
} elseif ($action==='connect') {
$data=pg173_read_secrets();
if (empty($data['client_id'])||empty($data['client_secret']))throw new RuntimeException('Najpierw zapisz Client ID i Client Secret.');
start_session();
$_SESSION['pg173_oauth_state']=bin2hex(random_bytes(32));
$_SESSION['pg173_oauth_started']=time();
$params=['client_id'=>(string)$data['client_id'],'redirect_uri'=>pg173_callback_url(),
'response_type'=>'code','scope'=>'https://www.googleapis.com/auth/calendar.events',
'access_type'=>'offline','prompt'=>'consent','state'=>$_SESSION['pg173_oauth_state']];
redirect('https://accounts.google.com/o/oauth2/v2/auth?'.http_build_query($params,'','&',PHP_QUERY_RFC3986));
} elseif ($action==='save_settings') {
$calendar=trim((string)($_POST['calendar_id']??'primary'));
if ($calendar===''||strlen($calendar)>250||preg_match('/[\x00-\x1f<>]/',$calendar))throw new RuntimeException('Nieprawidłowe ID kalendarza.');
$duration=(int)($_POST['duration']??90);
if ($duration<30||$duration>360)throw new RuntimeException('Długość rezerwacji od 30 do 360 minut.');
if (pg173_calendar_id()!==$calendar && empty($_POST['confirm_move']))throw new RuntimeException('Zmiana kalendarza wymaga potwierdzenia przeniesienia wydarzeń.');
pg173_upsert_setting('pg173_calendar_id',$calendar);
pg173_upsert_setting('pg173_duration_minutes',(string)$duration);
pg173_upsert_setting('pg173_include_contact',!empty($_POST['include_contact'])?'1':'0');
pg173_upsert_setting('pg173_enabled',!empty($_POST['enabled'])?'1':'0');
flash('success','Zapisano ustawienia. Jeśli zmieniłeś kalendarz lub długość wizyty, uruchom synchronizację.');
} elseif ($action==='sync_one') {
$rid=(int)($_POST['id']??0);
if($rid<1)throw new RuntimeException('Nieprawidłowa rezerwacja.');
$r=pg173_sync($rid);
flash($r['result']==='error'?'error':'success','#'.$rid.' — '.$r['message']);
} elseif ($action==='sync_batch') {
if (!pg173_connected())throw new RuntimeException('Najpierw połącz konto Google.');
$sql="SELECT r.id FROM reservations r LEFT JOIN polana_calendar_events c ON c.reservation_id=r.id WHERE (r.status='confirmed' AND (c.reservation_id IS NULL OR c.sync_status<>'synced' OR c.calendar_id<>?)) OR (c.reservation_id IS NOT NULL AND r.status IN ('cancelled','rejected','new')) ORDER BY r.reservation_date DESC,r.id DESC LIMIT 12";
$stmt=db()->prepare($sql);$stmt->execute([pg173_calendar_id()]);
$ok=0;$fail=0;
foreach($stmt->fetchAll(PDO::FETCH_COLUMN) as $rid){
$res=pg173_sync((int)$rid);
if($res['result']==='error'){$fail++;break;} // prevent long web requests while Google is unavailable
$ok++;
}
flash($fail?'error':'success','Partia zakończona: '.$ok.' OK, '.$fail.' błędów. Do 12 zaległych rezerwacji na kliknięcie; ponów, jeśli zostały kolejne.');
} elseif ($action==='disconnect') {
if (empty($_POST['confirm_disconnect']))throw new RuntimeException('Potwierdź odłączenie.');
$s=pg173_read_secrets();unset($s['refresh_token'],$s['access_token'],$s['expires_at']);pg173_write_secrets($s);
admin_log('calendar_disconnect','Odłączono Google OAuth; istniejące wydarzenia nie są kasowane.');
flash('success','Odłączono konto Google. Istniejące wydarzenia pozostają w kalendarzu.');
} else throw new RuntimeException('Nieznana operacja.');
}catch(Throwable $e){flash('error',$e->getMessage());}
redirect('/admin/polana-kalendarz.php');
}
$secretError='';$cfg=[];
try{$cfg=pg173_read_secrets();}catch(Throwable $e){$secretError=$e->getMessage();}
$connected=pg173_connected();$configured=!empty($cfg['client_id'])&&!empty($cfg['client_secret']);
$stats=['synced'=>0,'error'=>0];
try { foreach(db()->query('SELECT sync_status,COUNT(*) n FROM polana_calendar_events GROUP BY sync_status')->fetchAll(PDO::FETCH_ASSOC) as $row)$stats[$row['sync_status']]=(int)$row['n']; }catch(Throwable $e){}
$rows=[];
try {
$rows=db()->query("SELECT r.id,r.reservation_code,r.guest_name,r.reservation_date,r.reservation_time,r.guests,r.status,c.sync_status,c.last_error,c.last_synced_at FROM reservations r LEFT JOIN polana_calendar_events c ON c.reservation_id=r.id WHERE r.reservation_date >= DATE_SUB(CURDATE(), INTERVAL 10 DAY) OR c.sync_status='error' ORDER BY r.reservation_date ASC,r.reservation_time ASC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
}catch(Throwable $e){}
render_admin_header('Google Kalendarz — rezerwacje');
?>
<style>
.pg173{max-width:1320px;margin:10px auto 70px;padding:16px;color:#20382b}.pg173 *{box-sizing:border-box}.pg173 h1{font-size:clamp(25px,3vw,37px);margin:4px 0}.pg173 h2{font-size:21px}.pg173 p{line-height:1.55}.pg173 .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:15px}.pg173 .card{background:#fff;border:1px solid #d7e4da;border-radius:16px;padding:19px;margin:10px 0}.pg173 a.b,.pg173 button.b{display:inline-block;padding:11px 16px;border-radius:10px;min-height:44px;border:0;background:#216748;color:#fff;text-decoration:none;font:inherit;font-weight:750;cursor:pointer;margin:4px 4px 4px 0}.pg173 .b.alt{background:#e6f0e8;color:#235e43}.pg173 label{display:grid;gap:6px;font-weight:700;margin:12px 0}.pg173 input[type=text],.pg173 input[type=password],.pg173 input[type=number]{width:100%;padding:12px;border-radius:9px;border:1px solid #b0c5b5;font:inherit;max-width:700px}.pg173 input[type=checkbox]{width:19px;height:19px;vertical-align:middle}.pg173 label.check{display:block;font-weight:500}.pg173 .notice{background:#eef6f0;border-left:4px solid #378456;padding:12px 15px;border-radius:7px}.pg173 .warning{background:#fff1e0;border-left:4px solid #c68329;padding:12px 15px;border-radius:7px}.pg173 .pill{display:inline-block;padding:5px 9px;background:#e7f4ec;border-radius:99px;font-size:12px;font-weight:800}.pg173 .pill.off{background:#fff0dc;color:#8d511c}.pg173 table{width:100%;border-collapse:collapse}.pg173 th,.pg173 td{text-align:left;border-bottom:1px solid #e5eee7;padding:10px 8px;font-size:13px}.pg173 .scroll{overflow:auto}.pg173 code{word-break:break-all}.pg173 .muted{color:#6a7e6e}@media(max-width:760px){.pg173 .grid{grid-template-columns:1fr}.pg173 .card{padding:14px}.pg173 th,.pg173 td{padding:9px 5px}}
</style>
<div class="pg173">
<a class="b alt" href="/admin/polana-komunikacja.php">← Poczta i komunikacja</a><a class="b alt" href="/admin/reservations.php">Rezerwacje</a><a class="b alt" href="/admin/polana-newsletter.php">Newsletter</a><a class="b alt" href="/admin/polana-poczta.php">SMTP</a>
<h1>Google Kalendarz — rezerwacje na telefonach</h1>
<p>Wydarzenie powstaje <b>dopiero po kliknięciu „Potwierdź”</b> w rezerwacji. Ponowne zatwierdzenie aktualizuje to samo wydarzenie, odrzucenie lub anulowanie usuwa je z Google. Pracownicy widzą kalendarz na Androidzie i iPhonie po udostępnieniu im kalendarza Google.</p>
<div class="grid">
<section class="card"><h2>1. Stan połączenia</h2><p><span class="pill <?=$connected?'':'off'?>"><?=$connected?'Połączono z Google':'Nie połączono'?></span> <span class="pill <?=pg173_enabled()?'':'off'?>"><?=pg173_enabled()?'Synchronizacja włączona':'Synchronizacja wyłączona'?></span></p><p><b>Wydarzenia zsynchronizowane:</b> <?= (int)$stats['synced'] ?> · <b>Błędy:</b> <?= (int)$stats['error'] ?></p><p><b>Kalendarz:</b> <code><?=pc173_h(pg173_calendar_id())?></code></p><?php if($secretError):?><p class="warning"><?=pc173_h($secretError)?></p><?php endif; ?><p class="muted">Zaloguj właściciela kalendarza Google, a następnie udostępnij kalendarz kelnerom i managerowi. Nie udostępniaj go publicznie.</p></section>
<section class="card"><h2>2. Połącz konto Google</h2><p>Google Cloud Console → projekt → <b>Google Calendar API</b> (włącz) → <b>OAuth consent screen</b> → klient OAuth typu <b>Web application</b>. Przy trybie testowym dodaj swoje konto Google jako testowego użytkownika.</p><p><b>Authorized redirect URI:</b><br><code><?=pc173_h(pg173_callback_url())?></code></p>
<form method="post"><?=csrf_field()?><input type="hidden" name="action" value="save_oauth"><label>Client ID z Google Cloud<input type="text" name="client_id" autocomplete="off" value="<?=pc173_h($cfg['client_id']??'')?>" placeholder="...apps.googleusercontent.com" required></label><label>Client Secret — wpisz lub wklej<input type="password" name="client_secret" autocomplete="new-password" placeholder="<?= $configured ? 'Aby zmienić podaj nowy sekret' : 'Google OAuth client secret' ?>" required></label><button class="b" type="submit">Zapisz dane Google</button></form>
<?php if($configured):?><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="connect"><button type="submit" class="b">Połącz konto Google</button></form><?php endif; ?><p class="muted">Sekret klienta i token Google są szyfrowane w folderze prywatnym poza WWW; hasła Google nie wpisujesz w Polanie.</p></section>
</div>
<div class="grid">
<section class="card"><h2>3. Ustawienia kalendarza</h2><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="save_settings"><label>ID kalendarza Google<input type="text" name="calendar_id" value="<?=pc173_h(pg173_calendar_id())?>" required><small class="muted">Zwykle <code>primary</code> lub ID kalendarza „Rezerwacje — Moja Polana Smaków” skopiowane z ustawień Google Kalendarza.</small></label><label>Czas blokady stolika (minuty)<input type="number" name="duration" min="30" max="360" step="5" value="<?=pg173_duration()?>"></label><label class="check"><input type="checkbox" name="enabled" value="1" <?=pg173_enabled()?'checked':''?>> Włącz automatyczną synchronizację</label><label class="check"><input type="checkbox" name="include_contact" value="1" <?=setting('pg173_include_contact','0')==='1'?'checked':''?>> Dołącz telefon i e-mail gościa do prywatnego opisu wydarzenia</label><label class="check"><input type="checkbox" name="confirm_move" value="1"> Potwierdzam, że po zmianie ID kalendarza stare wpisy należy przenieść podczas kolejnej synchronizacji</label><button class="b" type="submit">Zapisz kalendarz</button></form><p class="warning">Używaj kalendarza <b>prywatnego</b>. Udostępnij pracownikom minimum „Wyświetlanie szczegółów wydarzeń”. Ogranicz dostęp ze względu na dane klientów.</p></section>
<section class="card"><h2>4. Synchronizacja i naprawa</h2><p>Nowo potwierdzone rezerwacje synchronizują się natychmiast po kliknięciu „Potwierdź”. W razie awarii Google status rezerwacji w Polanie pozostaje poprawny, a błąd można ponowić tutaj.</p><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="sync_batch"><button class="b" type="submit" <?=$connected?'':'disabled'?>>Synchronizuj zaległe rezerwacje</button></form><p class="muted">Ten przycisk synchronizuje po 12 brakujących lub błędnych wpisów oraz usuwa anulowane. Kliknij ponownie dla następnej partii. Zmianę długości wizyty już zsynchronizowanych rezerwacji odśwież przyciskiem w tabeli. Bez CRON — ręczna naprawa, gdy Google zwraca błąd.</p><h3>Jak pracownicy zobaczą rezerwacje na telefonie?</h3><p>Utwórz osobny kalendarz „Moja Polana — Rezerwacje”, udostępnij go konkretnym kontom Google obsługi z prawem podglądu i włącz wyświetlanie tego kalendarza w aplikacji na telefonie. Powiadomienia skonfiguruj w Google Kalendarzu.</p><a class="b alt" href="https://calendar.google.com/calendar/u/0/r/settings" target="_blank" rel="noopener noreferrer">Otwórz ustawienia Google Kalendarza ↗</a></section>
</div>
<section class="card"><h2>5. Najbliższe rezerwacje i status Google</h2><div class="scroll"><table><thead><tr><th>Termin</th><th>Rezerwacja</th><th>Goście</th><th>Status Polany</th><th>Google</th><th>Akcja</th></tr></thead><tbody>
<?php foreach($rows as $r):?><tr><td><?=pc173_h($r['reservation_date'])?><br><?=pc173_h(substr((string)$r['reservation_time'],0,5))?></td><td><a href="/admin/reservation.php?id=<?=(int)$r['id']?>"><?=pc173_h($r['reservation_code'])?></a><br><?=pc173_h($r['guest_name'])?></td><td><?=(int)$r['guests']?></td><td><?=pc173_h(reservation_status_label((string)$r['status']))?></td><td><span class="pill <?=($r['sync_status']??'')==='error'?'off':''?>"><?=pc173_h($r['sync_status']?:'—')?></span><?php if(($r['sync_status']??'')==='error'):?><small class="muted"><?=pc173_h($r['last_error']??'')?></small><?php endif;?></td><td><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="sync_one"><input type="hidden" name="id" value="<?=(int)$r['id']?>"><button class="b alt" type="submit">Synchronizuj</button></form></td></tr><?php endforeach; ?>
<?php if(!$rows):?><tr><td colspan="6">Brak rezerwacji.</td></tr><?php endif;?></tbody></table></div></section>
<section class="card"><h2>6. Odłączenie Google</h2><p>Odłączenie usuwa dostęp OAuth z Polany, ale nie usuwa wydarzeń wcześniej utworzonych w Kalendarzu Google. Przed odłączeniem usuń je ręcznie, jeśli tego potrzebujesz.</p><form method="post" onsubmit="return confirm('Odłączyć konto Google? Istniejące wydarzenia pozostaną w kalendarzu.');"><?=csrf_field()?><input type="hidden" name="action" value="disconnect"><label class="check"><input type="checkbox" name="confirm_disconnect" value="1" required> Potwierdzam odłączenie</label><button class="b alt" type="submit">Odłącz konto Google</button></form></section>
</div>
<?php render_admin_footer(); ?>