Prepare ERP source for Gitea deployment

This commit is contained in:
A R R R Associates
2026-06-20 15:01:44 +05:30
commit 5c75eb6bd9
450 changed files with 67698 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Engagement document streaming module."""
+527
View File
@@ -0,0 +1,527 @@
from __future__ import annotations
import base64
import io
import zipfile
# Embedded LSA4 tunnel-enabled Task Scheduler local storage agent package.
# Uses Windows Task Scheduler instead of pywin32 Windows Service.
# This is used by the ERP Branch Storage Dashboard to generate branch-specific
# pre-configured agent ZIP files with .env already filled.
LSA4_ZIP_B64 = """
UEsDBBQAAAAIAKqIuFw8Po1A0QAAAD4BAAAMABwALmVudi5leGFtcGxlVVQJAAPQLxNq0C8TanV4CwABBAAAAAAEAAAAAG2MwWoCMRBA7/mX1GwLtSzkMLuZ
1oWY2Mms7UEI0gbdg1Vi/H9XrLQHGZjDvHkPaREbCBh7snpbyqGeTKrH6YMap6pflFLCeYOxHZduCFw7k4E9wRtKpaorDNgSsj6sjyXJ/U+SZdgleUxfORW5
TTmJXyWS96xNvVrB6Xsor0PehbLP600SC29t7BwjLcFeit6ZoJ+UMP7DWQ8mEr73GPju0wyBuEG4Q5+VuJnczdH3/B/N4XMMM3U4dgT3zqGN6KCxaHTJp3S7
0cVx2P7ZlRJnUEsDBBQAAAAIAKqIuFxSLOFFWAIAADoEAAAKABwAUkVBRE1FLnR4dFVUCQAD0C8TatAvE2p1eAsAAQQAAAAABAAAAAB9U8tu2zAQvOsrFsjN
gJjmcQrQg5OobQDHMSQVQYsAFkWuLcI0VxXJPP6+Sykx0CCpDgZlDWdnZ2ePYB61CfDNDHuoAg1yizDfoguQQy39DirVoY4WBygYaMhlWd0ZD71UuwQeovMQ
OgRLSlrwrxxy5IjeuC3cG6fpyb/nM84HlBpoA/3Lk3FnpwdkhcOjUSiy7OiIX0LsgdyBW9G+jwGH7ERA8RwGqcKo4PfNCgKBhI15Rg0bsprL+Kg6kB6a4uJh
bDb1+trq2Ol6XpZl/uUkr+q7cv69aER2KqA02y7kyhq1gyZJldauA3ew9m8diFaGBqTToDoijzCbldGlWlLvjTOepXGd2UxkZwJqVpjug5N7BHaw+VBNI+Am
TK7KcDCEyw/JBeauflV1cSuycwFXHSZ1lrb+YTRc8HFSlPwoyhVcDtJx/2+jvZa+a0kOyZ0B7paLm2WRyEP0k9m30kUeI1u8Zxqf5XyVS4OjpwtoRhkfuTDi
qD/AqP8UlWpNVHz4BFXinh6RUdH9x3oGXmMbt6zWebJjFvkO/65HN9Y6fZ2g2ZIOKdPYo9Po1Esaw4B/ohk4L8axa/wHTjlnPxbV/Bzq6BxauCWNWf7u+XcV
iGl5ag4ohpYiT+Ee24rUDjmfEwvH81iTinuW549fA52Pao8niBjHpgk9m8m+46hsopNKofeJJI23nUa7uhK8vtw9p835TUp8MNby7jGFjIx0wSgZmOdHXa8q
4N57MlxfpNWC+udyWSzWxXJ+uSiuv26k5SRzCT5YaLm19MJwXkkORk/W8k6L7C9QSwMEFAAAAAgAqoi4XJHTLtOTAwAAOQcAACMAHABSRUFETUVfTFNBMV9M
T0NBTF9TVE9SQUdFX0FHRU5ULnR4dFVUCQADzy8Tas8vE2p1eAsAAQQAAAAABAAAAACNVe1uGzcQ/M+n2AfI3fkjTgsjDqDKahLAsQSd0vSHAYG6W+mYnMgL
P2wrQd+9Q95Jllu0iQQQFLk75C5nRqNQK0+/K7ulG1PJlkpvrNwwjTasPWV0U45O6cMfMyFmwXbGscj6j1g0ypHzUteyNZppZaWumkxprLUt1yQTRGW05so7
8oZ8A+B04mQ+o+vyPLsuL4CRjsz6eNZ1Z5T2LhfvPXWhbR11WFR6Q7WpwjYGDSn02awcra3ZJuiqNaGO0C9SAGNLtRiVTtv9BalNdQ4IYm3amu0LktUXbR5w
7Q0Synejs4tX1EjXFBGCnPqGfMTEKtIBKJvWocWuo1hEbR50a2RNlr8Gdqh3taPQxSXcXAyr6Ep/fH+xI8Rc9A3tsBQrqw32tfHA61pZMRlLW1Or9Q5H71L/
Oms+o7MJC+kl+9D1ryNOcxqbboeyAdmXuO//0IV9Byuz7YJnWzi292xzcYZUy9IzSZrtfGM03SvrAy7NGjOj4wtcCiLq+u1sS/fYojyOcT1N7srKqs67O1l5
dQ88cZ7T+54cqUnKckRyPZTqaGAOZfbZfu4fvXg5FJQDOedHue3wKqgo/k5vwWBVQkJnlr+Nysny4/zmqvG+uyyK07Nf8hN8Ty9/PTk5ia3cmWCf+EKIjbm3
0+vJcozh6nUiVWLpwDlH2ZuDOm5Nze7NIaWcjOeTxdVryCDzassEJrOV8bkdV5Z9Ci0X0/no7WQ5n04XV9eXd0kJUXoDqrjIaR40GV3xP/orY+hyeLNlEkq+
leB1lsVo8arPhNa80sEE1+5+EkGIWORBdBQc13uJH33EbFouqNgL0BXPRFs0LK1fgTXi7WRB/x0X9VoMav6Z0O8Yl6r+q9jL6wf3eJ4EKf3gkD1stlft4XL/
f86/874Ps3Rwr3tIUq7Z74Z2iow+Ooj6zyyyJxtjSMwdfpeJKIROQqwwvyyR7eBhRrfwEx2FfMyjFCe18jAoB9m0EmJjuIhvBmOMz7viNaDowSIONpoQY+a4
F//B8Ia4Iy+MxcfIT0hFXGs2ycjzIs6KnkaYwn5u+RF2Dctkd8wfgT+QM3pQ0DXYCYfxvSmBbAYO80lp9BIXgPsouFyxd4FoRUg971NlXZPpvDIaNuQD/lHa
oo2FOg+v2qKoFP3yKfrdh9G4cGqjpQ+Wi5oT/Eql103BF0/BLqxcciscQGtOKVQ1XH1x6Ym+BuMlRILuVENL/gZQSwMECgAAAAAAq4i4XAAAAAAAAAAAAAAA
ABQAHABhdWRpdF9zdG9yYWdlX2FnZW50L1VUCQAD0S8TatEvE2p1eAsAAQQAAAAABAAAAABQSwMECgAAAAAAqoi4XGtoY4klAAAAJQAAAB8AHABhdWRpdF9z
dG9yYWdlX2FnZW50L19faW5pdF9fLnB5VVQJAAPPLxNq0S8TanV4CwABBAAAAAAEAAAAAF9fdmVyc2lvbl9fID0gIjAuMy4wLXRhc2stc2NoZWR1bGVyIgpQ
SwMEFAAAAAgAqoi4XGFbMtTgAgAA8AsAAB0AHABhdWRpdF9zdG9yYWdlX2FnZW50L2NsaWVudC5weVVUCQADzy8TatEvE2p1eAsAAQQAAAAABAAAAADlVktv
nDAQvvMrLE4gsRCpt5W2UhX10EsVtb1FkeWFYdcJ2NQ2TVaU/96xebNRu22lRGo57MK8PPN9MwO5kiWhNK9NrYBSwstKKkOYENIww6XQnpdbm4qZY8H3g8EN
PnYKc6q4OAzyd+IUkQ8GFNsX4PVCBV9r0GYIFadS5HxyOYAw107keV5aMK3J+0831wVH+dYjeGWQY5JccENpoKHII9LF2M69w87YXtZmOGbX2y6VGrTG8lA7
ZBd/7kRB+KxlfASWgdJxXWXMQNAFHaShNyVaq6JP0oK2JdqokGze2v8pQwWIuCC538xyjUFVdM802BhtY91bf4qMZymzB2bG8KdCsmxLMp6aWwwfWfzv3GFL
0fxcXSGrgIUvqqukNsFoNRbvivGTTKZ1iUDrRBup2AE2zOKejBn5YbRwvtdS7Pr8lhrDS5C12c3L7imgvY5qQEWmJ8fwLP9YMY445VJRjZ1a6yBcYzua2mSC
kPB8EmF8gwUQKBCKxu9C+Fviywe/nRCvQGTY3bSvmt7LvXbgO4wLrs3tEugLkD7AHwBtD076bNZYvwCi2PHMjcoC0VGNyHLNBbqJFAJrHDlwZhM548Xq11xZ
mUPGt5X6Ebm9m41UJh+F7aQ5D/0M4B3lOAIcyfzuRu2vGMh/QUHTndcmQ0prMjADYOXui6rhtft+wo+lD0I+FpAhdBdA+HKb5WK4sYJ/fMWMPT68j15lzwxZ
bIYs/o+lM1S7Xjx15ShZc0NzXkA/PYNoNUHWgnbvf/utFBF4Mor91kA9cnOc4sQSmcBM935ImCZHJrIClqVaW42INb69w1YLJm/BSoh6r4j4rKoKnrovvESm
BsymW1x+2C5C9hw0D3ByHzLBN1bUEBIkjKAsIu4ZC+/qi7mBUneD0Gs0wU9J8lEKaFe8XLoulr37k5Vx3rzNxE6bdGSuG3kEbud+z5UWgp1rrzPVMAQlewou
GQTy5upqdfwLr50fUEsDBBQAAAAIAKqIuFxm9F9AyQQAAOYNAAAdABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9jb25maWcucHlVVAkAA9AvE2rRLxNqdXgLAAEE
AAAAAAQAAAAAvVZRc+I2EH7nV7h+sqeJk5vr3AMztEfA12OGmNSY681kMhqBRaKekVxJDkkp/70r2dgCm+StPGB79e1qd7/VateCbxyE1oUqBEHIoZucC+Vg
xrjCinIme71KxmVvrdEpVniVYSmJPMBrUYkoRJbRZZBjIckBAjLCVjwlJSTH6gkwh9U7+Kysc0XY80GecZyiUtTr9T7X+3iA/YewQSIK4veMyBk+EqZGnK3p
Y7/nwI+IHC2xJAj27jtSCSNl4APSjpyIJFkJohqhVFzgR4IE5yA1DmoxuIKZQjQ1SOdfJ+KMOAPzMIClwGz19AYg51mGKFNEPONMb8tZKvsOSAD18dpgUr5l
JnZB/i6IVO/hnwgWaknweeCnEniwp+iG8EKdQW3wC+ysBCXNRmX0BWMkQ5CDZUYgwiXnGSxqGux1oa0ysmrZ/3DdM7jPueA5Eeq1jJasdQQpEdKTJFv7zuWv
TkpX6h4yeKHT+FAyWkYApcqcXS3QP/f7ZQQkXo7gz4XEg5GgJvqiEzo3fB+ByxI4gS8kEZemtgDrDouUqi9UbOZleZiFq+vgl+DabRT356Ks8gMF2QQK4TXR
6XqFPBmf7PoNBMBo7rlXrl+D6drgA6mAfbml6slzn5TKZf8KYP2jOLYSVbbdrTQA52ejfQ/n8kjtodYj2bkd3tug037bvCTnbOhHvQQlK15BWPcQb+fW7Lbp
dlyLzQ6C9/5pMa3dXbX1/irlq2IDpMqrqgNcYsNxSd1vO+PM3oV2pAlFj0QhfQo8hjdlS7nQTOMiU+XxMBzrlzJUgbcQCZcBKEJXM2qlP5BrvUhl2TG40MY8
EPlByb3vDCC5busoVNv1LNGpZpDxLRFggcLJcT+4kCMFZ1Y/X4k0D/3Hmbu3A4ND2xkXyE1Y8PxfolLitbWufdNmylb/siK5cr7hrCChELALllpoaWEKhRUX
THc+A/HW7oRBp6SptkUeiXDWoLjTvu/7zg6M/yT2ru+YewmMVZkxfXll7hkPYkVrmpGOXm8S1LqTIB21Tu2bdcvVFqu4jo6IjfN7rTvuiAE3jO/QzXAeokU8
1dy6fquH1EfmWDOajUM0gr9KreLp9KrsUJqHozhMOtTsyxSdFos7T2bx8PcQxbNZl3J95R5rJWE0jBI0GYOKJR4uxpMEfZnEt+USWGtKDfhtX9THZm/iYTT6
Wuu2VMsLkkpJ2SNo3j8ceIWJ6XjkqHmr0AHOc8LSE2p8W78ZTs4qN+y0NasZ5m3diqQj7VN+zps44qq2UaHePG7ubZU0PYNQQVKnPESFMIMmXK5wY+ikB39x
yrzKpF8Vuu0hpF3PY96p135AXnLM0kLqXhcIInn2TDpKMNj8SKnwYEDVjd5MkRdwwikMRvxHNVTa7dQ6xl5zfVlcD+yPZg6o6Rx0DCMWY4PO8cP2eGB/NJD6
aAzqt2axLvBB/dYsdk6hg7rru3ez6RRNoiSMvw2numZm0XgO7Hy89hsj746plsHx7M9oOhuOURz+sQjnybvGz8+0ltWv4TBObsJhp7lPtrkzk69l6+BYMrkN
Z4vknCFrOLaUb4ffIbIknoQmEAt/PDMPmonBTRZRFE4R9LGbaaj7jam8lmZrmra2rUzE2tUoHNlOfzg47ff+A1BLAwQUAAAACACqiLhciumkADYCAABCCQAA
GQAcAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvZGIucHlVVAkAA88vE2rRLxNqdXgLAAEEAAAAAAQAAAAA7VVLb5wwEL7zK0Z7AolyaNUeVkorkrrVqnR3xZIqOVle
MI0bsDe20ab59TXmXTZK1UOiSrUQ0ni+mfnmBbkUJWCcV7qSFGNg5UFIDYRzoYlmgivHae/UXcE0fePktcmB6JuC7Tv81oiNIiOaalbSTtPJPtTvB8Gp4zhp
QZSCSKSk+Hi+dMCcjOaGB+NMY+wqWuQ+ZHtch1la714Ds9DmPjgQSbkOytuMSbcR1FkiKxOL3jOlsbi1otdb1n6D1hzOOkdTteXgek7PKhWc01RbUiMW9bXx
0VYlGKO6EN4EHEhxxDlJtZA/R4axOPYwSU0buEUP8RtCNjq8eg9rU8KBxZGZTGzMjoAHRFkPA6hnQO9pWmnqTjT1WSwWs7uLGIUJgiQ8jxCsPsF6kwC6Wu2S
HRykSKlSNMPKZEO+U/xD7BXM/dbHqDDLIEFXCWzj1dcwvoYv6No/CS7qmWj6Yw3qoOvLKDqNVjfk9dt3f4LMWUGxYg8UVusEfUbxE/ghQ6Kn/mdw78lqes/R
iUwceSFIhiW9q6jSj7WjVf9vyejYlqSiLKebL2kq5GTG2y9TM9JLUFr6o/q0F00JWqHPcgmM6+fd4NV6h+IENjHEaBuFF6iu9OaR7Z1Zu02W4wS73EZp+ZO+
zAv/LYwu0Q7cDz4Mz8n+zLvumgq2LLzTNExJ3Z6KwXR/m4CLo9v9cIJKp17AlMiFLImpr+f/9Sz8vmXtQAxL9e8PxexDMu/LkO5LT8fA5KUm5BdQSwMEFAAA
AAgAqoi4XDIlztSHAQAAPwMAAB0AHABhdWRpdF9zdG9yYWdlX2FnZW50L2xvZ2dlci5weVVUCQADzy8TatEvE2p1eAsAAQQAAAAABAAAAACVUttKxDAQfc9X
hMJCCmtdBEGE+qCwKCyrqO8httNu2DYpydQL+PFOs8naBV8spO3MnDNnLmmc7bmUzYijAym57gfrkCtjLCrU1njGoq+zbatNy5qJEo1ip0zdgfOJ+Bxopl3r
Du4PsQNhULjr9FvCPZHJGKuh4R5wHOSUEJx4Ux5krd11QPBvvrUGeBk+OT+7OQpvAv6acXqctUiYxOXWBXZRfdQiDwhi+RAqD+Bznk2u7CRY9Ht6i0E5MOjL
VzfCksOn9ijtPpg5SwTSplypmBbwUI/I1FhrlB6tUy1IOgazfMYqqN0NvEMnEvlhu348QaSZFlUHyoko2ljXK8QT3XXyiWwhlK9Q95B7mtpCdJOGUUd79tuD
91RY7rOYuqI12w5miV/QgerjAuMMI2pq4Ff3WNVJB6quEzeyUhN0LWTsj+T+uC3iuCraURhfQZ5syXv1efuF4MsLuVqtprOkjVf7cbizo8HyknZlKltTujIb
sTm7inOfa/6z+Dk1duDotjoTsewHUEsDBBQAAAAIAKqIuFzrTm6iUQIAAH0FAAAbABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9tYWluLnB5VVQJAAPQLxNq0S8T
anV4CwABBAAAAAAEAAAAAH1U22rcMBB991cMhoIXsm77WnAhbVMoLSUkeRdaa+yIypLRJY3/vqOL7c2tfvHunDNnLjryYM0EjA3BB4uMgZxmYz1wrY3nXhrt
qmqN2XHm1mE1xJyZ+3slT2vCNf1diW6hpERqeyVR+5V0dXP9NQVW1OhBjiuqDBcshwouNvlfpufq25cSV2Yc0a6YQx9mlmOF4Bbdr/CtN5aPeDnudX3QGtVr
hLuEbBNHHWmqqhI4wClIJVhagW0OcPy8baS9tGOYKP06gZ8qoCcToXuL1Qh0vZVzXHJXXwYhPXyXdsqzrl1BaguOcMfdH7jt71EERbI3sVFbH85KtVwIxkuN
pj4eUT/UF3CPau7qeD7gDbQUhEEqJISG4kH57rfR+F8ho/vI533u1VFryLwNuMlTO0AqkBbfL71CspAAfJS+9GjplKwuFcpGJy513qTU/tnWnm47YdSSI6Q0
mV6xTVfg4oruiSGaOHjb/xXNIbO8XXKl+BQDdufea6JkXNNho5EPu9WCZ4LwHmrBPa/jDx7PiRxb72nF/N3u+yaX2CkpixjnHiyki5JP53S6KMPtiXJI62jj
0ezzbJKtDZpFrNlTUFFS1i43gKHmJ4XimUB2fZRoXt6NJukfUoGBfPAQj+e8iHuzn42+4cUUH1IAH3ucPfzE5WS4FT+0R2vD7He5vIRW6sE0db4Y5MV5RgGn
BYLbLsRb2lfpRSamIWPshTSuhFV/4DIuCN458jqhL/Q/kpdpr4xpPsUPaNdBzVh0NmN11rdcOoTbxXmcruhGNNn3h+ofUEsDBBQAAAAIAKqIuFxT7nvCSQMA
AOQHAAAeABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9zdG9yYWdlLnB5VVQJAAPPLxNq0S8TanV4CwABBAAAAAAEAAAAAI1VTY/bNhC961dMeaKyNncTtAFiwAFS
YFv0kDRIcupmS9AWZROWSIGk1rtBfnxnSMkr2QEaHWzNJ/neG1K1dy1IWfex91pKMG3nfARlrYsqGmdDUQy+vQr7xmxG04XxzeuipjadipQw9viIZg7Ep87Y
3ej/3Vjln/76uyjk53d/3MrPt3++v/3wBdbYSGxd25lGc8/u/n23/Ectv90s3wgJy/srVhZFUekagrImmm9aBr1rtY38QTW9XkGIfgG1apqN2h6SiT2Zibpl
JSzfkmNVAD4pH2O5EJwHxkqBYdPxUnjdNWqrObtmC2CSTTxfvw6uWZsZDBH6DceURQ7PM9P/sBADMTbyGtm3cNrNiOGEt9bS6wb1eNCSWOZeHdNLRvkdPjir
n7HLGim0qs2cJOwkRgbPGPuU11OpM4ydk34L6Lx+QFJJMHJA9OpB+6AacH0MptLY03m1w0LnosB2qa2p4bSp5KCnUz4GxH13f3LViI/cYGzaFMcdnuBcUH3N
ylKkLs9Nh9XGJpxRokg/gpXzPHq2jtD0ehZIPYXqOm0rfjFQFC3L4my1sz0MqiUQL1J8pmYGd975QiLcduW2PQXFxljEO6judXAN6t3bSntJXPOB+GSs0gIL
mM1Fdp4JHpXf6UjjPq2H63kpUZ8W5AMKylnDtOQsA1lxQVAtndrW2TSZd0lPzC4XNHw8r17el/DLGk6xZya9MkHDpx41avWt985znM+6DzSA0cHR4wG+9lpV
P5y/4QTlVWhSiMf2UBnPsxHWX3yPLOtHE6J0h2TOhMq141nbq1e/vU7q8DNGT9dHZXY6EDnDjShy0UDL0eChSaw4HC7O/AZvHxUw2VaNXs0Ownbf2wMNMWL0
vFHtplKrIVMQZv7y5tWv8ALoDwndsPMBz3sRfVepqHnqNwM3xPf6Mb/xcbwSrxIxadXK6DLibK5OV/QCKqxBg74EEypi3zX6ztiYNL7/GVoifk8aDN5kq+3w
fdJcEG0y9HVtHvnUn11whaebzhibkIxNBo6PA8c4Ic/0HPcICUjuOWWZ9DSMCPaS5vOLBr+Euebyatlg8WHmxR2IRO1Ui/8Va3wySVdrwLthGiek4804IWc+
xlS7+JHg/wFQSwMEFAAAAAgAqoi4XHs9ObNfBQAA0w4AACcAHABhdWRpdF9zdG9yYWdlX2FnZW50L3N0b3JhZ2VfaWRlbnRpdHkucHlVVAkAA88vE2rRLxNq
dXgLAAEEAAAAAAQAAAAAjVZdb9s2FH3Xr7hVX+TNlbE9DS40LNtSwMDWFl32MASBQEuUzVomBZJK4mX577uXlKgPO0H7kNrk5bnnfp3rSqsj5HnV2lbzPAdx
bJS2wKRUllmhpImi7uyrUTKqyL5klhU1M4ab8MCUorDL4cpbNszua7HtrT7jV39hT42Qu/78Sp6iaPP79cebzc0/+YfNH9cfr/68hgzilLWlsLmxSrMdz6Uq
eUpE4iiKfgnOEsT8l8vsRrd8Ebkj+Ms/2ZRcWmFP6wjwH9dNvmWG562u12CsdqeEmhf4ZziyXDJpc1G6I/gPPirJ3c1WM1nsL930JLVSdkDCEwfkaeT3XBtM
6xqEtBjgDxhHySvAyPSR1cLw5J7VLTLBnCzg3c+E47lrjjWS9N2bgNIQx4sUD0STLFLtP8SreHEGmquGisnqC+hdCN6JR85e9DJmEu5dBrzLECdVPplmhKrv
nNKHWUyDHazgrBM68G0r6jKkMimUrMTOIV4sdgc+u0vc3bwZslEBPG46vl4sw6vQLBeehLuRfeik7FI5dhzHDHPtAZYQB/N46fK6GEGF1vs2qGB+AWqc8cyE
V+n4PNXcqPqeJ/3Dvq+4NKQWve2sIEv4bgmsrtVDLqSwwtFcw1apGhvLjei0CeI4/k1zZvkKO0rgUGNTSR5aCSpR4zcJds/B8JoXlpd9AOCIRg7nZi8MNFpZ
tDDO2mcg2FaqLrkGJ0CsKJwDJHqCLScx0rw1iLw9AXN4pagqrtEGrr98dnVfeUAMT5awE/d84oZIG8u2NZ+xI7CNhVKhPeoqmIL5aB72Co2PrNgLyd87N8aK
ukYqRyakR68EVhdYa/dKYzocWoVTxyX+LYg4OWaFRT6OpWNHZ74eWKrSEYEGg/dc0z7xflDoMoNXWyAYpsdDKXTSMMqMcZK7BP4ojM3VoVNgsiUBQNCpIBCA
v+aPjS9k9sJY+5qKymWMHqfOiUkW69DF3e1ZswUDR5rhEXxpEf3Ir7VWOpncu3zGnUrMu87AURhDSWYWnojGcwqb4OlSl7lsVgLlOI0njhbhmwvnAavJc8sf
bUIbLS3bY2MSv0iTPj2LJXY+Ucp+xI9corggmSxubfXup3hA7LSOgH3irD4NaXCZoyAyt8XTWrHSJI4FDl7pSZyB94UqeIMj4P5DpcFVT2cD+IX8vpLOVpJH
GpJVobRuG1wMPq/xwk8mgvsQij0vDgY5PwVf8ViU43XooolYDyoXB0Ee24bDkeEguyPDcDgyHER1ZBgOveGzp499XGMtzRqwVewtquwdBnN7Fyb4wE/LAJL7
dYo65+NOsTuOk27vq5j3S7o/SFH6EwQb2uEtfHJS17DigGXALmYnNyp7hiIR4loF4ilc0RDBtmbyEIBHeM4nCpJCsYS22WlWklZyfLTECbYgOf608aNIoVs0
wOCVPnVP0/HUIlmK9Gmy7ka5fXYaRoSnQU8nm9wI2fIxMu2yWaJe/p0Eb7LuwaQGLz848+8rnLKm4bLEvn/CyJ7XgXX2NOXyRj8PFafLsV+8jAfZG9rntUmb
8IndBpxtOzxhNc3cCdtQHrBoWEM2W279m9GSS2GqXvHfndwFMZnpXtdrlD3cGtyuij2TO/9mqo005eh17uF7iN/jL/6vSsgkRD/0dJeat/ArOqpoS/bS7Juo
441Kw+vSuAbSvEIqe+i2WL8GSfc6hSGGtIM+sNr4Pmob+gVCZ50Q+2gX06kNwzrX6/OxxVp2mGFO6WFCP8mW1GiOqgekjp/cTRuuw7lFDJIS92bakSEgWsTR
BffxeL37CQgyNvm5Hs2dTl/ewQsPo9e5dKcD/Cu7sPP8DStwvP7+B1BLAwQUAAAACACqiLhcnDkQJcEGAAB6GgAAGwAcAGF1ZGl0X3N0b3JhZ2VfYWdlbnQv
c3luYy5weVVUCQAD0C8TatEvE2p1eAsAAQQAAAAABAAAAADVWN2P1DYQf9+/wl0JKUEhXCn0YdWtRO9DVIIDAX0CFHkTZ9clG29th2M53f/eGTtO4sR7t0B5
6D3cXcbj+fjNePxLSim2JMvKRjeSZRnh252QmtC6FppqLmo1m7UytWk0r9yT5ls2K3F3QTXDJ7fXPSdG54uoW70d1ZuKr5zaK3i0C3q/4/XayZ/W+5mVp3nF
Wa3dwvnrV6dG4FZFXfJ+2xpWTo2oXS86V89FTquzP1q50kLSdReuZEpUn1jW1AWTmRRCJ0TRkmWSVYAArGDgINvQR09+zUpeMVSoueZfWKbYeguOE3IluYZH
LRndZloYPd9hxgvQ5HrvPLNaIejj5dlslldUKfLGLpjMFjMCPwUroVgcfGdZpFhVJsSisBjmD0KD06KHLCHFauFwSEgl1msmY2sUf9CWA3TZ2hwt2losW9v+
IkC9BA++0DqBBfvPaJEqnW0YlXrFKJo9SU98DQeHQRIUDqAV2WDjkO+U16WI5i2OBGtLuiJ8YpKXnBULck/Nk4DPeNZhLps6K4VksMfAHpMHv5NL6OzFrW4h
O2zsp03BNbngcmsr4Apra0bAMKlFwZZ9HDanFKVZDr/67K42iMZb2Qxcd+4xTFHnLIq9NTyGqaoY20Vb+jl64jvZiaqCltJMfqIV9DOICxWPkjdWb8s829L9
ivUVjUYF2UmRM6W68v0tVuqQTiGu6krQAk7gPw1TGhX79h87sscAQMzZgqyEqKBXLmilWCDUWlzBqsEDfw0C4CUsamsGpl9hVB8Ee/U3D75OPsHQL5BkMGHr
TqTl3l/XMG+rhDSKFZCNZNjyduKmBVcfs0YBbNHQs4MS29ov+I7uET+wcO3J8WeuYK43ar4g85eXz/+8PJ8nIZ3eNGjCVDvsObDd5JKt9pqhH5vZVAtT7ZRM
3lMdBKLTwYeADsVTlMHRVHBdYV4n6eP0JJSW1cTKZ43OQdVdVXDSriJ3W6WwFqdcCWiGLXayb+lmeu7sSOxbIWoLEE9VJ3MPPAe0hnPkWaet0IuBIVuvlvfS
R+W87ZWHJPr55NFjcv8++SXu3bLPOdvBzWn+ADqEKpQFRkfrkTnNoduSwsxxcxI0BqcxeKwPzIlJy6MydvkAwh2rCxiZh8bE9+RzKpqqsIec6XxDWlfE0QF0
NcxxaLI9vHePtAxv/Kwtf4SSO8DyN9hZZiOpuNLvCp7rd3D8EqRFHz4EUG0Hl9lz28CZ9tWFAMoDCYeQiFQMOFSstjn0aOBlBSLC64DLSYU7x1kgdzTtw3xU
eW8p8Zs+gbZp8V+4VwmTUkh7wYIkXTMdzXkxj4lNxwrgnwyF4yYPBt+VCqaIV6RAjaxhaPW7fQ/r2m7jamTOVJdyxcjrpsapdY7ZRaWX/5YrZVg1HNxrENzM
475/heRrXsOMRp5T0y0bxjZZHIXqi0sT+rWN9SZd8XreufEYdCbp1dBNhVTIJ9kjR6O1sFkcIBO2Hk08J9Oce4MFkAxYMi23DLwQHLz8Ej+WeHbLcTtrWc3o
pGF3akFcawKIyTCeYdZqB69jbDQxO7I0OllgaHBqIeVMwfsK2OZrsA5GQi8skXOSGsi8ODpjh9jFgFmcvnzx6vn52/Oz0S08b1+iNlRt8Po1sYxUTKx3KmAy
SAm6xHydUHNZHuMXLJVsV1HgtvP376EA84fzeBwxwAo8Bfjdt1GGG78l2qrR/CPshwG1ZoHCJWRCINrXLIg3F9IvNmbVFjw5MkUHbI/fMe9POFZysd1VTPeT
FTebv6ayS6+PJ20XuAynZP9Y+uA2ZAc4ROg1wu39EUTC+esC+yY2MYnaZwid9BgoQ+SiW/tahtFv/E6aMcap5xp9cj7haOVIOg5EcQTzGKPjvP23FORslJzj
Ie3jiIu0Up8TeML24XZeMknNK/URBKX3Yu6/rwlq2CYDM8eTlgliHnNphR57CdEKL76D1GKURZheDDiRp3+AF3k6Y240rkx23WN0G1k6mtVMyYxNvv1o9j1c
ZnT+e7vQ79yMU2K+XHlLXFkiEYcqfwErl0KbieDK/9riAUfEhAx9Y7/D4NAwdqs9tEHvweuE7oIxXxm7IJCIRDH8MWs9zXPUZ/ApN+q3DW+HHcshJENDxm0w
pDDhBmjXhgD6FvH7El7UnjSGaXLFJKD607IN1UmOof7PnkJUeHaAf8CltGL4vZI0O+y+tPO/vPZ83hCa64ZWy2vr0AN3Osr/2jkCa2qFo3k8zHHAYft4882Q
gTDQWtIAkfw/scQwwbPATyazY9khWBILx0HON53yJlBn6geRv+mF1lSwcXSnhVjgMMsAE/wXUEsDBBQAAAAIAKqIuFwwclIdhwMAAPUKAAAdABwAYXVkaXRf
c3RvcmFnZV9hZ2VudC90dW5uZWwucHlVVAkAA9AvE2rRLxNqdXgLAAEEAAAAAAQAAAAAlVZNz5s4EL7zKyyklcwqS/fzgpSVeuihqtRLe6sqy4Ehr7vETm3T
vOlq//vO2EAwkLTlkID9zMwzn3ZrzYkJ0fa+tyAEU6ezsZ5JrY2XXhntsmxcc1ddK5O1JNJID16dYBQYv3eMfr8aDRHnr2eljyPqpb5O6i5wcKb+BzxaCNCS
9I/Id95YeYSXR9A+y7K6k84li+97raGrMoZPAy36oLTyQnAHXbtjkjBVIlFEMD2EKQOE7SM03aqNbtVx3Bs+U0hnjkewEyR+ZgETAhVI2V6L1lj4AjbwKtgv
f7O3GJxqS1mpdGt4/s5L6yloPrjITqYBhlqYxpf9Ty7fzUmWtCpq/CkmnZcn1QF7b/uZHXq8vaYLge1FKh81ohq0WHthdA28SKDwXMPZszdwPRhpm9fag7X9
2a8VWqkcbMm+Cn9YUxghWluLzoMBI5rnMdesUW4gCM0LtG5sxUI4EJqSjT4NBVu6DuDMT/KZ/5WGLgZYWBj9dvTWuKJYJjKNzJ1URvhF+adZcZeDJM9WjqYs
etvtEgw1DhY1xvmL7PZ//LqxS71mer/arDvjYNr9bbGLkRBOfQXc+P1P9jOjvxuioOxM/KuNuE6bpQPd8Fg6n5zRwsOz5//m2PSQVyy3IJsr5icPPRL4iN7X
uBVltLnw4r+iWIcmNpU4yesBxBNgQxxAeo5dUMOeCnuR7xB56hErL0zpe/yD9+AcqsfOjSTO0mKsiD5H4WKFVy02nmfKKe28pPQPGnZYkPV8qiQZMBp7uIe1
eYdpw/ig/UFPeQTPY8w2rd8k9iyfGiDftrseJ0PzTIJDzyTGpxmSF2sKD91Z8qNE3KH2yRzc0mtaywuGifvwcVPIwucenHdiKdmYi+6MbMSIiGoSTLp1x0I6
K7wR/onqls/q8GxNjXqFi6eJINaCziwsniuR2AXntkP3A/pXPi2MTMuPLH2rOzdF6ZnaNnYrFrvvsVLu40MYHIBGoQ40DzF4gJ/oz2RuLj0QfDQ+tsWWQ2V8
vruOw/HyHT0WcFOTOZzVeCmYn01JQUat4/ESDhZyYjpPnLc3kxbwTqanm1VJyPFyVWIcilI5gzPvhINxrvGW63gVGsqnCuPqA5rY0T3s49rgcPUi+SUJWiub
/nR2fKpGBzg6JbaE2/N8R2O+Sl2bTdbIBOdrRSaDaeTw2PTqtjLnQhRcOrCXt4xNYTqxs/8BUEsDBAoAAAAAAKuIuFwAAAAAAAAAAAAAAAAgABwAYXVkaXRf
c3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9VVAkAA9EvE2rRLxNqdXgLAAEEAAAAAAQAAAAAUEsDBBQAAAAIAKuIuFwFuHqZmgAAAL4AAAA4ABwAYXVkaXRf
c3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9fX2luaXRfXy5jcHl0aG9uLTMxMy5weWNVVAkAA9EvE2rRLxNqdXgLAAEEAAAAAAQAAAAA+8zLy8UABOf1hbNU
gfRjBiTACKU/cwCJqQzBDEUM6YyaTFUiBnrGega6JYnF2brFyRmpKaU5qUV+moy3uOPjy1KLijPz8+LjVzJ8Bum9ZaKfm1ein5JYkqifU5xoEp+YngrkJ5am
ZJbEF5fkFwH5ULH4+Mw8oGC8XkHlLQ6b3HyQsXZFbFCHFIPc+YGZkZHxBp9KI3cRC5ALAFBLAwQUAAAACACriLhc2lwJpvgIAAA0FQAANgAcAGF1ZGl0X3N0
b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vY2xpZW50LmNweXRob24tMzEzLnB5Y1VUCQAD0S8TatEvE2p1eAsAAQQAAAAABAAAAADtWE1sG8cVnv3l8kciJVIk
basxKcmR6T+6/ktdOzEcyY0lwrTLDZ0mirNYkSt5bXJJzy4TW3DQngoZDeA4RWEHDRC3l1o9JUWB5Oiil95KVg5IbF30UASob0ItwKhPnbfLXVMm5R/ARS8l
pMc382bezLz3vfdmeL+vz4fI58/p8LkVP0L/QB0f1v6i7r9J6C+QiEQqg6YpTCWApzP0NIMZi2cy7DSHuWke81abzXiwR+REPiNMe7E3geJoBImejUgU9tO2
VuybZ1Pee8CmKNMva1rFkA21oumkyZ6SjbMp2mSOapdMYcpQsDxbUrL3qPboo/OKZkxUtDl1vtC5Y9rZcdHa8bsIUyLCtEhhRgyI9EZ0jsWs2CcyFseJ/SJr
cbwYFDmL84ghkbc4QRyAHRPOS6SCxfnEQdFrcX7RhwPz/lTY9B7LnZooqWRD98CUBbpjP4z9T93/tbWfH4VKrmjGHZdjUddnBO1vc3lUYpDL5/jusTmhR5+/
uy9P9Rq5n7K/E2geLVHZFG/yBcuwpoCVCzVFN3TTIyq6TlxjenSHOavIRQXrJl+rFmVDIb5idaU0h/uJKh2OlkiYe9JlzUgTsZwu6fI+SQavpeVaUTUk3ahg
0m73FSz77aoSZ0uSqhG5ZA66dt3l9MGh9L2E/AS1osMf+e/GNjdj43+JjS/HUovsR4GWELgauBJoCsMNYbgpjDaE0Zs/+E32V9lvhL330ePOoR3nHLCcQ4zb
bR2Uo7v7+okp+9FeegzhPtJM0dapzYCCq9KsrCtSDZdIZwQ62SpBctsgeMjqAbHZ33E60g7ByYbtkw1Gr2+/tr05uK0xuK0+sP3W5K0D9eCeurAHxx8/Au8c
4Z9POgLT3UdGct29IuWgIU89O9ZE+iCj0Xm6F+YcFCcQkQe65Vvb3zIxABkRXF/DGMkyIrubGkMpLrswki5WCrUyMZ+ebkNppw0lAkxszCqysUQgeU4HrBpq
WanUDJPXSYap6SZdOb8k4AFkuaeiGxgMb/sw1sa81J4j6QqJhqJuhrCsEs/OVbDU1uIhAoOsiBNkYoqx3e2pypdKFbkIwaNXSTJTdLB+wnG+192eGX6EALcz
TMbokLsIDMKx64evHW6GxxrhsVYgdHXqylQ9+v3lwKHWQLwViV/PXMs0I680Iq+seFBkywpiI75FYcWHvANNIdkQkneE0dbItubIHvK3ePHqh1c+/NmP68Lx
r+O399W/d5xwNpyoDlsDJCw49VMvGE7PAyZao0iqejKYqFzf+nKNnnGBlKdnQq68HTou6GiQjwHwepxJZNLuDAI6NruwZV3QnavM6umqohVVbX6JwkkE0ILO
Ja+NM2Ze6YAZfhnIOJAESH2qrmoEVlpBMdmSqhtLNpzwNksRZNC1MIq013LTKKxlvvQIUb3ksLx+Aa0B12gjPOqC69XlwGtrkcWhyNgKYixkCWuQFYzZiFpk
7/qHPi5+Ntn0Jxv+JFG8mGkFw1cvXrl4g79JLwdH68Jod+LyoDbSfoteDNIgI4v0XsZBnMg8B+bYZ0tgVup5+NKTUbDgJfIPNEgCb5KqSHIOVuSyBYkUb4EB
fxdIDzA4SYQniiS1aHn/Mb87qjsd2+n3XnJQrcMVbh2/xw988f4XZ79W69Hjy4EpMqQru4z1zi7B2GLQdm2no1zXrqD/jmvz9HO4liGuZfLMU1zLPLU2MU+p
TazIWbWJz+KDpGeBTcuF83gzYfEIkFEgY4SkWDu0DwFJoXaQ63AHTLh+jpHJWuWDklIkLux0dfKRq9cZshu0ldG6JaTt7WI9OrEcmPx/Lfkf1ZI962YRJ4R3
Otdvp7DgXWQ2ht1gcDLehx5PDxuctO+mAfcKP9pdG7oGAXCfWCCmlwOZF1kgfDdfXw6O14VxGz6d7yB4StlZxIYPnesEl2NWFxQ00lgRUniP63qeFZl9zG5K
40gU94CW4+owOs2f9ujMbvKACaPjUYRqRFt1Pu+B0nWC7CjPz3hcrR5nbYk+QbbvJ1DSBI3XPPlnf5GhXtATWSvjcR0Zj8sLHTBEuVD3rNxgD028AzrRc5DV
vCK8zV0gJtBb3txQ96yO0PDmot3yNXnRm9uwvgaSFwXRC3lRpxMEq5pH4+dQEu1wdRTJCeeIbDNZLYGyU68ym9EclfJlTRrPmuycWlIW4nK1WlIL1o8D6UrB
UIyddlFd2P4cQbTgSdeq0KXuoKDUmhwo1zHs1arOj/J0qs9kKyRKTFaTy4rJqYZS1k2GLGrX7y2oXb9NpixffPxGB0ez3wQ+u5z7nDeFWjS9sKgEb0KTUy4a
WDb5s7JWLCn4DUvfeeWSyb0vl2qKvTGrNkAsJNyPHerD9mG6gliCBcyXH0X7k8ZNgvYWsgK+L3r1vSvv3Zi805e8ebo1tOmG+rn2qXZrrn7gVP07P6xHcovH
WgcOfXX+y/O/L98Jv/278pLvFnO79vOTH098Fr6R/+XGT05+WV6cbITfbsU2Xb987XIztrUR29oKRa57r3nrw2/8yXf7cl18p75xZjn0biu6qTW04W58043J
zzOfZprDRxrDR26Hl+PHVrwonlpBXLx/8fgDD+ofWRl89jL07/sDKPIO9YAj877pSz7UwZ2fbJkYZ77yTiSFP7AhoJGjIdLzxyQPdNw3sd/TfsJb7s06DQhi
Uq03AZ8Bm/NYMWpYsxpEAMX7YX9RLRgzBBo7Eke1S2dwHkafRtavAsAvROA+P7N21BkihPr/0KtqRuJyggjI3dC6EbyFnGsBKOlQJ+ATrjwLBLIlPtljZB/8
iALIlSTTJ0nlSrFWAj4gSRdqcqktCUoEAlg3SqqmaBVJwlEH1Xg7EPhRBB8GcgTI60BEsMKQZL2A1YIkGwZWZ2uGokvSErJyuAVO7HMI/Os7CfkpWqUZbt+q
j+L2rvIebopaHfRyY6tRDxdfHXTaWx9E/YS1NcHclB9OMFcjZid7xlDs4IltnC2ps5YBTN64VIXaCHkdw73ALpCuA+29nAHi7s8UDttGeQ3LpAlJVofgX2Eo
ivobGvkWxf+OEn9FwW/R5gf8MYqaov6FrC9Lx38AUEsDBBQAAAAIAKuIuFz3aglpvAsAAGsWAAA2ABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVf
Xy9jb25maWcuY3B5dGhvbi0zMTMucHljVVQJAAPRLxNq0S8TanV4CwABBAAAAAAEAAAAAMUYW1Ab13VfeqMHkpB4GLwCGyxiwA9sx9QhESBjDIhUKzlOcbKz
SAuWI3bJ3ZVtSNsh04/ipjMmbjumX2WazJR8lU+3/bF/2nyuZt2RZkf2eCaezviPDJlJJ/3pvStpkYNcO19dm6tzzp577r3nfXfX6bRj8FEGfNf+5cSwr7Ca
x1T+wXfX4fg7jMEYfBK7iAOcRjAxSQAC/pKT5EUKUDqNmjRdNAOzDpsmLRetwKrD5knbRTuw09gVqhNjLIz1LN6MQcjWijH2U0R5nVOVZYGDcTGOVuwaBRoY
N9OgQ07GwzQyTgRfw4FrgQh7nyHmMK45OEEQZU7OiIIUg7gtzclcKstJEkJyIMsLKTHNQ4R6m5OvohlZkUuzaVHmhesJiJvngbgCuWoPT1YP/1Q//BUM4AwG
D4wD8jA8/BWKIXvgGx2iDMikQ2YIWXTICkyMDWL2CmbWMYeONQAL44SYq4JZdcytYx5g07HGCmbXMa+O+aCCEObXsSaopADEgjqGQ0Whd80Iu+JiDjAtSGWG
at2Q1s60PkfzMG2gccEa7tAckQVekEdFYT6z8Az5hUZKMtAaeLDEznESz0JlajYBKpNFGtUcOijxKcDLwIz4GyRZBNwCzwJRlGPf2eF0+qd0TBR4zQa1zQky
m0lrtjnACamrEHzWoa+SEWStaUnMZlkI8eA6l0VSRSEtaaG0eEPQ7QX4D3O8JO9jeXYOyWi9ynNAnuO5/QxasDpVzizyYk42ZiIja45F7iaLTpDhpYRGzYli
VnPJOUHgsyzc8lyWT2vNFRygiQKfMkSk8Bqfocp+g++e1n0micWx/Q+kEvupDM4Q/eQhLEzGVtoiuXRGPp8Bi0xZm7pZBo71D/YfC5Mrjst9Maj3vlE4rDgr
CKMbYcWelHjQp/N/QQCXbvJylFASn52X0G5p7cTAIpSHwmQgK3GDLKfL59CqbNWAZVpK94X+pWXNAvWb5oGk+WqcpL9CbIVipRAcVrEdM3aA/sPw74eL7aFC
e1++va/YeuZrC+Wx72CUzb6LDvuc0kxVpfXjL1ZanKynslMVOQKexOOmOhxEleNIhcJ5IZVM4rOW/TwMtF8fkkbM9L5AHlVXnuk5eVQdeUQSF4hZW5UHntG+
XzqkNtRZ09xv7FAgk4QL5s8k6cJOImexxjR8YMV6VZaXpKGBgRXzDUn/tSAKAkw3EA6QaVbogbSYyi1C40kDFSv3la1cdu43wxYASwCmmQEM28ySBqMXhpR0
IyNf1UiYRQFSSY1TUaATsVMoNWiWG5KeIzQTDDWwLKFYoOEDuhCPvRI/KIEEah1oj45kSZ9iyIeeuH0FN5130wV3T97ds0Wq7qNrVNHhvv3mrTc3hgqO7ryj
uxjo2PR/ntrq/UxUuk8roTP5wJlb40Vn4232FrvxbsHZk3f2FAPtm9TnzFbLZ+9vsgp9Kh84dWv8SaB1bbzkadmi/uz6wnVfLoy/k4f/Wy+rnnfXTEVvy3qL
0jp079S9HsU9olhHQDfc1RdYmICa4eUcEFZc6UxKnoVaOkrD4b0wAU4grSDzhb2alWUFbpFnWc3OsotiOpdFcAPLfpjjspU3bpadz0A9ZzMCL4hlQk0dY1ng
RRJ9aAigIYiGFjSgYAMH0ICSJziI9GtdAuISD+RlcAjRBhCtiYXBDMWlWE6G9pzLyTySexwxoBPplgH26oD+pE04/BJ74m1Sqaai2/uLWLHRr1L+ojegUoHi
AXp1Ym1FpegaKNSlUC0bJ1Wqq9g3oFCHNyWVGih2H1Gojk1SpY4Uuw4rVNsGo1KHi8Hm1cl1n0o1F9sOrsbWx1TqYPFQt0Id2EipVPcTk3OXMJnsuyRmdu0g
aMcFoV3CZhrFdWIZLG8ebTdVm0hJrJJH/o6hPDJrvKiXbjthtBlRRSSJuXYMmzUiPmlEXNyyf261cDL40GWYAaD0JH7oB8+O2178jiF+BEe9GGjYMBKs4cc1
fFkjREEjl3lJo2SQ48MmSJA08wKPGhnd+zRTOXRNWfEGD8KkRiF/0yxpfp7LZWWNBNwNCWnKiEwbC6ezqOiBCxBHniNNYigKS/aG2+FfhdfHPhlYHS1a7B9/
9NvURtdvrt4VPhXUQPdWpxo4WnQH1lwlh2tN+uT128O3hlVHW8ERyjtCqqNL6Tmfd5xXqPO6wZ6zFdKLbquH2P/DVh1YTS425h/CQtiskZctGNeMMu6ss0ph
iCTmQtWDOAez8EkjzydJHvbBAhkh5/F57A3yIDaPh6kYQDZc8U8IsBPJpGnUlCzwgJ4XAb1CDNEw2UYgAxjBKokDjKKhCdnEfonL5vgoACJsvuI5AbUtOgZT
bhRxnUfDOGIl+Zup7+Vaq25RuB6YhihqRaSFH2bQHYI0TeElt3f95Cc319z/LjmadjEckVytSvvx7UPbgW1wr+svQaUtorpGviQV6+R/dkjE8Z2EEtVf7RET
9sBkj/SQD9y+SCf5oNME4efqvrfqAzepct03airUxqzBuWd7Gpux79GrlqWxWcNn4tR+D+jUW6vqHaOejzDkXh/xUllUjaz/EcGw2r9MkukVJZEvlWR+RUnU
SyVZXsph3VvrVdZ8A69aNU0gW8E4Mb10DdurnaeObPMAJliSe/RG6D+WuGO/jL3ekYYZ4BX4qRp+8hX4TTX81Cvwm2v4LUYkoEy3l3/sjCPu3j83aanORd1m
Febx2UaDw9h93Ld/vlGRml78TrAmrfFgnX03wH/OswSMwhZjNTxJJMmkNWlKmmfbDE4X465atYbqqUttZLx1qL66VD/TZFDbDWqAaajDG2Saq1Sm5awTVtjW
mNYQjb/NjkSYKJuMT+lZGyADaLbYzFiUHYWD5tBBJjoajya0BiYxE4+MR9n4zExCsyWisUgswU6Mac5IcmwiwZ6fiE8j1DYSj8RGL0BwhZ7OSFJGWKDRRTQD
+DRdvljlgN7uDcGKcJROwAbTssQB1KBrVv5mBl5YxQ+0prdnpqbYiVgiGr8UmUKbmImNMcCPthgam3knNjUTGWPj0R8no0xiH5/WeiEaiSdGopH970AzkhGs
Tk1MTEdnkgljpmM6chnKTcQnogxAOtRciWQsFp1i4YlHpqJjWnMFj6MZseioMRcgFw879evE3oUBeNDwQ/rasBughF1TKcNYpVJqZm5piRfSADUrGnVNzAjl
TxB2/uYSJ6Rz8CKsWQAvidnr8FKy+EE6A8rNLqqKeq8TtkE1C9dhI57lv7dVzVP7IYOFXdPevjXLYtma+nkklKHo6lOuwOXvS2UTA1R730KMf8NREX5scZes
vvUL/7S2P7X6fu186mu6e/bO2Y33t+Kq71jBdybvO3OPVH3nVqdKHu9d2x3bxmubQPX0FjzH8p5jqufE6kTJ678bvhPemN46oXr7C97BvHdQ9Z5enSwFW+4u
31ne7Nj2qsHBQnAoHxxSg+cUym/I+hz8afmPy9sH73eqPVHVc77gmcp7plRPrOC5pMQvQeH1Vy14hrbTqxNPXJ7V8ceOYNHqvu265Vr/+UNr92OHv4p+9NDa
9dgRqKI/e2g9/NjR9hyzxYW6iLY37lP/sD2wfdmjvhXPt8VVF6NYmae+oNJ8RPWFC77BvG9Q9Z0u+IbzvmHV99bqVJHyFKhAngpsyFtpBUEnn8KlmkJFb1vR
f7ACeNtKHd1Kz/B9Qu0YLfWeUE5eUi7/RO2dLXW9phy98GWn2jVdoo8o4ZH7Iyo9XvIf2HhvK6L6+0tBWgkd3x5Ug6+XQmGld+z+nBqa+LrN1WDewVwms97F
hs3lBkxv2PQuDKWc8u2vve77JuM9gsJ4DPZwaYQ2Gi/suqejy+J8Dl4v4fUQkFWv1xzG10xeAqheag3wspzNzPXDVAF9XL8dwLQhX4W0cgSYy182y6Fjr3p7
2e+RN37vCqhZz5UvqcNgEaKoUkgX4QAbOhx/hHV+hdkeYSEIPMKaH2EdX5sxwrF+KI8HvyVC+Ci+g6ER3s+I5h2d8M0FnMLH8W9cFnwK/9bfi5dV919QSwME
FAAAAAgAq4i4XOVBTYQKBgAAZg8AADIAHABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHljYWNoZV9fL2RiLmNweXRob24tMzEzLnB5Y1VUCQAD0S8TatEvE2p1
eAsAAQQAAAAABAAAAADtV01sE0cUnv31ZhPnhzQQJVGzDVRkwU1UCJVQWiobHJrYGOS1UWgitht7nWxY74bZtQIoEj1VpKJNEaoEJ7hyQ4JDD1yKqFClShAF
KdEK1AMnbohEQuqpM7v+S9YWP6p6adf2zOx7b968+fy9N7vrwSAP0PVosH020gDAc1B10V5HrH+F2p+BBCQiBsYISAh4TMZISKKeilFjNKRdGR1jxhjIjLGQ
FUAn6AMS0wUk9gDpeYKBaVIMvMBDkXAaFcMwbcXWTMNKoHv6hGLPiKTDZRVbtbW86nC4vWAaaqY6LLIUVsQNaxJAQgIoFAJSUoNEdoFZFI5EdQHISLxEu/es
1IhDQaOA1CSx7oiTArBhmhODTiBuZhT9SOQFhzxmyKq1GPSj8FpZd600kQT+K0n5ZRKBPuRBUgDjrXrZYxokA37bA8VeANNApBIphEFgToGqYVsOp57TLFs2
z4i0w3pCh8mfyWrQCWSn5DkEmcPImqHZaBZtqXoOtiBXFl5SEJyBwbxhDyJAlUHdUoZkZRo5GFQKWc2WLduE6L4oy04NzJ13ONn1JctOaxGTgZKkAbvdjZpv
wRrXstS82LzK9SxzPTdTt488xqODazs+vNy0xjUt8Yv8D03reEsZomqfVAnMORfMiQqA1ZAXrz4MFl0DrKJHg6iaz/rt0oROV8a7EOESIuMErLO6Zqv7nUDG
NBCvbBcth0qa804jNOflnJJBsJy/RcI2rKCxWRFMuA11sBOLW0rgFL00Y2w+drF52rJ96eL3F6/ZNxauL6y0hC7RT7v7bgxfH75EP+E611raLzXCjq3IsCVk
7rnIoJ1Xa0s7L/Yk2jsiYk3SldARQB0LcpMFU38VN5/Rp5S7mJwfgRDoL+qzyE8O0bsXabDuC6oX5AhEXyWGNLyw5TqcjIZTUSEVjsSjwuiIkDieEqLjo1JK
EuagmVEtS82WGTlrTllCv88HvpBK1rJCKjqeEk4kR4+Fk6eEWPRUqKaxjv8lN0e8CXjRRDoer21tzSj7Dnz2NpY5TVdlS7ugCqOJVPRoNPkG+8oOFXuzf5+5
6JMoqffGM2vOG7qpZGWoni2oll0P1KL6PwasSHnJHFDPqZmCrTpsxsznNRt2g3IB9XK+FZsFywUR18N2bDPiZvxasG1pcnHy8ulr9lpT61J8Mf6S5No+2KD4
YPPL7QBpa4hii7HLx14PguaepdM/nv7LakSuruwPB6lfg3y4O5CpzszGUnXoJ96qOjBppnZ1mCh7TZcrxfjOifKRlKZL0gmuJEvyfk99YKKprG+uEUvRS7Kt
fpxDTKWyoHjb61u+ZyUiExfu+f7z0YQUTaaE40khGT0RDx+OYqIdr1OCfLP7veoTquJ/qMjuUIW7oU209PPuZDiejkpC/5chofL1m93iXXrCXoAPKMuGDqUZ
NmTcW8Och6w7KtgZp0GzzJwJ84oNBSQUWe/wYr1wHb4SrsN64ToN5XBhD7K1sDNvXZfyzjaoZky4CQ+nq5QBft0n2IcM6qUDjbjPg20dP5282n1t9u7O29qd
gQf9D0P3Q3+ce7Tw+8JKZHI1kluO5FYiM49btY1gAOVJayVP2kt5so55dyUsRijqPsVH2v5PFPBPJMpv754ovrPFny2VY+XfyRi4E1TIz1dWh3uQAO7FTQg3
NQm/o0jqrftyerewfqvBQextCryR+l9f7b25527mF/GO8WDmoX5ffyJNrErysiSvjHyzOjK7PDK7MqI/bs3XZz8cxBsk3GfWhEi6PcRPuejZn4WqXYCGQyfQ
+5LIQxHrduFmT3m0tzxyccAPrHAIN5hBaM6n7z4HvzEYSl5Fbwy8LOfNbEHH4yZZPltQ9KKmRZZzGrRsXTNUw5RliEnsVTd8tEJcPeA+/C90yKiqoFfCDOKB
DbUpdCxbsnwLuI/M3mHMlRr8s/BZ/R3YICmm4xVPMQjrTqZrY4hnul53osabhy1FDgeYKyCQUEgQpz3cgZcMYFLq2pQLpFte3crqrTOMm/LaDve5t8ND8BC6
JUoBvKQIgngG+p6D5meg80+w+zU7THS9AqhxJ/8NUEsDBBQAAAAIAKuIuFwtXS5klAMAAA4GAAA2ABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVf
Xy9sb2dnZXIuY3B5dGhvbi0zMTMucHljVVQJAAPRLxNq0S8TanV4CwABBAAAAAAEAAAAAJ1Uz28bRRSe3Z1dr72xceuGJlQNzi/IghojlEhUokUClLZJcCov
lkA9LBPv2HGy3o1mx+BaIG0lpIpbgaLmyBFu/BncGieRbI0UqVJz6Q0pB66dWf+IkX1iLO++N+/73nx6896eJ5MJwNfzXGb3EwWAl2BoKd2XdH6bP38DFrCk
DbAuESkrbHlDJjJ/KxvKOiQw2oMb6rpGNG5rVsxS3wK7cFcisYps6q9EKlNiBvI8nyJa9b0gz/1MIfK8ylrVxXeR57iY8G14H9GdkjSkJtZVJJ1rslBTBLek
pV7ImQQgCx4MoAUZjKw5sNqzPKkoWdINg1tyUS6oo1hL5j/lpsxzaoOc+ricFlztafSUolIwRjFDGSZGo312FnB2ajReuDSGA/qcodyXx6pTB+rgEPbKOOyg
OmpRLVwdRRThf7ROj0GoF4gH1wa7sqWJWvN+0K24lbgJPa2o/c8TtGHEAjCNPIOuXwm+NGUW20cEezRgOm5UA2r7eyyD6k6V2gH1Capgm/892lxZXEJBiVZr
2Ayy32cXl1z8LXY9NPCHzBoOAs4yg2Y8Ii/zw6rhjzPgleiaplqn5RsfmQrTa6jx6UOKA2Zso9Jeff8zv+5RrsQr+Q7vbTNFBIEppe8cptb2nCphMZ6rwmMs
XsF0k9uYMD3gppDD4L382hbTd7oDETC15GJEWHzNJzVEKccmLUowqvVGhk1w6kUwgRynFyGQn2zGmL6NAmyLkyHxfS5OFC7yNbd7erw84MdKfDx9F7OJMh9L
uy9DzGC2u9iHuZpHcw6iKOcGaKVb3dyYiue66Zf3H0Yi6/t2d4PM82xiqoLn/BGCjpFpG7NP6bPGL41fm+HnnTcyT8o//RDe7cCJx/lH+YOVPxbCfAvmzpJX
2smZo+TM4dsrx8nVcK0DE4+3Hm09vfNs8+fNEzjb89twugWnT+C1s/TVdnr2KD17OPfVcfrr8N5ZarKdmj9KzR+nFsM7HWi04VQLTh1w8Dwnt+FkC04+qZzA
mbPMO783/nq/9e6tv+OH9jeHe6SVCcLNDky34fUWvH5QPYFLFxSXJ3hhXAq/OBddakp5ExIxCk1DfM94T+V9DzON8DIQr5nqtcBy9/pNnSVsu1znMWzbRHx+
iZh/lu7j+vcQ3anoeLrjVrej3oqq+ScgC8KeE2H945rv1F18m7zHXTE4wZv88Y8iSdIpmHsJUqfgg1Mw9a+WkS5HvNdQSwMEFAAAAAgAq4i4XG4hMUfhBQAA
vwkAADQAHABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHljYWNoZV9fL21haW4uY3B5dGhvbi0zMTMucHljVVQJAAPRLxNq0S8TanV4CwABBAAAAAAEAAAAAJVV
zW8TRxSf/fB67fXXJjZxgiGLFaKYkIRIBDU00AYCam0aIW+CUoy62njXxtRZW7NrSixVinoK7SFqadWUVqLH9B/ooaeq5Q+w60gJi3NCPXCLFA6ovXRm1jZp
E5C6smfevHkz782b3/vNnt/vBeirjXXf+dQFwDOw72Odjtr7GbXfABnIVAokKUhJWKZTNKRRz6SYJAvZls4FXTIru1Jc0g3dSMfKXIpPeqCHyO6UNylAgch8
ypf0Qz+RPalAMgiDRPamQkkRiq39umCXHJCFPnCHhd1yUPYRKXwrIvvPLwCg+gG4dSQOboXjYKIV9gTl9DqVpxOh51hMULagGkbJUq1CyTBn0Zi9rlq3n1PO
pOdK+vrlYkE3LGxZLKmaki0ZuUIeDd3XSlm1OHMJiT5TtyplpVjK53WIx7JVgmpen847K7v2j+cqhqEXs/sTyqE/gxMKSUIznYk0DQ58cZRumZ6kDDAP0q6D
8zKDkgVk1yQjvc6Ck90yL3sciwGQ8M5Wz01XtIIlXS3AJYkcTGrFLJGgpRFpTjU/luTsbV2rFHUopfEx8GEFTTezsFDGGay6RkZ0425VxEmUrJI0ikZSrlDU
E7TN3taLZdut6Tm1UrSq3MhIycjqttdEjnTFghW9ehTtKpUMXTKXjayUXc4WdUk1NEm/V7DQDpyaxV5gAN8OY/MqzJdVaOp2YBrmK0sozut4CG2fqmmK2tKh
GDliBk18rZJ9ZmzJsMY01VLHiqZ6VlHxCcdUnADFdE7d0i2pBWO0vGz7FiuFoqY4u8AetEsI/c0Z1KyApj+y5R/4wz9QGy/U/XdWrm6z3i22p872rLtrg5O/
nq3hwXuvtPxGrDb1IdHe3BbElQ/28K0cjojPaIIIqj3RBjO5febg3XbmqUzn5jNcB0/8YXj6b4EY9HGQEdrz81Tad4ifti2TCf5fPzI1gnaXadx29mEzYscj
09G6Mt1t7cLEwqWJFvUYHIoqfNDHUKtX0V7zXLrnkLhbvQTyzDyTjr5hj1F0or7OiWKHnSjT34mZa8ecPvF6r20b4p17kyW2OAEy8bbeDdQw1s7T6ZMHV8ns
q5370crM4L51KHLDjdYlDlnnmndP0K9Wyshy2p3nnD5H5cAFph/kqIR71mZxwVR5Uhij2iLEy6oRhx1Q1ZTLuiYtLksVVCHVkKPOqajytfPSSRNiMCYipHRs
L6kjXJ8m9CAFxLdqM9lPNOgmEiINiBEEMaAgfohslnAFDyuGQqSARWhU0Q11Efmw3SqmjELJZpAJxOC1BWycQ9xyFzGCmNKXF0sq1N43LFTClbJlswUjV0IU
fy+rE/KyPXpbTPAwQrziGG3OIXabc7jfprVFJJNnwXaRfKCQ72VNHLLU+uARsh4TCBxDIj64+RI4fNH7xUeIJQRxS4jVhVhDOL4yg5Q/DDwaejj0/amG/+TK
1V1adIlNMbY+92jhu4WGOLjKNf3hr+e+Xfhq4cHNHwfrkaGN8/XIRMN/bpVuin3r5xpiHNmEYus3NtKN0Furrm0huDa5NrntE9dm789u+mK7XhASdz0gEN7y
x9Gv6eteS32e2oye2phoRMe3ohfq0QuN6Dubvnd3OWzKAH/Xlu9Y3Xds09e/HQy9bAqxPUC7xG0+sCbcF2rh4U3+9JNgqCmE2/otPlrno7Xe4Q15kx9vBkN/
77rQzF/mKXT4X6bHL0+B305fRO3vwyHcTh2ducg87vXgNt41M8k8nnQh2eYVBSdPUTDvQ/S+QqPa0yb80X/zfYKG4wQ4BcT2PbZXUXIVtEBXFIgpEnbhSXcZ
vUnFwmILbeayCU93MIaDI+CDw7gh4MP1QvBpsxhbDg45B3cEYRDXL4E0uWIcsqEuIa+2V142LX3pCnq2fgIQk4yDB35qqYSfz4vwbTTEJWteQ80uQ1HUDog/
A8EdEH0KvH+CYzugfwf07ABpBxzfAYNPQeAFx1JdLwIRStw9A1jfarXB9D7xhr+ce3Cj4Y3V2Bi6L7aPePsHUEsDBBQAAAAIAKuIuFxYn0FqmggAAJ4PAAA3
ABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9zdG9yYWdlLmNweXRob24tMzEzLnB5Y1VUCQAD0S8TatEvE2p1eAsAAQQAAAAABAAAAACNV91P
G9kVv/PhsfGMwRhIMAlhYEl2XcBOA0TNx1IIkCZxYFtPLKXr3VgDHsDBHrt3xiFBlUqeEqqsoB/Rou6qStVKpX2K1D7ksdt/oLYGxe6UVJW2qtS3REk31T71
3Bl/sXbTTsLcj3PuuWfu+d3fOX7p8bgRPH8Kddz8uxOhz1Hdw9gN9fJn8H6IJCRRYXSFwpRI+nSYxrTVMpixWhaz0DJhxxUOc5YOG3ZecWGXiD5gIz7U8Awg
yTFO2X3slnhJkLgedJO9SWFe8khO0seC1Cq5rJ5HapNarF6r5JXcVq9tmQ60/5MYCFAmL6tqRpf1ZEbV5mHMflvWV6B1XUiqMr5z+b319tiNqZH35ZH1kyNn
gnFx5MOhRbrOIdb+auplyfriKHqXeqcsSoAefBMVoRq/Y7zcRphGmURLzDj9Bjlbk6soxlbmI84mukwU1XSjqJkvtRNVm/gfpQZRgJs3kUmFTCpuUh+sM2Iw
HmBNh6bjZNZ0YiWbkhcV0xOXpi7OxqXZb83Nzl8zGS23EKBNxy05lVNM15KcSi3Ii6sa8UYUzdFQWtVDCVmXQylNHovLywqM5Vwiqcc1PYNhXJ4rj4LZO6ZX
k9WknlxX4pqynAYhJhgR4E/7Lrw2UMnj24ptLxSFvoLQZwj9RSFQEAK7b+1qhjBaFM4VhHNPZv7wliFc3JjdF/xFobcg9O4sPho3hMDGbElo3wpvhncchnAM
Rry3yB/dvpRnj74k53Eg7hwqx91N2XGvnJq8glAIqXSsetKxagwhFuXZiKNJHCiJrsSqqsc16nWgS8chVkyUkZhzZEdYM8eDdTrS0qgd46u7MxWrlVZEc28j
xJMo01X/XbCm6nuUPo0G68Z11iiJ/aq3gBTH+tWIouewKsqiJi8pIlZScL1uKWIWbtawmMXKLYhbUl22JkQdy7cUrMkpMZPTtWRCEcvhFnEmowcx4RoMLqEA
gwm+TSq4TgeD60Iis5gjCAguJNUAh8ktAMjpGBM0mI6sjHXN5ORsVlETFkoAsS4sr8XJtmZ7BY7xpWRKUeW0gg+Tday1jhgT4cHtZM5HviNe+Q5rPT5qxQFg
9z1EYPcPZ1vJ67vv2O/s/rRj5/onvUV/sOAP7uqPJcN/xug8W+yc3OucvD9TauvY/oHRduLP3m4CtqubV/f8gd1Bwx96Kpx84URdU9QXLiS073cc3unaPp8X
+vb5tj2vmO8/9Xje8E4b/EyencFk6+Zw7LTgeJ0agUC9iXhUujkZVOVMrGo/wjbqNSObWBV+NajVzdGVuRDdRFpdcXYNQMjXQ02iKjKFApALTbxpa5wDGgUi
PUMTcAMw2fn1oYiylNMI9PSMuIaTuhLCipxoirxrQF1OAAMgTDNdyu2kpsczqwEXoTstk7qlmHRGI3ABMLkXM+l0RrWAcQh2NoVIDiCeVmYxzmCTs82YjvRq
IokBhkKF38hGpucAskxOl/GyopssEX4VieW94zk1oWBrOR4iGqiKxFJrT6H1a0XPaMEzanjGNy6W+MNFvq/A9xl8/8bMvrPl3trdtaLTX3D6Px3d0T8586vF
3RO/XM33nDKco7/Xnoz97o7hnNxv7ckfuWS0Xs67LpdY9725u3NF9nCBPfyI3dXzpDdW4ts35mwk1uOoikQfzN5gHqJYLUr1iC0/AzXEUdH/QouV6NMozcSq
uIsyY0CYkNJdtXQI1NhpYZtqRoU1BALxHbKJT7L+VdaTdBfhG1dWfBxE/Wi4SvUJsLYE+OqDdf975btMH1qiAIcmjRcW63UYy3M4shPQmaAeomvN8zRVx7P0
PJQwXlLCsATD0HIrsppIKRrR2bDx4jqfktMLCXnC7NVW5FPjpy22C55PZRbllDYRrIiHQVlrhde/N9CjE78e+sXQ7urewFkruNYrwJnOFVlbSSUXTM42ZbIZ
oFaThVsEEM9lIZMrZsuKcjuRXFY0PcDiAeICZw9Nx+JKTl3FXyc7lUE9aTvJ17mGz5PNiM5N25t9T+fWjR/eMDy9gOQWz5Z/07899qBv5+J+l3+v5+yT6add
k/chb/u25jbndo4/FcQXDDo0Rb1mkNv/nEOCr8gfKfBHDL43z/a+dsLsg76G+S81ErndwFSA+SzgvsA5m3Przy1u/b/xTEsUJGmqGaZhtklil+gRVEumKhtl
m6FYYmr3QXUcI0VdE9xBcq7Y4aJcPWLfOxV1RDxNfOJqtwNYtgmjHtBgYu2N8yMekjnmwqjxXrGRjiYWqZrF66ciXY0aldMco9909+ZPlm+XYx6TLdcdQZLI
TXptAYdgHODxGDSYWDP5taS+EtdyS0vJ24Bmq8WnifgkETuszIC/QfoM1AZWQYHPECtOUNfhuqVNPgGghp8I5IcDPmst0+F3RMpk9HTWZCCjYFKaaYStRPux
0d5pWY/bZuJ6xob9JRBZV+O3iJD4AdQLHihF3V1Fd3/B3f9oqDgwVhgYe7xWGJgw3N/cmC61CFvdm93bgw+O7YyWWn2lzp6Pwz8KP+KNzuH7s3/t6C55u0pC
29blzcvbyp5wtHpX9gQRSpCfSjtdP3kfiuX7sy88qK2DXJzu517kcN0L3w1v83vsUVIH+wYKvgHDN5jnB/Ps4OsOUPro2JcagdFnzinPhQnmjxPuacFJ+Ai+
Lw0VGQGQlQ6xt9IzOWzVhtYAVPwkVG44C/H74nxGVXBPdc0V8mJttbcrffxOtVeT20RTN1s2Pk365O7hmQPy9TY9l00psaSqD0PKxx8GvKY7Hl/KgWdKPI4J
G9to6Scu01ghhYBOuM+uMjn9ThaKCMu26YTcnyUhJEdh1ZlWbWilZYvNrNj+xqbRCiunM4lcSpnA34Ehwb82Ca/nDEVRz9DA56j1L6gF/j9D3c/Qkb95fR+7
f+zOHwkZ3pMbni84lup91eqmZqlXPRzle+VzUMf+5fVQx60d/gNQSwMEFAAAAAgAq4i4XClngrReCwAAahUAAEAAHABhdWRpdF9zdG9yYWdlX2FnZW50L19f
cHljYWNoZV9fL3N0b3JhZ2VfaWRlbnRpdHkuY3B5dGhvbi0zMTMucHljVVQJAAPRLxNq0S8TanV4CwABBAAAAAAEAAAAALUYS2wbx3X2w+9SpESJtCRL8lqS
5dCWyKSy49iS3Bq2EkuOhYQrBmnlZLsil9La1JKdXdqREgQKeojcHCwUKKIERawCQeHcfCpy6MHoyc2lJLYJ2YWSGsjJhwI0ZDRFeumbXZKiRSYtipYEZmfe
e/P+8/bN7ra1eRH8/hTrvGr4EfoaNfwc9oPa/RjGXyEBCdQlNEthiidz+hKNaXgyl5hZFrOzDuyw4Owl56wLu6y545J71oM9MHdi7xXHIBJcgvs01Y1g5ulF
gvckbcs4WRWJOaFD4HrRVRb7hKDgs2ZtQqfQZs38Qpfgt2YB4BSIUkJIaCfrqzRuX6Ij4YeESYQyOUlVs7qkK1lVm4vQplPSUkpSNz0pSZeSGUnTgIh9SdKX
4cmcU1fXuqNSPqXooqZnsbQki2o2JUevall1HiicaZxdk9Vko3eYmndesbxzBWFKQOARCjNHwDtXWIE5ChhrxlozB8yc9ZmrjnVbMw92CF5YcdbKh51LbRG/
GRBsfWZSsqor+upDDrAmo+nY9Mk4Jy5Kmizmccb0EIXFJAzfegHLv8XPZVXZ9OiyKqm6qKRMzyKW1OQymfpqVuJsVn9IWSwVVTfDALOIbWHidRlr4MJP0FzE
ZbpFUZVWZFE0vaK4kk3lM2TuE8Wf5aVMFRMQxbSCNT2jqLKatQENoRBFHCLCQiL4GUBJUdJ1rCzmdZngwoDbJU41J2Mrqh4jwYplNOmEaOkVezJCNqy2qukc
za1iktLWQJylHYPhXfQgGDLYUDkQ/PlcOdBjsD3V0QYPDhXY3q3zX7BDuL+2OUk1RJu1I07tpqxoL9RxCTRFPVWdpyCbIdepk1VsnEFNv1qmxx3NOIGu7RxG
EWbORCYVizDYRzzigKAqOdOJrecnlOm4LmXyskY28HiAkHghafGKlFE0GY8AgLhaOwLDOtrh/Bv5W29u0QZ3sMQdLnKHDW6oxB0rcsfujBtcrMDGLMNb23zl
f2Sz2mon2PoJPYd5gERoy1Y8SIZhon2DdZ171onZHEknKYMjgOkhhEO2mb7ApuN9bito+PpKvsGib9DwDa9Pl7n2W1ObFwpsd7ORdM3ILsvIBFqoo8Y4KxDU
HFSAjpkL03PzM/M/Fp+feXF67tzl6QiFg/tUbKsfnBxUFjwKMALX/JZyZe7An3uPF7njBfZ4sx6+mh7f7nP2Qt2hCRRnWziXakHn/B46T50zt7dDgOJ1kvmP
KOkWlG0Nsv3NsuMdLfTZp5fAnHaAt9k53A4rTHZEHJgwxoTjHtRye8Rjn3KS5w1EJB9M15Ksk7piJ1PQAmFZy2auy6SWJ7NqWllqjJt/Ma9kUvUSgp8BIMlB
jRx2cnh6d0KHtp0lfrzIjxuhEzvBvq1XSv1jxf4xIxjdCR75eOlO+u61eyeMkYtGcKYZEOreCpd6Ros9o6WeE8WeE0bPs0bo1CPO6XNWkNPhtNIBdGuXMpns
DVFRFV2xMr2eIuTpRtUUmXTYqRpv9ur3nz86QbesOxT86dM0jxZc9UDWa5HK7CVCoh4ulU0wcQ41/Wqy6qf8AuzaO/OBxmQRmATjh45hnKlxlSngGmjmutBe
ty/YjB1EC6G6hmw9oRyC8zRtdRxgWYIZhsPYwCfcig9I72mG211L3XJHgo33NVMBdKAllG8JHWwhh4uyqiuGVHfCFT/SjK/5thNd/C1CeShduQ8TjvjRFvw9
dW25hEfwTcBMgigmuMbqexlkLETqe7im2txWr+rHv1ub+FgLS/z10rDH3/v/4H/mRtWW90CCOx5rpk14IMcCCW6Shmd7wkue447afh5dvkMaBcgQd007qf+J
LO0QgvFnWvB113gQBfcyWOhUfQvjdaq6JLWtVZb+mzgfq8b5o0Tb98dZ6CIxtjwBuZmATnmCqq7hTfrq3xIeq6+kVN/l0aq9LTkKoRpHyNFmtOVvqeO7sIk2
IVSTk/DV/Qlv5//uXAO/plN8GC301ihcSIL3tupsiJaH1JTxevVKOGVa8KrOc840lUZTzCGUpiJhaRHQ57Es6XIM+igFOk2Zh36Zr70D+LSSgZXK68syr8kZ
OanLKb7abPKkbY56vfPLisbncFYHrGZR2i12nS6dzaRkzMPtYYWXkkmLOZT4VX5RVtQlHst5DbgurvKSN6Wk0zIGPD8df4knzXzMZjbKS2qKX1Kuy0+IIMpC
D72YkfdpNaPzqSzQQtPNa0nJtuDGchYIV6TkMjTmE5YITVcyGVBhRVJUm3NagZaKl/L6chaDC7zpLOZlFcYkUZYIlJI66GFpZ2lFYPbrNI/BEKIAnwODbR2j
3nm4erlyEjFLM93yG4qmi9lra8PVS81+b2v8iqJpRJik82vHo/xM/UXYyruWOOu6EX1I2jjycldUwnLNkdfTY88BwA23tmwKWK6NfafQvAqJkCKujCWzGOdz
+hl+LsK26j3O0rWZDePJcBiGNe4Mb9kHoqbWuFFY5KycmVr7jZUm+1ICINC+gthVHu5K18B5epaX+CezoLanIRuifKLqjJq0/V7JSclr5AnBg6ZH1mPJZUld
svc86TmSlSAmyq/RE/zzmJxWu7fqtR6Y1FLTsXItpWCrl7X6ItNpCdZMXzwPblyRpzHOYtN7A1JGFnX5Dd1kybXZdKTyKzkNsxaTTFZKaaaHGGzTeKbfSMpW
F9/KzaYDmK1oJgOdXMOVYMiSL+VyspoCKVlFNVlyrY8E8NME/wOCZ4lxJks6cJJydhSqyQfuMhn5jSQ0gcty8hpoRLI3AyxA1jV51fTXNojW7Yqs7W3Vtct2
Zsp05XOkZqQwKd8aqWz8kz+7szwgqxocDXH/LRULgCU+1RSatJiVduQLlriBIjdQ4kaK3IjBPbV+ocy63519Z3ZT306tzxbZ4ztc91aXwQ2sX9gJ9G+ljcCR
9YsPuMCtiZsTt6ZuTn0VOLQTHCgHn72r300XJpcK7cu7DN3pryC6zV9xIk/brcDNwK/Z294PvR8/e+cpY2j803PF7rP3D2wEiu6Xy4HODa7CMo4r1E7ngffP
/PLMVuq2+oF6d2lLLQycNTp/uOGueFHP4VL3WLF7rBzqL4WOFkNHmyePPA6/c/35ig/1DazPlA8duf3WB2999HbRfXCDKfccvt33Qd920ug5tkG08gUKwVGD
G/3d63/xdZF76tulwNB20OCG7fspoErceJEbv3ve4J77/fl7/tKP5gvxeWMyUZpcKE4uGJOvlSZTxclUQV42JpXPOaXsC5V8fUVf3zazld++dNdfjJ27d/D+
68UXXy/0i1/4fvoogPjo34PI3bHj763QVNsi9cAfvKXcVLY8hn/wkYNuG9pl2IC3gliPt+JHga71F3b8wc3n3lNhEu7betUIj5TCo8Xw6Bfh6E0v2MUFSRw2
Nbhhb619zkXuyJ+e+Tw6XT7Ib56/+WI53PuLFx650IFYxY3cgXfffOfNzbOGa3B7uTQyURyZMFwT5cFjpcFTxcFTG65Nf9F9qBwMb7i+cvnLbt9e3LZP3X2m
2H3ynouE7GKZC6xf+sfjyxTyhXYRRSLn7y30z94/cX+kcPBlwx8vvLpQcC/8s8IQ5LdaN6TcHw4OzUyhP055Zw8yn7kCs2Hms7AD5lDxSDJjl3XKsKznsWod
vkYEnidDwIZaVcJZh5IZXOGtudemsE/kYjabaSDpIB+Q0nmdHAwRk/sKniJ0XP3jnGzXDUzuKuQFoi9nlEVrs+nUV3NwHm1ljtdE2fdA0mruVSrrjFnflOxv
OtaBdE/aX67O4p8g+1alvQYD+IeivkSDXyPuryjyJer+EoUfdEULqOORE9Hc5nCROvAN7aSG4bpGDe8yiO6ukOXjIEU9/dhNUz2PvRQ1+tjtopyVLnSOmqa+
QVNUe4UMlvh/AVBLAwQUAAAACACriLhcevoyGcwSAADpKgAANAAcAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vc3luYy5jcHl0aG9uLTMxMy5w
eWNVVAkAA9EvE2rRLxNqdXgLAAEEAAAAAAQAAAAA7VpbbBtXeh7ehxdJlEhZlExKFGVdKMWyYlnxxpYsy5Jli7JlmTQdObRDU+SQpk2R9JmhL0oWKPZhK29T
pMFuG6dYIN5ugchosFGABaJgA0TZBqiLPnRmKe8IEzkxsOhDXgolNprA+9JzznCGt5E3LtqHFqWEM/+c888/Z85//c7Mo5oaEwF/7B7b5cUWgvgDUfLTigfV
o2XY/jURIAKqacKnAio3otXTaqDGRw3QwKNmWuvTAq1PB3R4XDut9xmAAdO6adJnBEZI6wOGaZPPDMyYJqctvhpQg2njdK2vDtRh2jRt9dWDel8DaPDZgM1n
B3ZfI2jEY+bpHb4m0OQmHISHCFhaiEDNkFqcKXAk1N7arxDpVQnmSDqdYSJMMpOmZ7xqgYxFGIpJLlACidrFTJqCXNrZCHMJHjVj6ZtfqcQrjUf9s+OpJJVm
kJixBCTGM+l4MgFPDScy0Uhq4ohXJzQAis6krlHhXDpGgTDIZBihgY7EqTCgUvC+cCQLhQtm+lJk79AL4XgyRQlWOpJOMslFKkxTiQUoWbBfB0kGnjKAiiyE
mQzmg3dqotJ0DqCBDIgkqHAyBrmTzM1oqY40ko5ewzo6TwBVgIB6UUGd1AbULcRlqJNAXUCDKR2ktJjSB6yB+oAO0ZdVwAD79bifDDQEDJgyBmwBElMmOGrE
lDlgD5gwZQk0BsyYqglYQG2ixrtDsATEmeIF+8oKJxTVVZiTBk31V3iqc9aUPDTnTqlkeiCllunDKY1EB1RBIqWVzkJmiQqqhgrXwnG93Ev4Zbr489dV9wXU
kNdS3S8ZlZtIEF7NTELqX/QUHtONFO6WtOK+RoFkPEnFDrg7aS8p6KPYYuARG5Kgjs0L+lQmkaCAUJuK0Ez4EhUBzDwVYUA9FCvUSJJEO9Em0/EMNDItTaXi
YAfkAE2ocaCmGTY0Wls3+gkDexbSzB5o3pE9KTqyLxxBCtgTycWSjGw9Yh99Mx3tz94UyHA4Cc0wHBYaS5XWL3WjGdGTsPkzYmOH83Wz2DTYXzeIVGv7knad
3Lnp2rdyNu8aRictG6TlDfMtM086ONLBNo+sMvzoaW709H3S/wgtXFRVsrp6yRi2sDFAFVRrgPBrqvuQGfi1CrwK6i6qsBXdwajAQUgcIZMsScEYPERINh1s
MEozsG4/A8lE3cRJuKrQnBYHAwxUfzKdcI8hPbknk2DBjWOLW7IvrBJ3PAPc6UyMGumkv0Iq9xqx+oETNcguBCMaDkdhI5Aglw5n0lFoPzjS6egURWUFzULk
htCYzaRSUL8MBa5FUjD8QAuN0XdVwAWF0GiCbtCK5JmREHhbCpq04Cizj5KRFnQV0ho0kXLNz7CzZ/jZEDcb4mcpbpZaJ+MbtQ0bFusbJ2+dfP3UJiR8P/H9
jL79/O3YO1fevsK3jnKto/mmw+uWsa91RJ0NtBMV5oKWEZtLjCiYS+lolTKDiqZTNq572jj0eNWMVyNYwwuRm/NU0VUFexZkohRNy351OTNPC81SbyxzPZ3K
RGIwA1zNUTRDg97yxQWdaIUbqxYV6cyNWLukFeVJF0e61sk2TLdzZPs62YHpLo7suk/2iKukLnkAo7RKi2q0SiF5QMm5PPLzptVBVU+BjnWjaOxXV/PvNuGo
+j1d70ADQUTgYyeg64VImdNUzenZTqpSTC5oPQefMrsvrQuoim757FKCmqA2qAuoQ7Lj+huU5heyy+M7tpfmb1YYKxwDmn5DWg9n2KIwQ1d1X1BfDBiluRFK
aFWQ0F7dF0BPptttLs9j7UTII3EYiAi0tbRhG5mdCjL1QUNRWhss7NKGMUOicIyr4sSIpo2Iq7yGGUF/aubE1MzRRd1A/77+Aa9B0NOwFMvRgkVyHFwumRlY
oaXC8zcZihZMOZqKSXQcUFSBrsG5KwxjDg1rOaFWPEXxLZxjoovO45J3umnkTm58aWJ+pLN/bzwJ53p40V5kiUdgdhWTtA0gZ8OBTAykLbKfV4VJOP9LOSaZ
EkyxJH0lnKPhJABSr6CBhRvQYiqduQ50mILzEoxJOgNj5QJM8ChzC8ZiwpcDOCw1b0SpLKpRBSMlkV4Sx2RBBy+PUmAU03ilBC1aI0GLHlEwZCM3UbARNPBK
GjmZu/ATQ00/us5Vnt8r4hnSMv3vBAo5m7UNbyR/kvyLK0uaL+rst218214O/jfvXTnzYeiDED8U4oZC+ebQhqVhS11nHN10d/Hu53/nfn5l14d9H/Txe33c
Xl/ePc1ZXEtHb9sftng23R13Jt6d/sU0v2uY2zWcd49stO3aaPWg//aeTU/vu8/93XPv3Xj/tbuv5T3DvGeS80zmPce/riObapeObTUQNfVvTN+a5i3tnKX9
Tve6pXfD07luaUOZBPc7OYuTdR1cPbN2jhs5dd8y+91mneMRoTKOyizoUtbzg9UO3nKIsxz645YGjj6hkQI+1Y1rJ0zEZybTxAHNZ3bnxJDmsyEdpMsyD9In
jqm/JVBMbX2GUkWOr6hkMVePFyvXgm/K8avgm+ptqliFQgaWReoK31SPqROFY4lvqmcWe8YzuVQMFhXQGygmesmdpdIxVIkUXNONchr2EL1oufYCQ1nSA+NE
oRIFE4ipXSkzhuMgsxAu2KlXI5q1Fl/uQ6anEY1WtNc9WEyZvSrJPIAuDGCb3VJrjbYNW9NbB988yNsGOdtg3ja0NLFlIow1PNnPkf3LzH1y33ePjYS1+RGh
hszlhnHiXnTd4t+0NPxxSwdHnzxCof5HLf3Er0xjrZpV51iz5tNmHSTL8iwp2cQJFa5GitkTZ7xnKWZDch1StIaiHoMqG3G8EVkCtjtDtQSo9WJpCZVllmxJ
zrnQlk5BCZptZqVgl7DAVftrFPo10r1Gik9cj+epzK8tZtohjTRLbJuaMc3JNFGgSqxTN7PYNpmBeBpan5JZ9tDeIv6C54VwjkhYH7spADIAEoI6GRP0sBOC
Zq+hJNxqUlRasIcVzEo0Z2zJmgTFeLXYVsEp3IHGsb0iDy0G2Bk02P8nDbbMCdBt6FsEst4v6qA5NlQUzwc+Hl/T/mZqnZz83Na0NL6l1tTkVBtWO2/1clbv
csfvrf3f6Ai747vHesLugsFOHH7L/KaZt3Zy1k62y3dv/F+P/fMx9szcv5zkT6TZCxf5CzHuQoylFvIX0myW5q3M76wMioXw4iePEKT8df2YSf2pyXSkQ/ep
fUx7pFX321YdPFG2+481ot0rVeEB1bZWoswvWzCMd+poLcT2RcyvCapqiUGZg1JtI0OrcM/Wp9xTp8CvR/ywxlHXEgHDoAZ6jPK15DM+n1F+Pm3R2+fGJS9P
60KyX0MfJYmqn1LVHNTJ1+uV9y8U9zpMc11FTwwqQla/TeFuRR0ZQo1yr0GpHoa1a2FmOXjM3gqYg0b4R4bk+jgor75/p8IcLcUNvZBTnpVCjQorc7c87qke
l++iUMvKlXltvz5t2mYluqv75npLojPh76nm8PdV95U8u7yS/5X1mLs9pC25+/fVet1cT9BYvBKiW+sMQBkUnIHNoqM0oC4kaRoF3SSskYX6DEgmkmlYBqM9
qXQE7Z3KFErj4Sfa/vlkWrCl0M5Fxb5nTdnponuiAI4rYjoK3EwGBnzBOH7q5OyJo2eOTgiq84JqD6w/9hFoQ6KwgXopQl8SjGgCpSSdXKTARcRXg6RCCAHr
dwQM+kufK5pZyKYoppgr0GX4iGVDyrsDvISkWPy5NAIXR1EyASivAbTQ4h4cspuS/RexPJJRf2k+QQYlaEDkOjiEKAOgsqkIrOaRGnBJj6ECOIyaMcTRFIle
gQgCprMEVSYJxWe02RzNgLJbeM1ijpqTNAnOI876smUPoxlE8CrGKJqBysRggwQUnc2kaQqgjU1BH0sm4CiYgic0KgncxZ+Y684SVcWZUho9iQT8UIX3MOrt
bzW92XTb/lMXXz94p+Pd7l90L/fkPYNL+g1z/a1Dmw2tbPuLqx2rjWzbaL7hMGs5/NDZ9s6xt4+xu/bnnT/gnfHV8U+OfXRszZcfPgXP2OA59uWL7LkIG6HY
QJwlmzck/hfzzgO88+xq9JPER4m1TP7QWTi66ehl+4bzjtmlms0dPcv293fe3cl7xzjv2NpIfsfpJfPD8tR7cPXsOnm6rJxcmczbDi6Rm86BFceHzg+cq6/m
ndMc2bxk3NITTZ6Nne0bzW0bzZ7Njq47P8x3DPEdI1zHyGpsbTzfcXzT1fnO+b89/17P+8/dfS7v2s+7xjjXWN41/nWNwWZaMmxZYZkqTqCPI/uWJ9fJYVQN
GG4ZeNLDkZ6/H7xz7ZcHf61ZOZkfOMIP+LgB37197Okz+YEg+9KFdTJZUTtM3XuBfSV2n6SqN9D+D8KY3dvBGMkd3dImXCmWaZawTPVWXQWg6dp2U08R1Rjl
UQVoM4AFKlaKVdKRQ9LInSV843C+Y37bzDtGOMdI3jG6NFXAN4McObgydZ88tD2+Oc0Gzq5bXioCHHCaqNg0/H8w8z8IZnZVgZlK40SIpm+iolOCNYXTUmiD
87ZgKoxU4BsQRIPN4e1sqwhycKqTQM51QkxQIo8C0LmGGAa/n/mWOUcWyfolIaIdRzXaGf3MtnbuH1vXydnPm9xLxxDa8akx2unnrP3L139v3Q/RjqO9iHbw
cCnaCbDB83zwIhe8CNNCPhj/liDOqibU7OWr/OUcdzm3RRDXVePqRwRxBXbDs0n1lBoy1fvU3+AW4yCf+gl4mahwDWSi2DV+rP1fgHeedk+5wtwW1eifEUEZ
tkVQJEZQRoigSvHOvv9GvKNXclApTUmziTQiLKJUD1dyxpAO6uUnMwX1FTrQK2EiSYrfXj0GUVKTPHN5ZdPkNmtpfkZtWmSJEFnJzztchimMT8MU0tyD5NNG
D8C4EKkvs8+aoBHqtjZIwhXSlqwQ4XdWy/G3Kcy9rgyBkvDPoIiElN5pWAP1khUEGvq1GLcp4D4lvDfXFTSVId8uheu81X3fG7n9ifnO/XkZcvu+q2Wb6w6S
ZcjNXkBui7BZdFUljlL4hkERrv0xMAAXCIQOKgN2GISRqE6/eAqzDkJV7iQtVlY4f2F0l7oJRVJIUBxd0BM4PgaBE7rjQgSVX/MUejPszmWR9H43dSNLRaG4
kUXSHYkyuUhqZLE/mJUAIL4JerddmRBRskPABeU69KkLQK98YaaS74wRC362xf3ViTOXgpIrcmc11GvE+Q+gt0NKCE/QUzeSMJcJhiQtfo5RPwnbmQyD8zlG
hoIWvdESDHAFMQKtRRdjqKdLZa7DdF3MyuhDF7H6dIqrU5U18U1EoNdUAHqVLBLae03SPshIShXxnUnE4FgSXiKAemskPYiQWUfdYEBEGeK9SlQVqdtWEmgC
9Jcizmt0vhV6M3THnm/s4hvHl7vf77vbt5LK944vmTbMjZzZhZHeodXrq0m27Xi+YYq1TD10drwz/fY02zOSdx7ina+sdf9T36d99zL5I68gXGdrQRiMbdub
tw3ytmsr1z989YNX1w7k98/AM/YMxcazLHWVvZpj/dcgPCsAvWMQ6DV2L2vfN9818z2HuZ7Da7vzjbNwGnXNfJ2bq3PzdR6uzrPcy/cOc73DfO8hrhfi0A62
a/qe794wu2su33COtZx7aG9969Sbp/L2Tt6+m7PvXjJu1jtvv5yv74HY1dGGyvE7J/OOvbxjZiXx4ZUPrqz15V+YWar5wuz4eeCOK+/aw7uGONcQBH2rL3wy
/NHwbw7lXVN4Gabu7b7Xxs7BYiXBXrzEtiXzDZdZy+WHlUjuwDo5//Cp8PJro67etKTbssgocoAjB1Za7jWuk6dkHNnNkd3vaZZ9/1D3sW31Qv7Faf7FAPdi
gD0zx74MT19hL8bXSVBRlvnZM6+wV67eJwFGkgXsJPoH/gAJZQ3xxSmy8hnIgMdMkqEDo8yKTM2rFvSAYnIgLWhn8Hd3k9CjjyJ7085nMinwOuL8S9HR0Zb7
E3sK+l8olowyIViSPuceS9+8cKGMDe16PKkt5yhjQFUteAM1pb3Yff6qvLcOfQ+F9rTCYcEUDi9kYrkUoi3h8FUYtwojdWHoW4BmUsk0lc6EwwAFbIB2+/BH
Hfh1K36Hhd8L4A0TjPtw9Sw6V2M4jIJGMhqOMAxIzucYig6H7xJ4lUUntEkN2j/C77p+TDxWG3QB1eMGre6I6rFFo/P+h8mmG33ca9ANPW4gdTnVY7tDF1c9
3meAvbDDp/7WvlNHq0SxSJi3GT1YPMegDwrDACVCMIwa/AYcb0nhF9cGFHlTyXmgx1GQuZmFsRoYytVaVLWsYBQGcdgQAykOqTggovJL3AqzVn7HiD97E5/3
b1Ajr4FADosqOAR+Dk9RhqX3wwbW6SrVA8LzB6Lmc8L8JdH9gHA8IHb8G+F6QLQ9IJq+JLL3iewDYuhb/VG1ilZ9Q+ADFvyfUEsDBBQAAAAIAKuIuFwdGf3d
CwsAAC4VAAA2ABwAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy90dW5uZWwuY3B5dGhvbi0zMTMucHljVVQJAAPRLxNq0S8TanV4CwABBAAAAAAE
AAAAAJ0YbWxb1fXe9+HP59iJncTYTeKkSVOHNIGWgpoudGmpOseNhfIaWgjweLFfUnfOc7jvuSUBTROTNjoxtd3EAhISRSCRiklM2hDdr9EgQfvPxmh234Im
hPaj//o1hbFp2rnv2W6KHWCz5fPOO/ecc88599x7zvUtj8eF4JMf9p/4B2Bfog0fznrgW08CfAWJSMRxNI4JjlCciTOEgScbZ8dZwo5zhDPpXJwftxFbhcdO
7CIv2uKOcSdxRlAQ9SDRHkKiYzdjaSeuOSbqvEbRKDbcsqpmdVlPZ1UtEWUMR0rWFT09rxgOCpeyqgJc7Ji6eA1bEoKoZ4k8p4zNKaqe3Gg+UzX/UdP8JxHB
IgKTMWFFl8iE0AkwWXSLrInxgHEmZhMFkTcxu+gRbSbmEJuo2YA5RQeY7Ix6jZaNMx/JqaqSuSbAVEnmG0aw1IgjphHHfJna0BSe3MhZpaIM3sDBNeSoyc2h
CzhxgTV4mRph2JJZdTY9Z9gy2bk5hUAAOU3JzBIvsGpUKBIxdg7Pq/owxFUezmjyA5IpOSznUmld0iyPKjTd9GloYdFwSFJahXHJCNc7PVQd9NBZOgD8FJVb
Qy+7ym3h5cy5zEvcy+4a+iv3LWp3Em9wyAa/n9EYvQ7UrSgCcZpCk/WOo0m2niZi4G0QpUlbPa2acxHUCV+QczbgqTxjkL/HYb6T+BU2wQsgM+2r6W6pl+tB
0601m9hNbGrb3KbdeLN5J44i5ECJ/d1o2lWVsiOZoX4oaFrYQNuGkIo3iZ23niYyU/hOTLpgZhWP4UTOes7iWTTKdqFZDLMffIidxVEusRQRdZnoaXUuYqVH
ZD6bUiKzWRJRARnt05Y6rLSIpNIapKOqJHUlNawQkiUjkT7tGg+TRQXSDA+DS6sgSeNqOKm4lARgeKSKnJRVk4rRHFcWZ7IyScVUHdTkFnTDefC5pLJATwnD
qdRQu6wtqsl01uC1jKIsGOy8/JwRtMyUiFJVqlEspUUZQpfRYEFBZW8QPyW4SU6VwCzlpEKMzgb5vmGcatDeBnCNJr1DOOM+7S45ggVHMH/P3otH/jz9p+nS
6GRhdPIzh1huarnO8J7DuNwWKrVtL7RtL7YNlHwDed/AbRfyh5Z3/HrH2+z5sfP6O8+/+XypP17ojxcjh4stEyVhIi9M3PAir/8L38BXa/7eWwh72sq+1jX/
FoqCTl9g2X3OXfL1FXx9+W0HP9RKvnjBF//XdZYOf/2FMPFP7TAY+uLOAwJ+37/fgy458H43d8nJUdzzEJBXOXzAxa3yNoq7MdBXPYEDdna1e8sBhl0d5YH+
EcMD5SM7B/hdu9iNKrv4P1x1F0/fyb4GJ10P+v47V8TwZURW5PbwGMXw3XsEch432sug391AFw90Tz29uvNF2xBT3Y2b70rQ0dTA9uYG89lFxx4MEtiHWr6h
R2VAj7+B5TULVPbOuTLFTtdOkOq+3V55T8H7xP2UZ/KeBjY4a/q4KU50jRxDSN5hRijUwIuOBhrcm2gWarGqnSQTOdDMiR46yyFMT6pNZJuqsqN4ox8RNIxU
fhMZbwOZEJXZhN/3LXPYpjtrPkfqZXs2W+WeetoUX/W/QeX4/2bpbTCLbfNZ/qc90Dzdv8HySoQ20GxV2nfslZYh7rv3yiE88RMzI/w0I1IszYqJX2yafdEG
9gY2WV1/ffaB7vdpxTzEJJoA4kTZhB9AFfsEfr+FX9xFu1P41qIJnexxrmqzA1Gr51A3GmwwUssimHmWoXUzYnJbtdKsk62Ja3TJr9F6nUbIh6Kc4VmAogmd
EtSwk3LGEMxX2tRmc7rhSWaymlJ7dUDlkrT0kmLwRJFTi7SR0xcXFKPJ7MxMPimnJ49AA8xDKUoqZAjRIloruEuBShWuUWjxJTTc0BNCmTS4E9kZzWhOZU+p
maycggL5bE7RdM1w1DDBmk2DhjynRbnKHFQOiqiiGp4qp/lKhukwb1b6pfbK9Bp4q5BIrfxHWw3XKWVGyyZ/rMAM9op5Vg/gqtTqHMnQxlVNGS7phJZVJV15
Tjc4Sc2eMltZwyfNy4szinRcgV5kRpF1wy0tyAQiSNkNV1pLq2A17R64VDqpG+ycopstB6FJRLpMR/SspB+n8TW6F0g2qWharQE2fZwl2XlQu0jDY2yrstQF
7G4+NgOR2AP6o3arv3DW3DVYIp8y7POgBeaARdYgAWBVyS6T704w7TSPrY/ZkpAwZYg0ar03dkr01NZ4xmpG1vwdJX/fp/6+suA7Ez8dLwk9BaGnHNpSvidM
YWtk3W0LuG4iANcF1NRebgkvD50bOh94p+vNrov9+anHS1NPF6aeLk49U7hfLnbPFFuSJSGZF5JU4/jp8ZLQXxD6V1J5+tx1tWPbOmr1HMVnk+VQZyk0UAgN
rOwqhobP2tfau8/3/+6BUvt9hfb7rga2lMM9b+x9be/5k8Xw0FlH2R9+9fmif6AcCC7Hz8VLgd5CoDfft/fiUdo9XQ4XR6c+CzxGxYDx5Gfg0AbxUviB3/ec
faQc6X/H/aY7P7ivGPlhKSJ/eOiT+KV4/tEnio9Mw2teks/G1kLdpdC9n4buXXnkvfEL46XBY4XBY/knni6GpFJAygekOoanCoNP5aXjxVC6FEjnA+lyaOsb
Q68NrQTe67rQVY4MrfVtXwm89fhadHDl8Xc7ywND70UvRN+994aTH2m7hfiBdgDh4DriW4PXKbgpoNYO6sWpv/i31dztKwSgYdv3Yfsn4Uvhy0eLY1OlwGM3
R+naNCFPCt/kaFRvtKHWZ/DVQPoGQ9WFUWf/uh941lE9uE7Bv7WLkA4v3h8bxX/AMWz7IwvYB7tjW9lLgzjG2S7t6AJ8VeBiPm7Vh2N2x2ozS/HAgSEY+JjB
MZfjY3YnxXfjmOD4+MExAV4uw0CT4zLrprifsl7BGFRf4TDF7Rh0XHGZuGDCJhP6TNjsppxbcKyLu9Jho/hWW2zEfiWKY4PClQGB4g/agX5XZ8miyh16FqHv
0VVO13rKhnfAiuZvu/v1IkJrXpQnVJXB0nOHNzE4cw1nWsvCmTsv61FsXQCoyspO3UfZ2hvtVFDyMGWmhQG2p7f9zAu/fOH1H71x+LXDRW+05N1V8O4qenfn
HbsJbfLu+heBr0ZgwIwA9MIoTntHptGNeMrslPcwvVB1EoSqgfKBBw08AtXCpSlwUMpgn0aLinlc8qnc/IIWZa3zyl45ycgxai1rHUWWc9R+o6ORc7VTeowK
BU0X/+rwl73+M0unl17tXznx0lLeuyfv2FPvHFd1LnGXc3BB38S96hL2QoG+s9hw/WWta+wcql5co0wlAKzpjcFTxzTSYZGou2ScgnpXqR8NL38b6sxBKjVQ
9fU6wzqDZX/78si5kVeTv9mXF3q+WvMG4XrmDF71t8M9zBn8WqO39Z/72tGyq5d9C/eyhDZ+CbiL0rpGHNZi2Yii54hqcAn6fxdDJDP3NJ1ADX4C8KUmWtSm
gTAYGVMXnzIZiEyFOcshiltUugWibvoPjipD0yBBSZXgyp7LUFyQpGdzcqYy4pWk2TTR9ExaVdSsJJEAVUC7ULMAmblt5oAVnVbJ7AzSSUnWdZKeyemKJkkX
kLnCVhRdVUDd1rZS39FthuW33nY5+QS+3dbFH8W392PMP3jbxvAj6w6eD1ryLstusHY2B8EA+whdILN0m9vS2pE2KKDQSJlukhEKdlYDac0+R0HNIsPxA8v5
h8kJeKWJpHUDgNXB+HPU8yVq+hvq/xy1fYma/446120P4+BNBMBU8V9QSwMEFAAAAAgAqoi4XApngiOWAAAA2QAAABAAHABjaGVja19jb25maWcuYmF0VVQJ
AAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAF2MQQrCMBBF93OK2Qi6aBS8gHdwW5A4mbTBNhmTaWk3nl1DQdC//P+9f2HqEybvgRweHe5eTk4QPMakyEsoimbm
OLdXykG0tLJqn6LhhXEP+Ims2JyxGbFiGwwHIDsMf6YlDbNVNnersN1UTYJgiEWr0DSTdNk6ri38LBkzP6eQeeSoxejy/aCe6XGjFH3ojKwgdioMb1BLAwQU
AAAACACqiLhcjA5piscBAAArBAAADwAcAGNoZWNrX2NvbmZpZy5weVVUCQADzy8Tas8vE2p1eAsAAQQAAAAABAAAAAB9Ul1r2zAUffevEIKCDatW9ljwQ+p6
XWHYxfGehWJdJwJFMpIcKGP/fbIdpbG67MEy9+Pcj3NPb/QRDcwdpNghcRy0cejNm0k/BdjIhaPWacP2QP2nHOm06sU+5ErNOF1c/4FI4X8BUjZvxey4DZB6
vwcTABbcONDFdxsTLMG9Jdx7QIOyowEah5NkxyxQLgzK541TSnshgdKMGLBaniDNyMDMNOh5nnw1ShoKZMmZk/yajtS6jxT0FWEC6oSz7FyMCNXrFBdz7miY
E1rNcODIjl0H1vajlO84Anj26NNmW9Jfzc/8zuIvaGlHwAx0bjcaGWGq+rmkhX9WAKU5+Fk5RNnbtm42LyVt6rpdAQKFRmsXYdqy2lQtfX1eARwoppynHGmD
8H28ylOzqYofMWhnmOoO16BwMjrp1JN846LpUiDeZslCF1lMR0YnMKIXwB/R3HrVwZ9z0Wv+IdVLbS+NQSsLPrhkkQMw43bAXPobW8fcaPEjwkVdfX99oW25
bX19fE2dD0/S+BepPnMWM/XzWS+IqdID+UYe8J/PMkCXzsiBdSiMdt4pmFkyGOE3iKTGFJ/Wm1hX0M2uuUqnj4MEF6mQ+DP8BVBLAwQUAAAACACqiLhc+jEC
YSIBAADoAQAADwAcAGluc3RhbGxfZGV2LnBzMVVUCQADzy8Tas8vE2p1eAsAAQQAAAAABAAAAAB9kF9LwzAUxd/7Ka7Fhw1MQHzzbdaiwrAlqfgyKLG92wJt
EpPbsSF+d9NuqOCfQMLl5nfuPRyJxJa2UaStgfNSysZrR8JaSvQaZmezCgOxUtEWUr5Ds1sdibByB9paw3GP6XwObwnE4w7AroD1MJIw8cl7wld/KUfUaQfa
BFJdB4wNbuNVi2P3p067SfRJe/D4OmiPPRoKnPa/mY4Tvgxm1h3YA2F//IjjVO86TE/cxDx7TcjubSBIM4+KsIUJhrJDFRCw1QS5KOubhczrJ7G8gMfiNq+z
+JxKmWcir0CZFmRViMVdXouiqOAF19Yj+MEYbTY8jel8X7eUi0sISIODxo7G4m4OYjDX8H+Iaoie6kDWqw3W8RrivdImJmpNg2nyAVBLAwQUAAAACACqiLhc
Q213CvoBAACcAwAAGgAcAGluc3RhbGxfdGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAH1TTW+bQBC98ytGSI7ag6Fpb5Vc
FdnYiVo7FdBDpSjWZhnsVZZduh+ufclv7yw4qZtY4YBWw7x5b98bviLfatBNE1l0UnMmI15DWsPose4+RKIBpR3gXlgHCaodvIuAnh6VF8VN8XkoN0IiCAut
sFaoTQILVGiYw7TWf5TUrAa3ReBaNWLjDdbANqgcdIw/0Akao1sa+CPpx3fMWxyI9sJBeg+X0fsXYnZEe1tyIzpnb7uD22qV4B5PBU4NMkdqYCeM80wCQYTR
qiXmJDlSHWD8CcYthHnDVKIiH+QLCsad2NGFknvmooEvwDrRgVDWBcB47LuNYTWGavTfFwMGf3thMHDbxO1dMBziKiu/rVfZMp9kvhZuLkxbOm3Ikiz4Ew9d
2SJfVevZdTHpY4lPsNPlbMJbiozDEFwcj57bR3EMdxd3F2C8WveGB/UE51vH7IOFNFcEqlYQj56VjGL4oryEj+F90jpDiQ7PdKfz84De/rOAqniqkPxQKKdw
syqrrKggLX5C+aus8iUdv8PV9eIqL6k8D/mjMdpI3KGEy9OkK2KEkm+x9hLNk+0UPoXUMNrNOoHCK9pB2tF+V5mFrG6Fol2iNdXmrcX7d6Uw49V9ol5Cnx+E
AOGYIPQRgj3Koj8gqDxqC38AeU9n4+hsPedobeOlPCTRIOMvUEsDBBQAAAAIAKqIuFxOD3XONQAAADcAAAAQABwAcmVxdWlyZW1lbnRzLnR4dFVUCQAD0C8T
as8vE2p1eAsAAQQAAAAABAAAAAArSi0sTS0uKba1NdIzBiKugsqSjPw83ZT8ktS8MltbQz0DPUOu8tSk4vzk7FSQOkMjPQMuAFBLAwQUAAAACACqiLhcfDvP
u/wAAACUAQAADQAcAHJ1bl9hZ2VudC5iYXRVVAkAA88vE2rPLxNqdXgLAAEEAAAAAAQAAAAAXZAxT8QwDIX3/Iq3IMHQFMTGhIQQI6isJ51yiduzSJOQuKW3
8NtJr8txnixb733PfiZ7jIh9rwqJj9Z4ZR1ah5tfl+4V9whRQAsXgaYw41ah1ln12nXv3dM27tkTuGDkUjgMGm8UKBuh1sWf4KNxkCPBxtDzMGVyMAMFQTL2
q3bocxyr4Yfe7BcWtAc8qLurCHOF7T5t5iRll05yjEHTQpexXjIZqRkwc5bJeFQJ5xjGytN6A6QTmkc0I1a/zbWi6vX+CmGs8FzP0AcjauOtssQJHIqsgqaZ
0pCNo3Wq/m0yMn1PnGllFy3LpYeZHMu+SMz1AfvzO/RoOKg/UEsDBAoAAAAAAKqIuFytoohsLwAAAC8AAAATABwAcnVuX2FnZW50X2RlYnVnLmJhdFVUCQAD
zy8Tas8vE2p1eAsAAQQAAAAABAAAAABAZWNobyBvZmYKY2QgL2QgJX5kcDAKY2FsbCBydW5fYWdlbnQuYmF0CnBhdXNlClBLAwQUAAAACACqiLhcc9tLXl4A
AABmAAAADAAcAHJ1bl9vbmNlLmJhdFVUCQADzy8Tas8vE2p1eAsAAQQAAAAABAAAAAANyTEOgCAMAMC9r+jiKPoE/+BKYmqpSiKUSCVx8e063HST8KGo2wYc
cAjYvaGMwHSe6Jrk5me+YrHqiS02MnErGZTHDs3YJ6Q7RFuq6UW7LL9sLlH8r9fMAoXuKvABUEsDBAoAAAAAAKqIuFzlgx33OgAAADoAAAAYABwAc3RhcnRf
dGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAvUnVuIC9UTiAiQXVkaXRGaXJtU3RvcmFn
ZUFnZW50IgpwYXVzZQpQSwMECgAAAAAAqoi4XDqIHG1IAAAASAAAABkAHABzdGF0dXNfdGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAA
AAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAvUXVlcnkgL1ROICJBdWRpdEZpcm1TdG9yYWdlQWdlbnQiIC9WIC9GTyBMSVNUCnBhdXNlClBLAwQKAAAAAACq
iLhcQl08MToAAAA6AAAAFwAcAHN0b3BfdGFza19zY2hlZHVsZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAEBlY2hvIG9mZgpzY2h0YXNrcyAv
RW5kIC9UTiAiQXVkaXRGaXJtU3RvcmFnZUFnZW50IgpwYXVzZQpQSwMEFAAAAAgAqoi4XLi315BRAAAAdQAAABwAHAB1bmluc3RhbGxfdGFza19zY2hlZHVs
ZXIuYmF0VVQJAAPPLxNqzy8TanV4CwABBAAAAAAEAAAAAHNITc7IV8hPS+MqTs4oSSzOLlbQd81LUdAP8VNQcixNySxxyyzKDS7JL0pMT3VMT80rUVKwyyvN
UTACkUiaXFJzUktS8erTd+MqSCwtTuUCAFBLAQIeAxQAAAAIAKqIuFw8Po1A0QAAAD4BAAAMABgAAAAAAAEAAACkgQAAAAAuZW52LmV4YW1wbGVVVAUAA9Av
E2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcUizhRVgCAAA6BAAACgAYAAAAAAABAAAApIEXAQAAUkVBRE1FLnR4dFVUBQAD0C8TanV4CwABBAAA
AAAEAAAAAFBLAQIeAxQAAAAIAKqIuFyR0y7TkwMAADkHAAAjABgAAAAAAAEAAACkgbMDAABSRUFETUVfTFNBMV9MT0NBTF9TVE9SQUdFX0FHRU5ULnR4dFVU
BQADzy8TanV4CwABBAAAAAAEAAAAAFBLAQIeAwoAAAAAAKuIuFwAAAAAAAAAAAAAAAAUABgAAAAAAAAAEADtQaMHAABhdWRpdF9zdG9yYWdlX2FnZW50L1VU
BQAD0S8TanV4CwABBAAAAAAEAAAAAFBLAQIeAwoAAAAAAKqIuFxraGOJJQAAACUAAAAfABgAAAAAAAEAAACkgfEHAABhdWRpdF9zdG9yYWdlX2FnZW50L19f
aW5pdF9fLnB5VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XGFbMtTgAgAA8AsAAB0AGAAAAAAAAQAAAKSBbwgAAGF1ZGl0X3N0b3Jh
Z2VfYWdlbnQvY2xpZW50LnB5VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XGb0X0DJBAAA5g0AAB0AGAAAAAAAAQAAAKSBpgsAAGF1
ZGl0X3N0b3JhZ2VfYWdlbnQvY29uZmlnLnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XIrppAA2AgAAQgkAABkAGAAAAAAAAQAA
AKSBxhAAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvZGIucHlVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcMiXO1IcBAAA/AwAAHQAYAAAA
AAABAAAApIFPEwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9sb2dnZXIucHlVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhc605uolECAAB9
BQAAGwAYAAAAAAABAAAApIEtFQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9tYWluLnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XFPu
e8JJAwAA5AcAAB4AGAAAAAAAAQAAAKSB0xcAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvc3RvcmFnZS5weVVUBQADzy8TanV4CwABBAAAAAAEAAAAAFBLAQIeAxQA
AAAIAKqIuFx7PTmzXwUAANMOAAAnABgAAAAAAAEAAACkgXQbAABhdWRpdF9zdG9yYWdlX2FnZW50L3N0b3JhZ2VfaWRlbnRpdHkucHlVVAUAA88vE2p1eAsA
AQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcnDkQJcEGAAB6GgAAGwAYAAAAAAABAAAApIE0IQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9zeW5jLnB5VVQFAAPQ
LxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XDByUh2HAwAA9QoAAB0AGAAAAAAAAQAAAKSBSigAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvdHVubmVs
LnB5VVQFAAPQLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DCgAAAAAAq4i4XAAAAAAAAAAAAAAAACAAGAAAAAAAAAAQAO1BKCwAAGF1ZGl0X3N0b3JhZ2VfYWdl
bnQvX19weWNhY2hlX18vVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XAW4epmaAAAAvgAAADgAGAAAAAAAAAAAAKSBgiwAAGF1ZGl0
X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vX19pbml0X18uY3B5dGhvbi0zMTMucHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4
XNpcCab4CAAANBUAADYAGAAAAAAAAAAAAKSBji0AAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vY2xpZW50LmNweXRob24tMzEzLnB5Y1VUBQAD
0S8TanV4CwABBAAAAAAEAAAAAFBLAQIeAxQAAAAIAKuIuFz3aglpvAsAAGsWAAA2ABgAAAAAAAAAAACkgfY2AABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHlj
YWNoZV9fL2NvbmZpZy5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACriLhc5UFNhAoGAABmDwAAMgAYAAAAAAAA
AAAApIEiQwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9kYi5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMU
AAAACACriLhcLV0uZJQDAAAOBgAANgAYAAAAAAAAAAAApIGYSQAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9sb2dnZXIuY3B5dGhvbi0zMTMu
cHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XG4hMUfhBQAAvwkAADQAGAAAAAAAAAAAAKSBnE0AAGF1ZGl0X3N0b3JhZ2VfYWdl
bnQvX19weWNhY2hlX18vbWFpbi5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACriLhcWJ9BapoIAACeDwAANwAY
AAAAAAAAAAAApIHrUwAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy9zdG9yYWdlLmNweXRob24tMzEzLnB5Y1VUBQAD0S8TanV4CwABBAAAAAAE
AAAAAFBLAQIeAxQAAAAIAKuIuFwpZ4K0XgsAAGoVAABAABgAAAAAAAAAAACkgfZcAABhdWRpdF9zdG9yYWdlX2FnZW50L19fcHljYWNoZV9fL3N0b3JhZ2Vf
aWRlbnRpdHkuY3B5dGhvbi0zMTMucHljVVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAq4i4XHr6MhnMEgAA6SoAADQAGAAAAAAAAAAAAKSB
zmgAAGF1ZGl0X3N0b3JhZ2VfYWdlbnQvX19weWNhY2hlX18vc3luYy5jcHl0aG9uLTMxMy5weWNVVAUAA9EvE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAA
CACriLhcHRn93QsLAAAuFQAANgAYAAAAAAAAAAAApIEIfAAAYXVkaXRfc3RvcmFnZV9hZ2VudC9fX3B5Y2FjaGVfXy90dW5uZWwuY3B5dGhvbi0zMTMucHlj
VVQFAAPRLxNqdXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XApngiOWAAAA2QAAABAAGAAAAAAAAQAAAKSBg4cAAGNoZWNrX2NvbmZpZy5iYXRVVAUA
A88vE2p1eAsAAQQAAAAABAAAAABQSwECHgMUAAAACACqiLhcjA5piscBAAArBAAADwAYAAAAAAABAAAApIFjiAAAY2hlY2tfY29uZmlnLnB5VVQFAAPPLxNq
dXgLAAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XPoxAmEiAQAA6AEAAA8AGAAAAAAAAQAAAKSBc4oAAGluc3RhbGxfZGV2LnBzMVVUBQADzy8TanV4CwAB
BAAAAAAEAAAAAFBLAQIeAxQAAAAIAKqIuFxDbXcK+gEAAJwDAAAaABgAAAAAAAEAAACkgd6LAABpbnN0YWxsX3Rhc2tfc2NoZWR1bGVyLmJhdFVUBQADzy8T
anV4CwABBAAAAAAEAAAAAFBLAQIeAxQAAAAIAKqIuFxOD3XONQAAADcAAAAQABgAAAAAAAEAAACkgSyOAAByZXF1aXJlbWVudHMudHh0VVQFAAPQLxNqdXgL
AAEEAAAAAAQAAAAAUEsBAh4DFAAAAAgAqoi4XHw7z7v8AAAAlAEAAA0AGAAAAAAAAQAAAKSBq44AAHJ1bl9hZ2VudC5iYXRVVAUAA88vE2p1eAsAAQQAAAAA
BAAAAABQSwECHgMKAAAAAACqiLhcraKIbC8AAAAvAAAAEwAYAAAAAAABAAAApIHujwAAcnVuX2FnZW50X2RlYnVnLmJhdFVUBQADzy8TanV4CwABBAAAAAAE
AAAAAFBLAQIeAxQAAAAIAKqIuFxz20teXgAAAGYAAAAMABgAAAAAAAEAAACkgWqQAABydW5fb25jZS5iYXRVVAUAA88vE2p1eAsAAQQAAAAABAAAAABQSwEC
HgMKAAAAAACqiLhc5YMd9zoAAAA6AAAAGAAYAAAAAAABAAAApIEOkQAAc3RhcnRfdGFza19zY2hlZHVsZXIuYmF0VVQFAAPPLxNqdXgLAAEEAAAAAAQAAAAA
UEsBAh4DCgAAAAAAqoi4XDqIHG1IAAAASAAAABkAGAAAAAAAAQAAAKSBmpEAAHN0YXR1c190YXNrX3NjaGVkdWxlci5iYXRVVAUAA88vE2p1eAsAAQQAAAAA
BAAAAABQSwECHgMKAAAAAACqiLhcQl08MToAAAA6AAAAFwAYAAAAAAABAAAApIE1kgAAc3RvcF90YXNrX3NjaGVkdWxlci5iYXRVVAUAA88vE2p1eAsAAQQA
AAAABAAAAABQSwECHgMUAAAACACqiLhcuLfXkFEAAAB1AAAAHAAYAAAAAAABAAAApIHAkgAAdW5pbnN0YWxsX3Rhc2tfc2NoZWR1bGVyLmJhdFVUBQADzy8T
anV4CwABBAAAAAAEAAAAAFBLBQYAAAAAJQAlALsOAABnkwAAAAA=
"""
def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, storage_root: str, tenant_id: int | str | None = None, branch_id: int | str | None = None, sync_interval_seconds: int = 30, request_timeout_seconds: int = 60, tunnel_enabled: bool = True, tunnel_reconnect_seconds: int = 10) -> str:
erp_base_url = (erp_base_url or "").strip().rstrip("/")
storage_root = (storage_root or r"D:\AuditFirmStorage").strip()
return (
f"ERP_BASE_URL={erp_base_url}\n"
f"NODE_CODE={(node_code or '').strip()}\n"
f"NODE_SECRET={(node_secret or '').strip()}\n"
f"STORAGE_ROOT={storage_root}\n"
f"TENANT_ID={'' if tenant_id is None else tenant_id}\n"
f"AUDIT_FIRM_ID={'' if tenant_id is None else tenant_id}\n"
f"BRANCH_ID={'' if branch_id is None else branch_id}\n"
f"SYNC_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n"
f"POLL_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n"
f"REQUEST_TIMEOUT_SECONDS={int(request_timeout_seconds or 60)}\n"
f"TUNNEL_ENABLED={str(bool(tunnel_enabled)).lower()}\n"
f"TUNNEL_RECONNECT_SECONDS={int(tunnel_reconnect_seconds or 10)}\n"
)
def build_preconfigured_agent_zip(*, env_text: str, include_admin_readme: bool = False) -> bytes:
"""Build branch-specific LSA4 tunnel-enabled ZIP.
Partner/Branch Admin packages intentionally hide roadmap/readme text files and
.env.example. The package still contains requirements.txt because pip needs it.
"""
base = base64.b64decode(LSA4_ZIP_B64)
src = zipfile.ZipFile(io.BytesIO(base), "r")
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as dst:
for info in src.infolist():
name = info.filename.replace("\\", "/")
lower = name.lower()
if name == ".env" or lower.endswith(".env.example"):
continue
if (not include_admin_readme) and lower.endswith(".txt") and lower != "requirements.txt":
continue
dst.writestr(info, src.read(info.filename))
dst.writestr(".env", env_text)
if include_admin_readme:
dst.writestr(
"README_AUTO_SETUP.txt",
"This LSA4 tunnel-enabled Task Scheduler agent package was generated from Audit ERP Branch Storage Auto Setup.\n"
"The .env file is pre-filled for this branch storage node.\n\n"
"Recommended setup on branch storage computer:\n"
"1. Extract this ZIP to C:\\AuditFirmStorageAgent or any fixed folder.\n"
"2. Right-click Command Prompt / PowerShell and Run as Administrator.\n"
"3. Run check_config.bat.\n"
"4. Run install_task_scheduler.bat.\n"
"5. Check Documents -> Branch Storage Dashboard in ERP for Online/Tunnel status.\n"
)
return buffer.getvalue()
+360
View File
@@ -0,0 +1,360 @@
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.db.common import CommonBase
class EngagementDocument(CommonBase):
"""Logical document container linked to a client engagement.
The physical file is stored revision-wise in EngagementDocumentVersion.
DS1 keeps the file on the server/local project storage. DS3/DS4 can later
move the physical storage to branch local storage nodes without changing
the engagement-facing workflow.
"""
__tablename__ = "engagement_documents"
__table_args__ = (
UniqueConstraint("tenant_id", "engagement_id", "document_code", name="uq_engagement_documents_code"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
engagement_id: Mapped[int] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="CASCADE"), nullable=False, index=True)
task_instance_id: Mapped[int | None] = mapped_column(ForeignKey("client_service_task_instances.id", ondelete="SET NULL"), nullable=True, index=True)
document_requirement_id: Mapped[int | None] = mapped_column(ForeignKey("firm_task_document_requirements.id", ondelete="SET NULL"), nullable=True, index=True)
financial_year: Mapped[str] = mapped_column(String(9), nullable=False, index=True)
assessment_year: Mapped[str | None] = mapped_column(String(9), nullable=True, index=True)
document_code: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
document_type: Mapped[str] = mapped_column(String(80), nullable=False, default="GENERAL", index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
current_version_no: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True)
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
deleted_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
deleted_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
client = relationship("Client")
engagement = relationship("ClientServiceSubscription")
task_instance = relationship("ClientServiceTaskInstance", foreign_keys=[task_instance_id])
document_requirement = relationship("FirmTaskDocumentRequirement", foreign_keys=[document_requirement_id])
versions = relationship(
"EngagementDocumentVersion",
back_populates="document",
cascade="all, delete-orphan",
passive_deletes=True,
order_by="EngagementDocumentVersion.version_no.desc()",
)
class EngagementDocumentVersion(CommonBase):
"""Physical revision of an engagement document."""
__tablename__ = "engagement_document_versions"
__table_args__ = (
UniqueConstraint("document_id", "version_no", name="uq_engagement_document_versions_no"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
document_id: Mapped[int] = mapped_column(ForeignKey("engagement_documents.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
engagement_id: Mapped[int] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="CASCADE"), nullable=False, index=True)
version_no: Mapped[int] = mapped_column(Integer, nullable=False)
original_filename: Mapped[str] = mapped_column(String(255), nullable=False)
stored_filename: Mapped[str] = mapped_column(String(255), nullable=False)
content_type: Mapped[str | None] = mapped_column(String(150), nullable=True)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
file_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
storage_backend: Mapped[str] = mapped_column(String(40), nullable=False, default="LOCAL_YEAR_WISE", index=True)
local_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
storage_status: Mapped[str] = mapped_column(String(30), nullable=False, default="stored", index=True)
remarks: Mapped[str | None] = mapped_column(Text, nullable=True)
uploaded_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
uploaded_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
document = relationship("EngagementDocument", back_populates="versions")
class DocumentAccessLog(CommonBase):
"""Audit trail for document upload/download/delete activity."""
__tablename__ = "document_access_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int | None] = mapped_column(ForeignKey("tenants.id", ondelete="SET NULL"), nullable=True, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id", ondelete="SET NULL"), nullable=True, index=True)
engagement_id: Mapped[int | None] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="SET NULL"), nullable=True, index=True)
document_id: Mapped[int | None] = mapped_column(ForeignKey("engagement_documents.id", ondelete="SET NULL"), nullable=True, index=True)
version_id: Mapped[int | None] = mapped_column(ForeignKey("engagement_document_versions.id", ondelete="SET NULL"), nullable=True, index=True)
action: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
result: Mapped[str] = mapped_column(String(40), nullable=False, default="success", index=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
ip_address: Mapped[str | None] = mapped_column(String(80), nullable=True)
user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
message: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
class BranchStorageNode(CommonBase):
"""Branch-wise local storage connector registered with the cloud ERP.
DS3 stores only the hashed secret. The raw secret is generated once and must
be copied into the branch local storage app configuration.
"""
__tablename__ = "branch_storage_nodes"
__table_args__ = (
UniqueConstraint("tenant_id", "branch_id", "node_code", name="uq_branch_storage_node_code"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
node_code: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
node_name: Mapped[str] = mapped_column(String(200), nullable=False)
connector_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
secret_key_hash: Mapped[str] = mapped_column(String(64), nullable=False)
storage_root_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
storage_mode: Mapped[str] = mapped_column(String(40), nullable=False, default="pull_jobs", index=True)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True)
last_seen_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_seen_ip: Mapped[str | None] = mapped_column(String(80), nullable=True)
quota_limit_bytes: Mapped[int | None] = mapped_column(Integer, nullable=True)
used_storage_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
subscription_required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
class DocumentStorageJob(CommonBase):
"""Cloud-to-local branch storage transfer job.
The browser upload is first staged by ERP. The branch local app authenticates
with its node secret, pulls pending jobs, streams the staged file, stores it
in the branch year-wise folder, and acknowledges with the final hash.
"""
__tablename__ = "document_storage_jobs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
storage_node_id: Mapped[int] = mapped_column(ForeignKey("branch_storage_nodes.id", ondelete="CASCADE"), nullable=False, index=True)
document_id: Mapped[int] = mapped_column(ForeignKey("engagement_documents.id", ondelete="CASCADE"), nullable=False, index=True)
version_id: Mapped[int] = mapped_column(ForeignKey("engagement_document_versions.id", ondelete="CASCADE"), nullable=False, index=True)
job_type: Mapped[str] = mapped_column(String(40), nullable=False, default="store_version", index=True)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=5, index=True)
staging_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
target_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
expected_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
picked_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
acknowledged_hash_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
local_final_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
node = relationship("BranchStorageNode")
document = relationship("EngagementDocument")
version = relationship("EngagementDocumentVersion")
class DocumentDownloadRequest(CommonBase):
"""Secure local-to-cloud document retrieval request.
DS5 keeps user downloads permission-controlled in ERP while allowing the
branch local storage app to upload/stream the requested file back to ERP
through the existing zero-trust agent channel.
"""
__tablename__ = "document_download_requests"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
storage_node_id: Mapped[int] = mapped_column(ForeignKey("branch_storage_nodes.id", ondelete="CASCADE"), nullable=False, index=True)
document_id: Mapped[int] = mapped_column(ForeignKey("engagement_documents.id", ondelete="CASCADE"), nullable=False, index=True)
version_id: Mapped[int] = mapped_column(ForeignKey("engagement_document_versions.id", ondelete="CASCADE"), nullable=False, index=True)
request_status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
local_relative_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
expected_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
cached_relative_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
cached_hash_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
requested_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
requested_ip: Mapped[str | None] = mapped_column(String(80), nullable=True)
requested_user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
fulfilled_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
failed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
node = relationship("BranchStorageNode")
document = relationship("EngagementDocument")
version = relationship("EngagementDocumentVersion")
class PermanentClientDocument(CommonBase):
"""Permanent client document container not linked to a financial year or engagement.
Examples: incorporation certificate, GST registration, PAN/TAN, bank/KYC,
agreements, DSC and other standing records used across years.
"""
__tablename__ = "permanent_client_documents"
__table_args__ = (
UniqueConstraint("tenant_id", "client_id", "document_code", name="uq_permanent_client_documents_code"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
document_code: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
category: Mapped[str] = mapped_column(String(120), nullable=False, default="Other Permanent Documents", index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
current_version_no: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="active", index=True)
is_deleted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, index=True)
deleted_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
deleted_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False)
client = relationship("Client")
versions = relationship(
"PermanentClientDocumentVersion",
back_populates="document",
cascade="all, delete-orphan",
passive_deletes=True,
order_by="PermanentClientDocumentVersion.version_no.desc()",
)
class PermanentClientDocumentVersion(CommonBase):
"""Physical revision of a permanent client document."""
__tablename__ = "permanent_client_document_versions"
__table_args__ = (
UniqueConstraint("document_id", "version_no", name="uq_permanent_client_document_versions_no"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
document_id: Mapped[int] = mapped_column(ForeignKey("permanent_client_documents.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True)
version_no: Mapped[int] = mapped_column(Integer, nullable=False)
original_filename: Mapped[str] = mapped_column(String(255), nullable=False)
stored_filename: Mapped[str] = mapped_column(String(255), nullable=False)
content_type: Mapped[str | None] = mapped_column(String(150), nullable=True)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
file_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
storage_backend: Mapped[str] = mapped_column(String(40), nullable=False, default="LOCAL_PERMANENT", index=True)
local_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
storage_status: Mapped[str] = mapped_column(String(30), nullable=False, default="stored", index=True)
remarks: Mapped[str | None] = mapped_column(Text, nullable=True)
uploaded_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
uploaded_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
document = relationship("PermanentClientDocument", back_populates="versions")
class PermanentDocumentStorageJob(CommonBase):
"""Cloud-to-local transfer job for permanent client document versions."""
__tablename__ = "permanent_document_storage_jobs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
storage_node_id: Mapped[int] = mapped_column(ForeignKey("branch_storage_nodes.id", ondelete="CASCADE"), nullable=False, index=True)
document_id: Mapped[int] = mapped_column(ForeignKey("permanent_client_documents.id", ondelete="CASCADE"), nullable=False, index=True)
version_id: Mapped[int] = mapped_column(ForeignKey("permanent_client_document_versions.id", ondelete="CASCADE"), nullable=False, index=True)
job_type: Mapped[str] = mapped_column(String(40), nullable=False, default="store_permanent_version", index=True)
status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=5, index=True)
staging_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
target_relative_path: Mapped[str] = mapped_column(String(1000), nullable=False)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
expected_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
picked_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
acknowledged_hash_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
local_final_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
node = relationship("BranchStorageNode")
document = relationship("PermanentClientDocument")
version = relationship("PermanentClientDocumentVersion")
class PermanentDocumentDownloadRequest(CommonBase):
"""Local-to-cloud retrieval request for permanent client documents."""
__tablename__ = "permanent_document_download_requests"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True)
storage_node_id: Mapped[int] = mapped_column(ForeignKey("branch_storage_nodes.id", ondelete="CASCADE"), nullable=False, index=True)
document_id: Mapped[int] = mapped_column(ForeignKey("permanent_client_documents.id", ondelete="CASCADE"), nullable=False, index=True)
version_id: Mapped[int] = mapped_column(ForeignKey("permanent_client_document_versions.id", ondelete="CASCADE"), nullable=False, index=True)
request_status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending", index=True)
local_relative_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
expected_hash_sha256: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
cached_relative_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
cached_hash_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
file_size_bytes: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
requested_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
requested_ip: Mapped[str | None] = mapped_column(String(80), nullable=True)
requested_user_agent: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
fulfilled_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
failed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
node = relationship("BranchStorageNode")
document = relationship("PermanentClientDocument")
version = relationship("PermanentClientDocumentVersion")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">Document Download Requests</h1>
<p class="text-sm text-slate-500">Secure local-to-cloud retrieval queue for files stored on branch storage nodes.</p>
</div>
<div class="flex gap-2">
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Jobs</a>
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-900 text-white text-sm font-semibold">Documents</a>
</div>
</div>
<form method="get" class="rounded-2xl border bg-white p-4 flex gap-3 items-end">
<div><label class="text-sm font-medium">Status</label><input name="status" value="{{ status }}" class="mt-1 rounded-xl border p-2" placeholder="pending / ready / failed"></div>
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 font-semibold">Filter</button>
<a href="/documents/download-requests" class="rounded-xl bg-slate-100 text-slate-700 px-4 py-2 font-semibold">Clear</a>
</form>
<div class="rounded-2xl border bg-white overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Request</th><th class="p-3 text-left">Document</th><th class="p-3 text-left">Version</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Local Path</th><th class="p-3 text-left">Created</th><th class="p-3 text-right">Action</th></tr></thead>
<tbody>
{% for item in requests %}
<tr class="border-t">
<td class="p-3 font-semibold">#{{ item.id }}</td>
<td class="p-3">{{ item.document.title if item.document else item.document_id }}</td>
<td class="p-3">{{ item.version.version_no if item.version else item.version_id }}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs bg-slate-100 text-slate-700">{{ item.request_status.replace('_',' ').title() }}</span>{% if item.last_error %}<div class="mt-1 text-xs text-red-600">{{ item.last_error }}</div>{% endif %}</td>
<td class="p-3 font-mono text-xs break-all">{{ item.local_relative_path or '-' }}</td>
<td class="p-3">{{ item.created_at_utc }}</td>
<td class="p-3 text-right">{% if item.request_status == 'ready' %}<a href="/documents/download-requests/{{ item.id }}/download" class="font-semibold text-brand-700 hover:underline">Download</a>{% else %}<span class="text-xs text-slate-400">Not ready</span>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="7" class="p-6 text-center text-slate-500">No download requests found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,137 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Engagement Documents</h2>
<p class="text-sm text-slate-500">{{ engagement.client.client_name if engagement.client else '-' }} · {{ engagement.catalogue.service_name if engagement.catalogue else '-' }} · FY {{ engagement.financial_year }}</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/services/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement</a>
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
</div>
</div>
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Document uploaded successfully.</div>{% endif %}
{% if request.query_params.get('deleted') %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Document archived successfully.</div>{% endif %}
{% if request.query_params.get('download_queued') %}<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">The file is stored in branch local storage. A secure retrieval request #{{ request.query_params.get('download_queued') }} has been queued. Please refresh after the local storage app fulfils it.</div>{% endif %}
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check file size, permission and storage path.</div>{% endif %}
{% if can_upload %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Upload Document / New Revision</h3>
<form method="post" action="/documents/engagements/{{ engagement.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
<input type="text" name="title" placeholder="Document title" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Document Type</label>
<select name="document_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
{% for item in document_types %}<option value="{{ item }}">{{ item.replace('_',' ').title() }}</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">Create new document</option>
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
<textarea name="remarks" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="md:col-span-2">
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button>
</div>
</form>
</section>
{% endif %}
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Document</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Type</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Version</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Storage</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Revision History</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for doc in documents %}
{% set latest = doc.versions[0] if doc.versions else None %}
<tr>
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_code }}{% if doc.description %} · {{ doc.description }}{% endif %}</div></td>
<td class="px-4 py-3 text-sm text-slate-700">{{ doc.document_type.replace('_',' ').title() }}</td>
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div>v{{ latest.version_no }} · {{ latest.original_filename }}</div><div>{{ (latest.file_size_bytes / 1024)|round(1) }} KB · {{ latest.uploaded_at_utc }}</div>{% else %}-{% endif %}</td>
<td class="px-4 py-3 text-xs text-slate-600">
{% if latest %}
<div><span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ latest.storage_status.replace('_',' ').title() }}</span></div>
<div class="mt-1 font-mono text-[11px] break-all">{{ latest.local_relative_path }}</div>
{% else %}-{% endif %}
</td>
<td class="px-4 py-3 text-xs text-slate-600">
{% for v in doc.versions %}
<div><a href="/documents/versions/{{ v.id }}/download" class="text-brand-700 hover:underline">v{{ v.version_no }}</a> · {{ v.original_filename }}{% if v.remarks %} · {{ v.remarks }}{% endif %}</div>
{% endfor %}
</td>
<td class="px-4 py-3 text-right text-sm">
{% if latest %}<a href="/documents/{{ doc.id }}/download" class="font-medium text-brand-700 hover:underline">Download Latest</a>{% endif %}
{% if can_upload %}
<form method="post" action="/documents/{{ doc.id }}/delete" class="ml-3 inline" onsubmit="return confirm('Archive this document?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="font-medium text-red-600 hover:underline">Archive</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No documents uploaded for this engagement.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if download_requests %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-slate-900">Recent Local Storage Retrieval Requests</h3>
<p class="text-xs text-slate-500">Used when the ERP staged file is unavailable and the file must be pulled back from branch local storage.</p>
</div>
<a href="/documents/download-requests" class="text-sm font-medium text-brand-700 hover:underline">View All</a>
</div>
<div class="mt-4 overflow-hidden rounded-xl border border-slate-200">
<table class="min-w-full divide-y divide-slate-200 text-sm">
<thead class="bg-slate-50 text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-3 py-2 text-left">Request</th><th class="px-3 py-2 text-left">Version</th><th class="px-3 py-2 text-left">Status</th><th class="px-3 py-2 text-left">Created</th><th class="px-3 py-2 text-right">Action</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for req in download_requests %}
<tr>
<td class="px-3 py-2 font-medium">#{{ req.id }}</td>
<td class="px-3 py-2">v{{ req.version.version_no if req.version else req.version_id }}</td>
<td class="px-3 py-2"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-700">{{ req.request_status.replace('_',' ').title() }}</span></td>
<td class="px-3 py-2 text-xs text-slate-500">{{ req.created_at_utc }}</td>
<td class="px-3 py-2 text-right">{% if req.request_status == 'ready' %}<a href="/documents/download-requests/{{ req.id }}/download" class="font-medium text-brand-700 hover:underline">Download</a>{% else %}<span class="text-xs text-slate-400">Waiting for local app</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}
</div>
{% endblock %}
@@ -0,0 +1,55 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Engagement Documents</h2>
<p class="text-sm text-slate-500">Upload, stream, download and version documents engagement-wise with year-wise storage.</p>
</div>
</div>
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Financial Year</label>
<input type="text" name="financial_year" value="{{ financial_year or '' }}" placeholder="2025-26" class="w-36 rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search</label>
<input type="text" name="q" value="{{ q or '' }}" placeholder="Client, service or code" class="w-80 rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Filter</button>
</form>
<div class="flex gap-2 mt-3">
<a href="/documents/permanent" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Permanent Client Documents</a>
<a href="/documents/storage-nodes" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Branch Storage Nodes</a>
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Storage Jobs</a>
</div>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Engagement</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">FY / AY</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Assigned</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for row in engagements %}
<tr>
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ row.client.client_name if row.client else '-' }}</div><div class="text-xs text-slate-500">{{ row.client.client_code if row.client else '' }}</div></td>
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ row.catalogue.service_name if row.catalogue else '-' }}</div><div class="text-xs text-slate-500">{{ row.catalogue.service_code if row.catalogue else '' }}</div></td>
<td class="px-4 py-3 text-xs text-slate-600"><div>FY: {{ row.financial_year or '-' }}</div><div>AY: {{ row.assessment_year or '-' }}</div></td>
<td class="px-4 py-3 text-xs text-slate-600"><div>Partner: {{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</div><div>Staff: {{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</div></td>
<td class="px-4 py-3 text-right text-sm"><a href="/documents/engagements/{{ row.id }}" class="font-medium text-brand-700 hover:underline">Open Documents</a></td>
</tr>
{% else %}
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-slate-500">No engagements available for document access.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,97 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Permanent Client Documents</h2>
<p class="text-sm text-slate-500">{{ client.client_name }} · {{ client.client_code or '-' }}</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/documents/permanent" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
</div>
</div>
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Permanent document uploaded successfully.</div>{% endif %}
{% if request.query_params.get('deleted') %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Permanent document archived successfully.</div>{% endif %}
{% if request.query_params.get('download_queued') %}<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">The file is stored in branch local storage. Retrieval request #{{ request.query_params.get('download_queued') }} has been queued.</div>{% endif %}
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check file size, permission and storage path.</div>{% endif %}
{% if can_upload %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Upload Permanent Document / New Revision</h3>
<form method="post" action="/documents/permanent/clients/{{ client.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
<input type="text" name="title" placeholder="Document title" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Category</label>
<select name="category" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
{% for item in permanent_document_categories %}<option value="{{ item }}">{{ item }}</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">Create new document</option>
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
<textarea name="remarks" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="md:col-span-2"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button></div>
</form>
</section>
{% endif %}
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Document</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Category</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Version</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Storage Path</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Versions</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for doc in documents %}
{% set latest = doc.versions[0] if doc.versions else None %}
<tr>
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_code }}{% if doc.description %} · {{ doc.description }}{% endif %}</div></td>
<td class="px-4 py-3 text-sm text-slate-700">{{ doc.category }}</td>
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div>v{{ latest.version_no }} · {{ latest.original_filename }}</div><div>{{ (latest.file_size_bytes / 1024)|round(1) }} KB · {{ latest.uploaded_at_utc }}</div>{% else %}-{% endif %}</td>
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div><span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ latest.storage_status.replace('_',' ').title() }}</span></div><div class="mt-1 font-mono text-[11px] break-all">{{ latest.local_relative_path }}</div>{% else %}-{% endif %}</td>
<td class="px-4 py-3 text-xs text-slate-600">{% for v in doc.versions %}<div><a href="/documents/permanent/versions/{{ v.id }}/download" class="text-brand-700 hover:underline">v{{ v.version_no }}</a> · {{ v.original_filename }}{% if v.remarks %} · {{ v.remarks }}{% endif %}</div>{% endfor %}</td>
<td class="px-4 py-3 text-right text-sm">
{% if latest %}<a href="/documents/permanent/{{ doc.id }}/download" class="font-medium text-brand-700 hover:underline">Download Latest</a>{% endif %}
{% if can_upload %}
<form method="post" action="/documents/permanent/{{ doc.id }}/delete" class="ml-3 inline" onsubmit="return confirm('Archive this permanent document?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="font-medium text-red-600 hover:underline">Archive</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No permanent documents uploaded for this client.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,45 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Permanent Client Documents</h2>
<p class="text-sm text-slate-500">Store standing records like registration documents, GST, PAN/TAN, KYC, bank and agreements outside financial-year folders.</p>
</div>
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
</div>
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search Client</label>
<input type="text" name="q" value="{{ q or '' }}" placeholder="Client name or code" class="w-80 rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Filter</button>
</form>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</th>
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
{% for client in clients %}
<tr>
<td class="px-4 py-3 text-sm font-medium text-slate-900">{{ client.client_name }}</td>
<td class="px-4 py-3 text-sm text-slate-600">{{ client.client_code or '-' }}</td>
<td class="px-4 py-3 text-sm text-slate-600">{{ client.branch_id or '-' }}</td>
<td class="px-4 py-3 text-right text-sm"><a href="/documents/permanent/clients/{{ client.id }}" class="font-medium text-brand-700 hover:underline">Open Permanent Vault</a></td>
</tr>
{% else %}
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No clients available for permanent document access.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,35 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">Document Storage Jobs</h1>
<p class="text-sm text-slate-500">Cloud-to-local streaming queue for branch storage nodes.</p>
</div>
<div class="flex gap-2"><a href="/documents/download-requests" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Download Requests</a><a href="/documents/storage-nodes" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Nodes</a></div>
</div>
<form method="get" class="rounded-2xl border bg-white p-4 flex gap-3 items-end">
<div><label class="text-sm font-medium">Status</label><input name="status" value="{{ status }}" class="mt-1 rounded-xl border p-2" placeholder="pending / completed / failed"></div>
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 font-semibold">Filter</button>
<a href="/documents/storage-jobs" class="rounded-xl bg-slate-100 text-slate-700 px-4 py-2 font-semibold">Clear</a>
</form>
<div class="rounded-2xl border bg-white overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Job</th><th class="p-3 text-left">Version</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Target Path</th><th class="p-3 text-left">Created</th></tr></thead>
<tbody>
{% for job in jobs %}
<tr class="border-t">
<td class="p-3 font-semibold">#{{ job.id }}</td>
<td class="p-3">{{ job.version_id }}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs bg-slate-100 text-slate-700">{{ job.status }}</span></td>
<td class="p-3 font-mono text-xs break-all">{{ job.target_relative_path }}</td>
<td class="p-3">{{ job.created_at_utc }}</td>
</tr>
{% else %}
<tr><td colspan="5" class="p-6 text-center text-slate-500">No storage jobs found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,199 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">Branch Local Storage</h1>
<p class="text-sm text-slate-500">Partner/branch controlled storage setup for engagement document streaming.</p>
<p class="text-xs text-slate-400 mt-1">Scope: {{ storage_scope_title or '-' }}</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Back to Documents</a>
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Jobs</a>
<a href="/documents/download-requests" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Download Requests</a>
</div>
</div>
{% if request.query_params.get('error') == 'branch_not_linked' %}
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
Your user is not linked to a branch. Please update the Partner/Branch Manager user profile with branch_id before enabling local storage.
</div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Storage Nodes</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ nodes|length }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Online / Active</div>
<div class="mt-1 text-2xl font-bold text-emerald-700">{{ nodes|selectattr('is_active')|list|length }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Recent Storage Jobs</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_jobs|length if recent_jobs is defined else 0 }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Recent Download Requests</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_download_requests|length if recent_download_requests is defined else 0 }}</div>
</div>
</div>
{% if request.query_params.get('created') == '1' %}
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
Storage node created. Use the <strong>Download Agent Package</strong> button in the node row to generate the branch package. Secret and standalone .env are not displayed.
</div>
{% endif %}
{% if request.query_params.get('error') == 'env_download_disabled' %}
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
Standalone .env download is disabled for security. Download the pre-configured agent ZIP instead.
</div>
{% endif %}
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div class="space-y-6">
{% if can_manage_branch_storage %}
<form method="post" action="/documents/storage-nodes/auto-setup" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<h2 class="font-bold text-slate-900">One-click branch setup</h2>
<p class="text-xs text-slate-500 mt-1">Recommended. ERP will create or reuse the branch node and immediately download the pre-configured LSA2 Windows Service ZIP. Secret is not shown on screen. If a branch already has a storage root, the existing root will be reused and will not be silently changed.</p>
</div>
<div>
<label class="text-sm font-medium">Branch</label>
{% if forced_branch_id %}
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
</div>
{% else %}
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
<option value="">Firm-level / Head office storage</option>
{% for branch in branches or [] %}
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
{% endfor %}
</select>
{% endif %}
</div>
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" value="D:\AuditFirmStorage"><p class="text-xs text-amber-700 mt-1">For a new branch setup only. Existing branch nodes keep their already configured root.</p></div>
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
<button class="w-full rounded-xl bg-emerald-700 text-white py-2 font-semibold">Download / Regenerate Branch Agent Package</button>
</form>
<form method="post" action="/documents/storage-nodes" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<h2 class="font-bold text-slate-900">Manual node creation</h2>
<p class="text-xs text-slate-500 mt-1">Use only when you want a custom node code/name.</p>
</div>
<div><label class="text-sm font-medium">Node Code</label><input name="node_code" required class="mt-1 w-full rounded-xl border p-2" placeholder="CHN-STORAGE-01"></div>
<div><label class="text-sm font-medium">Node Name</label><input name="node_name" required class="mt-1 w-full rounded-xl border p-2" placeholder="Chennai Branch Storage"></div>
<div>
<label class="text-sm font-medium">Branch</label>
{% if forced_branch_id %}
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
</div>
{% else %}
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
<option value="">Firm-level / Head office storage</option>
{% for branch in branches or [] %}
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
{% endfor %}
</select>
{% endif %}
</div>
<div><label class="text-sm font-medium">Connector URL</label><input name="connector_url" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional zero-trust tunnel URL"></div>
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" placeholder="D:\AuditFirmStorage"></div>
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
<button class="w-full rounded-xl bg-slate-900 text-white py-2 font-semibold">Create Node</button>
</form>
{% else %}
<div class="rounded-2xl border bg-slate-50 p-5 text-sm text-slate-600">
Storage setup package generation is available to Firm Admin, Partner and Branch Manager. This screen is monitoring-only for your current role.
</div>
{% endif %}
</div>
<div class="xl:col-span-2 space-y-6">
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b flex items-center justify-between">
<div>
<h2 class="font-bold text-slate-900">Configured storage nodes</h2>
<p class="text-xs text-slate-500">Partners/Branch Managers see only their managed branch. One branch should have one active node and one fixed storage root. Existing node secrets are not shown again.</p>
</div>
</div>
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Node</th><th class="p-3 text-left">Branch</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Last Seen</th><th class="p-3 text-left">Root</th><th class="p-3 text-right">Action</th></tr></thead>
<tbody>
{% for node in nodes %}
{% set b = branch_map.get(node.branch_id) if branch_map and node.branch_id else None %}
<tr class="border-t align-top">
<td class="p-3"><div class="font-semibold">{{ node.node_name }}</div><div class="text-xs text-slate-500">{{ node.node_code }}</div></td>
<td class="p-3">{{ b.name if b else (node.branch_id or 'Firm-level') }}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs {{ 'bg-emerald-100 text-emerald-700' if node.is_active else 'bg-slate-100 text-slate-600' }}">{{ node.status }}</span></td>
<td class="p-3 text-xs">{{ node.last_seen_at_utc or '-' }}</td>
<td class="p-3 text-xs font-mono break-all">{{ node.storage_root_path or '-' }}</td>
<td class="p-3 text-right">
{% if can_manage_branch_storage %}
<div class="flex flex-col items-end gap-2">
{% if node.status == 'disabled_duplicate' %}
<span class="text-xs text-slate-500">Duplicate disabled</span>
{% else %}
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
<button class="text-sm text-emerald-700 font-semibold">Download Agent Package</button>
</form>
<form method="post" action="/documents/storage-nodes/{{ node.id }}/toggle">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="text-sm text-blue-700 font-semibold">{{ 'Disable' if node.is_active else 'Enable' }}</button>
</form>
{% endif %}
</div>
{% else %}-{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="p-6 text-center text-slate-500">No storage nodes registered yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Storage Jobs</h2></div>
<table class="w-full text-xs">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Job</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
<tbody>
{% for job in recent_jobs or [] %}
<tr class="border-t"><td class="p-2">#{{ job.id }}</td><td class="p-2">{{ job.status }}</td><td class="p-2">{{ job.created_at_utc or '-' }}</td></tr>
{% else %}
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent storage jobs.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Download Requests</h2></div>
<table class="w-full text-xs">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Request</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
<tbody>
{% for dr in recent_download_requests or [] %}
<tr class="border-t"><td class="p-2">#{{ dr.id }}</td><td class="p-2">{{ dr.request_status }}</td><td class="p-2">{{ dr.created_at_utc or '-' }}</td></tr>
{% else %}
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent download requests.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,108 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-xl font-semibold text-slate-900">Task Documents</h2>
<p class="text-sm text-slate-500">
{{ engagement.client.client_name if engagement.client else '-' }} ·
{{ engagement.catalogue.service_name if engagement.catalogue else '-' }} ·
{{ task.task_name }} · FY {{ engagement.financial_year }}
</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/services/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement</a>
<a href="/documents/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
</div>
</div>
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Task document uploaded successfully.</div>{% endif %}
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Upload failed. Please verify file, requirement and permission.</div>{% endif %}
<section class="grid gap-4 md:grid-cols-3">
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Mandatory Pending</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ requirement_status|selectattr('is_pending_mandatory')|list|length }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Requirements</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ requirements|length }}</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Uploaded Documents</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ documents|length }}</div></div>
</section>
{% if can_upload %}
<section class="rounded-2xl bg-white p-5 shadow-soft">
<h3 class="text-sm font-semibold text-slate-900">Upload Task Document</h3>
<form method="post" action="/documents/tasks/{{ task.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Requirement</label>
<select name="requirement_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">General task document</option>
{% for item in requirement_status %}
{% set req = item.requirement %}
<option value="{{ req.id }}">{{ req.document_name }}{% if req.is_mandatory %} (Mandatory){% endif %}</option>
{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
<option value="">Create new document</option>
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
<input type="text" name="title" placeholder="Leave blank to use requirement name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Document Type</label>
<select name="document_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
{% for item in document_types %}<option value="{{ item }}">{{ item.replace('_',' ').title() }}</option>{% endfor %}
</select>
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div>
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
<input type="text" name="remarks" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
</div>
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
</div>
<div class="md:col-span-2"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button></div>
</form>
</section>
{% endif %}
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-100 px-5 py-4"><h3 class="text-sm font-semibold text-slate-900">Requirement Status</h3></div>
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Requirement</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Type</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Documents</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for item in requirement_status %}
{% set req = item.requirement %}
<tr>
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ req.document_name }}</div>{% if req.instructions %}<div class="text-xs text-slate-500">{{ req.instructions }}</div>{% endif %}</td>
<td class="px-4 py-3 text-sm text-slate-700">{{ req.document_type.replace('_',' ').title() }}{% if req.allowed_file_types %}<div class="text-xs text-slate-500">{{ req.allowed_file_types }}</div>{% endif %}</td>
<td class="px-4 py-3 text-sm">{% if item.is_uploaded %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs text-emerald-800">Uploaded</span>{% elif req.is_mandatory %}<span class="rounded-full bg-red-100 px-2 py-1 text-xs text-red-800">Pending Mandatory</span>{% else %}<span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">Optional Pending</span>{% endif %}</td>
<td class="px-4 py-3 text-xs text-slate-600">{% for doc in item.documents %}<div><a href="/documents/{{ doc.id }}/download" class="text-brand-700 hover:underline">{{ doc.title }}</a> · v{{ doc.current_version_no }}</div>{% else %}-{% endfor %}</td>
</tr>
{% else %}<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No document requirements configured for this task template.</td></tr>{% endfor %}
</tbody>
</table>
</section>
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-100 px-5 py-4"><h3 class="text-sm font-semibold text-slate-900">All Task Documents</h3></div>
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Document</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Latest Version</th><th class="px-4 py-3 text-right text-xs font-semibold uppercase text-slate-500">Action</th></tr></thead>
<tbody class="divide-y divide-slate-100">
{% for doc in documents %}
{% set latest = doc.versions[0] if doc.versions else None %}
<tr><td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_type.replace('_',' ').title() }}{% if doc.document_requirement %} · {{ doc.document_requirement.document_name }}{% endif %}</div></td><td class="px-4 py-3 text-xs text-slate-600">{% if latest %}v{{ latest.version_no }} · {{ latest.original_filename }}{% else %}-{% endif %}</td><td class="px-4 py-3 text-right text-sm">{% if latest %}<a href="/documents/{{ doc.id }}/download" class="text-brand-700 hover:underline">Download</a>{% endif %}</td></tr>
{% else %}<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-slate-500">No task documents uploaded yet.</td></tr>{% endfor %}
</tbody>
</table>
</section>
</div>
{% endblock %}
File diff suppressed because it is too large Load Diff