feat: add ClinicInvitationWebController and related templates for handling clinic invitations

- Implemented ClinicInvitationWebController to manage the invitation process via web.
- Added view and respond methods to handle invitation display and responses.
- Created result.html.twig and view.html.twig templates for rendering invitation results and views.
- Integrated CSRF protection for form submissions.
- Established routes for invitation viewing and responding.
This commit is contained in:
hamed
2026-07-11 09:33:55 +03:30
parent a5e3408e85
commit 744a40c0f6
19 changed files with 2125 additions and 671 deletions
+109
View File
@@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>دعوت همکاری کلینیک</title>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<style>
@font-face {
font-family: 'Vazirmatn';
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2') format('woff2');
font-weight: 400; font-display: swap;
}
@font-face {
font-family: 'Vazirmatn';
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Bold.woff2') format('woff2');
font-weight: 700; font-display: swap;
}
:root {
--ok:#15a35a; --ok-soft:#e6f6ed;
--err:#e0394a; --err-soft:#fdebed;
--warn:#d98a09; --warn-soft:#fcf2df;
--ink:#0f1b2e; --muted:#56657c; --line:#e4e9f1;
--primary:#5457dd; --primary-600:#464ac9;
--shadow-lg:0 12px 32px rgba(15,27,46,.12), 0 4px 10px rgba(15,27,46,.06);
--ease:cubic-bezier(.22,.61,.36,1);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin:0; font-family:'Vazirmatn', ui-sans-serif, system-ui, sans-serif; color:var(--ink);
background: radial-gradient(1200px 600px at 50% -10%, #e9eefb 0%, #eef2f8 45%, #e6ecf4 100%);
min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px;
}
.card {
position:relative; overflow:hidden; background:#fff; border:1px solid var(--line);
border-radius:24px; box-shadow:var(--shadow-lg); width:100%; max-width:420px;
padding:40px 28px 30px; text-align:center; animation:rise .45s var(--ease) both;
}
@keyframes rise { from { opacity:0; transform:translateY(14px) scale(.98); } }
.accent { position:absolute; inset:0 0 auto 0; height:6px; }
.accent.ok { background:linear-gradient(90deg,#15a35a,#3fca7d); }
.accent.err { background:linear-gradient(90deg,#e0394a,#f07a86); }
.accent.warn { background:linear-gradient(90deg,#d98a09,#f2b545); }
.badge { width:96px; height:96px; margin:6px auto 20px; position:relative; }
.badge .ring { position:absolute; inset:0; border-radius:50%; animation:pop .5s cubic-bezier(.2,.8,.2,1.2) .05s both; }
.badge.ok .ring { background:var(--ok-soft); }
.badge.err .ring { background:var(--err-soft); }
.badge.warn .ring { background:var(--warn-soft); }
.badge svg { position:absolute; inset:0; width:96px; height:96px; }
@keyframes pop { from { transform:scale(.4); opacity:0; } }
.draw { fill:none; stroke-width:6; stroke-linecap:round; stroke-linejoin:round;
stroke-dasharray:80; stroke-dashoffset:80; animation:draw .6s ease .35s forwards; }
.badge.ok .draw { stroke:var(--ok); }
.badge.err .draw { stroke:var(--err); }
.badge.warn .draw { stroke:var(--warn); }
@keyframes draw { to { stroke-dashoffset:0; } }
h1 { font-size:21px; font-weight:700; margin:0 0 8px; }
p.msg { color:var(--muted); font-size:14px; margin:0 auto 22px; line-height:2; max-width:320px; }
.btn { display:block; width:100%; padding:13px; border-radius:12px; text-decoration:none;
font-weight:700; font-size:15px; background:var(--primary); color:#fff; border:none; cursor:pointer;
transition:filter .15s, transform .05s; }
.btn:hover { background:var(--primary-600); }
.btn:active { transform:translateY(1px); }
.hint { color:#9aa1ad; font-size:12.5px; margin:6px 0 0; }
.brand { margin-top:20px; font-size:11.5px; color:#aeb4c0; letter-spacing:.2px; }
</style>
</head>
<body>
{% set kind = state == 'accepted' ? 'ok' : (state == 'rejected' ? 'warn' : 'err') %}
<div class="card">
<div class="accent {{ kind }}"></div>
<div class="badge {{ kind }}">
<div class="ring"></div>
<svg viewBox="0 0 96 96" aria-hidden="true">
{% if state == 'accepted' %}
<path class="draw" d="M30 50 L44 63 L68 35"/>
{% elseif state == 'rejected' %}
<path class="draw" d="M48 30 L48 50 L62 58"/>
{% else %}
<path class="draw" d="M36 36 L60 60"/>
<path class="draw" d="M60 36 L36 60"/>
{% endif %}
</svg>
</div>
{% if state == 'accepted' %}
<h1>درخواست شما تایید شد</h1>
<p class="msg">می‌توانید وارد پنل ادمین شوید.</p>
<a class="btn" href="{{ admin_url }}">ورود به پنل ادمین</a>
{% elseif state == 'rejected' %}
<h1>دعوت رد شد</h1>
<p class="msg">درخواست همکاری این کلینیک رد شد.</p>
{% elseif state == 'notfound' %}
<h1>دعوتنامه یافت نشد</h1>
<p class="msg">این لینک معتبر نیست.</p>
{% else %}
<h1>دعوت منقضی شده است</h1>
<p class="msg">این دعوت منقضی شده یا قبلاً پاسخ داده شده است.</p>
{% endif %}
<div class="brand">کلینیک پرو</div>
</div>
</body>
</html>
+79
View File
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>دعوت همکاری کلینیک</title>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<style>
@font-face {
font-family: 'Vazirmatn';
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2') format('woff2');
font-weight: 400; font-display: swap;
}
@font-face {
font-family: 'Vazirmatn';
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Bold.woff2') format('woff2');
font-weight: 700; font-display: swap;
}
:root {
--ok:#15a35a; --ok-600:#12904f;
--err:#e0394a; --err-600:#c62f3f;
--ink:#0f1b2e; --muted:#56657c; --line:#e4e9f1;
--primary:#5457dd;
--shadow-lg:0 12px 32px rgba(15,27,46,.12), 0 4px 10px rgba(15,27,46,.06);
--ease:cubic-bezier(.22,.61,.36,1);
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin:0; font-family:'Vazirmatn', ui-sans-serif, system-ui, sans-serif; color:var(--ink);
background: radial-gradient(1200px 600px at 50% -10%, #e9eefb 0%, #eef2f8 45%, #e6ecf4 100%);
min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px;
}
.card {
position:relative; overflow:hidden; background:#fff; border:1px solid var(--line);
border-radius:24px; box-shadow:var(--shadow-lg); width:100%; max-width:420px;
padding:40px 28px 30px; text-align:center; animation:rise .45s var(--ease) both;
}
@keyframes rise { from { opacity:0; transform:translateY(14px) scale(.98); } }
.accent { position:absolute; inset:0 0 auto 0; height:6px; background:linear-gradient(90deg,#5457dd,#8a8cf0); }
.logo { width:70px; height:70px; margin:6px auto 20px; border-radius:20px; background:#eef0fe;
display:flex; align-items:center; justify-content:center; font-size:34px; }
h1 { font-size:21px; font-weight:700; margin:0 0 12px; }
p.msg { color:var(--muted); font-size:14.5px; margin:0 auto 10px; line-height:2.1; max-width:330px; }
p.msg b { color:var(--ink); }
.hint { color:#9aa1ad; font-size:12.5px; margin:4px 0 24px; }
.btn { display:block; width:100%; padding:13px; border-radius:12px; text-decoration:none;
font-weight:700; font-size:15px; border:none; cursor:pointer; color:#fff;
transition:filter .15s, transform .05s; }
.btn + .btn { margin-top:12px; }
.btn:active { transform:translateY(1px); }
.btn-ok { background:var(--ok); }
.btn-ok:hover { background:var(--ok-600); }
.btn-err { background:#fff; color:var(--err); border:1px solid var(--err); }
.btn-err:hover { background:#fdebed; }
.brand { margin-top:20px; font-size:11.5px; color:#aeb4c0; letter-spacing:.2px; }
</style>
</head>
<body>
<div class="card">
<div class="accent"></div>
<div class="logo">🩺</div>
<h1>دعوت به همکاری</h1>
{% if doctor %}<p class="msg">{{ doctor }} عزیز،</p>{% endif %}
<p class="msg">کلینیک <b>{{ clinic }}</b> شما را برای همکاری دعوت کرده است.</p>
<p class="hint">این دعوت تا ۷۲ ساعت معتبر است.</p>
<form method="post" action="{{ path('invitation_web_respond', {token: token}) }}">
<input type="hidden" name="_token" value="{{ csrf_token('invitation_' ~ token) }}">
<button type="submit" name="action" value="accept" class="btn btn-ok">تایید و پذیرش دعوت</button>
<button type="submit" name="action" value="reject" class="btn btn-err">رد دعوت</button>
</form>
<div class="brand">کلینیک پرو</div>
</div>
</body>
</html>