From 1837b36b143c58372e27496f9c464cc6e9bcdbbb Mon Sep 17 00:00:00 2001 From: Kirill Date: Thu, 21 May 2026 12:02:29 +0500 Subject: [PATCH] test commit --- .../product-review/ui/ReviewDialog.tsx | 32 ++++++---- .../product-review/ui/ReviewSection.tsx | 14 ++++- .../pages/admin-users/ui/AdminUsersPage.tsx | 16 ++++- .../me/ui/sections/NotificationsPage.tsx | 55 +++++++++++------- .../pages/me/ui/sections/OrderDetailPage.tsx | 17 +++++- .../widgets/reviews-block/ui/ReviewsBlock.tsx | 2 +- server/prisma/prisma/dev.db | Bin 311296 -> 323584 bytes server/src/routes/api/admin-users.js | 14 +++++ server/src/routes/user-cart.js | 4 +- server/src/routes/user-orders.js | 4 +- 10 files changed, 113 insertions(+), 45 deletions(-) diff --git a/client/src/features/product-review/ui/ReviewDialog.tsx b/client/src/features/product-review/ui/ReviewDialog.tsx index 2f9a01c..959ad2c 100644 --- a/client/src/features/product-review/ui/ReviewDialog.tsx +++ b/client/src/features/product-review/ui/ReviewDialog.tsx @@ -20,8 +20,8 @@ type Props = { error: unknown uploadError: unknown onClose: () => void - onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => void - onUploadImage: (file: File) => void + onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => Promise + onUploadImage: (file: File) => Promise<{ url: string }> } function reviewSubmitErrorMessage(err: unknown): string { @@ -55,11 +55,13 @@ export function ReviewDialog({ const [rating, setRating] = useState(5) const [text, setText] = useState('') const [imageUrl, setImageUrl] = useState(null) + const [localUploadError, setLocalUploadError] = useState(null) const reset = () => { setRating(5) setText('') setImageUrl(null) + setLocalUploadError(null) } const handleClose = () => { @@ -68,9 +70,9 @@ export function ReviewDialog({ onClose() } - const handleSubmit = () => { + const handleSubmit = async () => { if (isPending) return - onSubmit({ rating, text: text.trim(), imageUrl }) + await onSubmit({ rating, text: text.trim(), imageUrl }) } return ( @@ -96,11 +98,19 @@ export function ReviewDialog({ hidden type="file" accept="image/png,image/jpeg,image/webp" - onChange={(e) => { + onChange={async (e) => { const file = e.target.files?.[0] if (!file) return - onUploadImage(file) e.currentTarget.value = '' + setLocalUploadError(null) + try { + const result = await onUploadImage(file) + setImageUrl(result.url) + } catch (err) { + setLocalUploadError( + err instanceof Error ? err.message : 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.', + ) + } }} /> @@ -126,11 +136,13 @@ export function ReviewDialog({ }} /> )} - {uploadError ? ( + {uploadError || localUploadError ? ( - {uploadError instanceof Error - ? uploadError.message - : 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'} + {localUploadError + ? localUploadError + : uploadError instanceof Error + ? uploadError.message + : 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'} ) : null} {error ? ( diff --git a/client/src/features/product-review/ui/ReviewSection.tsx b/client/src/features/product-review/ui/ReviewSection.tsx index a9191e8..4cc1db4 100644 --- a/client/src/features/product-review/ui/ReviewSection.tsx +++ b/client/src/features/product-review/ui/ReviewSection.tsx @@ -17,7 +17,12 @@ type Props = { isUploadPending: boolean submitError: unknown uploadError: unknown - onSubmitReview: (params: { productId: string; rating: number; text: string; imageUrl: string | null }) => void + onSubmitReview: (params: { + productId: string + rating: number + text: string + imageUrl: string | null + }) => Promise onUploadImage: (file: File) => Promise<{ url: string }> } @@ -75,17 +80,20 @@ export function ReviewSection({ setTarget(null) setUploadedImageUrl(null) }} - onSubmit={(params) => { + onSubmit={async (params) => { if (!target) return - onSubmitReview({ + await onSubmitReview({ productId: target.productId, ...params, imageUrl: uploadedImageUrl, }) + setTarget(null) + setUploadedImageUrl(null) }} onUploadImage={async (file) => { const result = await onUploadImage(file) setUploadedImageUrl(result.url) + return result }} /> diff --git a/client/src/pages/admin-users/ui/AdminUsersPage.tsx b/client/src/pages/admin-users/ui/AdminUsersPage.tsx index 90ca7b9..6a18d72 100644 --- a/client/src/pages/admin-users/ui/AdminUsersPage.tsx +++ b/client/src/pages/admin-users/ui/AdminUsersPage.tsx @@ -9,6 +9,7 @@ import TableRow from '@mui/material/TableRow' import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useUnit } from 'effector-react' import { Controller, useForm } from 'react-hook-form' import { Link as RouterLink } from 'react-router-dom' import { createAdminUser, deleteAdminUser, fetchAdminUsers, updateAdminUser } from '@/entities/user/api/user-api' @@ -16,6 +17,7 @@ import type { AdminUser } from '@/entities/user/model/types' import { getErrorMessage } from '@/shared/lib/get-error-message' import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys' import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state' +import { $user } from '@/shared/model/auth' import { AdminDialog } from '@/shared/ui/AdminDialog/AdminDialog' import { AdminTable } from '@/shared/ui/AdminTable' import { EntityRowActions } from '@/shared/ui/EntityRowActions' @@ -44,6 +46,8 @@ export function AdminUsersPage() { const [q, setQ] = useState('') const [page, setPage] = useState(0) const [rowsPerPage, setRowsPerPage] = useState(20) + const currentUser = useUnit($user) + const currentUserId = currentUser?.id const userForm = useForm({ defaultValues: emptyUserForm(), @@ -192,7 +196,7 @@ export function AdminUsersPage() { openEdit(u)} - onDelete={() => deleteMut.mutate(u.id)} + onDelete={u.id === currentUserId ? undefined : () => deleteMut.mutate(u.id)} deleteDisabled={deleteMut.isPending} confirmDeleteMessage={`Удалить пользователя ${u.email}?`} /> @@ -237,7 +241,15 @@ export function AdminUsersPage() { } + render={({ field }) => ( + + )} /> ({ + orderCreated: on, + orderStatusChanged: on, + paymentStatusChanged: on, + deliveryFeeAdjusted: on, +}) export function NotificationsPage() { const queryClient = useQueryClient() @@ -45,9 +49,11 @@ export function NotificationsPage() { const handleToggle = (field: string, value: boolean) => { setError(null) - mutation.mutate({ [field]: value } as Record) + mutation.mutate({ [field]: value }) } + const statusChangesOn = isOrderStatusChangesOn(settings) + return ( @@ -80,19 +86,26 @@ export function NotificationsPage() { - {eventFields.map(({ key, label }) => ( - handleToggle(key, e.target.checked)} - /> - } - label={label} - /> - ))} + mutation.mutate(orderStatusChangesPayload(e.target.checked))} + /> + } + label="Изменения статуса заказа" + /> + handleToggle('orderMessageReceived', e.target.checked)} + /> + } + label="Сообщения в чате заказа" + /> diff --git a/client/src/pages/me/ui/sections/OrderDetailPage.tsx b/client/src/pages/me/ui/sections/OrderDetailPage.tsx index 353c515..ae3d180 100644 --- a/client/src/pages/me/ui/sections/OrderDetailPage.tsx +++ b/client/src/pages/me/ui/sections/OrderDetailPage.tsx @@ -7,7 +7,7 @@ import Link from '@mui/material/Link' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { Link as RouterLink, useParams, useSearchParams } from 'react-router-dom' +import { Link as RouterLink, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { confirmOrderReceived, createOrderPayment, @@ -30,6 +30,7 @@ import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' export function OrderDetailPage() { const { id } = useParams() const qc = useQueryClient() + const navigate = useNavigate() const [searchParams] = useSearchParams() const paidParam = searchParams.get('paid') @@ -58,6 +59,14 @@ export function OrderDetailPage() { }, }) + useEffect(() => { + const data = paymentStatusQuery.data + if (data && (data.paid || data.status === 'canceled') && paidParam === '1') { + qc.invalidateQueries({ queryKey: ['me', 'orders', id] }) + navigate(`/me/orders/${id}`, { replace: true }) + } + }, [paymentStatusQuery.data, paidParam, qc, id, navigate]) + const confirmMut = useMutation({ mutationFn: () => confirmOrderReceived(id!), onSuccess: () => @@ -224,7 +233,7 @@ export function OrderDetailPage() { {PICKUP_ADDRESS_FULL} - Заберите заказ точно ко времени, которое согласуем по телефону или в чате заказа. + Заберите заказ ко времени, которое согласуем в чате заказа. )} @@ -274,7 +283,9 @@ export function OrderDetailPage() { isUploadPending={uploadReviewImageMut.isPending} submitError={reviewMut.error} uploadError={uploadReviewImageMut.error} - onSubmitReview={(params) => reviewMut.mutate(params)} + onSubmitReview={async (params) => { + await reviewMut.mutateAsync(params) + }} onUploadImage={async (file) => { const result = await uploadReviewImageMut.mutateAsync(file) return result diff --git a/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx b/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx index 5d204db..d5a463b 100644 --- a/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx +++ b/client/src/widgets/reviews-block/ui/ReviewsBlock.tsx @@ -96,7 +96,7 @@ export function ReviewsBlock() { |t8Yh_ktg%*mSBozvUD3=oHsouUwKq{3Lk`h&Am2%MngQM?z@4m>W zj4fly4Ll|&B%y)E_BZ&OHrN8Mv}e3vY;2je{b8G7Z1-e+^UZi^JZ2l9jomXg7`ILL z*Ym$G_Ke63InSDBmR7s^Gy=2e@2uAN&<%`T@B%d_{MO(l}eqshc=lL@?%SwERrPCdGuSU8V= zD`(FpE-cQUn_YY?@nGt)I~sYa*=mmO$<|D&KR_~%GpviTP!qkHBa;R zIbJGfJwC?^)k-okzp$LTKegBvta)lS&oX?aoa-sCRC9CL$|^@=?md70Y-)C)eOls7 z>b}{Pv&)HqbVL2^QmzHPGU1*+MEBX7L+{iL_eqm{OaTciBJ)7=` zln&PTb~16_`Nh=y{R^T)6SpNh$;s)&V(PvWs-8R!k?(FM!?I6m(~9 zc4=<*OiCPCxggrnIx;z3FHMZcj~$C%nhSMrrNYbN-(;`Wi9=VVay-wn%`R^9YEmh& zN@cTH_8(xC)gC4AUMSX+cO;SpmT#KC^jd5@?wp9$r?Lf~Z_U=Kt8+!4r}iT;pNjx_qU0OFGUWC64KX= z$E6#i_2IDIW|p)ViG*f0zmE-6&b^r%>e)OB!wZMAY3bUr`0S0(B4Jy&>rPFM#qU10 zUv_~xt%&!-lOK+}_E`m=5P}==vH1M0`xRN>o9R|dl&&wwyz%k)X*i4VkW9;=x6!l? z92&@`mD&Crz@Bw z2=~=&PMCasQ@o}_=}t=bpV`xe;rWoXoh=D}ChAB|UpjpKc>Lta=;M!diyfZOUBh^@ z^4{rEqui^RGu2u%l)WA%l%{WT)1IZs&xgn8G{1Ll*Y7>Lo8AR?(8QrrXz#qCS;SRN z8_eNw40uOl3}`7?>+eW!FSHI{)fmtjvHQAU|4FM*l`C}4^-k%9njRnAX^A0)@cSXomXoz+sxH&O}at7nQ>5oxnc&h=)RZ8+AOSG%;mVBcA2-< zUF)7ID^*s;%(^>=D72NU-Q>1qd9`vGD~og$Q+b@6FEy)UrBW`7f+|&Bs-(SQtq``f zwftCFSi~$hIkIlcx~aGt zGnGKLbW_s<#sl4EObIlX`xe&%P4#S>TU@psH}Gv=HFeoEIrjo9`t;mat=m;uHDyC_ zWXG^&EnVjM;sz$Ag`il%DDLL4(h4)ax}JPhDBoLze1WP5mhC%|BHON{8?q1iWK+^K zS5ZCP(Oo?VnCd&sRux0H9OeY39>^wY3QWTc6vxz5S5^Z@w;c^KYqqRtp2i(VWwvP> zJ`bzCp(&p&JDRN+Y3BQBpXah0yu6(bIOZ_^#Tt6w7rD*7nJP0>Q?YeRRW;4g6-8G$`dPPa&k8h4;fBdv=IFX3tDfgO7UXgQ zMPm-v9p8bVj&ECT5Cjbo|GFchV##vIu~)=xP|SxSezqPz7K(Vkpr~kEfnHe>#1N`z zF`4LtOnX){}wp^BPqApu=Wf6a!uBDiRuON>F1M38X@g z89oew>7JolnqjE6<~xpNdQMv~D(plHT*)y_Px2MhHQm7Fn!#jlXs}M% z_BmH{hXuaJp>+;A5n`&E?*W=8^ixI?6xY-&PoOjcW--}S!ajRjOGG(*b?mqz_Da%X z8$|fdFz-@bxh@p(t)hQzn4Al9l>*rYN?%qaD2^gIPujC5ER%FHI-_%tkjLQeXroGaO%ceKgV&o+_+%p(&!G7_zQt=~|&u;vS3y{u_1@ zZpDb+u3{glnq~!f^<38HNwfWDd^~ z_$s#;(*oUuqM_Ga-&Jjec`}#b?>xjEo+wgd85xtwiEcwPO5YeEUn2?-6;aUGZGlA?GpY}a%o*9bJp4ph@~p_)3} zqJesBZtA)WxVmGR@Rxxm>%OPMW2vqm*q-7kFl39tkK=%1I35#?)V-$FOf@BxVKo>V zLfy6z9pNxgVc|tgg8_Z#zE}6cmq04<&Vuoi(rei_znZ_*3wU}uc zn(10FDcMpO*FCr-i$g~PLkS$)lWmMNu5ZKPSg=?Ib=G}X^IYA9euUNjLrXYVzouGE zd>FzjVui@6Rpzbc#kh_ccX-0r>L-qda)zUztjJJ!g<%vlxTMR>lx)wHB}-$>_Ryh0 zpt}x-5yJs$@XvsTk;Y+gap7>m74E5l>0!Xp1Jm_E$ixVUfq@6G2-S8&12?|ZmJ^O% zX~|h)+o5oIUR^CVGJdMAOoj4=rh{>T`L-rY7*Ns4GK1;Znj^uj1*QVc_cU}IJfo~* zNVZ{38rL~6N|%^4Q4uA)ntwV&@(k3W4L8nT41_4ognHNsmDq;Vx)f0z9S;GNb_n>A_M9F}Mmvn`L zug_JoU-GgqMhGiS*A_f=B z726G*u-c#{tBgsMrtdZL>UN2X$wruWu71O0C}L>mvZDl^5jc_xKQ3Wh)FsEpyp0DK zxna<9;Cd$d+gD&sa5>0}(a=^b%hWLkVxH;viVc^8i5|1zQ&olpX6Q_HWbQy&gV4bn zZ)k|9I*M+ns*_%?u>$t?x6$A0HSAqB^z-KWO%tJT4~f2JuC7_Cr%9S+V}Qi`Msiil zlVsU2JoJm9I$Tj?7qV-<##Oi)#}BkXMJiXo-Va>EP&C8gt^%j7sHP8fc1$Ry<#7#D z&IZ56EDgq|z>R5IgCFMOnB_P1G3;LbkLx!c3dO_qu+a(KvM@Bes$?4BB$C+}bW~Zg zT}228Ju^)c%7alxvs`ddHEzmgfO#W)p%r+RVHmJ{4rTIu)$%d7mEn6-h07j18%7{k zbeaCSHaktWAzo!QTPkG>8R0B3;8b`+x?ihbKORaqBc#)yFPNH{QlLZc;G=EH#;_w{ zKo&-BV>GtKAjjbBOjU(ZnBWNuh0GY|ITCye({Gid+cArmO~>Xa!Ei7^)|sU$N;t;o zzuYAqyja6fN~^^}sBijD)o&dOrAiB_97EF-Sym;*^KF=f2AAkES;9oym30rJv+Kc) zINY+JOr~l>)i8U+6r73a76wJvm1i1Uqo%4D zpS1u^Mt9+{?QW z-mf9qTk6vzp=6=^V{ixP3`@e~6+@rm!Tq5sNyc>6G0@A747VT#T4+%K4}+>PJiACT z874C@1~@D)a5Y`W*d$|q35OnN3f5pMrcGj!6asm)OEO*2+mcnpr8RL04QtlDGc(<( zzj8Q~H6{8Q7T~cUP$ZXIn0doGC9F?m33EPA#w=Au|2gPotO!&W8pRdN9l5MxiDj!G z%VGWjre9d-D7ugF+ma3L`*1H>z~DZy%4yh#){zySzOgtE+s@&JK-iks)Tf3*!89Ql zCeW}$tVtCp47%6i5>s?cB0TsM&TSqT=u_rc82MBuFkl{z=Q0DUEX)!xXGDi8SoS&a zJWTO*2h(!^hp&QX_beC)H^XXg?g)k{k1jT>S4D#MU+YuRjy#%+nJESY$%JEopM!bW z4%R7H4+j=zE*2&==s37CU&T^W1tm-?U=~>GLGv&OLw^EH`_ZSaj)sA}g-NhxSsDgq z--3mN)&5gU9;}$rC@hn+RdM;=JU)r{8xH?zWa_g=Ge>^m=&>Vza^yR)KRf#6qrZFf z4O8n!77zbv?ANE>KlQ-W%`rW8IQEvKKOTGHNaV;{r~c;1G)|=#5g-CYfCvx)B0vO) z01+Sp-+uzP4X;Ga0P9e3T_G?y))crrAz@=zmaqknZ5tJ91~0flWIV`4yW=9`L9U*? zs!{c`U%`rtj0d@n^h%NOAXjsaiHrxixbliH;{h&lTrV;ndDB}UP94AG_gKYmzgjFA4BkqvM zc#u7@agp&L+f-v=#slmWjf#v1*|-@I84t2+GAuG4WEW%T)bPs0LADpB;H!pWKN*Sr z9^U9h1c(3;AOb{y2oM1xKm>>Y5g-CYfCzl=2)uS^bmsW+LtegwAJo<=7xB~BP5f$k zRolEMugj~r1))+^wZ{AFqoXt0Eknrb7g?@^UlPe`hS`O!Rm&{oRAu*Pc0ZQ5d};UN zyC2#89sK*$?xzb}s;#jdRw$l|iR=GEu|J5!zJfP;5dk7V1c(3;AOb{y2oM1xKm>>Y z5g-CDUIHhEXQKOV77X1qJTtNH_JCObM`QmO!Tg^Iav%+oHscT2`-Q%WLbS{Z%4Jp}m?9 zcJzGCT5DHns0jF{nzn=x>xc+}(7cGIYFm;b0*xX@vv0UwSmnsGRXNgMB?9F_CY{R& zkLkCoMEFR}NAz^Wv{UeA`UttlbQQ78nPDK9wr1-}Smp4uRXN;Wr4oXz=}y6{thcK~ zv`WNC^bot+Q4!@2LC+<`G&Uqep+ziC1izGBKdf@-S*k1zp~{KgzLZ6k>lGETGXkYu zrLM{zBKjiMxTgUu!i!2aqCZNATq>KMj=;_;5378(8l$N3P*08Wj;PU>^RidU)Y>(2 zM2SW0NLvaN(UpkOi_n4K>X-=8j39uD&h3NL*!cbb*CVmNkA3}Fm76jU0U|&IhyW2F z0z`la5CI}U1c(3;AObID0+YkXqm6h0LsP@IOmrdui1q(i>|A*Nf9%7tr($dPLN6jf z1c(3;AOb{y2oM1xKm>>Y5g-CYV1EMb`|t<-@MCOmYR_*pM)#)n{G4KBZ)(pkABOj) z_WVepb^kv%8&dWDpg^quBiBSCvHuYJi`X-dksmN;8)qD6B%a?L|Z*EOD4ONjH z^&WAR|4y%%A3FJ(;qwz2UX@C0J1nu0M^V*QAtIZ&|36&+C#Qm`;5oN~3LpYRfCvx) zB0vO)01+SpM1TnVfD@>{dHBf8%*xHf*VpGGqocT$A2;I5$_^gZR^Cx|HZ!F_&FR|} zk)ZC_>schYBEhirva6LgaO=A4YPv5WA&?BbORjiRa8tWtF>bl0RjGL%=RWtN56g=y z_pVl}rOI8WPj45C`C@@@-&wAm_Ex#KR;*P|V@;Ung=*#WhI-m77OG|DRcDH2pO@$T z*B_*^=+pBzr=Aq=n}-lV{*%3}6u)Bzz%XzQOns{_F`a`H^RQtGnt)jBxJ&bt-Q0b>1~HNJy&nHPLb@e?|dCW7hKKt zWwYJ7=cVtSr84x|naDfD`|aNl>;H+v*F<9LN56b@Y3lc^uR8P^_DHB^p)aP?P4JxgC=ngk+ zu4;C!n8_9}B|oe9c%Pg2rTURWO+iMRg7`a32!aT1Sac}5TiGt+T3}OeDDJh z?1Ims`<98dWvr=mckuVHC@?vW!p4WSwVa_NbAX5w;bJ- zReV$}-C&C6nVQ=c$NG*C#kj?FrQWYd-CP- zo#k!j*tV)@mK`|U37Bfhfy=pJTTbB00lEjh;JC8q83t#N&(gJkIff5nfnvICBq#s- zDrC6n;g8jijP&#kx4it80~w5UBfGJ>mCu`b+38mmXc8j+$u6)vuF7Of<=o`18dw^0 zxyck)fi^+md{b8pT@x8qM9@PA>d>d42_yAie)3PSRQ~qS-^2U23P^S8a~I zQOY8cT}|=Jfl^-U7Y6|kWf0K<&+%9@HnD1!ZkUfKRlZ|s_vVdK^!AM z1VGgVLfK`8%YD}aHmvkK=W?-!8>~s?bJcrZHHQ^19!j1+o^*#qgwa>2LpGA z+V}qt+`H-A|37FerE~xPpq-7*{r`hD8rt{&e+AM1KKxSdR+60v5CI}U1c(3;AOb{y z2oM1xKm>@uOP)Y$XJO!7Bg1Ik-Qmp@gYLR$t^b38cP)h1|6|ce?5nZQ#s25mzmB~+ z7L7d`yE|5U$upw{5CI}U1c(3;AOb{y2oM1xKm>>Y5$F)OX+()f+dsE&-_H~NTz;ZA zv3-wCn0Tl=vFnbPFme1!iDU7RQ}JleJt5&qqunPd_w-gba;3!KD#fLt&i4s#+n}y6=JmUQN(2!X?{dZgyH5##3LA* z7#^L#{r`vs(2D>Y5g-CYfCvx)B0vO)01+Sp9RlxIj~<;lUOah8 ziHwg=jE>HPziH2C>s&lZAPkSU#*|{!%IyBu?x%M@`Q^92d-r|2AG`ea%kSO&&E02q zKb6@1iQP|Me*5k-m+J|nd=ektA-?W@Z1(cITqO1>wrGK(>9+HszcSTYt-A9rA6T83N&T#pikmI+)v-T_0 zL^ML#K?xs1gX=i+nae+R&$+3SAN)vt^`5ieh`s)+hJywE7tVaG@g9=ou2ld@Ly)Xq zEIQ>3%XIGlk6eqM80+)~eIf!xfCvx)B0vO)01+SpM1Tko0U|&IUP1(J8lIWhcPB-x z|Hoo;k=U1GpN;)|>{86b7kUu^B0vO)01+SpM1Tko0U|&IhyW2F0(%MEJTeoFh6`*z zU(Mmcg4^N(`$jFRSF+``(XlJ1kBnY9eR$-`=|jV>9+{cgzy6^UBQu8@>;FOGwXgqw zA@)15d@R4WarB)C5CI}U1c(3;AOb{y2oM1xKm>>Y5x7bMuNj#c6)W{X-7f8@*eTF= zl$}kcV6UL;o4C>c)g#{>L79cda(>Y7_3?B5Z$C0T+%8GqG1t~mlBRn(r&QYZws)8) z>7mz64&93C#L7QFrX22}kWGpU}oocWDzZUzW*dNCJ@G4tG*@*xVAOb{y z2oM1xKm>>Y5g-CYfCvzQ=as;XBhqMeF*SSUvGjehkz=FLGv^mlSi~nj{cW*la98w( zk=sY37g7sn<`?cyUzmODTxwytxf5_4NfU#TPEAd`CK4O^xyaF5r~dD$lSjUN?NGCjyqM@Mb|?nX@YQ zXOaI55H8Q3OZA^Hx3aj1eol+jrRCXk7nO8&klx3W}tdA@%0P2=&E+oSadvjv}T z?dhL%P2};@?b6a&e{Y`S%^sd#IFov`uZIVgkW8Fk=*yYBE!n6oc}F7It}r=Wzxl=k zNZ5uuK*CoYNWz{Ho?XIr_T=>IZy1X|bbB-s60EG}vQ?gDwQ4c^p5BX8*>m7JvM1pI zUMBXm*N-)9ZSMTS((>Z${K9gd+P4fOSmWEt#C_)%Q}g#P2(OoD+ru>c%zY`C`CMu# zkz9dE!fy(fIW6qyOzLb34t8#KX>RsRN*r0aApAw^$mDc=>ezVv!IROt6>8*#a?!7O z)%iTj@N}v9#ZOm?<*G2A?&RxxEvGwaz?luB>COb3ZdV}8TGWx8wq7wN#M#GQy3bE5 z-QOlI9lm}%e)44W@yEiEr#r3t^LX!=)17ox15T~n9B$ffx9K^C=v{$E^MO>X)t;NRU&WUJ!D%7gkT6J}<=<_t+ zDrGSdm<^Swb@ZBEMQR=H&D_wRRvL7tT~u;<=jeER7M0z)r?NcD=F+8Fxm2uhp}noM zWBXOO_k{jRMGm2?%~F%ohk4|Y>0{BihE)smOY^W4SgiPVv{}{4!u&%kDX2v_4)*42 z)DoI5nibYCefr2)eD1`4X3%O#T4{Znx?~<6kDoXZePSk5|JL!=-y^+h-#UC%D&G*T zcPiG>W_0cYtg<>#AKS17p4#ch{+Ud(^jf8NrqinQIn9B>$xHXFt=nu`e7*+`I1-7z z_U>c*^>nByig?HDIvR~1I~Ki^4trP}6aOZAdsZBJzIwI9DwWM*+3)o1^Fsa#v(HJG zR-rv#7k1W}nNNG;c z&+4mmm(-QDSy;K4%b`M-d28LZ?x_;X=Q7rJ-8n>2tz7LUx91gJtz5=3G+oW+IZn=( zn$@vVDVIe-l`1b)(q6G9ma2*N{A6We2~*0{nf?Oh$!QV)|Jum!M`Axbm7jXuVR`bu zPGlzT9Q(7e|7Gm{(T|UwANlQ(`0)FOk41j}=qsmwBw~bH8ugo`@%SUhqxF^0Oc(h^ zmT#hy8}mvb2&>j0Ny~YQfw__CwN1rn4w45tvth-JtbG$oQPVpYtnv8FO!Qs1x4q{1 zvd_!supNOp?jmRY67q53HwPyCP_Me~o3BS}dsJiKc15Kss=o62{gmQZvLq|`KOxI^ zh5sjR5C5EMc5i1b+^-w0ENI#M;H5_r&VAp+?(uNnR=?l_WTO!Wlcm|3^q|>otEIhj z+px#cb62xq8ah25pM!oIgY~->;uZr^Lo21G{`AfO`;@zL+Lctgkt;d<);k+Aoa&Zg zf6Kakpk)opzo+s>7BS&%+bcF*UR)Ml*(=^F?--9igca>v2q)~~rnRSA8q#i!@IKu+ zrh7?sCk;Ni0jxV8+)%TcJ%VL#ACFr%N9z+Ex#p`p-!8W$ia2=N06F$%?y6cOb=5mv zpS*26em~|)uMg{*!(bAg-PE#_407&Q;q>i4G!~yZzTae~X>Cg5@2&4#IyoLcemweAG#nEe zM;q_SrU=a~>E^+!*@+gjC9%u9cL$*9jjMbM1F#q$ng^OD);o!759_B&yx^m}mcMT+ z?!RQ;H;+{s#2dJn>hxEyBv%|nus1I@idI^CA^py}0kCWMALN@mb@zJb3k@B*8vnen z1WnB!RDH{-KF6Ij>or%}x$5>1X{GzyEtjspwXt)R4b`zbt^0GLS2eqnu4?YVuo+V< zFK?Ijx#H2$(DtOTsRYkS`HF+58i^#UR++aduAy8(kGfmic9?@YvsF#6CWWV8jUG)- zE6K5V>gN4CN?2g%QX73KKHmJ!iCf0wH{Tq6l65*#B(yWXdO#P73|G~E>s9Op@8?~z zRa|r{uu^5U*mN^{I}z(TkI#X2rIT-;(poGBdW4SA4odExu(g~2z38}p2hgd$E%Lw_ zVmlRE=iPhi`+K29Tvc0~pIaWZuYUT~SKdcx4#?O?XneZyQsG3nkMQK_P>Y5g-CDNdk!fH_?dyHz=M!d;R}~mt^Be zXd*xahyW2F0z`la5CI}U1c(3;AOb(|1X}U`xH)L$k4S=FIP*2}`~S!_kx1;rKk&E+ z01+SpM1Tko0U|&IhyW2F0z`la5P|PEfk&caGZUjTGhsCU{7w;%8B|O~EoaL`ebe>T z9R&J&;JrT-CI{QAH3a*2MDm6!yQ}P?g9L6~@%evW`F=N%>L&t3fCvx)B0vO)01+Sp zM1Tko0U|&Io&$lKhG!a20UWe%FnRP_k+Gw%iX45_(Z`Q2#NHRX?dT_;LtS(#5g-CY zfCvx)B0vO)01+SpM1Tmq#0VI}>l3GIrCgEumD8Hz2C}EPl4AR&q`OR$T*vYxEpT*2 z^KkuHH}Bl!Zpj>8kG9V;)Id{I)0TXTGf7uHTXHqqk`&dlUCnoV!}Y?mhI>xaRoT-~ zyrg*=PBS$}vUP4quFGW8)48UqJUnf*=QOUlxFl}aQlN<1l)#p3M^`1s^qFp2wxV-8 zJZ&tNju>qr4^&&Jkm;$*kxk7v4Moxe1#hO$CB{&f$C+VR2GeX^3F{h({ZUt42LiIU zE}Pr7r+JEGIjSw`hRP+!bqq$K}YU52{a_y2z-_6Kwiz)Q?X zNn9d81c(3;AOb{y2oM1xKm>>Y5qP-~I59jE6+iA*w@W)Je%!C`C_9@>*{Eg9zB$}V z)d&6bf2e)^f5+rIIG5R#O2sQzjn%bHwSE8pFC6`G@!!i09JPlC5CI}U1c(3;AOb{y z2oM1xKm=Z#1l}|(JGr;{^r&O%I51vc4LiQl(UXp>?*sR!4ny{zrE(`+42?2jy%&Y z+uX1->$pQ-E~~XQZ*^m}5)`%WvYQ#?ZINL+x4pSjDysXI{ibLa6FrmJ*@oO%aeMz3 zuKwrTywO#5(8`bz8ERX3XJgab-nZ;F&1@ZXD5D@ZCLKm>>Y5g-CYfCvx)B0vO) z01+SpFB1Z-Yxo2AJVwyS$HW?b;I73mQtu8|_=ENxI@kXP?HshO|G!KqQZtAE5g-CY zfCvx)B0vO)01+SpM1TkofdK?s*Z&73(IFy01c(3;AOb{y2oM1xKm>>Y5g-CY;AKLf zb^SjkAN`q^Nt37gelAZ>acz`2TgMOK)JMQn`p5_M=bF-?AKt_e-buz5QJj zIxwbQ`vC!EzLDjdmD6~9fv+&7el7};SVts%bt|(4krZ*eeX**`nyF|)q|5K#{pjvT zc0beoH_`YK{=wTGanF3~Hy~~*djA#09T?=VPh6K7IE$oMS&@jYq)qi0%tZ; zG}YBj@6J8(|6gkALGls-B0vO)01+SpM1Tko0U|&IhyW4zfg;d~9WZcvq;voOpq+}= z@Bd?8`GF!vjU@s^fCvx)B0vO)01+SpM1Tko0V42nBhY^S-=L=hw%7lkf4McA+Cv10 z01+SpM1Tko0U|&IhyW2F0z}|tNWdJKnVAr`GYq^xVBb3m+V}te?9q3M|6Yb>Y5g-CYfCvx)BJd(1@RpIW8F7n$cr*Tvqu>sIIVbM$cQ=aVg2^(tEB@9e z{$2R~dts*S(vFHuy2!+ojas(sn~k&W^(+!xkzm+*+0{xLkw`SWU0v+xp{e$2^9si?js z+JU2IRy_zN=S8f6TDhDrRkwunXa3zA!}kXTgZ>Y5g-CYfCvx)B0vO)z<~)!qcbN)qv0?7 z{d`q~N7xpj2@v-{uVl+>tW+u&5sv^d0B%_hhVBr#MJPaSK1=R1-};UA`u}fZe|}(j zC?gRd0z`la5CI}U1c(3;AOb{y2oM1x@ZunF{Rp-U_UsH?hhr06$FTmN8hvl%@bGIR zqjwK`!>^4!a`Zh%Uo{m>OdtB{p@q@Uw*PzI(C4QfpIV$++|#(nSLep#@p$yzr>o4( z@$+S$m&3Qa<`z@4%c;cj?7e4GiDY;znYe8-fmbr?Clkx5N0$=|=kagl?AgSH#rboy zi;pE9Og(l-BTuEus-&C!1ExkA%mu`&KhqDErZ(S%C{hC)zS8}yXdW~;4zmGMwa%Exu zp_LT6?o8^@L~?KDWa9in`)KmEq$nggZQV5%fAC~95|&?C&p{W`tX3_C-_z%3Yt_|R z&nwmn)wI(6O}Vt0<$o~J?hYe-AY{<2)$u? z%=4fl%f&U`s~lw>KzUaC6TI(>hJ?cQXhIjVbS-RPIzZRY`{eLEPHUQ-otpee#E8#G4~@lVZ``lombNO*5BENP=)!pX#v7wg zWkUUG9&Y|Q-K$m2qx}li<$&A9-Bg)swwl}PR+~woW%Ro8R?2FzTs^}pUO8K;X7_j* zf1l&)HC8}v=&)yXs(FwsXIXCl5#r5nX*s=8IqU7`dYj^A^XQ_La<1FSBuh0nm#wUF z_@;Z$pFf+LU1+0$dNc;203z^aAs%GP6wBNF&a7cqFR+<(n?kdn(;blpT0$pzk2TCS zn=-&67UAC)=jWCOnMD2i^W*VzO0;f-UJ<3RTrsnh6aFZCPgkH8p;@G}{#JkbolSLZ zdglHNy#+Kpb$>45v|AM=r{^9Vi$9|5XHWfSr(6&TS}H zPcG4S(~v1?L8SNqkudy-`A1JFc2kbtSx8S7agO*mSO$@RTBE-L5c|wW zdP`G!zFIhSJ$~v`^zj=*UG7Qj`C|5Ja8KG*DR0=6o+*D*XRk20xJ7^OoVIyVh`rAW z+CI58@$Pih{{FjKuC?9w3pI=V%wu{?I9@p-0`hg(|mZiqPcN=f)|?>GJEx<99>gYx@PRHROYJb(g328drBx zYc7T}2FlSWFgaa+;C16-MpfSqD;1&)oBb+D!Y&Av6Ly=3>z*OI-BdEcBJnWx*=|34r5i9Kt3`bq?d z01+SpM1Tko0U|&IhyW2F0z`layif$Jk(rtOf9j84^#|&prymZPIB)+S{fqMkJ{VA} z|Hoo~6^Z?q*gwYpF80mX|AQ~|A_7E!2oM1xKm>>Y5g-CYfCvx)B0vOQN(4rSqtVeZ z@jfyt-iJrT`_S;n@C4q64^2#=HPP75MDYKe_&D}bY8=T*1c(3;AOb{y2oM1xKm>>Y z5g-CY;AKI8MUT$J>Y5g-CYfCvx)B0vO)01+Sp zFA4(nsp!Pa@tfZo86BNz{V0DM3v^de%h_y(S^CBN);1F{%&=sabw#($Z8@FZ%-oxr zH|MLR&3o^^;I5p@o>|S6&OCUZeQxWGb1M%Q7al#E&01@ZJi4MvkF36FXYtWBJPu&) zJ(>Y5g-CYfCwCjz=`27 z?BC$%ej_O2vGDi&+_eg_tQg|={MCy^r<`G#VWi%@Z|YF@`hWlH|DTWDBK|v&tdxle z5CI}U1c(3;AOb{y2oM1xKm>@u%Z)%Q9{=Elx%geZx?`_r#ZUNml$}k(*2}I|+7K)D z4>#iY4_dN|)w<;3IDU(c4sTE2MY^UMu9vySt&%W7?HJGZ^LQ!1+M z`2T~${0lktOlC)1|Idiu|JSzi&c>#<-MRk%a_cy14-p^&M1Tko0U|&IhyW2F0z`la z5P=^k0tdVP|2IESOsKI$fCvx)B0vO)01+SpM1Tko0U|&IUbY0TA31*W#GcrKBL4sJ z$i0aFfAot-@1Od>RP@N35C6g8QxwPQTva_s))Cjx zR9uajN+4Uhsp$dZfo?OV1e(izi)(?VdbZ6iF58Y9__nW_x@?-9dw~^wdhV;%?W(Mr zvY|M#W7x8mF7teGgQv5FpjcuV?&gXf0w5yetLw>Eh4Q^c$QP)3VA;MSDYETKx*_|J zPc|h@LjXokcXU?|0;c*7vsJ~=Er&UQsRy!&ngY`>1H~~l)s@x2(QQYA%$hALnx}Ec zQJHPphR?%lZ)nPA%Z_F%Mw8NHfqp zMKPIVnzkALA?VYUC<7SdE@T}=nst#qMS%?4SIRkOuHy5gc1|Is(w3)5TP({tQkbo`*Lf;H5iv^6yjw<_53G|}| z0W5=Y&(iEbQ5g@r+WbsYL`zXMM^i!(vlVD$CA-t?-(RRFUeST4%7L#Kih-_m6$uSC zC8#ls1X7{L3?GKTbk9&N%`jA3^BqSsJtwdn?&zLrXtFCifz7z1d9nw=9hj0IsJ>?S zIO5m=YpB(SnvxlcsY9Rpl~7noQ^dE|6W4c%$P_owHAS*~6?UQpuH=}eC;5u$nr`57 z&0sP&G+3u>`(t`&E^}Rm+H!Op@?r4{cFSIT$rmA$Tm>=vLZoo6v=_w!(u%*fNeOI z!2!@hQ>t7oGhpeZH{ z2&(I#F{W*Y?$NxZC9_alLrxdV5UAsTJXS7en-X4Ezu}rt!gE4G!*pE7WxAv&9t_(x z9mzEUO|k>k^jxT>4!3Ba9-EuGE(5OaSSI{spvk)L>F`*p>j$={cnS>JV({ZQpcszF zL?d;tX*E+#31wIf#)eQgGc1=A{xO?}3m5o|f3Ba5g~EA4IEx$1VV*9TzNw+heOF?J zXGo@FLGhW!EX%c+X&IX7S}-ZuQW)1gxFm~1M*~9%9NUv^j5Mxq!{J!4SO#_0eOL2b z-GzRH)&4_EI9R`?T1|X;Ua3H^G^gLYPwd=g(rNie&T2-XE+MViVTHU7)C*Z zOS;TV$@W}XvNXnQ4;>l=y6bQlF&v-<{|smtX&eR@7Y-L(;hq|p9tIpeFkK&nOpJgS z7uI0TrDrGnkI8 zITGAjU@FjjPeaGSGs-%KWE<9`ab5GcWoj5iT+^0eL#71}kshQSR-;lAt}ifV9*F#vj| z=3@-EOiK$47Yfd0&1;M}#!rSa>za(tz~m%dtH5$vU0(+MbdlAnt7+zW#af|S3HyJo zo;V!J8V=yP2Q`BtN(MZH8%ATmZEBdxc&_5ZUR~KT zT}Oe*S+;BH9(3C@EZaaAF}PT+*lys2)dnqDWlW+peXp5Uw@X}1Hp0Af^&2Kb5kosi z6zIS+0!LEe$0dx5y5!iHxA6cYHw;=1T+c**`wFZHE(dur8rq6wnL6e`%rhNdvEgzs z(PK7zs>+bS44tWt%pE9e5IT6{4Gj@hN6`&cb<*oKR;XsH!eg%2vgO9;W87T7X(ANv zA<@^&)iq1?G)c2;43L=LNUmymk}MmBhkh|shbxNgLUzs9xC&R}_<7^DAkmvr!A4MQod77L-i=|5G!bu5%BEu?Y`O;co9l@!mn zVG=9FNCZ<~$6kU%iuCHS}cXbDj2Q5%k zp@|I31>eR%5HfpTTPg^O*;uJu^lKQG+j^JBkX_EQT!US$9v=;*4CjoFW?){$FyB@b zSViFYP(01YYC)F+Oy!vd*Qlu~#%C>nlhIvxY78^J3s0}W?*y)oNseKmzhzCw1WD+F zk9h_rnaB!5Z%A2cN(l=x1iO}$;mFsJ>@D@_kx;VG{V})$bcQ8i@`|BP@!lo-|M}}Jv11+>DfQLcV7@l1unGBN|7y}%Z7r2_PV{DQ!zl1{%GzDug71Jg$NecNr z+9esH&$lJ3tQN~vpI5we|IAEx>aQFQWlf2`h6Q*m2o%ZX7G~bCP6_K1S;CyplQBzG z(SHtl87l(Sg+_4&b4M<#SYp{K$a0u}faw<&I*RUN{I+C+`##)@7BIL^ta2Lmp><@1 zr*A9{LU&zYjVYu4n)=jGD3~S$!vq?3h&8DKg+cdPTw;ojNrVTV!nw@@1AWRI3nQQE z1P08*@mywLm4#UX=8Wi21>Y5g-CYfCvx)BJg|>*cmx8#?iz|h3^da6|%kVW2;G)*_N5Nc1rRF zw~ALTq%2B9VdXr_@zrXzRJrT)>8{F7vkg{d+ursLGYiFVD_p|PDy}DED^9^RhkJy$ zqLrxUq1TNz-j^1mM`o}A*;`_cqeN^H=Nvu1WowyLJ;SRKE&+6vsHkc;y0Exg8}FU_ z|6_j;`!L1-Z{nptM1Tko0U|&IhyW2F0z`la5CI}U1YQ^dt@!@~w?IbPse|@5hTExw zHW)hb|98y(&4Tv&|IcDi#GZIznntA(0U|&IhyW2F0z`la5CI}U1c(3;AOeEGZNn>3 zO~FmEn(pH+Cpo~aVzMmZHaZ#io@lt^1~)RhrX`*Z@DT14!~JTW+n3R_bQ?FonamD` zP7SY2geBs(#&$;Bh8C8HyMJ(h0Pa4J&8eg3B4bDXHge=|V-HQGj{f}A&rJOT^$8Im z0z`la5CI}U1c(3;AOb|-B~Ret=;+M)g!rX7H{C6(jKvV{Mz>dUrSSgh-04~=S7d(W zH16<51R=$h6dS>rM63h}p@ck1L)b4wjB=6PymOPgrRdYCKY!!bA0F;0S3G150nF^x zfYsK`&Ai?&*HCfCy=vMLVw~YNZ^XtyI1}5F6cOnIL6UsK^*ZJLzYmXgmun-YfV?Wo zE#@7g$a%h9uCB@+BFZ4vm8YRxgtw4vL?4h4IYTx*9f5^Z-YNGVA0F!|m!aHjDDQ^I zyQ;R!<%mUs2nDtjC_-Mug24^yx{6p(2n2*%%@v*7opQf>^t+LVi-%f#pR2s30)A`{ zybq7`NUK8HjF7f!xsJc7uePN{s0z(Tq&D0?ui(w}aeF_5v>s=Mfxumwtt%aA|33E1 z-O^gmvDezt4s}awiMttq-(FSLt5vU{Xl-c`R0E+LJcQVCR0JbHY&Hp@mkbHZeS{1_ zj1JlLJJNm=#`j2v?Q_-Lmb70N*Z;?2=OVGMG=BddTf-N65dk7V1c(3;AOb{y2oM1x zKm>>Y5g-El6KMVpUu-VfeVZy{-Kl+BIiuaFeOnx@-~Wf(R(%^4t>6ELseKy`t>6C- z-tw9nzZ97m`&MLpdF)$9{`|Xqx4Y?d3cSB5+{(iILn{MGcJj7l zL}3S%@%8Dk_(QixBcX(q^<1`!P}#L=G5ntHmPqutvgg2k?|jt{kH>GnJ^Ey(%G?~^ zlf36^s@am|*?Z5X`nx!B+hhW-WY$k6mQ#-|Cl=1*-^$sui3^MK=VlilOFWo*?2bmB zCgLz(^AO)aZ0}_RPu_EOGuc~Zg#ynFPHgH{Z?&y*%jIIZaa!{T;&g}|=fOoG&N$DP zs+A;kemQl2YO&LdGpYM#SI#ac4t?Zk0=AC!{cjNJyndrMlLv7nPu`tK}MR_Fa|dc&5zq&wf&XkjHz^npQAT%8dQCV^G%OQyS#9pBz-6Y0hAxO8K*J{&rx3(fe@m0T^8h7LEs zj}3H{dowpYWAmsOSWrlET6$e?zpX@08doa+Z(nOuc>R9T+wB@XV1l;K-s^iykB!HTlhOM1p|#xK zctXd#ctD4E_=T{T?qfF%G@I@mt+N`Y)14xwJLOz*+In;>{@}^|%%}T|w9@_U?t1=_ z@%YJ;(Mu;nL+VcJ{(M)j(R3$WovA?Bfx~>ZverE$jE_@`+ z9<}Y1SS;kS1>PzT6Zm{(speLTRhFB>l=vVs(UwqX@j*|ys`9;BS**mcDCwQpxA`xw z+}mnE^D!Aq1y;i23#xCd6bn625MWO{D5v+>3p}8C@1JFdYWY2I?;`$xH1>I5CI}U1c(3;AOb{y2oM1xKm>>Y5qK5?b>oJ^EA^?-dQ?1U&x}4saT2?C^{Z3iH8;AVTZQq7~HWO@vvY13Z{Ed|Kn<&1X~N2LpH(w&~>Kxb$nHv znafmtgapWpw1PqO{i@zZ*nvG~mc^OtJh$c=8GE>OX5TOFZG?l^b7o1LX_(jpFjn26 z=9&Eg5-bD{nCP@qlw1hUtOUMREv&Xq?7Kn0LS&0WofE4#F(*#U^KuRw1ML(0?nB0vO)01+SpM1Tko0U|&Ih`>vkK>Hs4L3||G$~qv)9nM z|G$~qv!Bqp|G$~qvuDt`{~xJ+8w29{f8?4-B=)akHynD;q4&I$@ssRCfCvx)B0vO) zz)OQb{m95QGt$KI$noJr^$0#6YkVHQekOuTH@M&ts5|z078f+!9c5?Ju=TR5l{U_t zUr3d*-de5HxMwx`u#Eq4@8?^StvgV|duQ)THgY9@B>MFHt4F?z_r#~a9TvM?+7Z`L zbWto*Hfq_jZ{o+0e>Zdcp%W*M4-XGt|D@KGfm{COmbmKVts5e+!1kK2XUi8-3uopR z?oVHsee4|KsK1jaSX~{c8NR+#h;WeI&w$5c) z2_r{aYfLFttqd~cn2WeGcO`d!YxmQ;pTvPO_p+rd?qOZP&pwgx<+s0k_kFt`yZrXc z@7?{)-Dh?`mDv4>-A`YB`|dNB>j|WM5+B|nzV3c(_oMjyWOo@CR*MBu$nA(oZOWSF zIEIDv=3TUBac3|RFJIdI1WtTn_alkjpA|Kso{xp4edLbB?guWv`*QvAdr)@ow~y_9 z>T-Se88J`NvcxLxAyC2#81Wx$W<#&suPw##lvR~T$O_cS)u=G!M&O;J% z|E@?1oA6O&|HSTZw=-ORC*=68@T~m`H5J6wc2L5HK&6f|pSk>FXkzO|;JXU7TuxxF zChs!rI~7^A6xp;*+i-Lp#|s%8x9+qR)j^zbU9n_SQK4C>Z$C2Jcn^DzyH)|cXNW6s z)r&=^oMD+rWaNK>a-X<)k7k)0c4i%#wOv+gYumZ)&7D$F724g>i~HsuPVLd9zy4Q4 z_xw2N(#uqrzCQT@=+a-Wef|o%H1wd*r4xH~DZ@5AC|>xG0Cly<*NgccT{{1!^YBX# zzlq#Z!%1DqEv*k-GjsdIiOBIohla1mkNO*fk-lTDm0+%h?&X|7_sslGF0;=-(4_pt zhhVB0cOH7(WaC|oKBAm8l%t7qN~LXYdxx3p(S54SiSf@CS*yXC?#rrr&?--bRi0{A znZuwWo1z>&zh!HgRb5>F57+-9{{K_4r(PPf29lBp5CI}U1c(3;AOb{y2oM1xKm>@u zOO!zKw*!NByGPKd$HaEfzdo@lpBdn z1c(3;AOb{y2oM1xKm>>Y5g-CYfCvZzHx18Bgg+(m^VJ-72e!q}lJIjky^<}jwSWKr zd4fd*hyW2F0z`la5CI}U1c(3;AOb{y2)w8XIMKtV5j{LJF%h4@YgB|o6qoxm8kYNk zU8z)ZZmw#|N=;EU@#KK{hwk>Y5g-CYfCvx)B0vO)01+Sp zS0>QB{y%s_U=-&L#l9m-I3N3N>^q15*Tc@_+b2FZ5j*tA_`AnGGy4CH-ZFA-czft= z(YHmuZ%+oSFI|6RCUPo%W8@t-9}2fOIO3+PI*4_liMT0S+oiQ^pjySU&&zjtWzMSH z=XsXRm3YC=7BX*5hDr1706T-Ct{@Oht-{NlOoQUMAXBwiWw|*7-Kiw+QZ&nyWrXgj z@_c2kSSuiMjB>}`7(}gDE+Pt1>g$ug+IX-3$aQFyb^@(pU9D2kDpj;f*4M1{3g=5lIUt(*vAuIQ6vx3jJ?kv}ykM{pk*#2Lt?_4X| zFM74B{h2MaUlHxE?kI*{HOd3p|AO`H3*Y#k3mgA)^@pxO;v1$xz+W- za=$RWa0Q{iC4~Owjeqr}Z`I91kb6e(KG)*CS~h$PrCZ|Xo<*fx%&-z`=uT@oav$fj z{>54alcg8T8u}+oLh1i<{WrvW$7x{XP_oEKw@1AL8 z#MJ9fL?ndY5a%Jyu}xEx@jY9a6|=wOUDa}pha6w|CLz&He_P)fg*cj+PPFGz8MbLd zoG===@7g-IHk<~><_x)aF4-7@!wj7f=(!A2FZ5jM3)$Py_%D>a`sRpeyqI@%HGTu} z)s*llXq)Bo2HTQ12Q>bLQJ)vS<=2XqR|{XrURz%pM&nP3o^P*>&}$_$J}(qeF4tUb zb=~M`{P)n@uWD`dh5x_3YY&dAyzW}-<$c_Hk!4vn!qQ3@+rl>9_wF?iRY)td#*$?{ zBA^h>uJ*mFeLu7d4BP@^TEftoCLL1Zal%Y(oYs#(GLVvzPD4skW;)`JKspl$Zil*d zCbSuwW|Gb{J?HMdd+(>cYb0dZ?)2{1tMB{HckeyFbH3L--?!&PIw~rK1UJ48fuUCA zz)-6(1ZFPfEe=Q4L;zbGebx>KD@4VsEBh9z%b)>N_y4^AzY|Q18D;h|ZS*Djr}S&|*XT#+hv@;j zmu{p!rrxGbQqNNJ)FH}DwHrP)yl?oP;YGt^hFcA@hCxHK{=EJ<{ZI6NrBCV)>#h0@ z-EVaNtb0RuRQI^fs~ghYrfbz+(7vNRrF|YAsD4xls1i^mph`fMfGPo10{>qmAo_W- zMND>Fqg%k%A4z8BlJZ(%1kqFx43P`m$5_YFER&YwxPzFu0aGWy}3C4=Wxx#$d zQKg3RCNx-Nkeapl*=Q+fwoBBo$qv*QQsbGtBNtCNZA+<9q{dj-Sc#QOR*?#4NB!^% zQgawXCM5mQq(8jCTI5tPyqQBtHJ4d7Qd&rxt5i!Ofm9Z$ev{8@vMpFlaw>W*cOf+$ zNe2>H*Zf{N)d^H5Qhio$BI9r+?vSW~JQ$YDR-~5AiMTJB4)2ju@s+`XR9C8y3C0)f zyXDjnP|Zm7N7-Q1n`C!Ysb)5hl3YM)%Ccbd+AZwuaw=SFf@cC(wrDJ7pAYuSsTgg$ zfYfp>;fy3>shuJfVp?PXuPKVuoHH_SN+irXWYje3mzQM5oGG#aUwpfant?qcQ-r^> zH)~CVS@Uf&Y8rM>OuUYYu}C_V%?Gy0sd(ogUTZEc#PhC5@m7I~-o;9|0TII}7cQ5T zb%nBpJ~`EbcSdT?no6ZY4s)-Zig=?)g)jz%xj9GPD5t_cnJI$Q5}WgTU6w$PoQjA- zNHyEyv4Y(d?XFVi5>brK?m%iR8kx(6v#zZo)ltsQqqQiB<~gg&SA-Dj`CH^vXzk_% zQj0OCwUCI+cS+QAVjigTNcFpHv3NG-xLHmG?VICB1?5@id|`G=m0Bvfuw6xv8h|U4 zg+!ch?RYI~ON3avHMB`iMY|1eC@m#z5c(ry>5!$pNx=Bv0+2&!YRHY3QT28Im zf1o6)f;rzHr`8zA^;N2p$yq0-));oRRjQKtbiJHfW20=9s0xNaDgK|5T~3Ps2ez9U z!<7{OPsy?)`u__!glh@&i0J<>;7~F%Naz2V6U>PQ8m3`A{KsMlhNFH|38)fKC7?<` zm4GS%RRXF6R0*gOP$i&BK$Ui_S%OqA)lhKSThsS;2nph`fMfGPo10;&X538)fKC7?<`m4GUN8%F}3 zM*RS|)swvHSxTug9L_K~@Bq!iLT-_jT8_vjzeXX$Uy&(V+3QTjo8KkcNq(HrR|>etlk z)Q8kN)XUUYsi!HH+CueH6O@nA8s0Pfo#EdMMZ;M`$nXurpBik2VZ%24Cx(p%3}~o+ zR0*gOP$i&BK$UGQnGQW8T znb&TkG71QKitRh)6?=-C`$55S=`5|%-Q2icehK7ng@3Xl_vM)#ePmwOJEQ9*i`tDd z?MAYQ?wO%`$Rb_$jILW=bSeFXt@8Xu&o^%&^GCZpN4tokfmd%P^JlgUoY^8Tx^U*F zo5}oQ=b6P$dC~b9(CpgX>SEPAO;_fng@2pm@3 zMCK1L@dJ#!2plHSWd0cS!ZC^{I-)a>`4;_=7QMU({L1La{3Y$VOImpmctTlE=1;e# zPqz|9n;vYD=fUqqGnsE|inKM6MIDWijz+SmZC#{o9SuW(wlV~`fA;LZ=;&PyE^2op z^C@$Id6)SBya0TUImLXF`6~0r%wtTN2`~?UM}R@b&h#@|nKq^wtO38G&w+P|QYq1`u0Wma&Ryw0u<4BWL$W=~KJKG@5Cu7o+t>B%skkOryaaXOrr@xN-uVbX|y{?={+6QG#DGC^r?0! zeSCwIeyL4NqfJ3dA7I2Z8WN=RF{+vd>w%PR(Tiy`8A$0%S}~1w0x5mEwVDRwfS5+Z zfRt`)lF}WGQo3!O4jQr+{4B!zKczj=a8tv#8sIIxPv5Hh@%oR~4>iYik7(a-(rZsQ zen<1?%)1&2DqQ!EcfPgL+NI$%5%feqUU3{eE4VA2jrhf){X-M(!6~=VGdAqL-`J;6 zvCnvDtV-zHSq%-)&$Yx`JNLsgoWwIq$z;r%&3c7fdHl1nKuO53!qcY5Jcp-i9;$fd z(#O|S>g#{1E85!G)unlc%6Wb9fbcBgk6w7>)Zje_-4YJtPMr}xec;mAIOV>7$~bli z{!bq~XdIvLj1EpbV7$-$!0t*NL264X3dCA|$omP^DlVIqZ{eNCNC=e>38086_kQ<8 zl^@~yxqK!QNX(U%+`%~P9vPfIIAz>1F@4Vtv2HREjAZ7)-F-4Korza25om#hR3sC~ z4(9rd!=U#m&!}6xsuBl6PN_0ceaz7G#DsfnY8J1XoEjV*7cS1H{1`++@#0FAp+jSn zQxk&#ud0Ms8tymO=Zk zxFGKTqc4S8J3BfwPxtVR4vF~RNj5kBnw7M*%H0*4kUx@5#l0my8UbHHkUi)+o2w~} zdld>p5C8+^hb>;X+FS7Eykt=-oJ<6WN-5~j=nwc_Z#GO21lP!ldS6we(e*z)+$Lr#dY)Apkc1}CPmD)G|6ajx#P!Uzc24ioaQE>8lAMo& z$&koERT4N=FR61tXx+k;y@aZGhspr9q$Z4I0;b)@zUl+}`new8l{l=-WV6)^6gc!; znM0*J)Z?&Hw~yqIc@-R8A7V8eOUQ68r8d0z?Z^#8HRoE(^CKor#b!9iJMK0Y^Qc?eX zftJo$wjP)|PcbW(AG~|ErStHv<=hP0Bykrg7

*ms&e_?a~|x)eOx*mOm4+?)rl< zP(Sx5ER3Xq0_@LEl~TfdRULH&@*KpLNd$x?&{Cq%wiE~TKn8-k5K|qDhw-Gh&K%&) zK}4$*gt)3%zg1^JlgQGwTW2lNgxKg-JGxD}g{88>YU}XBEuDdZdM%5O#~|lwGLXZd zOxam;UDEJVcRtkGIWVAkrkQVTbt~2-43RCcZoL(?MSh{SqDfzjBJkN41-}Q&u0VvA1mTN>_3(1m9Aa%@F|fQD87`XOLuxIrI=bN>f+!rr`BWU&YS1g|vL**v zolK;+l4@tIcA(Gm?xDj2B;EDL%{{S}&cUvF%~g1XoHYvxTIT=nN8tZY{iqU9C7?<` zm4GS%RRXF6R0*gOP$i&BK$U$5FCX#UUU-loyU(z-`Ak2ajVUIO4I+qv=fnrM&( zSGXqF1-}s`hD+y7T8eQ^l{_UvphRK&U|%1cbE!F!!e&C=5L8-aG|2P1Q*id;{HFW4 z@&^VQS91-e~H)p-<;RI#2vl~!1rU=)-7WEV5JZ% z8QxeaHfYgyT4~c6*gH;y`u5qOO$HWnpt8GCUr5I8+MD_UXeyOS`tx(SsYou4L4?2P zi)4aA6X*BV_w_CQ#p3JlKC$@v;v0)^7$NiK;`bNNLcyDhuRz7hlfgnO^oE4Vj-i7x zsG3-Qz8nrlQ+)JnIQj&z&L#hW8)Fa?DpZcCCNK<%Nv6vi2G9WnlzIMK`~Y{57CULP zs3)axHkD3V*?%5*+K$3;mNn}Q zwTmS_ZhiXO!21W;J2`7BGL8b5GhP#0t1jbfBlLP)pGK~W-~AwcnlrQj<76D~utM0c zs}aYWbvYbX6O1EE<9NG2*z_QBbjz=}ax<_)t|gV?j8)`FtfkgW^=rMHg5%Br{^zMV zuGoa&iG`$67&Qg(Ytwwz9yuRSK9|6N@{^4Keg`fjd&I7<6eW%Tzh*kG?KseC;Ur0R z2u}p}?KAK06GtEZjLW`imLG_J9_gs46cXI{Is}GVl>@`zctn6Z0@&Kg-UMn{nqja_1VTgIb!+e7#%~k* zr0^nyT-QczC178nPp%HLy!Eae63s)}3qDY|(QRUfl%cAPHcOzY*CDSbB>3$MYN@+f z+xnG;EdvAm7$b+2F>#}}Dq+QjOT^B-;js!W5}GDg?!&%_6NOipC=>(!5|BRI+ywVn zD1cbzFg$O-UyAu{k<60rSl5)c))OVT%7Jn2@ z$u}}`e674Z`>o3}I&RX4jaj}#K=3u;nhEe%7>-tlTs|C`t;-o=#T5g;M7TWDUQsC| zxbYsbmx@aS1XdGingF&o`dl?C7-vCUZ(W|j7`)=3Bu8a7apT=eaKT_qhR*)K0JqXQ zKg8B)*b1nK6_HaZFYc(sah~T%Spnyy1EO+o0V22+js}?e)7~>ToFW+#n z8ms)u4Sw+RSb%fU^A_jnLO3|^$>G!lMOT1Rx1%_&HI}1@<=|gm^c>|pHw)U4jAUhn zDDDbyt~8)sHAKs>lU*Em72ynC;zqUzsv)q{#GF^avi7Q37LYRtWcW812hMQz&47h8 z_?1d7d|8X&hCv_}&nC-3R?%>_W}2yMJA+??1M3&Q^Fz+kiF`x%kPLpncMwhU$hRwP zv9kGML6PsZ(ZUs&UPPu}d;7wfMb5Da_$Cd0g+v^;6kNzRYfFS!yERm4JK_X75(o_X zqxmfSuyxI3PE3S0oZivk2S2mN@B9CWvvwfkDDbMm4;TlZ`er9GUP{_>&QQium+`d` zdOfc9AlDE5{`|}yZgL|qPAsBe)Ci*oe;YEMu;sBI>;E6$G}y zPHATvHZ*+N#kA04)MK#!|6Bb=-M!k>`o~*Ow7lQa+H7mOyYZQI-`AXP_;h7&|2JxF z4t^b%bb%`QeM-?PVT+jTDNv;aV=g2!b4h!^Ze7_v<-5-=zOs0B@dxt1#!80&17B;h zGaIyp7b=_PJnuQ1$?7m$ajlF#U-4TT31yowziUvG63_manKck-=_YB=PJh57Jx0rlDQCwkyTATKkJ;4F5)BA+|9{2f(;d3qaye zqqGU*gVkpD$6-VaV?djvKbrK17g%ycBxg6p!y8$D5dSTE;G0i*V=lXiw8@euQG!-$ z(T1GyhPZOtREj#wIeusW&rG1A-Xjcmz>IkCtw)Y03|H}XXLFflBc+8jiAScR8v+c* z4+1c~Pdd116ZSN;;E3b5)sjdc9v1cEH~G9K+k&uaTJB5f>TIy97{b9*7C&SIWB$~i@4H}bRdy+35zNMmrPlFM-xxYTt`qCq{*23Gw@5U{|HSuHMpE6ZsHCl zz)n!469N0I-bBXXN~~ngf~|!#&g4G4f!$@x_^heI(jikZKGo_bVsKw30=99UJwgMm z&c#+sAfE(WR%EMePQ-o5w9;Hmjv{NM%VkW*#*woKXY^riWVfIr0t>#&S`dpXRmcS6 z3-;@xqh$es1EvVXIC6@%iw%*$5;AckyOcnN0Hhf~`lDP@mYF36K2$djD?cEBPx zJ_N01Hjj&^1;moFEZDqu%ZiN;s~bw{uAjhLAF$q2dYQBIBi}OP1J5_;zyuuz1_N6( z7PHR>SGMR|)o^eHrZ-oZh8PAF19c7GUK3~8sRRf&8VU%goJ%+($yn;f00jF8s0Hwa z$Q5^BXTa%IT*%^nElfkEDDs_iM&?b4gt^igmV0_;bo=5UlUq*%rCZz2}2_&v(Ax?{~i68U0W; zI$AbXc@TRLLMzOZ-~khF{Fu4Yc-Z}dbdcT(*3UyKI5!`|*56VyWz3&;qwh$Vfe@k|CA1VXnEP0=l= zzD{jboE!I7at>U@t=#?7>FM98k9~{`$_TF(PW>4E{(f+EvvT9FS$%Ymdfym99`{ ztS%B*PGjMpJ%2%+1*qei)J9@-!ebfCQqkEkxCLUAbT|Rhs`ISk8A=Rrcq*c5!YtpQ zaWz92fqtu_0SMH`!ghyspg!hnsH=_N%aG>NP0IE*e%NHS-SLH0J+U4VYgwW#9I^HI z+k2NKK1B}~E@IMf8fU5|37>7GrEm$>G)#98-GSD8qS{U?cSM4AH6pr78-c1q9k)n{ zQ*oa;Uke6xu^I+C1DuDl&ht|V9CnNF@J*OCkHy`Fp-_J;f*QQ3a4 z3%%>c00bax>;d)l1v@~t+#eW@M(iD3LvmeR4k%|0R^-8>Y-EZEeU0d62rv?oYmL?y zR!@#`PEi+B2q)il@e&kb@((dn@6hE1j<;xh8bbtMfdgHzxaWB0K!XI$-b8d0PW2US zu;OB;D6=3SQ51!|xNgv_Mu&5IasEGknbE8}#@3V)_9Tx(UFd=G>2LG|hpZ zELGxdRKhKy(rD?aVx#Ge@I`PNLZ+MIJHA+)=N1!}Et8zK_?X`{2%YRY_eDb5{8L^i V!q?&>&-iBCxIBYX*{ipo$#4F-Yc>D? diff --git a/server/src/routes/api/admin-users.js b/server/src/routes/api/admin-users.js index f6b3f6d..cc26341 100644 --- a/server/src/routes/api/admin-users.js +++ b/server/src/routes/api/admin-users.js @@ -92,6 +92,7 @@ export async function registerAdminUserRoutes(fastify) { fastify.patch('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { id } = request.params const body = request.body ?? {} + const adminUserId = request.user.sub const existing = await prisma.user.findUnique({ where: { id } }) if (!existing) { @@ -99,9 +100,15 @@ export async function registerAdminUserRoutes(fastify) { return } + const isSelf = id === adminUserId + const data = {} if (body.email !== undefined) { + if (isSelf) { + reply.code(403).send({ error: 'Нельзя изменить свою почту через панель администратора' }) + return + } const email = normalizeEmail(body.email) if (!email || !email.includes('@')) { reply.code(400).send({ error: 'Некорректная почта' }) @@ -139,6 +146,13 @@ export async function registerAdminUserRoutes(fastify) { fastify.delete('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { id } = request.params + const adminUserId = request.user.sub + + if (id === adminUserId) { + reply.code(403).send({ error: 'Нельзя удалить свою учётную запись' }) + return + } + try { await prisma.user.delete({ where: { id } }) reply.code(204).send() diff --git a/server/src/routes/user-cart.js b/server/src/routes/user-cart.js index b432d1c..136453b 100644 --- a/server/src/routes/user-cart.js +++ b/server/src/routes/user-cart.js @@ -29,7 +29,7 @@ export async function registerUserCartRoutes(fastify) { const product = await prisma.product.findFirst({ where: { id: productId, published: true } }) if (!product) return reply.code(404).send({ error: 'Товар не найден' }) - const available = product.inStock ? product.quantity : 1 + const available = product.quantity const existing = await prisma.cartItem.findUnique({ where: { userId_productId: { userId, productId } } }) const nextQty = (existing?.qty ?? 0) + Math.floor(qty) if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` }) @@ -57,7 +57,7 @@ export async function registerUserCartRoutes(fastify) { return reply.code(204).send() } - const available = existing.product.inStock ? existing.product.quantity : 1 + const available = existing.product.quantity const nextQty = Math.floor(qty) if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` }) diff --git a/server/src/routes/user-orders.js b/server/src/routes/user-orders.js index 0e20eb1..8ced32d 100644 --- a/server/src/routes/user-orders.js +++ b/server/src/routes/user-orders.js @@ -65,7 +65,7 @@ export async function registerUserOrderRoutes(fastify) { if (cartItems.length === 0) return reply.code(400).send({ error: 'Корзина пуста' }) for (const ci of cartItems) { - const available = ci.product.inStock ? ci.product.quantity : 1 + const available = ci.product.quantity if (ci.qty > available) { return reply.code(409).send({ error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.`, @@ -112,8 +112,6 @@ export async function registerUserOrderRoutes(fastify) { try { created = await prisma.$transaction(async (tx) => { for (const ci of cartItems) { - if (!ci.product.inStock) continue - const res = await tx.product.updateMany({ where: { id: ci.productId, quantity: { gte: ci.qty } }, data: { quantity: { decrement: ci.qty } },