Commit 969b2048 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(proposal): show the real app and portal, and drop the phases

The Sayd proposal carried a hand-built imitation of the app and a mocked
portal. Both are replaced with screenshots of the real thing: 17 screens
captured from the Flutter app (sayd-mobile-app-main) driving the live demo
API on an emulator, and 19 portal screens (sayd-mobile-portal-main) wearing
the club's own branding.

The phase framing is gone — no "first/second phase", no week-by-week plan.
Scope now lists what the system actually does (installments, fines and
appeals, credit balance, transfer proofs, medical certificates, attendance
and evaluations, events, audit log). Captions reuse the proposal's own
Arabic and the Arabic user guide's wording.

The PDF export prints both screenshot sets as captioned grids.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent f4f4b150
/* ==========================================================================
نادي الصيد المصري — النموذج التفاعلي لتطبيق الأعضاء
Interactive prototype. Arabic-first, RTL, dark + light.
Logical properties throughout so the layout mirrors correctly.
========================================================================== */
:root{
--gold:#C9A227; --gold-2:#E8CC6A; --gold-3:#F3E2A6;
--mint:#16B981; --rose:#E0556B; --sky:#3E8CE0; --amber:#E8A33D;
--r-s:10px; --r-m:16px; --r-l:22px; --r-xl:28px;
--f-head:'Cairo',system-ui,sans-serif;
--f-body:'IBM Plex Sans Arabic',system-ui,sans-serif;
--tab-h:68px; --hd-h:56px;
--ease:cubic-bezier(.22,1,.36,1);
}
:root[data-theme="dark"]{
--bg:#070F1E; --bg-2:#0C1A31; --surface:#101F3A; --surface-2:#16294A;
--line:rgba(255,255,255,.09); --line-2:rgba(255,255,255,.16);
--txt:#F2F6FC; --txt-2:#A9BCD8; --txt-3:#7288A8;
--shadow:0 18px 44px rgba(0,0,0,.55);
--sheet:#0E1C33;
}
:root[data-theme="light"]{
--bg:#F4F6FA; --bg-2:#FFFFFF; --surface:#FFFFFF; --surface-2:#F0F3F9;
--line:#E4E9F2; --line-2:#D2DAE8;
--txt:#0C1A2E; --txt-2:#4C5D77; --txt-3:#8494AB;
--shadow:0 14px 34px rgba(12,26,46,.12);
--sheet:#FFFFFF;
}
*,*::before,*::after{ margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ height:100%; }
body{
font-family:var(--f-body); background:var(--bg); color:var(--txt);
line-height:1.7; font-size:15px; overflow:hidden;
-webkit-font-smoothing:antialiased;
transition:background .35s var(--ease), color .35s var(--ease);
}
h1,h2,h3,h4{ font-family:var(--f-head); font-weight:800; line-height:1.3; }
button{ font-family:inherit; border:none; background:none; color:inherit; cursor:pointer; }
input,select{ font-family:inherit; color:inherit; }
img{ display:block; max-width:100%; }
.num{ font-variant-numeric:tabular-nums; direction:ltr; unicode-bidi:isolate; display:inline-block; }
.ico{ width:1em; height:1em; fill:none; stroke:currentColor; stroke-width:1.8;
stroke-linecap:round; stroke-linejoin:round; flex-shrink:0; }
.sprite{ position:absolute; width:0; height:0; overflow:hidden; }
/* ================= APP SHELL ================= */
#app{ position:relative; height:100%; overflow:hidden; background:var(--bg); }
.screen{
position:absolute; inset:0; display:flex; flex-direction:column;
background:var(--bg); will-change:transform,opacity;
}
.screen-body{ flex:1; overflow-y:auto; overscroll-behavior:contain; -webkit-overflow-scrolling:touch; }
.screen-body::-webkit-scrollbar{ width:0; }
.pad{ padding:16px; }
.pad-b{ padding-bottom:26px; }
.has-tab .screen-body{ padding-bottom:calc(var(--tab-h) + 16px); }
/* transitions: forward = new slides in from inline-start */
@keyframes inFwd { from{ transform:translateX(var(--slide-in)); opacity:.4 } to{ transform:none; opacity:1 } }
@keyframes outFwd{ from{ transform:none; opacity:1 } to{ transform:translateX(var(--slide-out)); opacity:0 } }
@keyframes inBack { from{ transform:translateX(var(--slide-out)); opacity:.4 } to{ transform:none; opacity:1 } }
@keyframes outBack{ from{ transform:none; opacity:1 } to{ transform:translateX(var(--slide-in)); opacity:0 } }
@keyframes fadeIn{ from{opacity:0; transform:scale(.98)} to{opacity:1; transform:none} }
:root{ --slide-in:-100%; --slide-out:100%; } /* RTL: forward comes from the left */
.anim-in-fwd { animation:inFwd .34s var(--ease) both; }
.anim-out-fwd{ animation:outFwd .34s var(--ease) both; }
.anim-in-back { animation:inBack .34s var(--ease) both; }
.anim-out-back{ animation:outBack .34s var(--ease) both; }
.anim-fade{ animation:fadeIn .3s var(--ease) both; }
@media (prefers-reduced-motion:reduce){
.anim-in-fwd,.anim-out-fwd,.anim-in-back,.anim-out-back,.anim-fade{ animation-duration:.01s; }
}
/* ================= STATUS BAR + HEADER ================= */
.sbar{
height:38px; display:flex; align-items:flex-end; padding-bottom:5px; justify-content:space-between;
padding:0 18px; font-size:.7rem; font-weight:700; color:var(--txt);
flex-shrink:0; opacity:.9;
}
.sbar .dots{ display:flex; gap:3px; align-items:center; }
.sbar .dots i{ width:3px; height:9px; background:currentColor; border-radius:1px; display:block; }
.sbar .dots i:nth-child(1){ height:5px } .sbar .dots i:nth-child(2){ height:7px }
.sbar .bat{ width:18px; height:9px; border:1.2px solid currentColor; border-radius:2.5px; position:relative; }
.sbar .bat::after{ content:''; position:absolute; inset:1.5px; inset-inline-end:5px; background:currentColor; border-radius:1px; }
.hd{
height:var(--hd-h); flex-shrink:0; display:flex; align-items:center; gap:12px;
padding:0 14px; background:var(--bg); position:relative; z-index:5;
}
.hd h1{ font-size:1.02rem; flex:1; }
.hd .sub{ font-size:.7rem; color:var(--txt-3); font-weight:500; font-family:var(--f-body); }
.hd-btn{
width:38px; height:38px; border-radius:12px; display:grid; place-items:center;
background:var(--surface); border:1px solid var(--line); color:var(--txt);
font-size:1.05rem; transition:.2s; flex-shrink:0;
}
.hd-btn:active{ transform:scale(.92); }
.hd-btn .ico{ width:19px; height:19px; }
.hd-btn.plain{ background:none; border:none; }
.hd-badge{ position:absolute; top:6px; inset-inline-end:6px; width:7px; height:7px; border-radius:50%; background:var(--rose); }
/* ================= TAB BAR ================= */
.tabbar{
height:var(--tab-h); flex-shrink:0; display:flex; align-items:stretch;
background:var(--bg-2); border-top:1px solid var(--line);
position:absolute; inset-inline:0; bottom:0; z-index:20;
padding-bottom:2px;
}
.tabbar button{
flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
color:var(--txt-3); font-size:.63rem; font-weight:700; transition:color .2s;
}
.tabbar button .ico{ width:21px; height:21px; }
.tabbar button.on{ color:var(--gold); }
.tabbar button:active{ transform:scale(.94); }
.tabbar .dot{ width:4px; height:4px; border-radius:50%; background:var(--gold); margin-top:1px; opacity:0; }
.tabbar button.on .dot{ opacity:1; }
/* ================= CARDS / LISTS ================= */
.card{
background:var(--surface); border:1px solid var(--line); border-radius:var(--r-m);
padding:15px; margin-bottom:12px;
}
.card-t{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:10px; }
.card-t h3{ font-size:.9rem; }
.card-t .more{ font-size:.72rem; color:var(--gold); font-weight:700; }
.row{
display:flex; align-items:center; gap:12px; padding:12px 0;
border-bottom:1px solid var(--line);
}
.row:last-child{ border-bottom:none; }
.row .l{ flex:1; min-width:0; }
.row .l b{ display:block; font-size:.85rem; font-weight:700; }
.row .l span{ font-size:.72rem; color:var(--txt-3); }
.row .r{ text-align:end; flex-shrink:0; }
.ic-box{
width:40px; height:40px; border-radius:12px; display:grid; place-items:center;
background:var(--surface-2); color:var(--gold); flex-shrink:0;
}
.ic-box .ico{ width:20px; height:20px; }
.ic-box.mint{ background:rgba(22,185,129,.14); color:var(--mint); }
.ic-box.rose{ background:rgba(224,85,107,.14); color:var(--rose); }
.ic-box.sky{ background:rgba(62,140,224,.14); color:var(--sky); }
.chip{
display:inline-flex; align-items:center; gap:5px; font-size:.66rem; font-weight:700;
padding:3px 10px; border-radius:99px; background:var(--surface-2); color:var(--txt-2);
}
.chip.ok{ background:rgba(22,185,129,.15); color:var(--mint); }
.chip.due{ background:rgba(224,85,107,.15); color:var(--rose); }
.chip.soon{ background:rgba(232,163,61,.16); color:var(--amber); }
.chip.info{ background:rgba(62,140,224,.15); color:var(--sky); }
/* ================= BUTTONS ================= */
.btn{
width:100%; height:50px; border-radius:15px; font-family:var(--f-head);
font-weight:800; font-size:.92rem; display:flex; align-items:center;
justify-content:center; gap:8px; transition:transform .12s, box-shadow .2s;
}
.btn:active{ transform:scale(.975); }
.btn-gold{ background:linear-gradient(135deg,var(--gold-2),var(--gold)); color:#2A2003;
box-shadow:0 10px 26px rgba(201,162,39,.32); }
.btn-ghost{ background:var(--surface); border:1px solid var(--line-2); color:var(--txt); }
.btn-mint{ background:var(--mint); color:#04241A; }
.btn:disabled{ opacity:.4; box-shadow:none; }
.btn-sm{ height:40px; font-size:.8rem; border-radius:12px; width:auto; padding:0 18px; }
.segs{ display:flex; gap:4px; background:var(--surface-2); border-radius:13px; padding:4px; margin-bottom:14px; }
.segs button{ flex:1; height:34px; border-radius:10px; font-size:.76rem; font-weight:700; color:var(--txt-3); }
.segs button.on{ background:var(--surface); color:var(--txt); box-shadow:var(--shadow); }
:root[data-theme="light"] .segs button.on{ box-shadow:0 2px 8px rgba(12,26,46,.10); }
.chips{ display:flex; gap:7px; overflow-x:auto; padding-bottom:4px; margin-bottom:12px; scrollbar-width:none; }
.chips::-webkit-scrollbar{ display:none; }
.chips button{
flex-shrink:0; padding:7px 15px; border-radius:99px; font-size:.75rem; font-weight:700;
background:var(--surface); border:1px solid var(--line); color:var(--txt-2);
}
.chips button.on{ background:var(--gold); border-color:var(--gold); color:#2A2003; }
/* ================= MEMBERSHIP CARD ================= */
.mcard{
position:relative; overflow:hidden; border-radius:var(--r-l); padding:18px;
background:linear-gradient(140deg,#123566,#0A1C38 60%,#16407A);
color:#fff; box-shadow:var(--shadow); margin-bottom:14px;
}
.mcard::after{
content:''; position:absolute; inset-inline-end:-42px; top:-52px; width:180px; height:180px;
border-radius:50%; background:radial-gradient(circle,rgba(232,204,106,.30),transparent 68%);
}
.mcard::before{
content:''; position:absolute; inset-inline-start:-14%; bottom:-58%;
width:190px; aspect-ratio:1240/1754; opacity:.13;
background:url(../../assets/club-logo.png) center/contain no-repeat;
filter:grayscale(1) sepia(1) saturate(2.4) brightness(1.2);
}
.mcard .in{ position:relative; z-index:2; }
.mcard .who{ font-family:var(--f-head); font-weight:800; font-size:1rem; }
.mcard .meta{ font-size:.72rem; color:#B6C9E4; margin-top:2px; }
.mcard .idno{
font-family:var(--f-head); font-size:1.5rem; font-weight:900; color:var(--gold-2);
letter-spacing:.14em; margin-top:14px;
}
/* ================= BIG BALANCE ================= */
.bal{ text-align:center; padding:8px 0 4px; }
.bal .lbl{ font-size:.76rem; color:var(--txt-3); }
.bal .amt{ font-family:var(--f-head); font-weight:900; font-size:2.5rem; line-height:1.15; color:var(--rose); }
.bal .amt.zero{ color:var(--mint); }
.bal .cur{ font-size:.85rem; color:var(--txt-3); font-weight:600; }
/* ================= SELECTABLE DUE ROW ================= */
.due{
display:flex; gap:12px; align-items:flex-start; background:var(--surface);
border:1px solid var(--line); border-radius:var(--r-m); padding:13px;
margin-bottom:10px; transition:border-color .2s, background .2s;
border-inline-start:3px solid var(--rose);
}
.due.sel{ border-color:var(--gold); background:linear-gradient(var(--surface),var(--surface)) padding-box; }
.due.soon{ border-inline-start-color:var(--amber); }
.due .box{
width:22px; height:22px; border-radius:7px; border:1.8px solid var(--line-2);
display:grid; place-items:center; flex-shrink:0; margin-top:2px; transition:.2s;
}
.due.sel .box{ background:var(--gold); border-color:var(--gold); color:#2A2003; }
.due .box .ico{ width:13px; height:13px; stroke-width:3; opacity:0; }
.due.sel .box .ico{ opacity:1; }
.due .mid{ flex:1; min-width:0; }
.due .mid b{ font-size:.86rem; display:block; }
.due .mid span{ font-size:.71rem; color:var(--txt-3); display:block; margin-top:1px; }
.due .val{ font-family:var(--f-head); font-weight:800; font-size:.95rem; white-space:nowrap; }
.paybar{
position:absolute; inset-inline:0; bottom:0; z-index:25;
background:var(--bg-2); border-top:1px solid var(--line);
padding:12px 16px calc(12px + env(safe-area-inset-bottom));
display:flex; align-items:center; gap:14px;
animation:slideUp .3s var(--ease) both;
}
@keyframes slideUp{ from{ transform:translateY(100%) } to{ transform:none } }
.paybar .t{ flex:1; }
.paybar .t span{ font-size:.68rem; color:var(--txt-3); display:block; }
.paybar .t b{ font-family:var(--f-head); font-size:1.15rem; }
.paybar .btn{ width:auto; padding:0 26px; }
/* ================= QR ================= */
.qr-wrap{ display:grid; place-items:center; padding:10px 0 4px; }
.qr-card{
background:#fff; border-radius:var(--r-l); padding:16px; box-shadow:var(--shadow);
position:relative;
}
.qr-grid{ display:grid; grid-template-columns:repeat(21,1fr); width:196px; height:196px; }
.qr-grid i{ background:#0A1428; border-radius:.5px; }
.qr-grid i.off{ background:transparent; }
.ring{ transform:rotate(-90deg); }
.ring circle{ fill:none; stroke-width:3.4; stroke-linecap:round; }
.ring .bg{ stroke:var(--line); }
.ring .fg{ stroke:var(--gold); transition:stroke-dashoffset 1s linear; }
/* ================= INVITES ================= */
.inv-ring{ position:relative; width:132px; height:132px; margin:6px auto 12px; }
.inv-ring .n{
position:absolute; inset:0; display:grid; place-items:center; text-align:center;
font-family:var(--f-head); font-weight:900; font-size:2rem; line-height:1;
}
.inv-ring .n small{ display:block; font-size:.62rem; color:var(--txt-3); font-weight:600; margin-top:3px; }
.stepper{ display:flex; align-items:center; gap:16px; justify-content:center; margin:14px 0; }
.stepper button{
width:42px; height:42px; border-radius:14px; background:var(--surface-2);
border:1px solid var(--line); font-size:1.3rem; font-weight:700; display:grid; place-items:center;
}
.stepper button:active{ transform:scale(.9); }
.stepper .v{ font-family:var(--f-head); font-size:1.9rem; font-weight:900; min-width:52px; text-align:center; }
/* ================= SHEET / MODAL ================= */
.scrim{
position:absolute; inset:0; background:rgba(3,9,20,.62); z-index:60;
animation:fadeIn .22s var(--ease) both; backdrop-filter:blur(2px);
}
.sheet{
position:absolute; inset-inline:0; bottom:0; z-index:61; background:var(--sheet);
border-radius:var(--r-xl) var(--r-xl) 0 0; padding:8px 16px calc(20px + env(safe-area-inset-bottom));
max-height:82%; overflow-y:auto; animation:sheetUp .32s var(--ease) both;
box-shadow:0 -12px 40px rgba(0,0,0,.4);
}
@keyframes sheetUp{ from{ transform:translateY(100%) } to{ transform:none } }
.sheet .grab{ width:38px; height:4px; border-radius:99px; background:var(--line-2); margin:6px auto 14px; }
.sheet h3{ font-size:1rem; margin-bottom:4px; }
.sheet .sub{ font-size:.76rem; color:var(--txt-3); margin-bottom:14px; }
/* ================= TOAST ================= */
.toast{
position:absolute; inset-inline:16px; bottom:calc(var(--tab-h) + 16px); z-index:90;
background:var(--surface-2); border:1px solid var(--line-2); border-radius:14px;
padding:12px 14px; display:flex; align-items:center; gap:10px; font-size:.8rem;
box-shadow:var(--shadow); animation:toastIn .3s var(--ease) both;
}
@keyframes toastIn{ from{ transform:translateY(18px); opacity:0 } to{ transform:none; opacity:1 } }
.toast.out{ animation:toastOut .3s var(--ease) both; }
@keyframes toastOut{ to{ transform:translateY(18px); opacity:0 } }
.toast .ico{ width:18px; height:18px; color:var(--mint); }
/* ================= SPINNER / SUCCESS ================= */
.spin{ width:46px; height:46px; border-radius:50%; border:3px solid var(--line);
border-top-color:var(--gold); animation:sp .8s linear infinite; margin:0 auto; }
@keyframes sp{ to{ transform:rotate(360deg) } }
.tick-big{
width:84px; height:84px; border-radius:50%; background:rgba(22,185,129,.14);
display:grid; place-items:center; margin:0 auto 18px; color:var(--mint);
animation:pop .45s var(--ease) both;
}
.tick-big .ico{ width:42px; height:42px; stroke-width:2.4; }
@keyframes pop{ 0%{ transform:scale(.4); opacity:0 } 60%{ transform:scale(1.08) } 100%{ transform:scale(1); opacity:1 } }
/* ================= LOGIN / OTP ================= */
.brandwrap{ text-align:center; padding:24px 0 12px; }
.brandwrap img{ height:88px; margin:0 auto 14px; filter:drop-shadow(0 10px 26px rgba(0,0,0,.5)); }
.brandwrap h2{ font-size:1.15rem; }
.brandwrap p{ font-size:.78rem; color:var(--txt-3); margin-top:5px; }
.phone-in{
display:flex; align-items:center; gap:10px; direction:ltr;
background:var(--surface); border:1px solid var(--line); border-radius:15px;
padding:14px 16px; margin:20px 0 8px;
}
.phone-in .flag{ width:22px; height:15px; border-radius:2px; overflow:hidden; flex-shrink:0; }
.phone-in .cc{ font-size:.86rem; color:var(--txt-2); font-weight:600; }
.phone-in .sep{ width:1px; height:18px; background:var(--line-2); }
.phone-in .val{ font-size:1rem; font-weight:700; letter-spacing:.05em; flex:1; }
.phone-in .cursor{ display:inline-block; width:2px; height:17px; background:var(--gold);
vertical-align:-3px; animation:blink 1.05s step-end infinite; }
@keyframes blink{ 50%{ opacity:0 } }
.otp{ display:flex; gap:11px; justify-content:center; direction:ltr; margin:22px 0 12px; }
.otp i{
width:52px; height:60px; border-radius:15px; background:var(--surface);
border:1.6px solid var(--line); display:grid; place-items:center;
font-family:var(--f-head); font-size:1.5rem; font-weight:800; font-style:normal;
}
.otp i.fill{ border-color:var(--gold); color:var(--gold-2); }
.keypad{ display:grid; grid-template-columns:repeat(3,1fr); gap:9px; margin-top:auto; padding-top:14px; }
.keypad button{
height:52px; border-radius:15px; background:var(--surface); border:1px solid var(--line);
font-family:var(--f-head); font-size:1.25rem; font-weight:800;
}
.keypad button:active{ background:var(--surface-2); transform:scale(.95); }
.keypad button.fn{ background:transparent; border-color:transparent; font-size:1rem; color:var(--txt-3); }
/* ================= MISC ================= */
.empty{ text-align:center; padding:40px 20px; color:var(--txt-3); }
.empty .ico{ width:40px; height:40px; margin:0 auto 12px; opacity:.5; }
.empty p{ font-size:.82rem; }
.bar{ height:7px; border-radius:99px; background:var(--surface-2); overflow:hidden; }
.bar i{ display:block; height:100%; background:linear-gradient(90deg,var(--gold),var(--gold-2)); border-radius:99px;
transition:width .5s var(--ease); }
.sec-t{ font-size:.78rem; font-weight:800; color:var(--txt-3); margin:16px 0 8px; }
.tt-day{ font-size:.74rem; font-weight:800; color:var(--gold); margin:14px 0 6px; }
.grid2{ display:grid; grid-template-columns:1fr 1fr; gap:9px; }
.qa{
background:var(--surface); border:1px solid var(--line); border-radius:15px;
padding:13px 8px; text-align:center; transition:.18s;
}
.qa:active{ transform:scale(.95); }
.qa .ico{ width:21px; height:21px; color:var(--gold); margin:0 auto 6px; }
.qa span{ font-size:.68rem; font-weight:700; color:var(--txt-2); }
/* Long titles must not widen the shell — the phone viewport is fixed. */
.tr{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.screen-body{ overflow-x:hidden; }
.hd .sub{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.row .l b, .row .l span{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.due .mid b{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* ================= NEWS =================
Article artwork is a branded placeholder, not stock photography: the club
supplies real images at launch, and inventing them here would misrepresent
what has been approved. Each tone tints the same crest-backed panel. */
.nimg{
position:relative; height:104px; border-radius:var(--r-m); overflow:hidden;
flex-shrink:0; width:112px; display:grid; place-items:center;
background:linear-gradient(140deg,#123566,#0A1C38);
}
.nimg.tall{ width:auto; height:186px; border-radius:0; }
.nimg.gold{ background:linear-gradient(140deg,#8A6E14,#2A2003); }
.nimg.mint{ background:linear-gradient(140deg,#0E7355,#04241A); }
.nimg.sky { background:linear-gradient(140deg,#1D5E9E,#0A1C38); }
.nimg.rose{ background:linear-gradient(140deg,#9B3244,#2C0F16); }
.nimg .crest{
position:absolute; inset:0; opacity:.30;
background:url(../../assets/club-logo.png) center/58% no-repeat;
filter:grayscale(1) brightness(2.4);
}
.nimg.tall .crest{ background-size:34%; }
.nimg .ph{ position:relative; width:22px; height:22px; color:#fff; opacity:.5; }
.nimg.tall .ph{ width:30px; height:30px; }
.ncard{
display:flex; gap:12px; align-items:stretch; background:var(--surface);
border:1px solid var(--line); border-radius:var(--r-m); overflow:hidden;
margin-bottom:10px; cursor:pointer; transition:border-color .2s;
}
.ncard:active{ border-color:var(--line-2); }
.ncard.lead{ display:block; }
.ncard .nbody{ flex:1; min-width:0; padding:12px 13px; display:flex; flex-direction:column; gap:5px; justify-content:center; }
.ncard.lead .nbody{ padding:14px; }
.ncard .nmeta{ display:flex; align-items:center; gap:8px; font-size:.66rem; color:var(--txt-3); }
.ncard b{ font-family:var(--f-head); font-size:.87rem; line-height:1.5;
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ncard.lead b{ font-size:1rem; -webkit-line-clamp:3; }
.ncard .nlead{ font-size:.75rem; color:var(--txt-3); line-height:1.7;
display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ncard:not(.lead) .nlead{ display:none; }
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>نادي الصيد المصري — نموذج تطبيق الأعضاء</title>
<meta name="theme-color" content="#070F1E">
<link rel="icon" href="../assets/club-logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700;800;900&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/app.css">
<style>
.ico.flip{ width:16px;height:16px;color:var(--txt-3);transform:scaleX(-1) }
.ico.tiny{ width:12px;height:12px;stroke-width:3 }
.ico.big{ width:30px;height:30px }
.flagsvg{ width:22px;height:15px;stroke:none }
</style>
</head>
<body>
<svg class="sprite" aria-hidden="true"><defs>
<symbol id="i-home" viewBox="0 0 24 24"><path d="M3.8 10.4 12 3.8l8.2 6.6"/><path d="M6 9.6V20.2h12V9.6"/><path d="M9.9 20.2v-5h4.2v5"/></symbol>
<symbol id="i-activity" viewBox="0 0 24 24"><path d="M4 9v6M7.2 6.8v10.4M16.8 6.8v10.4M20 9v6M7.2 12h9.6"/></symbol>
<symbol id="i-gate" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.3"/><path d="M13.5 14.2h3v3h-3zM19.4 14.2h1.1M13.5 20.4h3M19.6 18.6v1.8"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="3.6"/><path d="M4.6 20.2c0-3.9 3.4-6 7.4-6s7.4 2.1 7.4 6"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M6.2 9.4a5.8 5.8 0 0 1 11.6 0c0 3.9 1.5 5.4 1.5 5.4H4.7s1.5-1.5 1.5-5.4Z"/><path d="M10 18.1a2.2 2.2 0 0 0 4 0"/></symbol>
<symbol id="i-mail" viewBox="0 0 24 24"><rect x="2.8" y="5.4" width="18.4" height="13.2" rx="2.2"/><path d="m3.8 7.1 7.3 5.3a1.6 1.6 0 0 0 1.8 0l7.3-5.3"/></symbol>
<symbol id="i-receipt" viewBox="0 0 24 24"><path d="M6 3.6h12v16.8l-2.4-1.6-2.4 1.6-2.4-1.6-2.4 1.6Z"/><path d="M9.4 8.6h5.2M9.4 12.6h5.2"/></symbol>
<symbol id="i-card" viewBox="0 0 24 24"><rect x="2.6" y="5" width="18.8" height="14" rx="2.4"/><path d="M2.6 9.8h18.8"/><path d="M6.2 14.6h3.6"/></symbol>
<symbol id="i-wallet" viewBox="0 0 24 24"><rect x="6.6" y="2.6" width="10.8" height="18.8" rx="2.4"/><path d="M10.6 5.7h2.8M11 18.4h2"/></symbol>
<symbol id="i-bank" viewBox="0 0 24 24"><path d="M3.6 9.6 12 4.2l8.4 5.4"/><path d="M5.8 10.8v6.8M10 10.8v6.8M14 10.8v6.8M18.2 10.8v6.8"/><path d="M3.6 20.2h16.8"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12.5 4.6 4.5L19 7.5"/></symbol>
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24"><path d="M20 12H8.5"/><path d="m12.5 7-5 5 5 5"/><path d="M4 7v10"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 4.4v11.8"/><path d="m7.6 11.8 4.4 4.4 4.4-4.4"/><path d="M4.6 15v3.4a2 2 0 0 0 2 2h10.8a2 2 0 0 0 2-2V15"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M12 6.9v5.4l3.4 2"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20.1 12a8.1 8.1 0 1 1-2.6-6"/><path d="M20.3 4.3v4.4h-4.4"/></symbol>
<symbol id="i-news" viewBox="0 0 24 24"><path d="M17.5 20.4H5.2a2 2 0 0 1-2-2V5.6a2 2 0 0 1 2-2h12.3v14.8a2 2 0 0 0 2 2 2 2 0 0 0 2-2V9.4h-4"/><path d="M6.6 7.6h7.4M6.6 11.4h7.4M6.6 15.2h4.6"/></symbol>
<symbol id="i-image" viewBox="0 0 24 24"><rect x="3.2" y="4.6" width="17.6" height="14.8" rx="2.2"/><circle cx="8.6" cy="9.8" r="1.7"/><path d="m3.8 17.2 4.9-4.4a1.8 1.8 0 0 1 2.4 0l4.2 3.8"/><path d="m13.6 13.4 2.2-1.9a1.8 1.8 0 0 1 2.4 0l2.4 2.1"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14.3A8.6 8.6 0 0 1 9.7 3.5a8.6 8.6 0 1 0 10.8 10.8Z"/></symbol>
<symbol id="i-swim" viewBox="0 0 24 24"><circle cx="15.6" cy="6.4" r="2"/><path d="M4.6 13.2 9.2 10l4 3.1"/><path d="M2.6 17.9c1.7 0 1.7 1.5 3.5 1.5s1.7-1.5 3.5-1.5 1.7 1.5 3.5 1.5 1.7-1.5 3.5-1.5 1.7 1.5 3.4 1.5"/></symbol>
<symbol id="i-football" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="m12 7.6 3.5 2.5-1.3 4.1H9.8l-1.3-4.1Z"/><path d="M12 3.6v4M19.6 9.7l-3.8.4M17 18.9l-2.8-4.7M7 18.9l2.8-4.7M4.4 9.7l3.8.4"/></symbol>
<symbol id="i-karate" viewBox="0 0 24 24"><path d="M3.2 9.4h17.6v4H3.2z"/><path d="m10 13.6-1.4 6.2 3.4-2.2 3.4 2.2-1.4-6.2"/></symbol>
<symbol id="i-tennis" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M6.1 5.6c2.5 2 3.8 4.6 3.8 7.9 0 1.6-.3 3.2-1 4.7"/><path d="M17.9 5.6c-2.5 2-3.8 4.6-3.8 7.9 0 1.6.3 3.2 1 4.7"/></symbol>
<symbol id="i-gym" viewBox="0 0 24 24"><circle cx="12" cy="4.8" r="2.1"/><path d="M12 7.3v6.3"/><path d="m6.6 9.6 5.4 1.6 5.4-1.6"/><path d="m9.1 20.4 2.9-6.8 2.9 6.8"/></symbol>
<symbol id="i-flag-eg" viewBox="0 0 24 16">
<rect x="0" y="0" width="24" height="5.33" fill="#CE1126"/>
<rect x="0" y="5.33" width="24" height="5.34" fill="#F5F5F5"/>
<rect x="0" y="10.67" width="24" height="5.33" fill="#0B0B0B"/>
</symbol>
</defs></svg>
<div id="app"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
</body>
</html>
/* ==========================================================================
Interactive prototype — router, state and screens.
Hash routed so any screen is linkable (#/dues) and the proposal page can
drive it over postMessage. State is in-memory: a demo session mutates it
(paying clears dues, subscribing adds an invoice) and a reload restores
the scripted starting point, which is what a demo wants.
========================================================================== */
(function () {
'use strict';
// ---------- helpers ----------
const $ = (s, r = document) => r.querySelector(s);
const fmt = n => Number(n).toLocaleString('en-US');
const ico = (n, cls = '') => `<svg class="ico ${cls}"><use href="#${n}"/></svg>`;
const app = $('#app');
const clone = o => JSON.parse(JSON.stringify(o));
let S = clone(window.DB); // live demo state
let stack = []; // route history for back-direction
let current = null;
// ---------- theme ----------
function theme(next) {
const t = next || localStorage.getItem('sayd-theme') || 'dark';
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('sayd-theme', t);
}
function toggleTheme() {
theme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
render();
}
// ---------- derived ----------
const totalDue = () => S.dues.reduce((a, d) => a + d.amount, 0);
const invLeft = () => S.invites.free + S.invites.bought - S.invites.used;
const invTotal = () => S.invites.free + S.invites.bought;
const actById = id => S.activities.find(a => a.id === id);
const unreadCount= () => S.notifications.filter(n => n.unread).length;
// ---------- chrome ----------
function statusbar(light) {
return `<div class="sbar">
<span class="num">9:41</span>
<span style="display:flex;align-items:center;gap:6px;">
<span class="dots"><i></i><i></i><i></i></span><span class="bat"></span>
</span>
</div>`;
}
function header(o = {}) {
const { back, title, sub, right = '' } = o;
if (back) {
return `<div class="hd">
<button class="hd-btn" data-back>${ico('i-chev')}</button>
<div style="flex:1;min-width:0;">
<h1 style="font-size:.98rem" class="tr">${title || ''}</h1>
${sub ? `<div class="sub tr">${sub}</div>` : ''}
</div>${right}
</div>`;
}
return `<div class="hd">
<button class="hd-btn plain" data-go="#/notifications" style="position:relative">
${ico('i-bell')}${unreadCount() ? '<span class="hd-badge"></span>' : ''}
</button>
<div style="flex:1;min-width:0;text-align:end">
<h1 style="font-size:.92rem" class="tr">${title || 'نادي الصيد المصري'}</h1>
${sub ? `<div class="sub tr">${sub}</div>` : ''}
</div>
<img src="../assets/club-logo.png" alt="" style="height:34px;flex-shrink:0">
</div>`;
}
function tabbar(active) {
const t = [
['home', 'i-home', 'الرئيسية'],
['activities', 'i-activity', 'الأنشطة'],
['news', 'i-news', 'الأخبار'],
['qr', 'i-gate', 'الدخول'],
['profile', 'i-user', 'حسابي']
];
return `<nav class="tabbar">${t.map(([r, i, l]) => `
<button data-go="#/${r}" class="${active === r ? 'on' : ''}">
${ico(i)}<span>${l}</span><span class="dot"></span>
</button>`).join('')}</nav>`;
}
// ---------- overlays ----------
function toast(msg, icon = 'i-check') {
const old = $('.toast'); if (old) old.remove();
const t = document.createElement('div');
t.className = 'toast';
t.innerHTML = `${ico(icon)}<span>${msg}</span>`;
app.appendChild(t);
setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 300); }, 2400);
}
function closeSheet() {
const s = $('.sheet'), c = $('.scrim');
if (s) { s.style.animation = 'sheetUp .25s var(--ease) reverse both'; setTimeout(() => s.remove(), 240); }
if (c) c.remove();
}
function sheet(html, onMount) {
closeSheet();
const scrim = document.createElement('div');
scrim.className = 'scrim'; scrim.onclick = closeSheet;
const el = document.createElement('div');
el.className = 'sheet';
el.innerHTML = `<div class="grab"></div>${html}`;
app.appendChild(scrim); app.appendChild(el);
bindAll(el);
if (onMount) onMount(el);
}
// ---------- QR bitmap ----------
function qrCells(seed) {
const n = 21; let s = seed || 7; const out = [];
for (let i = 0; i < n * n; i++) {
s = (s * 1103515245 + 12345) & 0x7fffffff;
const c = i % n, r = (i / n) | 0;
const finder = (c < 7 && r < 7) || (c > n - 8 && r < 7) || (c < 7 && r > n - 8);
let on;
if (finder) {
const cc = c > n - 8 ? c - (n - 7) : c, rr = r > n - 8 ? r - (n - 7) : r;
const ring = Math.max(Math.abs(cc - 3), Math.abs(rr - 3));
on = ring !== 2;
} else on = (s % 100) > 47;
out.push(on);
}
return out.map(o => `<i class="${o ? '' : 'off'}"></i>`).join('');
}
// ==========================================================
// SCREENS
// ==========================================================
const SCREENS = {};
SCREENS.splash = () => ({
body: `<div style="flex:1;display:grid;place-items:center;text-align:center;
background:linear-gradient(165deg,#0C2247,#070F1E);">
<div class="anim-fade">
<img src="../assets/club-logo.png" style="height:120px;margin:0 auto 20px;
filter:drop-shadow(0 14px 34px rgba(0,0,0,.6))">
<h2 style="color:#fff;font-size:1.2rem">نادي الصيد المصري</h2>
<p style="color:#8FA6C4;font-size:.8rem;margin-top:4px">تطبيق الأعضاء</p>
<div class="spin" style="margin-top:26px;width:26px;height:26px;border-width:2.4px"></div>
</div></div>`,
mount() { setTimeout(() => { if (current === 'splash') go('#/login', true); }, 1300); }
});
SCREENS.login = () => {
let val = '';
return {
body: `${statusbar()}<div class="screen-body pad" style="display:flex;flex-direction:column">
<div class="brandwrap">
<img src="../assets/club-logo.png" alt="">
<h2>أهلًا بك في نادي الصيد</h2>
<p>أدخل رقم الموبايل المسجَّل في بيانات عضويتك<br>وسيصلك كود تحقق برسالة قصيرة</p>
</div>
<div class="phone-in">
<span class="flag">${ico('i-flag-eg', 'flagsvg')}</span>
<span class="cc">+20</span><span class="sep"></span>
<span class="val" id="pv"><span class="cursor"></span></span>
</div>
<p style="font-size:.7rem;color:var(--txt-3);text-align:center">
جرّب الرقم المسجَّل: <b class="num">100 123 4567</b></p>
<div class="keypad" id="kp">
${[1,2,3,4,5,6,7,8,9].map(n => `<button data-k="${n}">${n}</button>`).join('')}
<button class="fn" data-k="fill">تعبئة</button>
<button data-k="0">0</button>
<button class="fn" data-k="del">${ico('i-back')}</button>
</div>
<div style="padding-top:14px">
<button class="btn btn-gold" id="send" disabled>إرسال كود التحقق</button>
<p style="font-size:.66rem;color:var(--txt-3);text-align:center;margin-top:12px;line-height:1.7">
بالدخول أنت توافق على لائحة النادي وشروط استخدام التطبيق</p>
</div></div>`,
mount(root) {
const pv = $('#pv', root), send = $('#send', root);
const paint = () => {
const d = val.replace(/(\d{3})(\d{3})(\d{0,4})/, '$1 $2 $3').trim();
pv.innerHTML = `${d}<span class="cursor"></span>`;
send.disabled = val.length !== 10;
};
$('#kp', root).addEventListener('click', e => {
const b = e.target.closest('[data-k]'); if (!b) return;
const k = b.dataset.k;
if (k === 'del') val = val.slice(0, -1);
else if (k === 'fill') val = '1001234567';
else if (val.length < 10) val += k;
paint();
});
send.onclick = () => go('#/otp');
paint();
}
};
};
SCREENS.otp = () => {
let code = '';
return {
body: `${statusbar()}${header({ back: true, title: 'كود التحقق' })}
<div class="screen-body pad" style="display:flex;flex-direction:column">
<div style="text-align:center;padding-top:8px">
<div class="ic-box" style="width:56px;height:56px;margin:0 auto 14px;border-radius:18px">
${ico('i-mail')}</div>
<h2 style="font-size:1.05rem">أدخل الكود المرسل</h2>
<p style="font-size:.78rem;color:var(--txt-3);margin-top:5px">
أرسلنا كودًا من ٤ أرقام إلى <b class="num">+20 100 123 4567</b></p>
</div>
<div class="otp" id="otp">${[0,1,2,3].map(() => '<i></i>').join('')}</div>
<p style="font-size:.72rem;color:var(--txt-3);text-align:center">
لم يصلك الكود؟ إعادة الإرسال خلال <b class="num" id="cd">00:23</b></p>
<div class="keypad" id="kp">
${[1,2,3,4,5,6,7,8,9].map(n => `<button data-k="${n}">${n}</button>`).join('')}
<button class="fn" data-k="fill">تعبئة</button>
<button data-k="0">0</button>
<button class="fn" data-k="del">${ico('i-back')}</button>
</div>
<div style="padding-top:14px">
<button class="btn btn-gold" id="ok" disabled>تأكيد ودخول</button></div></div>`,
mount(root) {
const cells = [...root.querySelectorAll('#otp i')], ok = $('#ok', root);
const paint = () => {
cells.forEach((c, i) => { c.textContent = code[i] || ''; c.classList.toggle('fill', !!code[i]); });
ok.disabled = code.length !== 4;
};
$('#kp', root).addEventListener('click', e => {
const b = e.target.closest('[data-k]'); if (!b) return;
const k = b.dataset.k;
if (k === 'del') code = code.slice(0, -1);
else if (k === 'fill') code = '4921';
else if (code.length < 4) code += k;
paint();
if (code.length === 4) setTimeout(() => go('#/home', true), 260);
});
ok.onclick = () => go('#/home', true);
let t = 23; const cd = $('#cd', root);
const iv = setInterval(() => {
if (!document.body.contains(cd)) return clearInterval(iv);
t--; cd.textContent = '00:' + String(Math.max(t, 0)).padStart(2, '0');
if (t <= 0) { clearInterval(iv); cd.parentElement.innerHTML = '<b style="color:var(--gold)">إعادة إرسال الكود</b>'; }
}, 1000);
paint();
}
};
};
SCREENS.home = () => {
const due = totalDue(), late = S.dues.some(d => d.state === 'late');
return {
tab: 'home',
body: `${statusbar()}${header({ title: 'صباح الخير، ' + S.member.short, sub: S.member.type })}
<div class="screen-body pad">
<div class="mcard"><div class="in">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
<div><div class="who">${S.member.name}</div>
<div class="meta">${S.member.type} — سارية حتى <span class="num">${S.member.validUntil}</span></div></div>
<span class="chip ok">سارية</span></div>
<div class="idno num">${S.member.id}</div>
</div></div>
${due > 0 ? `
<div class="card" style="border-color:rgba(224,85,107,.34)">
<div class="card-t"><h3>إجمالي المستحق عليك الآن</h3>
${late ? '<span class="chip due">متأخر</span>' : ''}</div>
<div style="font-family:var(--f-head);font-size:1.85rem;font-weight:900;color:var(--rose);line-height:1.15">
<span class="num">${fmt(due)}</span> <span style="font-size:.8rem;color:var(--txt-3)">ج.م</span></div>
<p style="font-size:.72rem;color:var(--txt-3);margin:2px 0 12px">
${S.dues.length} بنود مستحقة</p>
<button class="btn btn-gold" data-go="#/dues">عرض المستحقات والسداد</button>
</div>` : `
<div class="card" style="border-color:rgba(22,185,129,.34);text-align:center">
<div class="tick-big" style="width:56px;height:56px;margin-bottom:10px">${ico('i-check')}</div>
<h3 style="font-size:.95rem">لا مستحقات عليك</h3>
<p style="font-size:.75rem;color:var(--txt-3);margin-top:3px">حسابك مسدَّد بالكامل</p>
</div>`}
<div class="grid2" style="margin-bottom:12px">
<button class="qa" data-go="#/qr">${ico('i-gate')}<span>كود الدخول</span></button>
<button class="qa" data-go="#/invites">${ico('i-mail')}<span>الدعوات</span></button>
<button class="qa" data-go="#/activities">${ico('i-activity')}<span>الأنشطة</span></button>
<button class="qa" data-go="#/receipts">${ico('i-receipt')}<span>الإيصالات</span></button>
</div>
<div class="card">
<div class="card-t"><h3>اشتراكات أبنائك</h3>
<span class="more" data-go="#/activities">الكل</span></div>
${S.subs.length ? S.subs.map(s => { const a = actById(s.actId); return `
<div class="row" data-go="#/schedule">
<div class="ic-box sky">${ico(a ? a.icon : 'i-activity')}</div>
<div class="l"><b>${s.kid}</b><span>${a ? a.name : ''}${s.group}</span></div>
<div class="r"><span class="chip ${s.state === 'نشط' ? 'ok' : 'soon'}">${s.state}</span></div>
</div>`; }).join('') : '<p style="font-size:.78rem;color:var(--txt-3)">لا اشتراكات حالية</p>'}
</div>
<div class="card">
<div class="card-t"><h3>رصيد الدعوات هذا الشهر</h3>
<span class="more" data-go="#/invites">إدارة</span></div>
<div style="display:flex;align-items:center;gap:12px">
<div class="bar" style="flex:1"><i style="width:${(invLeft() / invTotal()) * 100}%"></i></div>
<b style="font-size:.82rem"><span class="num">${invLeft()}</span> من <span class="num">${invTotal()}</span></b>
</div>
</div>
<div class="card-t" style="margin:18px 0 10px"><h3 style="font-size:.9rem">آخر أخبار النادي</h3>
<span class="more" data-go="#/news">كل الأخبار</span></div>
${S.news.slice(0, 2).map(a => `
<div class="ncard row-style" data-go="#/article/${a.id}">
${newsArt(a.tone)}
<div class="nbody">
<div class="nmeta"><span class="chip info">${a.cat}</span><span>${a.date}</span></div>
<b>${a.title}</b>
</div></div>`).join('')}
</div>${tabbar('home')}`
};
};
SCREENS.dues = () => {
let sel = new Set(S.dues.map(d => d.id));
return {
tab: 'home',
body: `${statusbar()}${header({ back: true, title: 'المستحقات', sub: `${S.dues.length} بنود` })}
<div class="screen-body pad" id="db"></div>
<div id="pb"></div>`,
mount(root) {
const box = $('#db', root), bar = $('#pb', root);
const paint = () => {
if (!S.dues.length) {
box.innerHTML = `<div class="empty">${ico('i-check')}<p>لا مستحقات على حسابك</p></div>`;
bar.innerHTML = ''; return;
}
box.innerHTML = S.dues.map(d => `
<div class="due ${sel.has(d.id) ? 'sel' : ''} ${d.state === 'soon' ? 'soon' : ''}" data-id="${d.id}">
<div class="box">${ico('i-check')}</div>
<div class="mid"><b>${d.title}</b><span>${d.sub}</span>
<span>تاريخ الاستحقاق: <span class="num">${d.due}</span></span></div>
<div style="text-align:end">
<div class="val" style="color:${d.state === 'late' ? 'var(--rose)' : 'var(--txt)'}">
<span class="num">${fmt(d.amount)}</span></div>
<span class="chip ${d.state === 'late' ? 'due' : 'soon'}" style="margin-top:5px">
${d.state === 'late' ? 'متأخر' : 'يستحق قريبًا'}</span></div>
</div>`).join('');
const tot = S.dues.filter(d => sel.has(d.id)).reduce((a, d) => a + d.amount, 0);
bar.innerHTML = tot ? `<div class="paybar">
<div class="t"><span>إجمالي المحدد (${sel.size})</span>
<b><span class="num">${fmt(tot)}</span> ج.م</b></div>
<button class="btn btn-gold" data-pay>سدّد الآن</button></div>` : '';
box.querySelectorAll('.due').forEach(el => el.onclick = () => {
const id = el.dataset.id;
sel.has(id) ? sel.delete(id) : sel.add(id);
paint();
});
const pay = $('[data-pay]', bar);
if (pay) pay.onclick = () => { PAY = [...sel]; go('#/pay'); };
};
paint();
}
};
};
let PAY = []; // ids being paid
SCREENS.pay = () => {
if (!PAY.length) PAY = S.dues.map(d => d.id); // deep-linked: default to everything due
const items = S.dues.filter(d => PAY.includes(d.id));
const tot = items.reduce((a, d) => a + d.amount, 0);
let method = 'card';
const methods = [
['card', 'i-card', 'بطاقة بنكية', 'Visa · Mastercard · Meeza'],
['wallet', 'i-wallet', 'محفظة إلكترونية', 'فودافون كاش · إنستاباي'],
['desk', 'i-bank', 'الدفع في خزنة النادي', 'يصدر كود سداد يقدَّم للخزنة']
];
return {
body: `${statusbar()}${header({ back: true, title: 'إتمام السداد' })}
<div class="screen-body pad">
<div class="mcard" style="background:linear-gradient(150deg,#0C2247,#123566)"><div class="in">
<div style="font-size:.74rem;opacity:.75">المبلغ المطلوب سداده</div>
<div style="font-family:var(--f-head);font-size:2.1rem;font-weight:900;color:var(--gold-2);line-height:1.2">
<span class="num">${fmt(tot)}</span> <span style="font-size:.82rem;color:#C0D2EA">ج.م</span></div>
<div style="font-size:.7rem;opacity:.7;margin-top:2px">
${items.length} بنود — عضوية <span class="num">${S.member.id}</span></div>
</div></div>
<div class="sec-t">البنود</div>
<div class="card">${items.map(d => `
<div class="row"><div class="l"><b>${d.title}</b><span>${d.sub}</span></div>
<div class="r"><b class="num">${fmt(d.amount)}</b></div></div>`).join('')}</div>
<div class="sec-t">اختر وسيلة الدفع</div>
<div id="ms">${methods.map(([k, i, t, s]) => `
<div class="card" data-m="${k}" style="display:flex;align-items:center;gap:12px;cursor:pointer">
<div class="ic-box">${ico(i)}</div>
<div style="flex:1"><b style="font-size:.86rem;display:block">${t}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${s}</span></div>
<div class="mk" style="width:21px;height:21px;border-radius:50%;border:1.8px solid var(--line-2);
display:grid;place-items:center;color:#2A2003"></div>
</div>`).join('')}</div>
<button class="btn btn-gold" id="doPay" style="margin-top:6px">
ادفع <span class="num">${fmt(tot)}</span> ج.م</button>
<p style="font-size:.68rem;color:var(--txt-3);text-align:center;margin-top:10px">
نموذج تجريبي — لا تُنفَّذ أي عملية دفع حقيقية</p>
</div>`,
mount(root) {
const paint = () => root.querySelectorAll('[data-m]').forEach(c => {
const on = c.dataset.m === method;
c.style.borderColor = on ? 'var(--gold)' : 'var(--line)';
c.querySelector('.mk').style.background = on ? 'var(--gold)' : 'transparent';
c.querySelector('.mk').style.borderColor = on ? 'var(--gold)' : 'var(--line-2)';
c.querySelector('.mk').innerHTML = on ? ico('i-check', 'tiny') : '';
});
root.querySelectorAll('[data-m]').forEach(c => c.onclick = () => { method = c.dataset.m; paint(); });
paint();
$('#doPay', root).onclick = () => { PAYM = method; go('#/paying'); };
}
};
};
let PAYM = 'card';
SCREENS.paying = () => ({
body: `<div style="flex:1;display:grid;place-items:center;text-align:center;padding:24px">
<div><div class="spin"></div>
<h3 style="margin-top:18px;font-size:1rem">جارٍ تنفيذ العملية…</h3>
<p style="font-size:.78rem;color:var(--txt-3);margin-top:5px">لا تغلق التطبيق</p></div></div>`,
mount() {
setTimeout(() => {
const items = S.dues.filter(d => PAY.includes(d.id));
const tot = items.reduce((a, d) => a + d.amount, 0);
const label = { card: 'بطاقة بنكية', wallet: 'محفظة إلكترونية', desk: 'خزنة النادي' }[PAYM];
S.receipts.unshift({
id: 'r' + Date.now(), title: items.map(i => i.title).join(' + '),
amount: tot, date: '2026/03/12', method: label
});
S.dues = S.dues.filter(d => !PAY.includes(d.id));
S.notifications.unshift({ id: 'n' + Date.now(), title: 'تم استلام دفعتك',
body: `وصل سداد ${fmt(tot)} ج.م بنجاح.`, when: 'الآن', kind: 'ok', unread: true });
LAST = { tot, label, n: items.length };
go('#/success', true);
}, 1500);
}
});
let LAST = null;
SCREENS.success = () => ({
body: `<div class="screen-body pad" style="display:flex;flex-direction:column;justify-content:center;text-align:center">
<div class="tick-big">${ico('i-check')}</div>
<h2 style="font-size:1.2rem">تم السداد بنجاح</h2>
<p style="font-size:.82rem;color:var(--txt-3);margin-top:6px">
سُدِّد <b class="num">${LAST ? fmt(LAST.tot) : 0}</b> ج.م عبر ${LAST ? LAST.label : ''}</p>
<div class="card" style="margin-top:22px;text-align:start">
<div class="row"><div class="l"><b>رقم الإيصال</b></div>
<div class="r"><b class="num">2026${Math.floor(Math.random() * 9000 + 1000)}</b></div></div>
<div class="row"><div class="l"><b>عدد البنود</b></div>
<div class="r"><b class="num">${LAST ? LAST.n : 0}</b></div></div>
<div class="row"><div class="l"><b>الحالة</b></div>
<div class="r"><span class="chip ok">مسدَّد</span></div></div>
</div>
<p style="font-size:.74rem;color:var(--txt-3);margin:14px 0 18px">
تحدَّث رصيدك فورًا، ووصلك الإيصال داخل التطبيق.</p>
<button class="btn btn-gold" data-go="#/receipts">عرض الإيصال</button>
<div style="height:10px"></div>
<button class="btn btn-ghost" data-go="#/home">العودة إلى الرئيسية</button>
</div>`
});
SCREENS.receipts = () => ({
tab: 'home',
body: `${statusbar()}${header({ back: true, title: 'الإيصالات', sub: `${S.receipts.length} إيصال` })}
<div class="screen-body pad">
${S.receipts.length ? S.receipts.map(r => `
<div class="card" style="margin-bottom:10px">
<div class="row" style="padding-top:0">
<div class="ic-box mint">${ico('i-receipt')}</div>
<div class="l"><b>${r.title}</b><span>${r.method} — <span class="num">${r.date}</span></span></div>
<div class="r"><b class="num" style="font-family:var(--f-head)">${fmt(r.amount)}</b>
<div style="font-size:.66rem;color:var(--txt-3)">ج.م</div></div>
</div>
<button class="btn btn-ghost btn-sm" style="width:100%;margin-top:4px"
data-toast="نموذج تجريبي — التحميل غير مفعَّل">${ico('i-download')} تحميل PDF</button>
</div>`).join('')
: `<div class="empty">${ico('i-receipt')}<p>لا إيصالات بعد</p></div>`}
</div>`
});
SCREENS.qr = () => ({
tab: 'qr',
body: `${statusbar()}${header({ title: 'كود الدخول', sub: 'وجّه الكود إلى قارئ البوابة' })}
<div class="screen-body pad">
<div class="qr-wrap"><div class="qr-card">
<div class="qr-grid" id="qg">${qrCells(7)}</div></div></div>
<div style="text-align:center;margin-top:14px">
<div class="num" style="font-family:var(--f-head);letter-spacing:.2em;color:var(--gold);font-size:1rem">
${S.member.id}</div>
<div style="font-size:.76rem;color:var(--txt-2);margin-top:2px">${S.member.name}${S.member.type}</div>
<div class="chip ok" style="margin-top:12px">${ico('i-check')} العضوية سارية — الدخول مسموح</div>
</div>
<div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-top:16px">
<svg class="ring" width="34" height="34" viewBox="0 0 34 34">
<circle class="bg" cx="17" cy="17" r="14"/>
<circle class="fg" cx="17" cy="17" r="14" id="rg"
stroke-dasharray="87.9" stroke-dashoffset="0"/></svg>
<span style="font-size:.74rem;color:var(--txt-3)">
يتجدد الكود خلال <b class="num" id="qt">60</b> ثانية</span>
</div>
<div class="card" style="margin-top:18px">
<div class="card-t"><h3>آخر عمليات الدخول</h3></div>
${S.entries.map(e => `<div class="row"><div class="l"><b>${e.gate}</b></div>
<div class="r"><span style="font-size:.73rem;color:var(--txt-3)" class="num">${e.when}</span></div></div>`).join('')}
</div>
</div>${tabbar('qr')}`,
mount(root) {
let t = 60; const g = $('#qg', root), lbl = $('#qt', root), rg = $('#rg', root);
const iv = setInterval(() => {
if (!document.body.contains(g)) return clearInterval(iv);
t--;
if (t <= 0) { t = 60; g.innerHTML = qrCells(Math.floor(Math.random() * 9999)); toast('تم تجديد الكود'); }
lbl.textContent = t;
rg.setAttribute('stroke-dashoffset', String(87.9 * (1 - t / 60)));
}, 1000);
}
});
SCREENS.invites = () => ({
tab: 'qr',
body: `${statusbar()}${header({ back: true, title: 'الدعوات', sub: 'رصيد مارس 2026' })}
<div class="screen-body pad">
<div class="card" style="text-align:center">
<div class="inv-ring">
<svg class="ring" width="132" height="132" viewBox="0 0 132 132">
<circle class="bg" cx="66" cy="66" r="58" stroke-width="8"/>
<circle class="fg" cx="66" cy="66" r="58" stroke-width="8"
stroke-dasharray="364" stroke-dashoffset="${364 * (1 - invLeft() / invTotal())}"/></svg>
<div class="n"><div><span class="num">${invLeft()}</span>
<small>من <span class="num">${invTotal()}</span> دعوة</small></div></div>
</div>
<p style="font-size:.72rem;color:var(--txt-3)">يتجدد الرصيد أول كل شهر ميلادي حسب نوع عضويتك</p>
<div style="height:14px"></div>
<button class="btn btn-gold" id="issue" ${invLeft() ? '' : 'disabled'}>إصدار دعوة جديدة</button>
<div style="height:9px"></div>
<button class="btn btn-ghost" id="buy">شراء دعوات إضافية</button>
</div>
<div class="card">
<div class="card-t"><h3>دعوات هذا الشهر</h3></div>
${S.issued.length ? S.issued.map(i => `
<div class="row"><div class="ic-box">${ico('i-mail')}</div>
<div class="l"><b>${i.name}</b><span class="num">${i.date}</span></div>
<div class="r"><span class="chip ${i.state === 'دخل' ? 'ok' : 'info'}">${i.state}</span></div></div>`).join('')
: '<p style="font-size:.78rem;color:var(--txt-3)">لم تُصدر دعوات بعد</p>'}
</div>
</div>${tabbar('qr')}`,
mount(root) {
$('#issue', root).onclick = () => sheet(`
<h3>إصدار دعوة</h3><p class="sub">تُرسل الدعوة برسالة نصية وتسري ليوم واحد</p>
<div class="card"><div class="row"><div class="l"><b>الرصيد المتاح</b></div>
<div class="r"><b class="num">${invLeft()}</b></div></div>
<div class="row"><div class="l"><b>الصلاحية</b></div><div class="r"><b>يوم واحد</b></div></div></div>
<button class="btn btn-gold" id="cfm">تأكيد الإصدار</button>`,
el => $('#cfm', el).onclick = () => {
S.invites.used++;
S.issued.unshift({ id: 'i' + Date.now(), name: 'دعوة مفتوحة', date: '12/03', state: 'سارية اليوم' });
closeSheet(); render(); toast('صدرت الدعوة وأُرسلت برسالة نصية');
});
$('#buy', root).onclick = () => {
let q = 1;
sheet(`<h3>شراء دعوات إضافية</h3>
<p class="sub">دعوة بالغ <b class="num">${S.invites.priceAdult}</b> ج.م — الحد الأقصى
<b class="num">${S.invites.maxBuy}</b> شهريًا</p>
<div class="stepper"><button id="mn">−</button>
<div class="v num" id="qv">1</div><button id="pl">+</button></div>
<div class="card"><div class="row"><div class="l"><b>الإجمالي</b></div>
<div class="r"><b class="num" id="tv">${S.invites.priceAdult}</b> ج.م</div></div></div>
<button class="btn btn-gold" id="cfm">ادفع واشترِ</button>`,
el => {
const qv = $('#qv', el), tv = $('#tv', el);
const up = () => { qv.textContent = q; tv.textContent = fmt(q * S.invites.priceAdult); };
$('#mn', el).onclick = () => { q = Math.max(1, q - 1); up(); };
$('#pl', el).onclick = () => { q = Math.min(S.invites.maxBuy, q + 1); up(); };
$('#cfm', el).onclick = () => {
S.invites.bought += q; closeSheet(); render();
toast(`أُضيفت ${q} دعوة إلى رصيدك`);
};
});
};
}
});
SCREENS.activities = () => {
let cat = 'الكل';
return {
tab: 'activities',
body: `${statusbar()}${header({ title: 'الأنشطة الرياضية', sub: 'الأسعار معلنة — الاشتراك من التطبيق' })}
<div class="screen-body pad">
<div class="chips" id="cats"></div><div id="list"></div>
</div>${tabbar('activities')}`,
mount(root) {
const cats = ['الكل', ...new Set(S.activities.map(a => a.cat))];
const cbox = $('#cats', root), list = $('#list', root);
const paint = () => {
cbox.innerHTML = cats.map(c => `<button class="${c === cat ? 'on' : ''}" data-c="${c}">${c}</button>`).join('');
cbox.querySelectorAll('[data-c]').forEach(b => b.onclick = () => { cat = b.dataset.c; paint(); });
const items = S.activities.filter(a => cat === 'الكل' || a.cat === cat);
list.innerHTML = items.map(a => {
const full = a.taken >= a.cap;
return `<div class="card" data-go="#/activity/${a.id}"
style="display:flex;align-items:center;gap:12px;cursor:pointer;${full ? 'opacity:.62' : ''}">
<div class="ic-box ${a.cat === 'سباحة' ? 'sky' : a.cat === 'كرة' ? 'mint' : ''}">${ico(a.icon)}</div>
<div style="flex:1;min-width:0">
<b style="font-size:.87rem;display:block">${a.name}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${a.age} · <span class="num">${a.sess}</span> حصة/شهر</span>
${full ? '<span class="chip due" style="margin-top:5px">اكتمل العدد — قائمة انتظار</span>' : ''}
</div>
<div style="text-align:end">
<b style="font-family:var(--f-head);font-size:1rem" class="num">${fmt(a.price)}</b>
<div style="font-size:.64rem;color:var(--txt-3)">ج.م / شهر</div></div>
</div>`;
}).join('');
bindAll(list);
};
paint();
}
};
};
SCREENS.activity = (id) => {
const a = actById(id); if (!a) return SCREENS.activities();
const full = a.taken >= a.cap;
const already = S.subs.some(s => s.actId === a.id);
return {
body: `${statusbar()}${header({ back: true, title: a.name, sub: a.cat })}
<div class="screen-body pad">
<div class="card" style="text-align:center;padding:22px 15px">
<div class="ic-box ${a.cat === 'سباحة' ? 'sky' : a.cat === 'كرة' ? 'mint' : ''}"
style="width:64px;height:64px;border-radius:20px;margin:0 auto 12px">
${ico(a.icon, 'big')}</div>
<h2 style="font-size:1.1rem">${a.name}</h2>
<p style="font-size:.76rem;color:var(--txt-3);margin-top:4px">${a.coach}</p>
<div style="font-family:var(--f-head);font-size:1.9rem;font-weight:900;color:var(--gold);margin-top:12px">
<span class="num">${fmt(a.price)}</span>
<span style="font-size:.78rem;color:var(--txt-3)">ج.م / شهر</span></div>
</div>
<div class="card">
<div class="row"><div class="l"><b>الفئة السنية</b></div><div class="r"><b>${a.age}</b></div></div>
<div class="row"><div class="l"><b>عدد الحصص</b></div>
<div class="r"><b><span class="num">${a.sess}</span> حصة شهريًا</b></div></div>
<div class="row"><div class="l"><b>رسوم الانضمام</b></div>
<div class="r"><b><span class="num">${fmt(a.join)}</span> ج.م</b></div></div>
<div class="row"><div class="l"><b>الأماكن المتاحة</b></div>
<div class="r"><b class="${full ? '' : ''}"><span class="num">${a.cap - a.taken}</span> من <span class="num">${a.cap}</span></b></div></div>
</div>
<div class="card">
<div class="card-t"><h3>السعة</h3><span style="font-size:.72rem;color:var(--txt-3)">
<span class="num">${a.taken}/${a.cap}</span></span></div>
<div class="bar"><i style="width:${(a.taken / a.cap) * 100}%;
background:${full ? 'var(--rose)' : 'linear-gradient(90deg,var(--gold),var(--gold-2))'}"></i></div>
</div>
${already ? `<div class="card" style="text-align:center;border-color:rgba(22,185,129,.35)">
<span class="chip ok">أنت مشترك في هذا النشاط</span></div>
<button class="btn btn-ghost" data-go="#/schedule">عرض جدول التدريب</button>`
: full ? `<button class="btn btn-ghost" data-toast="أُضفت إلى قائمة الانتظار">
الانضمام لقائمة الانتظار</button>`
: `<button class="btn btn-gold" id="sub">اشترك الآن</button>`}
<div style="height:14px"></div>
</div>`,
mount(root) {
const b = $('#sub', root); if (!b) return;
b.onclick = () => {
let kid = S.member.kids[0].name;
sheet(`<h3>الاشتراك في ${a.name}</h3><p class="sub">اختر الابن المشترك</p>
<div id="kids">${S.member.kids.map(k => `
<div class="card" data-k="${k.name}" style="display:flex;align-items:center;gap:11px;cursor:pointer">
<div class="ic-box">${ico('i-user')}</div>
<b style="flex:1;font-size:.86rem">${k.name}</b>
<div class="mk" style="width:20px;height:20px;border-radius:50%;
border:1.8px solid var(--line-2);display:grid;place-items:center;color:#2A2003"></div>
</div>`).join('')}</div>
<div class="card">
<div class="row"><div class="l"><b>اشتراك الشهر الأول</b></div>
<div class="r"><b class="num">${fmt(a.price)}</b></div></div>
<div class="row"><div class="l"><b>رسوم الانضمام</b></div>
<div class="r"><b class="num">${fmt(a.join)}</b></div></div>
<div class="row"><div class="l"><b>الإجمالي المستحق</b></div>
<div class="r"><b class="num" style="color:var(--gold)">${fmt(a.price + a.join)}</b> ج.م</div></div>
</div>
<button class="btn btn-gold" id="cfm">تأكيد الاشتراك</button>`,
el => {
const paint = () => el.querySelectorAll('[data-k]').forEach(c => {
const on = c.dataset.k === kid;
c.style.borderColor = on ? 'var(--gold)' : 'var(--line)';
const m = c.querySelector('.mk');
m.style.background = on ? 'var(--gold)' : 'transparent';
m.style.borderColor = on ? 'var(--gold)' : 'var(--line-2)';
m.innerHTML = on ? ico('i-check') : '';
});
el.querySelectorAll('[data-k]').forEach(c => c.onclick = () => { kid = c.dataset.k; paint(); });
paint();
$('#cfm', el).onclick = () => {
S.subs.push({ id: 's' + Date.now(), actId: a.id, kid, group: 'مجموعة جديدة', state: 'نشط' });
a.taken++;
S.dues.push({ id: 'd' + Date.now(), title: `اشتراك ${a.name}`,
sub: `${kid} — الشهر الأول + رسوم الانضمام`, due: '2026/03/20',
amount: a.price + a.join, state: 'soon', kind: 'activity' });
closeSheet(); go('#/home', true);
setTimeout(() => toast('تم الاشتراك — صدرت فاتورة الشهر الأول'), 380);
};
});
};
}
};
};
SCREENS.schedule = () => {
const first = S.subs[0];
const sc = first ? S.schedule[first.actId] : null;
return {
tab: 'activities',
body: `${statusbar()}${header({ back: true, title: 'جدول التدريب',
sub: first ? `${first.kid}${actById(first.actId) ? actById(first.actId).name : ''}` : '' })}
<div class="screen-body pad">
${!sc ? `<div class="empty">${ico('i-clock')}<p>لا اشتراكات لعرض جدولها</p></div>` : `
<div class="card" style="display:flex;align-items:center;gap:12px">
<div class="ic-box sky">${ico('i-clock')}</div>
<div style="flex:1"><b style="font-size:.86rem;display:block">${sc.group}</b>
<span style="font-size:.71rem;color:var(--txt-3)">${sc.coach} · ${sc.place}</span></div>
<span class="chip ok">نشط</span></div>
${sc.days.map(d => `<div class="tt-day">${d.d}</div>
<div class="card" style="margin-bottom:8px">
<div class="row" style="padding:4px 0"><div class="ic-box">${ico('i-clock')}</div>
<div class="l"><b><span class="num">${d.f}</span> — <span class="num">${d.t}</span></b>
<span>${sc.place}</span></div></div>
${d.note ? `<div style="margin-top:8px;background:rgba(232,163,61,.12);border-radius:10px;
padding:9px 11px;font-size:.72rem;color:var(--amber)">ملاحظة المدرب: ${d.note}</div>` : ''}
</div>`).join('')}
${sc.changes.length ? `<div class="sec-t">تغييرات على الجدول</div><div class="card">
${sc.changes.map(c => `<div class="row"><div class="ic-box rose">${ico('i-bell')}</div>
<div class="l"><span style="font-size:.76rem;color:var(--txt-2)">${c}</span></div></div>`).join('')}
</div>` : ''}`}
</div>${tabbar('activities')}`
};
};
/* Placeholder artwork: a branded gradient plus the crest, not stock photography.
The club supplies real images at launch; inventing them here would misrepresent
what has been approved. */
function newsArt(tone, tall) {
return `<div class="nimg ${tone} ${tall ? 'tall' : ''}">
<span class="crest"></span>${ico('i-image', 'ph')}</div>`;
}
SCREENS.news = () => {
let cat = 'الكل';
return {
tab: 'news',
body: `${statusbar()}${header({ title: 'أخبار النادي', sub: 'آخر ما نُشر' })}
<div class="screen-body pad">
<div class="chips" id="cats"></div><div id="list"></div>
</div>${tabbar('news')}`,
mount(root) {
const cats = ['الكل', ...new Set(S.news.map(a => a.cat))];
const cbox = $('#cats', root), list = $('#list', root);
const paint = () => {
cbox.innerHTML = cats.map(c => `<button class="${c === cat ? 'on' : ''}" data-c="${c}">${c}</button>`).join('');
cbox.querySelectorAll('[data-c]').forEach(b => b.onclick = () => { cat = b.dataset.c; paint(); });
const items = S.news.filter(a => cat === 'الكل' || a.cat === cat);
list.innerHTML = items.map((a, i) => {
const lead = i === 0 && cat === 'الكل';
return `<div class="ncard ${lead ? 'lead' : ''}" data-go="#/article/${a.id}">
${newsArt(a.tone, lead)}
<div class="nbody">
<div class="nmeta"><span class="chip info">${a.cat}</span>
<span>${a.date}</span></div>
<b>${a.title}</b>
<span class="nlead">${a.lead}</span>
</div></div>`;
}).join('');
bindAll(list);
};
paint();
}
};
};
SCREENS.article = (id) => {
const a = S.news.find(x => x.id === id);
if (!a) return SCREENS.news();
return {
body: `${statusbar()}${header({ back: true, title: a.cat, sub: a.date })}
<div class="screen-body">
${newsArt(a.tone, true)}
<div class="pad">
<h2 style="font-size:1.15rem;line-height:1.45">${a.title}</h2>
<p style="font-size:.85rem;color:var(--txt-2);margin-top:8px;font-weight:600">${a.lead}</p>
<div style="height:1px;background:var(--line);margin:14px 0"></div>
${a.body.split('\n\n').map(par =>
`<p style="font-size:.85rem;color:var(--txt-2);line-height:1.95;margin-bottom:12px">${par}</p>`).join('')}
<button class="btn btn-ghost" data-toast="نموذج تجريبي — المشاركة غير مفعَّلة">مشاركة الخبر</button>
<div style="height:16px"></div>
</div>
</div>`
};
};
SCREENS.notifications = () => ({
body: `${statusbar()}${header({ back: true, title: 'الإشعارات',
sub: unreadCount() ? `${unreadCount()} غير مقروء` : 'لا جديد' })}
<div class="screen-body pad">
${S.notifications.map(n => {
const m = { due:['rose','i-card'], sched:['sky','i-clock'], act:['','i-activity'], ok:['mint','i-check'] }[n.kind] || ['', 'i-bell'];
return `<div class="card" style="${n.unread ? 'border-color:var(--line-2)' : 'opacity:.68'}">
<div class="row" style="padding:0;border:none;align-items:flex-start">
<div class="ic-box ${m[0]}">${ico(m[1])}</div>
<div class="l"><b>${n.title}</b>
<span style="display:block;color:var(--txt-2);margin-top:2px;line-height:1.65">${n.body}</span>
<span style="display:block;margin-top:4px">${n.when}</span></div>
${n.unread ? '<span style="width:8px;height:8px;border-radius:50%;background:var(--gold);margin-top:6px"></span>' : ''}
</div></div>`;
}).join('')}
<button class="btn btn-ghost" id="mr">تعليم الكل كمقروء</button>
<div style="height:14px"></div>
</div>`,
mount(root) {
$('#mr', root).onclick = () => { S.notifications.forEach(n => n.unread = false); render(); toast('عُلِّمت كل الإشعارات كمقروءة'); };
}
});
SCREENS.profile = () => ({
tab: 'profile',
body: `${statusbar()}${header({ title: 'حسابي', sub: S.member.type })}
<div class="screen-body pad">
<div class="mcard"><div class="in">
<div class="who">${S.member.name}</div>
<div class="meta">${S.member.type} — سارية حتى <span class="num">${S.member.validUntil}</span></div>
<div class="idno num">${S.member.id}</div>
</div></div>
<div class="card">
<div class="row" data-go="#/receipts" style="cursor:pointer">
<div class="ic-box">${ico('i-receipt')}</div>
<div class="l"><b>الإيصالات</b><span><span class="num">${S.receipts.length}</span> إيصال</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/invites" style="cursor:pointer">
<div class="ic-box">${ico('i-mail')}</div>
<div class="l"><b>الدعوات</b><span><span class="num">${invLeft()}</span> متاحة</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/notifications" style="cursor:pointer">
<div class="ic-box">${ico('i-bell')}</div>
<div class="l"><b>الإشعارات</b><span><span class="num">${unreadCount()}</span> غير مقروء</span></div>
${ico('i-chev', 'flip')}</div>
<div class="row" data-go="#/schedule" style="cursor:pointer">
<div class="ic-box">${ico('i-clock')}</div>
<div class="l"><b>جداول التدريب</b><span>اشتراكات الأبناء</span></div>
${ico('i-chev', 'flip')}</div>
</div>
<div class="card">
<div class="row"><div class="ic-box">${ico('i-moon')}</div>
<div class="l"><b>المظهر</b><span>فاتح / داكن</span></div>
<button class="btn btn-ghost btn-sm" id="th">تبديل</button></div>
<div class="row"><div class="ic-box rose">${ico('i-refresh')}</div>
<div class="l"><b>إعادة ضبط النموذج</b><span>يعيد البيانات التجريبية</span></div>
<button class="btn btn-ghost btn-sm" id="rs">إعادة</button></div>
</div>
<p style="font-size:.68rem;color:var(--txt-3);text-align:center;margin-top:6px">
نموذج تفاعلي — شركة الأركيد · 2026</p>
<div style="height:10px"></div>
</div>${tabbar('profile')}`,
mount(root) {
$('#th', root).onclick = toggleTheme;
$('#rs', root).onclick = () => { S = clone(window.DB); go('#/home', true); setTimeout(() => toast('أُعيد ضبط النموذج'), 300); };
}
});
// ==========================================================
// ROUTER
// ==========================================================
function parse() {
const h = (location.hash || '#/splash').replace(/^#\//, '');
const [name, ...rest] = h.split('/');
return { name: SCREENS[name] ? name : 'home', arg: rest.join('/') };
}
function bindAll(root) {
root.querySelectorAll('[data-go]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', e => { e.stopPropagation(); go(el.dataset.go); });
});
root.querySelectorAll('[data-back]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', back);
});
root.querySelectorAll('[data-toast]').forEach(el => {
if (el.__b) return; el.__b = 1;
el.addEventListener('click', () => toast(el.dataset.toast));
});
}
let busy = false;
function paint(dir) {
const { name, arg } = parse();
const def = SCREENS[name](arg);
current = name;
const old = $('.screen', app);
const el = document.createElement('div');
el.className = 'screen' + (def.tab ? ' has-tab' : '');
el.innerHTML = def.body;
app.appendChild(el);
bindAll(el);
if (def.mount) def.mount(el);
if (old && dir) {
busy = true;
el.classList.add(dir === 'back' ? 'anim-in-back' : 'anim-in-fwd');
old.classList.add(dir === 'back' ? 'anim-out-back' : 'anim-out-fwd');
setTimeout(() => { old.remove(); busy = false; }, 350);
} else if (old) old.remove();
try { parent.postMessage({ type: 'sayd:route', route: '#/' + name }, '*'); } catch (e) {}
}
const render = () => paint(null);
function go(hash, replace) {
if (busy) return;
const target = hash.replace(/^#/, '');
if ('#' + target === location.hash) return;
if (replace) stack = [];
else stack.push(location.hash || '#/splash');
location.hash = target;
paint('fwd');
}
function back() {
if (busy) return;
const prev = stack.pop();
if (prev) { history.replaceState(null, '', prev); paint('back'); }
else go('#/home', true);
}
window.addEventListener('hashchange', () => { if (!busy) paint(null); });
// parent (proposal page) can drive the prototype
window.addEventListener('message', e => {
const d = e.data || {};
if (d.type === 'sayd:goto') go(d.route, true);
if (d.type === 'sayd:theme') { theme(d.value); render(); }
if (d.type === 'sayd:reset') { S = clone(window.DB); go('#/home', true); }
});
// ---------- boot ----------
theme();
if (!location.hash) location.hash = '/splash';
paint(null);
window.__sayd = { go, toast, get state() { return S; } };
})();
/* Mock dataset for the prototype. Everything the screens read and mutate lives
here, so a demo session behaves like a real account: paying clears dues and
creates a receipt, subscribing adds an invoice, issuing an invite decrements
the balance. Reset from the profile screen. */
window.DB = {
member: {
id: 10245,
name: 'محمد أحمد سيد إبراهيم',
short: 'محمد أحمد',
type: 'عضو عامل',
validUntil: '2026/12/31',
phone: '100 123 4567',
kids: [
{ id: 'k1', name: 'يوسف محمد' },
{ id: 'k2', name: 'لُجين محمد' }
]
},
dues: [
{ id:'d1', title:'قسط تجديد العضوية', sub:'القسط ٢ من ٣ — فبراير 2026', due:'2026/02/28',
amount:1500, state:'late', kind:'renewal' },
{ id:'d2', title:'اشتراك سباحة مبتدئين', sub:'يوسف محمد — مارس 2026', due:'2026/03/05',
amount:900, state:'soon', kind:'activity' },
{ id:'d3', title:'غرامة تأخير سداد', sub:'عن تأخر قسط يناير', due:'2026/02/28',
amount:350, state:'late', kind:'fine' }
],
receipts: [
{ id:'r0', title:'اشتراك جمباز فني — لُجين', amount:1000, date:'2026/02/03', method:'بطاقة بنكية' },
{ id:'r1', title:'قسط تجديد العضوية — القسط ١', amount:1500, date:'2026/01/08', method:'محفظة إلكترونية' }
],
invites: { free: 4, used: 1, bought: 0, priceAdult: 150, priceChild: 75, maxBuy: 10 },
issued: [
{ id:'i1', name:'أحمد سامي', date:'15/03', state:'دخل' }
],
activities: [
{ id:'SW-01', name:'سباحة مبتدئين', cat:'سباحة', icon:'i-swim', age:'٦ - ٩ سنوات', sess:12, price:900, join:300, cap:24, taken:18, coach:'كابتن هشام صلاح' },
{ id:'SW-02', name:'سباحة متقدم', cat:'سباحة', icon:'i-swim', age:'١٠ - ١٤ سنة', sess:16, price:1200, join:300, cap:20, taken:14, coach:'كابتن هشام صلاح' },
{ id:'FB-01', name:'كرة القدم — الموهبة', cat:'كرة', icon:'i-football', age:'٨ - ١١ سنة', sess:12, price:1100, join:400, cap:28, taken:26, coach:'كابتن طارق منير' },
{ id:'KR-01', name:'كاراتيه مبتدئين', cat:'فردي', icon:'i-karate', age:'٦ - ١٢ سنة', sess:8, price:700, join:250, cap:30, taken:22, coach:'كابتن أمير فؤاد' },
{ id:'TN-01', name:'تنس أرضي — أطفال', cat:'فردي', icon:'i-tennis', age:'٧ - ١٢ سنة', sess:8, price:1400, join:350, cap:12, taken:12, coach:'كابتن نهى سامي' },
{ id:'GY-01', name:'جمباز فني — سيدات', cat:'فردي', icon:'i-gym', age:'١٢ - ١٨ سنة', sess:12, price:1000, join:300, cap:18, taken:11, coach:'كابتن دينا عادل' }
],
subs: [
{ id:'s1', actId:'SW-01', kid:'يوسف محمد', group:'مجموعة أ — صباحي', state:'نشط' },
{ id:'s2', actId:'GY-01', kid:'لُجين محمد', group:'مجموعة ب — مسائي', state:'تُجدَّد خلال ٥ أيام' }
],
schedule: {
'SW-01': { group:'مجموعة أ — صباحي', coach:'كابتن هشام صلاح', place:'حمام السباحة الأولمبي',
days:[ {d:'السبت',f:'09:00',t:'10:00'}, {d:'الاثنين',f:'09:00',t:'10:00'}, {d:'الأربعاء',f:'09:00',t:'10:00', note:'حصة قياس مستوى'} ],
changes:['أُلغيت حصة الاثنين 10/03 لصيانة حمام السباحة.','حصة تعويضية الجمعة 14/03 الساعة 10:00.'] },
'GY-01': { group:'مجموعة ب — مسائي', coach:'كابتن دينا عادل', place:'صالة الجمباز',
days:[ {d:'الأحد',f:'17:00',t:'18:30'}, {d:'الثلاثاء',f:'17:00',t:'18:30'} ], changes:[] }
},
entries: [
{ gate:'البوابة الرئيسية', when:'اليوم 08:14' },
{ gate:'بوابة حمام السباحة', when:'أمس 17:02' },
{ gate:'البوابة الرئيسية — ومعه ضيف', when:'أمس 16:58' }
],
/* Club blog. Images are described by a tone key rather than a file: the
prototype paints a branded placeholder, so the demo never ships stock
photos the club has not approved. */
news: [
{ id:'a1', cat:'بطولات', tone:'gold', date:'11 مارس 2026', title:'فريق السباحة يحصد ٧ ميداليات في بطولة الجمهورية',
lead:'حقق فريق النادي للسباحة نتائج هي الأفضل منذ خمس سنوات في بطولة الجمهورية للناشئين.',
body:'اختتمت أمس منافسات بطولة الجمهورية للناشئين، وحصد فريق نادي الصيد المصري سبع ميداليات: ثلاث ذهبيات وميداليتين فضيتين وبرونزيتين، ليحتل المركز الثاني في الترتيب العام.\n\nوجّه مجلس الإدارة الشكر إلى الجهاز الفني بقيادة كابتن هشام صلاح، وإلى أولياء الأمور على دعمهم المتواصل. وتبدأ استعدادات الفريق للبطولة العربية مطلع الشهر القادم.', pinned:true },
{ id:'a2', cat:'فعاليات', tone:'mint', date:'9 مارس 2026', title:'فتح باب التسجيل في معسكر الصيف للأطفال',
lead:'يبدأ المعسكر أول يوليو ويستمر ستة أسابيع، ويشمل السباحة وكرة القدم والتنس.',
body:'أعلنت إدارة الأنشطة الرياضية فتح باب التسجيل في معسكر الصيف للأطفال من سن ٦ إلى ١٤ سنة.\n\nيشمل المعسكر تدريبات يومية في السباحة وكرة القدم والتنس، بالإضافة إلى أنشطة ترفيهية ووجبة غداء. الأماكن محدودة والتسجيل من التطبيق مباشرة عبر قسم الأنشطة.' },
{ id:'a3', cat:'إعلانات', tone:'sky', date:'6 مارس 2026', title:'صيانة حمام السباحة الأولمبي الأسبوع القادم',
lead:'يغلق الحمام الأولمبي من الاثنين إلى الأربعاء لأعمال الصيانة الدورية.',
body:'تُجرى أعمال الصيانة الدورية لحمام السباحة الأولمبي خلال الفترة من الاثنين ١٠ مارس إلى الأربعاء ١٢ مارس.\n\nتُنقل حصص السباحة خلال هذه الأيام إلى الحمام المغطى، وستصل رسالة بالمواعيد الجديدة لكل مجموعة. نعتذر عن أي إزعاج.' },
{ id:'a4', cat:'أخبار المجلس', tone:'rose', date:'2 مارس 2026', title:'مجلس الإدارة يعتمد خطة تطوير الملاعب',
lead:'اعتمد المجلس في اجتماعه الأخير خطة تطوير ملاعب التنس وإنشاء صالة جديدة متعددة الأغراض.',
body:'اعتمد مجلس إدارة النادي خطة تطوير المنشآت الرياضية للعام الجاري، وتشمل إعادة تأهيل ملاعب التنس الأربعة وإنشاء صالة متعددة الأغراض.\n\nمن المقرر أن تبدأ الأعمال مطلع الشهر القادم وتنتهي قبل بداية الموسم الصيفي.' }
],
notifications: [
{ id:'n1', title:'قسط مستحق منذ يومين', body:'قسط تجديد العضوية ٢ من ٣ بقيمة 1,500 ج.م.', when:'اليوم 09:10', kind:'due', unread:true },
{ id:'n2', title:'حصة تعويضية', body:'أُضيفت حصة سباحة الجمعة 14/03 الساعة 10:00.', when:'أمس 18:40', kind:'sched', unread:true },
{ id:'n3', title:'تجديد اشتراك لُجين', body:'اشتراك الجمباز يُجدَّد خلال ٥ أيام.', when:'أمس 09:00', kind:'act', unread:true },
{ id:'n4', title:'تم استلام دفعتك', body:'وصل سداد 1,000 ج.م — إيصال رقم 20260203.', when:'03/02', kind:'ok', unread:false }
]
};
......@@ -561,24 +561,35 @@ footer > *{ position:relative; z-index:1; }
inside a .dark section inherited navy-on-navy headings. Raise specificity. */
.dark .card h4{ color:#fff; }
/* ================= LIVE PROTOTYPE =================
The phone now hosts the real prototype in an iframe rather than a stack of
static divs, so the screens in the proposal are the screens that ship. */
.phone iframe.screen{ width:100%; border:0; display:block; background:#070F1E; }
/* ================= REAL SCREENSHOTS =================
The phone and the browser frame now hold captures of the shipped app and
portal, not a hand-built imitation of them. */
.phone .shot{ display:block; width:100%; height:auto; border-radius:34px; background:#F4F6FA; }
/* A real capture has no status bar to hide behind, so the drawn notch would
sit on top of the screen title. */
.shot-phone::before{ display:none; }
.proto-tools{ display:flex; gap:8px; flex-wrap:wrap; margin-top:14px; }
.proto-tools .pbtn{ cursor:pointer; }
.proto-nav{ max-height:none; }
@media (max-width:1020px){
.proto-tools{ justify-content:center; }
}
/* .tbl clips rather than scrolls and its cells are nowrap, so a long Arabic
headline in a narrow column loses the status badge off the edge. Let the
title column wrap in the news table only. */
.news-tbl td:first-child{ white-space:normal; min-width:150px; line-height:1.55; }
@media (max-width:1020px){
#portal .grid[style*="1.3fr"]{ grid-template-columns:1fr !important; }
.shot-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(132px,1fr)); gap:16px; }
.shot-grid button{
display:flex; flex-direction:column; align-items:center; gap:8px; padding:8px;
background:#fff; border:1px solid var(--line); border-radius:var(--r-m);
font:inherit; cursor:pointer; transition:.18s;
}
.shot-grid button:hover{ border-color:var(--line-2); box-shadow:var(--sh-s); transform:translateY(-2px); }
.shot-grid button.on{ border-color:var(--navy); box-shadow:0 0 0 3px rgba(18,53,102,.12); }
.shot-grid button:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
.shot-grid img{ display:block; width:100%; height:auto; border-radius:14px; }
.shot-grid span{ font-family:var(--f-head); font-weight:700; font-size:.76rem; color:var(--navy); line-height:1.4; text-align:center; }
.bshot{ display:block; width:100%; height:auto; }
.shot-note{ margin-top:16px; background:#fff; border:1px solid var(--line); border-radius:var(--r-m); padding:18px 20px; }
.shot-note h4{ color:var(--navy); font-size:1rem; margin-bottom:6px; }
.shot-note p{ font-size:.88rem; color:var(--txt-2); line-height:1.85; }
/* ================= PRINT / PDF =================
The page is built for scrolling: reveals start invisible, the nav is fixed,
......@@ -596,7 +607,7 @@ footer > *{ position:relative; z-index:1; }
/* Never split an atomic block across a page boundary. */
.card,.pain,.tmpl,.pay-step,.tl-i,.faq,.kpi,.pline,.note,.drop,
.price-hero,.phone,.mcard,.stat,.fstep,.tbl,.scope-list li{ break-inside:avoid; }
.price-hero,.phone,.mcard,.stat,.fstep,.tbl,.scope-list li,.shot-grid button,figure{ break-inside:avoid; }
.faq{ break-inside:avoid; }
.faq p{ display:block !important; } /* <details> print collapsed otherwise */
......
/**
* Renders the proposal microsite to PDF.
*
* The site is built for scrolling, so two things need undoing before printing:
* the phone shows one prototype screen at a time, and the portal shows one
* admin screen at a time. On paper that would lose almost the whole prototype,
* so both are expanded into labelled grids first.
* The site is built for scrolling: the phone shows one app screenshot at a
* time and the browser frame one portal screenshot at a time. On paper that
* would hide almost every screen, so both viewers are replaced by labelled
* grids of all their screenshots before printing.
*
* node generate-pdf.mjs # from the working tree
* node generate-pdf.mjs --live # from the deployed site
......@@ -20,40 +20,12 @@ const SRC = LIVE
: 'file://' + path.join(__dirname, 'index.html');
const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf');
/* Screens worth a page in the printed prototype, in reading order. */
const APP_SCREENS = [
['home', 'الرئيسية وبطاقة العضوية'],
['dues', 'المستحقات والأقساط'],
['pay', 'السداد الإلكتروني'],
['qr', 'كود دخول البوابة'],
['invites', 'الدعوات وشراء المزيد'],
['activities', 'الأنشطة الرياضية'],
['news', 'أخبار النادي'],
['article/a1', 'صفحة الخبر'],
['schedule', 'جدول التدريب'],
];
const PORTAL_LABELS = {
dash: 'لوحة التحصيل',
import: 'رفع بيانات الأعضاء',
members: 'الأعضاء والمستحقات',
catalog: 'تسعير الأنشطة',
invites: 'سياسة الدعوات',
sched: 'جداول المدربين',
news: 'أخبار النادي',
};
const wait = ms => new Promise(r => setTimeout(r, ms));
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--allow-file-access-from-files', // the phone frames are file:// iframes
'--font-render-hinting=none',
],
args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'],
});
const page = await browser.newPage();
......@@ -63,75 +35,61 @@ const wait = ms => new Promise(r => setTimeout(r, ms));
await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 });
await page.emulateMediaType('print');
console.log('· expanding prototypes');
await page.evaluate(({ screens, labels }) => {
console.log('· expanding screenshot viewers');
await page.evaluate(() => {
document.querySelectorAll('.reveal').forEach(el => el.classList.add('in'));
const nav = document.getElementById('nav'); if (nav) nav.remove();
const tools = document.querySelector('.proto-tools'); if (tools) tools.remove();
/* --- mobile prototype: one live frame -> a grid of labelled frames --- */
const stage = document.querySelector('.phone-stage');
const protoNav = document.querySelector('.proto-nav');
const proto = document.querySelector('.proto');
if (stage && proto) {
const desc = protoNav ? protoNav.querySelector('.pn-desc') : null;
const intro = desc ? desc.outerHTML : '';
proto.style.display = 'block';
if (protoNav) protoNav.remove();
stage.outerHTML = `
<div class="print-intro">${intro}</div>
<div class="print-phones">
${screens.map(([route, label]) => `
<figure class="print-phone">
<div class="phone"><iframe class="screen" data-route="${route}"
src="app/index.html#/${route}" loading="eager"></iframe></div>
<figcaption>${label}</figcaption>
/* --- app: the single-phone viewer goes; the all-screens grid stays and
becomes the printed gallery, with its images loaded eagerly. --- */
const proto = document.querySelector('#app .proto');
if (proto) proto.remove();
document.querySelectorAll('#shot-grid img').forEach(img => { img.loading = 'eager'; });
/* --- portal: button row + single frame -> every screen, captioned --- */
const btns = [...document.querySelectorAll('.pn-item[data-pshot]')];
const shots = btns.map(b => ({ k: b.dataset.pshot, t: b.textContent.replace(/^\s*\S+\s+/, '').trim() }));
const row = btns.length ? btns[0].parentElement : null;
const frame = document.querySelector('#portal .browser');
const note = document.getElementById('pshot-note');
if (frame) {
frame.insertAdjacentHTML('beforebegin', `
<div class="print-portal">
${shots.map(s => `
<figure>
<img src="screenshots/portal/${s.k}.jpg" alt="">
<figcaption>${s.t}</figcaption>
</figure>`).join('')}
</div>`;
</div>`);
frame.remove();
}
if (row) row.remove();
if (note) note.remove();
/* --- portal: one visible screen -> all of them, stacked ---
Each .pscr already opens with its own .ptop heading, so no caption is
injected here; adding one printed the screen name twice. */
document.querySelectorAll('.pscr').forEach(scr => { scr.style.display = 'block'; });
const pnav = document.querySelector('[data-pscr]');
if (pnav && pnav.parentElement) pnav.parentElement.remove();
const side = document.querySelector('.side'); if (side) side.style.display = 'none';
const bbar = document.querySelector('.bbar'); if (bbar) bbar.style.display = 'none';
/* <details> print collapsed unless forced open */
document.querySelectorAll('details').forEach(d => d.open = true);
const css = document.createElement('style');
css.textContent = `
.print-intro{ max-width:760px; margin:0 0 26px; font-size:.9rem; color:var(--txt-2); line-height:1.85; }
.print-intro .pn-desc{ border:0; padding:0; margin:0; }
.print-phones{ display:grid; grid-template-columns:repeat(3,1fr); gap:24px 18px; }
/* 600px keeps two rows inside one 1754px page; at 660 the second row
overflowed by a few pixels and half a page was left blank. */
.print-phone{ margin:0; break-inside:avoid; text-align:center; }
.print-phone .phone{ width:100%; }
.print-phone .screen{ width:100%; height:600px; border:0; }
.print-phone figcaption{ margin-top:9px; font-family:var(--f-head); font-weight:700;
font-size:.82rem; color:var(--navy); }
.pscr{ break-inside:avoid; margin-bottom:8px; }
.bwin{ display:block; } .pmain{ padding:18px 4px; }
/* Six across keeps all seventeen screens on one page; at five the last
row spilled onto a page of its own. */
.shot-grid{ grid-template-columns:repeat(6,1fr); gap:14px 12px; }
.shot-grid button{ border:0; padding:0; box-shadow:none; }
.shot-grid img{ border:1px solid var(--line); }
.print-portal{ display:grid; grid-template-columns:repeat(2,1fr); gap:22px 18px; margin-top:26px; }
.print-portal figure{ margin:0; break-inside:avoid; }
.print-portal img{ display:block; width:100%; height:auto; border:1px solid var(--line); border-radius:10px; }
.print-portal figcaption{ margin-top:7px; font-family:var(--f-head); font-weight:700; font-size:.82rem; color:var(--navy); }
`;
document.head.appendChild(css);
}, { screens: APP_SCREENS, labels: PORTAL_LABELS });
console.log('· waiting for prototype frames');
await page.waitForFunction(() => {
const frames = [...document.querySelectorAll('.print-phone iframe')];
if (!frames.length) return false;
return frames.every(f => {
try { return !!f.contentDocument?.querySelector('#app .screen'); }
catch { return false; }
});
}, { timeout: 60000 });
});
console.log('· waiting for images');
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalWidth > 0),
{ timeout: 60000 },
);
await page.evaluate(() => document.fonts.ready);
await wait(2500); // let the in-frame reveal/transition settle
await wait(1200);
console.log('· printing');
await page.pdf({
......
......@@ -61,12 +61,11 @@
</div>
<div class="nav-links" id="navlinks">
<a href="#problem">المشكلة</a>
<a href="#scope">النطاق</a>
<a href="#scope">المزايا</a>
<a href="#app">التطبيق</a>
<a href="#portal">البوابة</a>
<a href="#templates">القوالب</a>
<a href="#flow">آلية العمل</a>
<a href="#plan">الجدول الزمني</a>
<a href="#price">العرض المالي</a>
</div>
<a href="#price" class="nav-cta">العرض المالي</a>
......@@ -78,7 +77,7 @@
<section class="hero" id="top">
<div class="hero-in">
<img src="assets/club-logo.png" alt="نادي الصيد المصري" class="hero-logo">
<div class="hero-tag">المرحلة الأولى &mdash; تطبيق الأعضاء وبوابة الإدارة والمحتوى</div>
<div class="hero-tag">تطبيق الأعضاء وبوابة الإدارة والمحتوى</div>
<h1>تطبيق الأعضاء<br>ومتابعة <em>التحصيلات</em></h1>
<p class="hero-sub">
تطبيق موبايل لأعضاء نادي الصيد المصري، يعرض على العضو ما عليه من مستحقات ويتيح له سدادها إلكترونيًا،
......@@ -86,13 +85,13 @@
تسنده بوابة ويب تدير منها إدارة النادي الأعضاء والاشتراكات والدعوات وتنشر عليها الأخبار بالصور.
</p>
<div class="btn-row">
<a href="#app" class="btn btn-gold">شاهدوا نموذج التطبيق ↓</a>
<a href="#portal" class="btn btn-ghost">شاهدوا بوابة الإدارة</a>
<a href="#app" class="btn btn-gold">شاهدوا شاشات التطبيق ↓</a>
<a href="#portal" class="btn btn-ghost">شاهدوا شاشات البوابة</a>
<a href="عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf" class="btn btn-ghost" download>
<svg class="ico"><use href="#i-download"/></svg> تحميل العرض PDF</a>
</div>
<div class="hero-stats">
<div class="hero-stat"><b class="num">4</b><span>محاور في المرحلة الأولى</span></div>
<div class="hero-stat"><b class="num">4</b><span>محاور يغطيها النظام</span></div>
<div class="hero-stat"><b class="num">2 &ndash; 4</b><span>أسابيع حتى الإطلاق</span></div>
<div class="hero-stat"><b>تطبيق + بوابة</b><span>موبايل وويب معًا</span></div>
<div class="hero-stat"><b class="num">500,000</b><span>جنيه &mdash; غير شامل ض.ق.م</span></div>
......@@ -118,7 +117,7 @@
<p class="lede">
لا يحتاج النادي إلى نظام إداري متكامل ليعالج ما يواجهه اليوم.
فالمستحقات قائمة ومعروفة، <strong>غير أنها تصل إلى الخزنة متأخرة أو لا تصل.</strong>
وقد وُضع هذا العرض لمعالجة هذه النقطة دون غيرها، على أن يبقى ما عداها مطروحًا لمرحلة تالية.
وقد وُضع هذا العرض لمعالجة هذه النقطة دون غيرها، على أن يبقى ما عداها مطروحًا متى شاء النادي.
</p>
</div>
......@@ -153,7 +152,7 @@
<strong>منهج العرض:</strong>
بدلًا من انتظار مشروع ERP كامل بميزانية بالملايين ومدة تنفيذ طويلة، نبدأ بنطاق محدود يعالج أكبر خسارة قائمة:
تطبيق موبايل للأعضاء وبوابة ويب للإدارة. يبدأ التشغيل خلال أربعة أسابيع على الأكثر، ويبدأ التحصيل من الشهر الأول للإطلاق،
ويبقى ما عدا ذلك قرارًا مفتوحًا لمرحلة تالية.
ويبقى ما عدا ذلك قرارًا مفتوحًا للنادي متى شاء.
</div>
</div>
</section>
......@@ -163,11 +162,11 @@
<div class="wrap">
<div class="reveal">
<div class="eyebrow">حدود المشروع</div>
<h2 class="title">نطاق المرحلة الأولى: أربعة محاور</h2>
<h2 class="title">ما يقدّمه النظام: أربعة محاور</h2>
<p class="lede">
حصر النطاق في أربعة محاور هو ما يجعل التنفيذ ممكنًا في أسبوعين إلى أربعة أسابيع
وبميزانية 500,000 جنيه غير شاملة ضريبة القيمة المضافة.
وما يقع خارج هذه المحاور مؤجَّل صراحةً إلى مرحلة ثانية تُسعَّر على حدة.
وما يقع خارج هذه المحاور غير مشمول في هذا العرض، ويُسعَّر على حدة إن طلبه النادي.
</p>
</div>
......@@ -208,7 +207,7 @@
<div class="grid g2 reveal" style="margin-top:44px; align-items:start;">
<div>
<h3 style="color:var(--navy); font-size:1.1rem; margin-bottom:12px;">داخل نطاق المرحلة الأولى</h3>
<h3 style="color:var(--navy); font-size:1.1rem; margin-bottom:12px;">داخل النطاق</h3>
<ul class="scope-list">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>تطبيق موبايل للأعضاء على iPhone و Android بواجهة عربية</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>دخول العضو برقم موبايله بكود تحقق &mdash; بلا كلمات مرور</span></li>
......@@ -222,20 +221,27 @@
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رصيد الدعوات وإصدارها وشراء دعوات إضافية</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>قسم أخبار النادي بالصور داخل التطبيق</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>إشعارات بالمستحقات وبالأخبار الجديدة فور نشرها</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>أقساط بجدول محدَّد وسعر معلن للسداد المبكر</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>غرامات بأسبابها، وتظلّم من التطبيق يردّ عليه موظف الإدارة</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رصيد دائن يُخصم تلقائيًا من الفاتورة التالية</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>رفع إيصال التحويل البنكي من التطبيق واعتماده من البوابة</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>شهادة طبية تُرفع بالتصوير وتُراجع وتُعتمد أو تُرفض بسبب مكتوب</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>حضور اللاعبين وتقييمهم، ونشر التقييم لولي الأمر</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>فعاليات النادي بفئات تذاكر وسياسة استرداد وحجز من التطبيق</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>بوابة ويب لإدارة الأعضاء والمستحقات والتسعير والدعوات والأخبار</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>سجل مراجعة يوضّح من غيّر أي حقل ومن أي قيمة إلى أي قيمة</span></li>
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>نشر التطبيق على App Store و Google Play باسم النادي</span></li>
</ul>
</div>
<div>
<h3 style="color:var(--txt-3); font-size:1.1rem; margin-bottom:12px;">خارج النطاق &mdash; مؤجَّل للمرحلة الثانية</h3>
<h3 style="color:var(--txt-3); font-size:1.1rem; margin-bottom:12px;">خارج النطاق</h3>
<ul class="scope-list">
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>حجز الملاعب والكورتات وحمامات السباحة</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>محفظة إلكترونية ورصيد داخلي</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>المطاعم والكافيتريات والطلب من التطبيق</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>القرى والشاليهات والحجوزات الموسمية</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>الشكاوى والطلبات والدعم الفني</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>العيادة الطبية والكشف الرياضي</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>الإنجازات وتقييم اللاعبين</span></li>
<li><span class="tick later"><svg class="ico"><use href="#i-arrow"/></svg></span><span>العيادة الطبية والكشف الرياضي داخل النادي</span></li>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>نظام ERP محاسبي كامل (دفتر عام وميزانيات وأصول)</span></li>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>الموارد البشرية والرواتب والحضور</span></li>
<li><span class="tick no"><svg class="ico"><use href="#i-x"/></svg></span><span>المخازن والمشتريات وأذون الاستلام</span></li>
......@@ -247,455 +253,116 @@
</div>
</section>
<!-- ============ MOBILE APP PROTOTYPE ============ -->
<!-- ============ MOBILE APP SCREENS ============ -->
<section id="app">
<div class="wrap">
<div class="reveal">
<div class="eyebrow">نموذج تفاعلي للتطبيق</div>
<h2 class="title">شاشات التطبيق كما سيراها العضو</h2>
<div class="eyebrow">شاشات التطبيق</div>
<h2 class="title">التطبيق كما يراه العضو على هاتفه</h2>
<p class="lede">
الشاشات التالية مبنية بالتصميم نفسه الذي سيُسلَّم، وليست رسومًا تقريبية.
اضغطوا على أي شاشة في القائمة لعرضها.
الصور التالية ملتقطة من التطبيق نفسه وهو يعمل على هاتف، متصلًا بالخادم وببيانات فعلية &mdash;
لا رسوم تقريبية ولا شاشات مُعادة الرسم. اضغطوا على أي شاشة في القائمة لعرضها.
</p>
</div>
<div class="proto reveal" style="margin-top:38px;">
<div class="proto-nav">
<h4>شاشات التطبيق</h4>
<button class="pn-item on" data-route="home"><span class="n">١</span> الرئيسية وبطاقة العضوية</button>
<button class="pn-item" data-route="dues"><span class="n">٢</span> المستحقات والأقساط</button>
<button class="pn-item" data-route="pay"><span class="n">٣</span> السداد الإلكتروني</button>
<button class="pn-item" data-route="qr"><span class="n">٤</span> كود دخول البوابة</button>
<button class="pn-item" data-route="invites"><span class="n">٥</span> الدعوات وشراء المزيد</button>
<button class="pn-item" data-route="activities"><span class="n">٦</span> الأنشطة الرياضية</button>
<button class="pn-item" data-route="activity/SW-01"><span class="n">٧</span> تفاصيل نشاط والاشتراك</button>
<button class="pn-item" data-route="schedule"><span class="n">٨</span> جدول التدريب</button>
<button class="pn-item" data-route="news"><span class="n">٩</span> أخبار النادي</button>
<button class="pn-item" data-route="article/a1"><span class="n">١٠</span> صفحة الخبر</button>
<button class="pn-item" data-route="receipts"><span class="n">١١</span> الإيصالات</button>
<button class="pn-item" data-route="notifications"><span class="n">١٢</span> الإشعارات</button>
<button class="pn-item" data-route="profile"><span class="n">١٣</span> حسابي والإعدادات</button>
<button class="pn-item" data-route="login"><span class="n">١٤</span> الدخول والتحقق</button>
<button class="pn-item on" data-shot="01-signin"><span class="n">١</span> الدخول برقم الموبايل</button>
<button class="pn-item" data-shot="02-otp"><span class="n">٢</span> كود التحقق</button>
<button class="pn-item" data-shot="03-home"><span class="n">٣</span> الرئيسية وأفراد الأسرة</button>
<button class="pn-item" data-shot="04-card"><span class="n">٤</span> بطاقة العضوية الرقمية</button>
<button class="pn-item" data-shot="05-dues"><span class="n">٥</span> كشف المستحقات</button>
<button class="pn-item" data-shot="06-select"><span class="n">٦</span> تحديد المستحقات للسداد</button>
<button class="pn-item" data-shot="07-checkout"><span class="n">٧</span> إتمام السداد</button>
<button class="pn-item" data-shot="08-gate"><span class="n">٨</span> كود دخول البوابة</button>
<button class="pn-item" data-shot="09-invitations"><span class="n">٩</span> الدعوات ورصيدها</button>
<button class="pn-item" data-shot="10-activities"><span class="n">١٠</span> اشتراكات الأسرة</button>
<button class="pn-item" data-shot="12-catalog"><span class="n">١١</span> كتالوج الأنشطة</button>
<button class="pn-item" data-shot="11-schedule"><span class="n">١٢</span> جدول التدريب الأسبوعي</button>
<button class="pn-item" data-shot="14-news"><span class="n">١٣</span> أخبار النادي</button>
<button class="pn-item" data-shot="15-events"><span class="n">١٤</span> الفعاليات والحجز</button>
<button class="pn-item" data-shot="13-more"><span class="n">١٥</span> المزيد</button>
<button class="pn-item" data-shot="16-profile"><span class="n">١٦</span> الملف الشخصي والأسرة</button>
<button class="pn-item" data-shot="17-settings"><span class="n">١٧</span> الإعدادات</button>
<div class="pn-desc">
هذا نموذج يعمل فعليًا لا صور ثابتة: اضغطوا داخل الشاشة نفسها للتنقل والسداد
والاشتراك في نشاط وإصدار دعوة وقراءة خبر، وستتغير الأرصدة والمستحقات أمامكم.
التطبيق يعمل على أجهزة iPhone و Android بواجهة عربية بالكامل.
التطبيق يعمل على أجهزة iPhone و Android بواجهة عربية بالكامل،
ويدخل إليه العضو برقم موبايله المسجَّل في بيانات النادي دون كلمة مرور.
</div>
</div>
<div class="phone-stage">
<div class="phone">
<iframe id="proto" class="screen" src="app/index.html#/home"
title="النموذج التفاعلي لتطبيق أعضاء نادي الصيد المصري"></iframe>
<div class="phone shot-phone">
<img id="shot" class="shot" src="screenshots/app/01-signin.jpg"
alt="شاشة الدخول في تطبيق أعضاء نادي الصيد المصري">
</div>
<div style="flex:1;min-width:260px;max-width:360px;">
<div id="scr-note"></div>
<div class="proto-tools">
<button class="pbtn ghost" id="proto-theme">تبديل المظهر</button>
<button class="pbtn ghost" id="proto-reset">إعادة ضبط النموذج</button>
<a class="pbtn" href="app/index.html#/home" target="_blank" rel="noopener">فتح بملء الشاشة</a>
<a class="pbtn" id="shot-full" href="screenshots/app/01-signin.jpg" target="_blank" rel="noopener">فتح الصورة بحجمها الكامل</a>
</div>
</div>
</div>
</div>
<div class="reveal" style="margin-top:44px;">
<h3 style="color:var(--navy);font-size:1.05rem;margin-bottom:14px;">كل الشاشات في لقطة واحدة</h3>
<div class="shot-grid" id="shot-grid"></div>
</div>
</div>
</section>
<!-- ============ PORTAL PROTOTYPE ============ -->
<!-- ============ PORTAL SCREENS ============ -->
<section class="alt" id="portal">
<div class="wrap">
<div class="reveal">
<div class="eyebrow">نموذج تفاعلي للبوابة</div>
<div class="eyebrow">شاشات البوابة</div>
<h2 class="title">بوابة الإدارة والمحتوى على الويب</h2>
<p class="lede">
من البوابة تدير الإدارة الأعضاء والاشتراكات والمستحقات وسياسة الدعوات،
وتنشر أخبار النادي بالصور فتظهر في التطبيق فورًا.
لا تتطلب تدريبًا طويلًا ولا موظفًا متخصصًا: ترفعون ملفاتكم مرة واحدة، ثم تُدار كل شيء من الشاشات مباشرة.
لا تتطلب تدريبًا طويلًا ولا موظفًا متخصصًا: ترفعون ملفاتكم مرة واحدة، ثم يُدار كل شيء من الشاشات مباشرة.
والصور أدناه من البوابة نفسها وهي تعمل.
</p>
</div>
<div class="reveal" style="margin-top:34px; display:flex; gap:8px; flex-wrap:wrap;">
<button class="pn-item on" style="width:auto;" data-pscr="dash"><span class="n">١</span> لوحة التحصيل</button>
<button class="pn-item" style="width:auto;" data-pscr="import"><span class="n">٢</span> رفع بيانات الأعضاء</button>
<button class="pn-item" style="width:auto;" data-pscr="members"><span class="n">٣</span> الأعضاء والمستحقات</button>
<button class="pn-item" style="width:auto;" data-pscr="catalog"><span class="n">٤</span> تسعير الأنشطة</button>
<button class="pn-item" style="width:auto;" data-pscr="invites"><span class="n">٥</span> سياسة الدعوات</button>
<button class="pn-item" style="width:auto;" data-pscr="sched"><span class="n">٦</span> جداول المدربين</button>
<button class="pn-item" style="width:auto;" data-pscr="news"><span class="n">٧</span> أخبار النادي</button>
<button class="pn-item on" style="width:auto;" data-pshot="dashboard"><span class="n">١</span> لوحة التحصيل</button>
<button class="pn-item" style="width:auto;" data-pshot="collections"><span class="n">٢</span> المتأخرات</button>
<button class="pn-item" style="width:auto;" data-pshot="members"><span class="n">٣</span> الأعضاء</button>
<button class="pn-item" style="width:auto;" data-pshot="revenue"><span class="n">٤</span> تفاصيل الإيرادات</button>
<button class="pn-item" style="width:auto;" data-pshot="programs"><span class="n">٥</span> البرامج والأسعار</button>
<button class="pn-item" style="width:auto;" data-pshot="groups"><span class="n">٦</span> المجموعات والمواعيد</button>
<button class="pn-item" style="width:auto;" data-pshot="enrollments"><span class="n">٧</span> الاشتراكات</button>
<button class="pn-item" style="width:auto;" data-pshot="roster"><span class="n">٨</span> تسجيل الحضور</button>
<button class="pn-item" style="width:auto;" data-pshot="medical"><span class="n">٩</span> مراجعة الشهادات الطبية</button>
<button class="pn-item" style="width:auto;" data-pshot="news"><span class="n">١٠</span> الأخبار والإعلانات</button>
<button class="pn-item" style="width:auto;" data-pshot="events"><span class="n">١١</span> الفعاليات</button>
<button class="pn-item" style="width:auto;" data-pshot="invitation-policy"><span class="n">١٢</span> سياسة الدعوات</button>
<button class="pn-item" style="width:auto;" data-pshot="access-log"><span class="n">١٣</span> سجل الدخول</button>
<button class="pn-item" style="width:auto;" data-pshot="wizards"><span class="n">١٤</span> المعالجات</button>
<button class="pn-item" style="width:auto;" data-pshot="imports"><span class="n">١٥</span> استيراد البيانات</button>
<button class="pn-item" style="width:auto;" data-pshot="billing-runs"><span class="n">١٦</span> دورات الفوترة</button>
<button class="pn-item" style="width:auto;" data-pshot="campaigns"><span class="n">١٧</span> حملات الإشعارات</button>
<button class="pn-item" style="width:auto;" data-pshot="branding"><span class="n">١٨</span> الهوية والعلامة التجارية</button>
<button class="pn-item" style="width:auto;" data-pshot="audit"><span class="n">١٩</span> سجل المراجعة</button>
</div>
<div class="browser reveal" style="margin-top:18px;">
<div class="bbar">
<div class="bdots"><i style="background:#FF5F57;"></i><i style="background:#FEBC2E;"></i><i style="background:#28C840;"></i></div>
<div class="burl" id="burl">https://portal.sayd-club.eg/dashboard</div>
</div>
<div class="bwin">
<aside class="side">
<div class="side-b">
<img src="assets/club-logo.png" alt=""><span>نادي الصيد<br>بوابة الإدارة</span>
</div>
<div class="si on" data-jump="dash"><i><svg class="ico"><use href="#i-dashboard"/></svg></i> لوحة التحصيل</div>
<div class="si" data-jump="import"><i><svg class="ico"><use href="#i-upload"/></svg></i> رفع البيانات</div>
<div class="si" data-jump="members"><i><svg class="ico"><use href="#i-members"/></svg></i> الأعضاء والمستحقات</div>
<div class="si" data-jump="catalog"><i><svg class="ico"><use href="#i-activity"/></svg></i> تسعير الأنشطة</div>
<div class="si" data-jump="invites"><i><svg class="ico"><use href="#i-mail"/></svg></i> سياسة الدعوات</div>
<div class="si" data-jump="sched"><i><svg class="ico"><use href="#i-clock"/></svg></i> جداول المدربين</div>
<div class="si" data-jump="news"><i><svg class="ico"><use href="#i-news"/></svg></i> أخبار النادي</div>
<div class="si"><i><svg class="ico"><use href="#i-receipt"/></svg></i> الإيصالات</div>
<div class="si"><i><svg class="ico"><use href="#i-bell"/></svg></i> الإشعارات</div>
<div class="si"><i><svg class="ico"><use href="#i-settings"/></svg></i> الإعدادات</div>
</aside>
<main class="pmain">
<!-- P1 DASHBOARD -->
<div class="pscr" data-pscr="dash">
<div class="ptop">
<div><h3>لوحة التحصيل</h3><p>مارس 2026 &mdash; محدَّثة الآن</p></div>
<div style="display:flex;gap:8px;"><span class="pbtn ghost">تصدير Excel</span><span class="pbtn">إرسال تذكير جماعي</span></div>
</div>
<div class="grid g4" style="gap:12px;margin-bottom:18px;">
<div class="kpi"><div class="k">محصَّل هذا الشهر</div><div class="v num">1,284,500</div><div class="d up">▲ 34% مقارنة بفبراير</div></div>
<div class="kpi"><div class="k">مستحق ولم يُحصَّل</div><div class="v num" style="color:var(--rose);">742,300</div><div class="d dn">▼ 12% عن الشهر الماضي</div></div>
<div class="kpi"><div class="k">نسبة التحصيل</div><div class="v num">63%</div><div class="d up">▲ من 41% قبل التطبيق</div></div>
<div class="kpi"><div class="k">سُدِّد من التطبيق</div><div class="v num">71%</div><div class="d up">من إجمالي المحصَّل</div></div>
</div>
<div class="grid g2" style="gap:14px;align-items:start;">
<div class="tbl">
<table>
<thead><tr><th>البند</th><th>عدد</th><th>القيمة</th><th>الحالة</th></tr></thead>
<tbody>
<tr><td>تجديدات عضوية 2026</td><td class="num">1,842</td><td class="num">8,289,000</td><td><span class="badge ok">جارٍ</span></td></tr>
<tr><td>أقساط مستحقة هذا الشهر</td><td class="num">613</td><td class="num">919,500</td><td><span class="badge warn">منها 214 متأخرًا</span></td></tr>
<tr><td>فواتير أنشطة رياضية</td><td class="num">487</td><td class="num">498,400</td><td><span class="badge ok">جارٍ</span></td></tr>
<tr><td>غرامات تأخير</td><td class="num">214</td><td class="num">74,900</td><td><span class="badge err">غير محصَّل</span></td></tr>
<tr><td>بيع دعوات إضافية</td><td class="num">1,106</td><td class="num">165,900</td><td><span class="badge ok">محصَّل</span></td></tr>
</tbody>
</table>
</div>
<div>
<div class="tbl" style="margin-bottom:14px;">
<table>
<thead><tr><th>أعلى المتأخرات</th><th>المبلغ</th><th>التأخر</th></tr></thead>
<tbody>
<tr><td>عمرو مصطفى فتحي &mdash; <span class="num">10247</span></td><td class="num">6,350</td><td><span class="badge err">92 يومًا</span></td></tr>
<tr><td>هالة سمير عبدالله &mdash; <span class="num">10312</span></td><td class="num">5,900</td><td><span class="badge err">78 يومًا</span></td></tr>
<tr><td>كريم وائل نبيل &mdash; <span class="num">10488</span></td><td class="num">4,500</td><td><span class="badge warn">44 يومًا</span></td></tr>
<tr><td>منى رضا شعبان &mdash; <span class="num">10501</span></td><td class="num">3,150</td><td><span class="badge warn">31 يومًا</span></td></tr>
</tbody>
</table>
</div>
<div class="kpi">
<div class="k">تنبيه ذكي</div>
<p style="font-size:.8rem;color:var(--txt-2);line-height:1.8;margin-top:6px;">
<span class="num">214</span> عضوًا لم يفتحوا التطبيق منذ التسجيل ولديهم مستحقات.
الاقتراح: إرسال رسالة نصية بدلًا من الإشعار داخل التطبيق.
</p>
<div style="margin-top:10px;"><span class="pbtn gold">أرسل رسالة نصية لهؤلاء الأعضاء</span></div>
</div>
</div>
</div>
</div>
<!-- P2 IMPORT WIZARD -->
<div class="pscr" data-pscr="import" style="display:none;">
<div class="ptop">
<div><h3>رفع بيانات الأعضاء</h3><p>ملف members.csv &mdash; 1,842 صفًا</p></div>
<span class="pbtn ghost">تحميل القالب الفارغ</span>
</div>
<div class="steps">
<div class="step done"><span class="b"><svg class="ico"><use href="#i-check"/></svg></span> اختيار القالب</div>
<div class="step-line done"></div>
<div class="step done"><span class="b"><svg class="ico"><use href="#i-check"/></svg></span> رفع الملف</div>
<div class="step-line done"></div>
<div class="step now"><span class="b">٣</span> مطابقة الأعمدة</div>
<div class="step-line"></div>
<div class="step"><span class="b">٤</span> مراجعة الأخطاء</div>
<div class="step-line"></div>
<div class="step"><span class="b">٥</span> الاعتماد</div>
</div>
<p style="font-size:.8rem;color:var(--txt-2);margin-bottom:12px;">
تعرّف النظام تلقائيًا على أعمدة ملفكم. راجعوا المطابقة وعدّلوها إن لزم &mdash; ثم تابعوا.
</p>
<div class="map-row" style="background:var(--paper-2);font-weight:700;color:var(--txt-2);">
<span>العمود في ملفكم</span><span></span><span>الحقل في النظام</span><span style="text-align:center;">الحالة</span>
</div>
<div class="map-row"><span>رقم العضوية</span><span class="arw"></span>
<select><option>كود العضوية (إلزامي)</option></select><span class="badge ok">مطابَق</span></div>
<div class="map-row"><span>الاسم</span><span class="arw"></span>
<select><option>الاسم الرباعي (إلزامي)</option></select><span class="badge ok">مطابَق</span></div>
<div class="map-row"><span>موبايل</span><span class="arw"></span>
<select><option>رقم الموبايل (إلزامي)</option></select><span class="badge ok">مطابَق</span></div>
<div class="map-row"><span>الرقم القومى</span><span class="arw"></span>
<select><option>الرقم القومي</option></select><span class="badge ok">مطابَق</span></div>
<div class="map-row"><span>الفئة</span><span class="arw"></span>
<select><option>نوع العضوية</option><option>الحالة</option><option>— تجاهل هذا العمود —</option></select><span class="badge warn">راجِع</span></div>
<div class="map-row"><span>تاريخ النهاية</span><span class="arw"></span>
<select><option>تاريخ انتهاء الاشتراك</option></select><span class="badge ok">مطابَق</span></div>
<div class="map-row"><span>ملاحظات الموظف</span><span class="arw"></span>
<select><option>— تجاهل هذا العمود —</option></select><span class="badge">متجاهَل</span></div>
<div style="display:flex;gap:14px;align-items:center;margin-top:18px;flex-wrap:wrap;">
<span class="pbtn">تابع إلى مراجعة الأخطاء</span>
<span class="pbtn ghost">إلغاء</span>
<span style="font-size:.76rem;color:var(--txt-3);">
معاينة: <b class="num">1,796</b> صفًا سليمًا &bull;
<b class="num" style="color:var(--gold);">39</b> تحذيرًا &bull;
<b class="num" style="color:var(--rose);">7</b> أخطاء توقف الاستيراد
</span>
</div>
<div class="tbl" style="margin-top:18px;">
<table>
<thead><tr><th>الصف</th><th>الحقل</th><th>القيمة</th><th>المشكلة</th><th>الإجراء</th></tr></thead>
<tbody>
<tr><td class="num">142</td><td>رقم الموبايل</td><td class="num">0100123</td><td><span class="badge err">رقم غير مكتمل</span></td><td>تصحيح يدوي</td></tr>
<tr><td class="num">389</td><td>كود العضوية</td><td class="num">10245</td><td><span class="badge err">مكرر مع الصف 12</span></td><td>حذف أو دمج</td></tr>
<tr><td class="num">604</td><td>تاريخ الانتهاء</td><td>31/13/2026</td><td><span class="badge err">تاريخ غير صالح</span></td><td>تصحيح يدوي</td></tr>
<tr><td class="num">877</td><td>نوع العضوية</td><td>عامل م</td><td><span class="badge warn">غير معروف &mdash; هل تقصد «عضو عامل»؟</span></td><td>قبول الاقتراح</td></tr>
</tbody>
</table>
</div>
</div>
<!-- P3 MEMBERS -->
<div class="pscr" data-pscr="members" style="display:none;">
<div class="ptop">
<div><h3>الأعضاء والمستحقات</h3><p>1,842 عضوًا &mdash; 613 عليهم مستحقات هذا الشهر</p></div>
<div style="display:flex;gap:8px;"><span class="pbtn ghost">تصفية</span><span class="pbtn gold">إضافة مستحق</span></div>
</div>
<div class="grid g4" style="gap:10px;margin-bottom:16px;">
<div class="field" style="margin:0;"><input placeholder="بحث بالاسم أو كود العضوية أو الموبايل"></div>
<div class="field" style="margin:0;"><select><option>كل أنواع العضوية</option><option>عضو عامل</option><option>عضو منتسب</option></select></div>
<div class="field" style="margin:0;"><select><option>الحالة: الكل</option><option>متأخر السداد</option><option>منتظم</option></select></div>
<div class="field" style="margin:0;"><select><option>مارس 2026</option><option>فبراير 2026</option></select></div>
</div>
<div class="tbl">
<table>
<thead>
<tr><th>كود</th><th>الاسم</th><th>النوع</th><th>سريان العضوية</th><th>مستحق الشهر</th><th>إجمالي المتأخر</th><th>التطبيق</th><th>الحالة</th></tr>
</thead>
<tbody>
<tr><td class="num">10245</td><td>محمد أحمد سيد إبراهيم</td><td>عضو عامل</td><td class="num">2026/12/31</td><td class="num">1,850</td><td class="num" style="color:var(--rose);">350</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge warn">متأخر يومين</span></td></tr>
<tr><td class="num">10246</td><td>ياسمين خالد محمود علي</td><td>عضو عامل</td><td class="num">2026/12/31</td><td class="num">4,500</td><td class="num">0</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge ok">منتظم</span></td></tr>
<tr><td class="num">10247</td><td>عمرو مصطفى فتحي حسن</td><td>عضو منتسب</td><td class="num" style="color:var(--rose);">2026/06/30</td><td class="num">1,200</td><td class="num" style="color:var(--rose);">6,350</td><td><span class="badge err">لم يُفعَّل</span></td><td><span class="badge err">موقوف</span></td></tr>
<tr><td class="num">10312</td><td>هالة سمير عبدالله رشاد</td><td>عضو عامل</td><td class="num">2026/12/31</td><td class="num">2,400</td><td class="num" style="color:var(--rose);">5,900</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge err">متأخر 78 يومًا</span></td></tr>
<tr><td class="num">10488</td><td>كريم وائل نبيل فؤاد</td><td>عضو عامل</td><td class="num">2026/12/31</td><td class="num">1,500</td><td class="num" style="color:var(--gold);">4,500</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge warn">متأخر 44 يومًا</span></td></tr>
<tr><td class="num">10501</td><td>منى رضا شعبان محمد</td><td>عضو منتسب</td><td class="num">2026/12/31</td><td class="num">900</td><td class="num" style="color:var(--gold);">3,150</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge warn">متأخر 31 يومًا</span></td></tr>
<tr><td class="num">10533</td><td>طارق حسن عبدالعزيز</td><td>عضو فخري</td><td></td><td class="num">0</td><td class="num">0</td><td><span class="badge ok">مفعَّل</span></td><td><span class="badge ok">معفى</span></td></tr>
</tbody>
</table>
</div>
<p style="font-size:.74rem;color:var(--txt-3);margin-top:10px;">
كل تعديل هنا ينعكس على تطبيق العضو خلال ثوانٍ، ويُسجَّل في سجل المراجعة باسم الموظف والوقت.
</p>
</div>
<!-- P4 CATALOG -->
<div class="pscr" data-pscr="catalog" style="display:none;">
<div class="ptop">
<div><h3>تسعير الأنشطة الرياضية</h3><p>ما تحدّدونه هنا هو ما يظهر للعضو في التطبيق</p></div>
<div style="display:flex;gap:8px;"><span class="pbtn ghost">رفع من Excel</span><span class="pbtn gold">+ نشاط جديد</span></div>
</div>
<div class="grid g2" style="gap:18px;align-items:start;">
<div class="tbl">
<table>
<thead><tr><th>الكود</th><th>النشاط</th><th>الفئة السنية</th><th>حصص/شهر</th><th>الاشتراك</th><th>الانضمام</th><th>السعة</th></tr></thead>
<tbody>
<tr><td>SW-01</td><td>سباحة مبتدئين</td><td>6-9</td><td class="num">12</td><td class="num">900</td><td class="num">300</td><td class="num">18/24</td></tr>
<tr><td>SW-02</td><td>سباحة متقدم</td><td>10-14</td><td class="num">16</td><td class="num">1,200</td><td class="num">300</td><td class="num">14/20</td></tr>
<tr><td>KR-01</td><td>كاراتيه مبتدئين</td><td>6-12</td><td class="num">8</td><td class="num">700</td><td class="num">250</td><td class="num">22/30</td></tr>
<tr><td>FB-01</td><td>كرة القدم &mdash; الموهبة</td><td>8-11</td><td class="num">12</td><td class="num">1,100</td><td class="num">400</td><td class="num">26/28</td></tr>
<tr><td>TN-01</td><td>تنس أرضي &mdash; أطفال</td><td>7-12</td><td class="num">8</td><td class="num">1,400</td><td class="num">350</td><td class="num" style="color:var(--rose);">12/12</td></tr>
<tr><td>GY-01</td><td>جمباز فني &mdash; سيدات</td><td>12-18</td><td class="num">12</td><td class="num">1,000</td><td class="num">300</td><td class="num">11/18</td></tr>
<tr><td>SQ-01</td><td>اسكواش &mdash; ناشئين</td><td>10-16</td><td class="num">8</td><td class="num">1,300</td><td class="num">350</td><td class="num">6/10</td></tr>
</tbody>
</table>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px;">
<h4 style="color:var(--navy);font-size:.94rem;margin-bottom:14px;">تعديل: سباحة مبتدئين</h4>
<div class="field"><label>اسم النشاط</label><input value="سباحة مبتدئين"></div>
<div class="grid g2" style="gap:10px;">
<div class="field"><label>الاشتراك الشهري</label><input value="900"></div>
<div class="field"><label>رسوم الانضمام</label><input value="300"></div>
</div>
<div class="grid g2" style="gap:10px;">
<div class="field"><label>الفئة السنية</label><input value="6 - 9 سنوات"></div>
<div class="field"><label>السعة القصوى</label><input value="24"></div>
</div>
<div class="field"><label>حالة الاشتراك عند اكتمال العدد</label>
<select><option>فتح قائمة انتظار</option><option>إخفاء النشاط</option><option>السماح بالتجاوز</option></select>
<div class="hint">يظهر للعضو في التطبيق كـ «اكتمل العدد &mdash; قائمة انتظار».</div>
</div>
<div class="field"><label>خصم الأخ الثاني</label><input value="15%"><div class="hint">يُطبَّق تلقائيًا على الفاتورة الثانية لنفس العضوية.</div></div>
<span class="pbtn">حفظ ونشر للتطبيق</span>
</div>
</div>
</div>
<!-- P5 INVITES -->
<div class="pscr" data-pscr="invites" style="display:none;">
<div class="ptop">
<div><h3>سياسة الدعوات</h3><p>عدد الدعوات المجانية وسعر الإضافية &mdash; حسب نوع العضوية</p></div>
<span class="pbtn ghost">رفع من Excel</span>
</div>
<div class="grid g2" style="gap:18px;align-items:start;">
<div class="tbl">
<table>
<thead><tr><th>نوع العضوية</th><th>مجانية/شهر</th><th>سعر الإضافية</th><th>حد الشراء</th><th>دعوة طفل</th><th>الصلاحية</th></tr></thead>
<tbody>
<tr><td>عضو عامل</td><td class="num">4</td><td class="num">150</td><td class="num">10</td><td class="num">75</td><td>يوم واحد</td></tr>
<tr><td>عضو منتسب</td><td class="num">2</td><td class="num">200</td><td class="num">6</td><td class="num">100</td><td>يوم واحد</td></tr>
<tr><td>عضو فخري</td><td class="num">8</td><td></td><td></td><td></td><td>يوم واحد</td></tr>
<tr><td>عضو مؤقت</td><td class="num">0</td><td class="num">250</td><td class="num">4</td><td class="num">125</td><td>يوم واحد</td></tr>
</tbody>
</table>
<div style="padding:14px;border-top:1px solid var(--line);">
<div class="grid g3" style="gap:10px;">
<div class="kpi" style="border:none;padding:0;"><div class="k">دعوات مجانية مستهلكة</div><div class="v num">4,812</div></div>
<div class="kpi" style="border:none;padding:0;"><div class="k">دعوات مباعة</div><div class="v num">1,106</div></div>
<div class="kpi" style="border:none;padding:0;"><div class="k">إيراد الدعوات</div><div class="v num" style="color:var(--mint);">165,900</div></div>
</div>
</div>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px;">
<h4 style="color:var(--navy);font-size:.94rem;margin-bottom:14px;">قواعد عامة</h4>
<div class="field"><label>موعد تجديد الرصيد</label>
<select><option>أول كل شهر ميلادي</option><option>تاريخ تجديد العضوية</option></select></div>
<div class="field"><label>هل يُرحَّل الرصيد غير المستخدم؟</label>
<select><option>لا &mdash; يسقط آخر الشهر</option><option>نعم &mdash; بحد أقصى شهر</option></select></div>
<div class="field"><label>منع إصدار دعوة عند وجود متأخرات</label>
<select><option>نعم &mdash; يُمنع فوق 30 يومًا من التأخير</option><option>لا</option></select>
<div class="hint">أداة تحصيل فعّالة: الدعوة امتياز مرتبط بانتظام السداد.</div></div>
<div class="field"><label>أيام الذروة المستثناة</label><input value="الجمعة &mdash; الأعياد الرسمية"><div class="hint">تُمنع الدعوات في هذه الأيام تلقائيًا.</div></div>
<span class="pbtn">حفظ السياسة</span>
</div>
</div>
</div>
<!-- P6 SCHEDULES -->
<div class="pscr" data-pscr="sched" style="display:none;">
<div class="ptop">
<div><h3>جداول المدربين</h3><p>يرفع كل مدرب جدول مجموعته من قالب موحّد &mdash; ثم يظهر لكل مشترك في تطبيقه</p></div>
<span class="pbtn ghost">تحميل قالب الجدول</span>
</div>
<div class="grid g2" style="gap:18px;align-items:start;">
<div>
<div class="drop" style="margin-bottom:14px;">
<div class="ic"><svg class="ico"><use href="#i-file"/></svg></div>
<h4>اسحب ملف الجدول هنا أو اضغط للاختيار</h4>
<p>training_schedule.csv أو .xlsx &mdash; حتى 5 ميجابايت</p>
<div style="margin-top:14px;"><span class="pbtn">اختر ملفًا</span></div>
</div>
<div class="tbl">
<table>
<thead><tr><th>المدرب</th><th>المجموعة</th><th>آخر رفع</th><th>الحالة</th></tr></thead>
<tbody>
<tr><td>كابتن هشام صلاح</td><td>سباحة &mdash; مجموعة أ</td><td class="num">2026/03/01</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>كابتن دينا عادل</td><td>جمباز &mdash; مجموعة ب</td><td class="num">2026/03/01</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>كابتن طارق منير</td><td>الموهبة 2016</td><td class="num">2026/02/26</td><td><span class="badge warn">بانتظار الاعتماد</span></td></tr>
<tr><td>كابتن نهى سامي</td><td>تنس &mdash; أطفال</td><td></td><td><span class="badge err">لم يُرفع</span></td></tr>
</tbody>
</table>
</div>
</div>
<div>
<div style="background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px;margin-bottom:14px;">
<h4 style="color:var(--navy);font-size:.94rem;margin-bottom:6px;">معاينة الجدول المرفوع</h4>
<p style="font-size:.75rem;color:var(--txt-3);margin-bottom:12px;">سباحة مبتدئين &mdash; مجموعة أ &mdash; كابتن هشام صلاح</p>
<div class="tbl">
<table>
<thead><tr><th>اليوم</th><th>من</th><th>إلى</th><th>المكان</th></tr></thead>
<tbody>
<tr><td>السبت</td><td class="num">09:00</td><td class="num">10:00</td><td>حمام السباحة الأولمبي</td></tr>
<tr><td>الاثنين</td><td class="num">09:00</td><td class="num">10:00</td><td>حمام السباحة الأولمبي</td></tr>
<tr><td>الأربعاء</td><td class="num">09:00</td><td class="num">10:00</td><td>حمام السباحة الأولمبي</td></tr>
</tbody>
</table>
</div>
<div style="display:flex;gap:8px;margin-top:14px;">
<span class="pbtn">اعتمد وانشر لـ 18 مشتركًا</span>
<span class="pbtn ghost">رفض</span>
</div>
</div>
<div class="kpi">
<div class="k">تنبيه تعارض</div>
<p style="font-size:.8rem;color:var(--txt-2);line-height:1.8;margin-top:6px;">
حمام السباحة الأولمبي محجوز للسباحة المتقدمة الأربعاء <span class="num">09:00</span>.
راجع الجدولين قبل الاعتماد.
</p>
</div>
</div>
</div>
</div>
<!-- P7 NEWS / CMS -->
<div class="pscr" data-pscr="news" style="display:none;">
<div class="ptop">
<div><h3>أخبار النادي</h3><p>ما يُنشر هنا يظهر في التطبيق فورًا ويصل الأعضاء إشعار به</p></div>
<span class="pbtn gold">+ خبر جديد</span>
</div>
<div class="grid" style="grid-template-columns:1.3fr 1fr;gap:18px;align-items:start;">
<div class="tbl news-tbl">
<table>
<thead><tr><th>العنوان</th><th>التصنيف</th><th>التاريخ</th><th>الحالة</th></tr></thead>
<tbody>
<tr><td>فريق السباحة يحصد ٧ ميداليات</td><td>بطولات</td><td class="num">2026/03/11</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>فتح باب التسجيل في معسكر الصيف</td><td>فعاليات</td><td class="num">2026/03/09</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>صيانة حمام السباحة الأولمبي</td><td>إعلانات</td><td class="num">2026/03/06</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>مجلس الإدارة يعتمد خطة تطوير الملاعب</td><td>أخبار المجلس</td><td class="num">2026/03/02</td><td><span class="badge ok">منشور</span></td></tr>
<tr><td>نتائج بطولة الاسكواش الودية</td><td>بطولات</td><td></td><td><span class="badge warn">مسودة</span></td></tr>
</tbody>
</table>
<div style="padding:14px;border-top:1px solid var(--line);">
<div class="grid g3" style="gap:10px;">
<div class="kpi" style="border:none;padding:0;"><div class="k">منشور هذا الشهر</div><div class="v num">4</div></div>
<div class="kpi" style="border:none;padding:0;"><div class="k">قرأها أعضاء</div><div class="v num">1,932</div></div>
<div class="kpi" style="border:none;padding:0;"><div class="k">متوسط القراءة</div><div class="v num">63%</div></div>
</div>
</div>
</div>
<div style="background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px;">
<h4 style="color:var(--navy);font-size:.94rem;margin-bottom:14px;">تحرير: فريق السباحة يحصد ٧ ميداليات</h4>
<div class="field"><label>عنوان الخبر</label><input value="فريق السباحة يحصد ٧ ميداليات في بطولة الجمهورية"></div>
<div class="grid g2" style="gap:10px;">
<div class="field"><label>التصنيف</label>
<select><option>بطولات</option><option>فعاليات</option><option>إعلانات</option><option>أخبار المجلس</option></select></div>
<div class="field"><label>تاريخ النشر</label><input value="2026/03/11"></div>
</div>
<div class="field"><label>مقدمة قصيرة</label>
<input value="نتائج هي الأفضل منذ خمس سنوات في بطولة الجمهورية"></div>
<div class="drop" style="margin-bottom:12px;padding:18px;">
<div class="ic"><svg class="ico"><use href="#i-image"/></svg></div>
<h4>اسحب صور الخبر هنا</h4>
<p>JPG أو PNG &mdash; عدد غير محدود</p>
</div>
<div class="field"><label>إشعار الأعضاء عند النشر</label>
<select><option>نعم &mdash; إشعار لكل الأعضاء</option><option>لا</option></select>
<div class="hint">يصل الإشعار خلال ثوانٍ من الضغط على «نشر».</div></div>
<div style="display:flex;gap:8px;">
<span class="pbtn">نشر الآن</span>
<span class="pbtn ghost">حفظ كمسودة</span>
</div>
</div>
</div>
</div>
</main>
<div class="burl" id="burl">https://portal.sayd-club.eg/portal</div>
</div>
<img id="pshot" class="bshot" src="screenshots/portal/dashboard.jpg"
alt="لوحة التحصيل في بوابة إدارة نادي الصيد المصري">
</div>
<div id="pshot-note" class="shot-note reveal"></div>
<div class="note reveal" style="margin-top:24px;">
<strong>بوابة بيانات لا نظام ERP:</strong>
ما يحتاجه النادي في هذه المرحلة مصدر بيانات موثوق يغذّي التطبيق، لا نظامًا يدير كل عملية داخلية.
ما يحتاجه النادي مصدر بيانات موثوق يغذّي التطبيق، لا نظامًا يدير كل عملية داخلية.
يديرها موظف واحد أو اثنان، ويكفي يوم تدريبي واحد لإتقانها، وهي مبنيّة بحيث يمكن ربطها لاحقًا
بأي نظام ERP يختاره النادي دون إعادة إدخال البيانات.
</div>
......@@ -796,38 +463,25 @@
</div>
<div class="card" style="background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.1);">
<div class="card-ico"><svg class="ico"><use href="#i-layers"/></svg></div><h4>قابلية التوسع</h4>
<p style="color:#A9BBD4;">ما يُسلَّم اليوم أساس تُبنى عليه إضافات المرحلة الثانية دون هدم ولا إعادة بناء ولا هدر لما دُفع.</p>
<p style="color:#A9BBD4;">ما يُسلَّم اليوم أساس تُبنى عليه أي إضافة لاحقة دون هدم ولا إعادة بناء ولا هدر لما دُفع.</p>
</div>
</div>
</div>
</section>
<!-- ============ PLAN ============ -->
<section class="alt" id="plan">
<!-- ============ INPUTS ============ -->
<section class="alt" id="inputs">
<div class="wrap">
<div class="reveal">
<div class="eyebrow">خطة التنفيذ</div>
<h2 class="title">خطة التنفيذ في أسبوعين إلى أربعة</h2>
<div class="eyebrow">مدخلات التشغيل</div>
<h2 class="title">ما نحتاجه منكم</h2>
<p class="lede">
جدول بتسليمات محددة في نهاية كل مرحلة، ومراجعة مشتركة مع إدارة النادي قبل الانتقال إلى التالية.
أسبوعان إذا وصلت البيانات والحسابات مبكرًا، وأربعة أسابيع في الحد الأقصى.
لا يتوقف بناء التطبيق والبوابة على وصول هذه المدخلات، وإنما يتوقف عليها موعد الإطلاق وحده.
</p>
</div>
<div class="grid g2 reveal" style="margin-top:34px;align-items:start;gap:40px;">
<div class="tl">
<div class="tl-i"><div class="tl-w">الأسبوع ١</div><h4>التجهيز وبوابة الإدارة</h4>
<p>جلسة عمل مع إدارة العضويات والأنشطة لتثبيت أنواع العضوية وبنود المستحقات وسياسة الدعوات وقائمة الأنشطة. <strong>تسليم: بوابة عاملة ترفع عليها الإدارة بياناتها الحقيقية.</strong></p></div>
<div class="tl-i"><div class="tl-w">الأسبوع ٢</div><h4>تطبيق العضو: العضوية والسداد</h4>
<p>دخول العضو، وبطاقة العضوية، وكشف المستحقات، والسداد الإلكتروني والإيصالات، مع ربط بوابة الدفع واختبارها بمعاملات حقيقية.</p></div>
<div class="tl-i"><div class="tl-w">الأسبوع ٣</div><h4>الأنشطة والدخول والأخبار</h4>
<p>كتالوج الأنشطة والاشتراك وجداول التدريب، وكود الدخول المتجدد وشاشة المسح على البوابات، ورصيد الدعوات وشراؤها، وقسم الأخبار بالصور.</p></div>
<div class="tl-i" style="padding-bottom:0;"><div class="tl-w">الأسبوع ٤</div><h4>التجربة والتدريب والإطلاق</h4>
<p>تشغيل تجريبي مغلق مع مجموعة من الأعضاء وتصحيح ما يظهر، ثم تدريب موظفي البوابة، ونشر التطبيق على App Store و Google Play باسم النادي، وبدء التشغيل الفعلي.</p></div>
</div>
<div>
<div style="background:#fff;border:1px solid var(--line);border-radius:var(--r-m);padding:24px;margin-bottom:18px;">
<div class="grid g2 reveal" style="margin-top:34px;align-items:start;gap:34px;">
<div style="background:#fff;border:1px solid var(--line);border-radius:var(--r-m);padding:24px;margin-bottom:18px;">
<h4 style="color:var(--navy);margin-bottom:14px;">ما نحتاجه منكم</h4>
<ul class="scope-list" style="font-size:.88rem;">
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>ملف الأعضاء الحالي بأي صيغة متاحة (Excel أو تصدير من نظامكم)</span></li>
......@@ -841,14 +495,14 @@
<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>مسؤول تواصل واحد من النادي طوال المشروع</span></li>
</ul>
</div>
<div class="note">
<div class="note">
<strong>نبدأ التنفيذ فور التوقيع &mdash; لا ننتظر بياناتكم:</strong>
بناء التطبيق والبوابة يبدأ من اليوم الأول ببيانات تجريبية، فلا يتوقف العمل على وصول
ملف الأعضاء أو فتح الحسابات. ما يتوقف عليه هو <em>موعد الإطلاق</em> وحده:
كلما وصلت البيانات والحسابات مبكرًا اقتربت المدة من أسبوعين، وكلما تأخرت اقتربت من أربعة.
والنظام مصمَّم ليقبل بياناتكم كما هي دون تكلفة إضافية.
</div>
</div>
</div>
</div>
</section>
......@@ -864,7 +518,7 @@
<div class="price-hero reveal" style="margin-top:28px;">
<div style="position:relative;z-index:2;display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:24px;">
<div>
<div style="font-size:.86rem;color:#B9CCE6;margin-bottom:6px;">إجمالي قيمة المشروع &mdash; المرحلة الأولى</div>
<div style="font-size:.86rem;color:#B9CCE6;margin-bottom:6px;">إجمالي قيمة المشروع</div>
<div class="amt num">500,000</div>
<div class="cur">جنيه مصري &mdash; شامل التطبيق والبوابة والنشر والتدريب</div>
<div style="font-size:.82rem;color:var(--gold-2);margin-top:8px;font-weight:700;">
......@@ -937,7 +591,7 @@
<p>يكفي أن ترتفع نسبة التحصيل في موعده ٢٠٪ عمّا هي عليه اليوم، حتى تتجاوز الزيادةُ السنوية في المُحصَّل قيمةَ المشروع كاملةً خلال أقل من شهرين من التشغيل.</p></div>
<div class="card"><div class="card-ico"><svg class="ico"><use href="#i-plus"/></svg></div><h4>خصم التوقيع المبكر</h4>
<p>عند التوقيع خلال ٣٠ يومًا من تاريخ العرض: خصم <b class="num">5%</b> من الإجمالي &mdash; أي <span class="num">25,000</span> ج.م تُخصم من الدفعة الثانية.</p></div>
<div class="card"><div class="card-ico sky"><svg class="ico"><use href="#i-layers"/></svg></div><h4>المرحلة الثانية</h4>
<div class="card"><div class="card-ico sky"><svg class="ico"><use href="#i-layers"/></svg></div><h4>إضافات لاحقة</h4>
<p>الحجوزات والمحفظة والمطاعم والقرى والشكاوى &mdash; تُسعَّر على حدة عند طلبها، وتُبنى فوق النظام نفسه دون هدم ولا إعادة بناء.</p></div>
</div>
</div>
......@@ -965,7 +619,7 @@
<p>رسوم الربط البرمجي مشمولة. أما عمولة المعاملات (عادة ١.٥٪ &mdash; ٢.٥٪ حسب البوابة والوسيلة) فهي علاقة مباشرة بين النادي وبوابة الدفع، ويمكن للنادي تحميلها على العضو أو استيعابها &mdash; والنظام يدعم الحالتين.</p></details>
<details class="faq"><summary>من يرفع أخبار النادي، وهل نحتاج مصمِّمًا؟</summary>
<p>يرفعها موظف الإدارة من البوابة مباشرة: عنوان ونص وصور، ثم نشر. لا حاجة إلى مصمِّم ولا إلى أي معرفة تقنية، ويصل الإشعار لأعضاء التطبيق فور النشر. عدد المنشورات والصور غير محدود.</p></details>
<details class="faq"><summary>ماذا لو أردنا المرحلة الثانية لاحقًا؟</summary>
<details class="faq"><summary>ماذا لو أردنا إضافات جديدة لاحقًا؟</summary>
<p>قاعدة البيانات مفتوحة لكم والنظام مبني ليُضاف إليه، فأي إضافة مستقبلية &mdash; حجوزات أو محفظة أو مطاعم أو ربط بنظام محاسبي كامل &mdash; تُبنى فوق ما سُلِّم دون إعادة بناء ودون هدر ما دُفع.</p></details>
<details class="faq"><summary>ما مدة صلاحية هذا العرض؟</summary>
<p><span class="num">30</span> يومًا من تاريخ تقديمه. <strong>الأسعار بالجنيه المصري وغير شاملة ضريبة القيمة المضافة</strong>، وتخضع للمراجعة بعد انتهاء المدة.</p></details>
......@@ -977,7 +631,7 @@
<img src="assets/al-arcade-white.png" alt="AL-Arcade">
<div style="font-family:var(--f-head);font-weight:800;color:#fff;font-size:1.05rem;">شركة الأركيد &mdash; AL-Arcade</div>
<p style="font-size:.85rem;max-width:520px;margin:10px auto 0;line-height:1.9;">
عرض فني ومالي مقدَّم إلى مجلس إدارة نادي الصيد المصري &mdash; المرحلة الأولى: تطبيق الأعضاء وبوابة الإدارة والمحتوى.
عرض فني ومالي مقدَّم إلى مجلس إدارة نادي الصيد المصري &mdash; تطبيق الأعضاء وبوابة الإدارة والمحتوى.
</p>
<div class="fl"><span>500,000 ج.م غير شاملة ض.ق.م</span><span>2 &ndash; 4 أسابيع</span><span>دفعتان</span><span>تسليم كامل بالكود المصدري</span></div>
<small>وثيقة سرية &mdash; لا تُتداول خارج مجلس الإدارة &mdash; 2026</small>
......
......@@ -42,129 +42,174 @@
g.appendChild(frag);
});
/* ---------- live prototype controller ----------
The phone is an iframe running the real prototype. Clicks in the side list
drive it over postMessage; the prototype reports its route back so the list
and the annotation stay in sync when someone navigates inside the phone. */
var NOTES = {
login: { t:'الدخول برقم الموبايل',
/* ---------- note card (shared by the app and portal viewers) ---------- */
function noteHtml(n) {
return '<div class="card" style="border-color:var(--line-2);">' +
'<div class="card-ico"><svg class="ico"><use href="#i-note"/></svg></div>' +
'<h4>' + n.t + '</h4><p>' + n.p + '</p>' +
(n.b ? '<ul class="scope-list" style="margin-top:14px;">' +
n.b.map(function (x) {
return '<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>' + x + '</span></li>';
}).join('') + '</ul>' : '') +
'</div>';
}
/* ---------- app screens ----------
Every image is a capture of the real Flutter app running against the demo
server — nothing here is redrawn. The captions are the proposal's own
wording, corrected only where the shipped app behaves differently. */
var SHOTS = {
'01-signin': { t:'الدخول برقم الموبايل',
p:'يدخل العضو برقم موبايله المسجَّل في بيانات النادي، دون إنشاء حساب أو كلمة مرور، ودون تفعيل يدوي من أي موظف.',
b:['التعرف الفوري على العضو من ملف الأعضاء المرفوع','كود تحقق صالح لدقيقتين','جهاز واحد نشط لكل عضوية لمنع تداول الحساب'] },
home: { t:'الشاشة الرئيسية',
b:['التعرف الفوري على العضو من ملف الأعضاء المرفوع','لا كلمة مرور ولا تسجيل ذاتي ولا خطوة تفعيل','جهاز واحد نشط لكل عضوية لمنع تداول الحساب'] },
'02-otp': { t:'كود التحقق',
p:'كود من أربع خانات يصل إلى موبايل العضو وحده. تنتقل الخانات تلقائيًا، ويُرسل الكود بمجرد كتابة الرقم الرابع.',
b:['طلب كود جديد بعد دقيقتين','يقبل الأرقام العربية والإنجليزية','تنبيه صريح إذا كان الحساب مفتوحًا على جهاز آخر'] },
'03-home': { t:'الشاشة الرئيسية',
p:'تعرض حالة العضوية وإجمالي المستحق وطريقة السداد في موضع واحد، وتغني البطاقة الرقمية عن الكارنيه الورقي.',
b:['حالة العضوية وتاريخ سريانها','إجمالي المستحق مع تنبيه التأخير','اشتراكات الأبناء ورصيد الدعوات'] },
dues: { t:'بيان المستحقات',
p:'يظهر كل بند مستحق على حدة بسببه وتاريخه. حدّدوا البنود داخل الشاشة وسيتغير الإجمالي أمامكم قبل السداد.',
b:['تجديدات وأقساط وغرامات وفواتير أنشطة','سداد بند واحد أو عدة بنود معًا','الإجمالي يُحتسب لحظيًا حسب ما تختارونه'] },
pay: { t:'وسائل السداد',
p:'بطاقة بنكية أو محفظة إلكترونية، أو كود سداد يُقدَّم لخزنة النادي لمن يفضّل الدفع نقدًا. وتُسجَّل الوسائل الثلاث في التقرير نفسه.',
b:['Visa و Mastercard و Meeza','فودافون كاش و إنستاباي','كود سداد للخزنة يربط الدفع النقدي بالنظام'] },
qr: { t:'الدخول بكود QR',
p:'يتجدد الكود كل ستين ثانية أمامكم في النموذج، فلا يصلح تصويره وإرساله لغير العضو، ويعرض القارئ حالة العضوية فور المسح.',
b:['صفّ أفراد الأسرة، والنقطة البرتقالية تعني أن على الفرد مبلغًا','حالة العضوية وتاريخ سريانها','إجمالي المستحق على الأسرة مع تنبيه التأخير'] },
'04-card': { t:'بطاقة العضوية الرقمية',
p:'تفتح بملء الشاشة بالاسم ورقم العضوية ونوعها والفرع وتاريخ السريان والحالة.',
b:['حالة العضوية ظاهرة على البطاقة','زر يقلب البطاقة ليظهر كود الدخول','تُعرض دون إنترنت من نسخة محفوظة على الجهاز'] },
'05-dues': { t:'بيان المستحقات',
p:'يظهر كل بند مستحق على حدة بسببه وتاريخه، في ثلاثة تبويبات: مستحق الآن وقادم ومتأخر.',
b:['تجديدات وأقساط وغرامات وفواتير أنشطة','توزيع التأخير على شرائح بعدد الأيام','مستحقات الأسرة كلها أو فرد واحد منها'] },
'06-select': { t:'تحديد المستحقات للسداد',
p:'يحدّد العضو البنود التي يريد سدادها، فيتغير الإجمالي أمامه قبل أن يدفع.',
b:['سداد بند واحد أو عدة بنود معًا','الإجمالي يُحتسب لحظيًا حسب ما يُختار','الرصيد الدائن يُخصم تلقائيًا'] },
'07-checkout': { t:'وسائل السداد',
p:'بطاقة بنكية أو محفظة إلكترونية، ويُسجَّل السداد في التقرير نفسه الذي يُسجَّل فيه ما يُدفع في خزنة النادي.',
b:['خصم الرصيد الدائن من الإجمالي','إيصال فوري بصيغة PDF بعد نجاح العملية','«جارٍ التأكيد» بدل رسالة خطأ إذا تأخر البنك'] },
'08-gate': { t:'الدخول بكود QR',
p:'يتجدد الكود كل ثلاثين ثانية، فلا يصلح تصويره وإرساله لغير العضو، ويعرض القارئ حالة العضوية فور المسح.',
b:['منع الدخول عند إيقاف العضوية أو تجاوز حد التأخير','سجل دخول لحظي لكل بوابة','يعمل حتى لو انقطع الإنترنت عن هاتف العضو'] },
invites: { t:'رصيد الدعوات وبيع الإضافي',
p:'رصيد شهري محدد لكل نوع عضوية. جرّبوا إصدار دعوة أو شراء دعوات إضافية وسيتغير الرصيد فورًا.',
'09-invitations': { t:'رصيد الدعوات وبيع الإضافي',
p:'رصيد شهري محدد لكل نوع عضوية، يصدر منه العضو دعوة لضيفه ويشارك كودها معه.',
b:['رصيد مجاني يتجدد شهريًا','شراء دعوات إضافية بحد أقصى','ربط اختياري بين الدعوة وانتظام السداد'] },
activities: { t:'كتالوج الأنشطة والاشتراك',
'10-activities': { t:'اشتراكات الأسرة',
p:'كل اشتراك قائم لأي فرد في الأسرة بحالته، ومنه يصل ولي الأمر إلى الجدول وكتالوج الأنشطة.',
b:['نسبة الحضور وآخر تقييم داخل كل اشتراك','إبلاغ عن غياب أو إيقاف مؤقت مع حفظ المكان','جدول الأسبوع للأسرة كلها'] },
'12-catalog': { t:'كتالوج الأنشطة والاشتراك',
p:'يعطّل الاشتراكَ في الأنشطة أن ولي الأمر لا يعرف السعر ولا الأماكن المتاحة، ويعالج الكتالوج المعلن هذه النقطة.',
b:['أسعار وفئات سنية وعدد حصص معلن','السعة المتاحة وقائمة انتظار عند اكتمال العدد','تصفية حسب نوع النشاط'] },
activity: { t:'تفاصيل النشاط والاشتراك',
p:'اضغطوا «اشترك الآن» داخل النموذج: يختار ولي الأمر الابن، فتصدر فاتورة الشهر الأول ورسوم الانضمام وتظهر مباشرة في المستحقات.',
b:['اختيار الابن المشترك من أبناء العضوية','فاتورة فورية بالشهر الأول + رسوم الانضمام','تحديث السعة والأماكن المتاحة'] },
schedule: { t:'جداول التدريب',
p:'يرفع المدرب جدول مجموعته من قالب موحَّد، فيظهر لكل مشترك في تطبيقه، ويصل تنبيه فوري عند أي إلغاء أو حصة تعويضية.',
b:['جدول أسبوعي لكل لاعب على حدة','ملاحظات المدرب على الحصة','إشعار فوري بالتغييرات والحصص التعويضية'] },
news: { t:'أخبار النادي',
b:['أسعار وفئات سنية وعدد حصص معلن','السعة المتاحة وقائمة انتظار عند اكتمال العدد','القائمة مُصفّاة بسنّ الفرد المختار'] },
'11-schedule': { t:'جداول التدريب',
p:'يظهر جدول المجموعة لكل مشترك في تطبيقه، ويصل تنبيه فوري عند أي إلغاء أو حصة تعويضية.',
b:['جدول أسبوعي لكل لاعب على حدة','المكان والمدرب لكل حصة','إشعار فوري بالتغييرات والحصص التعويضية'] },
'14-news': { t:'أخبار النادي',
p:'قسم أخبار داخل التطبيق تنشر عليه الإدارة الأخبار والفعاليات والبطولات بالصور، ويصل الإشعار للعضو فور النشر.',
b:['نشر من البوابة: عنوان ونص وصور','تصنيف الأخبار (بطولات، فعاليات، إعلانات)','إشعار فوري للأعضاء عند النشر'] },
article: { t:'صفحة الخبر',
p:'الخبر كاملًا بصوره داخل التطبيق دون فتح متصفح. الصور المعروضة في النموذج نائبة عن صور النادي الحقيقية التي تُرفع عند التشغيل.',
b:['عرض كامل بالصور داخل التطبيق','عدد غير محدود من المنشورات والصور','مشاركة الخبر خارج التطبيق'] },
receipts: { t:'الإيصالات',
p:'كل عملية سداد تُنشئ إيصالًا فوريًا داخل التطبيق. جرّبوا السداد من شاشة المستحقات وسيظهر الإيصال هنا.',
b:['إيصال فوري بعد كل عملية','أرشيف كامل بوسيلة الدفع والتاريخ','قابل للتحميل بصيغة PDF'] },
notifications: { t:'الإشعارات',
p:'تذكير قبل موعد الاستحقاق وبعده، وتنبيه بتغييرات جدول التدريب وبتجديد اشتراكات الأبناء.',
b:['تذكير بالاستحقاق قبل موعده وبعده','تنبيه بتغييرات الجداول','إشعار بتأكيد السداد'] },
profile: { t:'حسابي والإعدادات',
p:'بطاقة العضوية الرقمية ومداخل الإيصالات والدعوات والجداول، مع تبديل المظهر بين الفاتح والداكن.',
b:['بطاقة عضوية رقمية','مظهر فاتح وداكن','إعادة ضبط النموذج للعرض من جديد'] }
'15-events': { t:'الفعاليات والحجز',
p:'رحلات وبطولات وحج وعمرة وفعاليات اجتماعية، يحجزها العضو لنفسه ولأسرته من التطبيق.',
b:['فئات تذاكر بأسعار معلنة وسعة محدودة','سياسة الاسترداد معروضة قبل زر الحجز','تذكرة بكود لكل مشترك'] },
'13-more': { t:'المزيد',
p:'مدخل واحد إلى حياة النادي والحساب والمساعدة، ويحمل عدّاد الإشعارات غير المقروءة.',
b:['الأخبار والفعاليات والحجوزات','الملف الشخصي والأسرة والإشعارات','الأسئلة الشائعة ووسائل التواصل مع النادي'] },
'16-profile': { t:'الملف الشخصي والأسرة',
p:'بيانات العضوية وأفراد الأسرة، وأمام كل فرد ما يملكه ولي الأمر من صلاحيات عليه.',
b:['صلاحيات المستحقات والسداد والطبي والحضور والتقييمات','بيانات العضوية كما هي في سجل النادي','رقم الموبايل يُغيَّر من مكتب العضوية لا من التطبيق'] },
'17-settings': { t:'الإعدادات',
p:'مظهر فاتح وداكن وتلقائي، وأرقام عربية أو إنجليزية، وقفل التطبيق ببصمة الإصبع.',
b:['تبديل صيغة الأرقام في التطبيق كله','قفل ببصمة الإصبع إن دعمها الهاتف','تغيير كود الدخول وإبطال القديم فورًا'] }
};
var noteBox = document.getElementById('scr-note');
var frame = document.getElementById('proto');
var pnBtns = Array.prototype.slice.call(document.querySelectorAll('.pn-item[data-route]'));
function renderNote(k) {
var n = NOTES[k];
if (!n || !noteBox) return;
noteBox.innerHTML =
'<div class="card" style="border-color:var(--line-2);">' +
'<div class="card-ico"><svg class="ico"><use href="#i-note"/></svg></div>' +
'<h4>' + n.t + '</h4><p>' + n.p + '</p>' +
'<ul class="scope-list" style="margin-top:14px;">' +
n.b.map(function (x) {
return '<li><span class="tick"><svg class="ico"><use href="#i-check"/></svg></span><span>' + x + '</span></li>';
}).join('') +
'</ul></div>';
}
var shotImg = document.getElementById('shot');
var shotFull = document.getElementById('shot-full');
var noteBox = document.getElementById('scr-note');
var shotBtns = Array.prototype.slice.call(document.querySelectorAll('.pn-item[data-shot]'));
var grid = document.getElementById('shot-grid');
function syncNav(route) {
var base = String(route).replace(/^#\//, '').split('/')[0];
pnBtns.forEach(function (b) {
b.classList.toggle('on', b.dataset.route.split('/')[0] === base);
function showShot(k) {
var n = SHOTS[k];
if (!n) return;
var src = 'screenshots/app/' + k + '.jpg';
if (shotImg) { shotImg.src = src; shotImg.alt = n.t + ' — تطبيق أعضاء نادي الصيد المصري'; }
if (shotFull) shotFull.href = src;
if (noteBox) noteBox.innerHTML = noteHtml(n);
shotBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.shot === k); });
if (grid) grid.querySelectorAll('button').forEach(function (b) {
b.classList.toggle('on', b.dataset.shot === k);
});
renderNote(base);
}
pnBtns.forEach(function (b) {
b.addEventListener('click', function () {
var r = '#/' + b.dataset.route;
if (frame && frame.contentWindow) frame.contentWindow.postMessage({ type: 'sayd:goto', route: r }, '*');
syncNav(r);
// The thumbnail strip follows the side list's order, so both read the same.
if (grid) {
shotBtns.forEach(function (b) {
var k = b.dataset.shot, n = SHOTS[k];
if (!n) return;
var t = document.createElement('button');
t.type = 'button';
t.dataset.shot = k;
t.innerHTML = '<img loading="lazy" src="screenshots/app/' + k + '.jpg" alt="">' +
'<span>' + n.t + '</span>';
t.addEventListener('click', function () {
showShot(k);
var stage = document.querySelector('#app .phone-stage');
if (stage) stage.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
grid.appendChild(t);
});
});
window.addEventListener('message', function (e) {
var d = e.data || {};
if (d.type === 'sayd:route') syncNav(d.route);
});
}
var themeBtn = document.getElementById('proto-theme');
var resetBtn = document.getElementById('proto-reset');
var protoDark = true;
if (themeBtn) themeBtn.addEventListener('click', function () {
protoDark = !protoDark;
frame.contentWindow.postMessage({ type: 'sayd:theme', value: protoDark ? 'dark' : 'light' }, '*');
});
if (resetBtn) resetBtn.addEventListener('click', function () {
frame.contentWindow.postMessage({ type: 'sayd:reset' }, '*');
shotBtns.forEach(function (b) {
b.addEventListener('click', function () { showShot(b.dataset.shot); });
});
showShot('01-signin');
renderNote('home');
/* ---------- portal screen switcher ---------- */
var URLS = {
dash: 'https://portal.sayd-club.eg/dashboard',
import: 'https://portal.sayd-club.eg/import/members',
members: 'https://portal.sayd-club.eg/members',
catalog: 'https://portal.sayd-club.eg/activities/pricing',
invites: 'https://portal.sayd-club.eg/invitations/policy',
sched: 'https://portal.sayd-club.eg/schedules',
news: 'https://portal.sayd-club.eg/news'
/* ---------- portal screens ---------- */
var PSHOTS = {
'dashboard': { u:'/portal', t:'لوحة التحصيل',
p:'الصفحة الرئيسية للبوابة: المحصَّل هذا الشهر وإجمالي المتأخرات ونسبة التحصيل ونسبة السداد عبر التطبيق، وتحتها تنبيهات بما يحتاج قرارًا اليوم.' },
'collections': { u:'/portal/collections', t:'المتأخرات',
p:'كل من عليه مبلغ متأخر، مع زر إرسال تذكير لكل صف، وتصدير الكشف كاملًا.' },
'members': { u:'/portal/members', t:'الأعضاء',
p:'بحث بالاسم أو رقم العضوية أو الموبايل، ومن كل صف يُفتح ملف العضو: البيانات والأسرة وكشف الحساب والمدفوعات والأنشطة والطبي والدعوات.' },
'revenue': { u:'/portal/revenue', t:'تفاصيل الإيرادات',
p:'الإيراد مفصَّلًا ببنوده وبوسائل سداده، شهرًا بشهر.' },
'programs': { u:'/portal/programs', t:'البرامج والأسعار',
p:'ما تحدّدونه هنا هو ما يظهر للعضو في التطبيق: الاشتراك الشهري والفئة السنية والشهادة الطبية المطلوبة.' },
'groups': { u:'/portal/groups', t:'المجموعات والمواعيد',
p:'المدرب والسعة القصوى والمكان لكل مجموعة، والمواعيد الأسبوعية التي يُبنى منها جدول كل مشترك.' },
'enrollments': { u:'/portal/enrollments', t:'الاشتراكات',
p:'حالة كل اشتراك: بانتظار السداد، نشط، موقوف مؤقتًا، ملغي، منتهي — مع قائمة الانتظار وطلبات النقل.' },
'roster': { u:'/portal/roster', t:'تسجيل الحضور',
p:'الافتراضي أن الجميع حاضرون فتُعلَّم الاستثناءات فقط، وأمام كل لاعب حالته الطبية حتى لا يُدرَّب طفل ممنوع طبيًا.' },
'medical': { u:'/portal/medical', t:'مراجعة الشهادات الطبية',
p:'الشهادة بصورها، ثم القرار: اعتماد كامل، أو اعتماد مشروط بقيود مكتوبة، أو رفض بسبب يقرأه العضو في تطبيقه.' },
'news': { u:'/portal/news', t:'الأخبار والإعلانات',
p:'عنوان وملخص ونص وصورة غلاف، وخانة «تثبيت في الأعلى» تجعل الخبر يظهر على الشاشة الرئيسية للتطبيق.' },
'events': { u:'/portal/events', t:'الفعاليات',
p:'فعالية بفئات تذاكر وسياسة استرداد ومستندات مطلوبة، ولا تظهر في التطبيق قبل نشرها.' },
'invitation-policy': { u:'/portal/invitation-policy', t:'سياسة الدعوات',
p:'لكل نوع عضوية: الدعوات المجانية شهريًا وسعر الإضافية والحد الأقصى للشراء ومدة الصلاحية — وهي بالضبط ما يراه العضو في تطبيقه.' },
'access-log': { u:'/portal/access-log', t:'سجل الدخول',
p:'من دخل، ومن أي بوابة، ومتى، وهل سُمح له أم رُفض — مع عدد الموجودين داخل النادي الآن.' },
'wizards': { u:'/portal/wizards', t:'المعالجات',
p:'ثلاث وثلاثون عملية مُرشدة، كلٌّ منها يعرض مراجعة قبل الكتابة، وينفّذ كل شيء أو لا ينفّذ شيئًا.' },
'imports': { u:'/portal/imports', t:'استيراد البيانات',
p:'رفع ثم مطابقة أعمدة ثم تحقق صف بصف ثم اعتماد. لا يُكتب شيء قبل مراجعتكم، وكل استيراد قابل للتراجع بعد اعتماده.' },
'billing-runs': { u:'/portal/billing-runs', t:'دورات الفوترة',
p:'إصدار مستحقات الشهر على الأعضاء دفعة واحدة، بعد تشغيل تجريبي يعرض النتيجة قبل الإنشاء الفعلي.' },
'campaigns': { u:'/portal/campaigns', t:'حملات الإشعارات',
p:'رسالة موجّهة إلى شريحة بعينها — نوع عضوية، أو متأخرين أكثر من عدد أيام، أو من لم يفتحوا التطبيق — مع معاينة عدد المستلمين قبل الإرسال.' },
'branding': { u:'/portal/settings/branding', t:'الهوية والعلامة التجارية',
p:'اسم النادي وشعاره وألوانه في البوابة والإيصالات، بمعاينة حية قبل الحفظ، ودون نشر جديد.' },
'audit': { u:'/portal/audit', t:'سجل المراجعة',
p:'كل تغيير: من فعله، ومن أي جهاز، وأي حقل تغيّر ومن أي قيمة إلى أي قيمة. وهو المرجع عند أي خلاف.' }
};
var burl = document.getElementById('burl');
var pBtns = document.querySelectorAll('.pn-item[data-pscr]');
var sideItems = document.querySelectorAll('.side .si[data-jump]');
var pImg = document.getElementById('pshot');
var burl = document.getElementById('burl');
var pNote = document.getElementById('pshot-note');
var pBtns = Array.prototype.slice.call(document.querySelectorAll('.pn-item[data-pshot]'));
function showPortal(k) {
pBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.pscr === k); });
sideItems.forEach(function (s) { s.classList.toggle('on', s.dataset.jump === k); });
document.querySelectorAll('.pscr').forEach(function (s) {
s.style.display = (s.dataset.pscr === k) ? 'block' : 'none';
});
if (burl && URLS[k]) burl.textContent = URLS[k];
var n = PSHOTS[k];
if (!n) return;
if (pImg) { pImg.src = 'screenshots/portal/' + k + '.jpg'; pImg.alt = n.t + ' — بوابة إدارة نادي الصيد المصري'; }
if (burl) burl.textContent = 'https://portal.sayd-club.eg' + n.u;
if (pNote) pNote.innerHTML = '<h4>' + n.t + '</h4><p>' + n.p + '</p>';
pBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.pshot === k); });
}
pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pscr); }); });
sideItems.forEach(function (s) { s.addEventListener('click', function () { showPortal(s.dataset.jump); }); });
pBtns.forEach(function (b) { b.addEventListener('click', function () { showPortal(b.dataset.pshot); }); });
showPortal('dashboard');
})();
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